前言
首先写一个接口,会抛出算术异常ArithmeticException。
@GetMapping("/testException")
@ResponseBody
public String testException() {
int i= 4/0;
return "testException";
}
浏览器访问,发现返回了一个页面,但是是说500异常,找不到/error访问路径。
使用PostMan访问,发现返回了Json数据。
那么Spring MVC中时如何进行异常处理的呢?
核心类简介
spring-framework
spring原生框架提供了核心的异常处理机制。
HandlerExceptionResolver
HandlerExceptionResolver接口,控制器异常解析器,对处理映射器或执行期间抛出的异常进行解析处理。
其定义了一个解析异常方法,尝试解决执行期间抛出的给定异常,返回一个ModelAndView对象,然后可以通过ModelAndView 返回异常页面。
public interface HandlerExceptionResolver {
ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);
}
在spring-framework中,HandlerExceptionResolver有这些实现类。
HandlerExceptionResolverComposite
HandlerExceptionResolverComposite是一个异常解析器的组合类,它成员属性维护了一个解析器集合。
@Nullable
private List<HandlerExceptionResolver> resolvers;
SimpleMappingExceptionResolver
SimpleMappingExceptionResolver是一个基础的异常解析器,可以将将异常类名映射为视图名,即发生异常时使用对应的视图返回异常。
可以通过这个解析器,配置异常页面。
ExceptionHandlerExceptionResolver
ExceptionHandlerExceptionResolver处理使用@ExceptionHandler注解方法,并调用它来处理引发的异常。
DefaultHandlerExceptionResolver
DefaultHandlerExceptionResolver可以解析 Spring MVC 异常并将它们转换为相应的HTTP 状态代码。
ResponseStatusExceptionResolver
处理使用了@ResponseStatus注解的相关异常。
spring-boot-project
在spring-boot的spring-boot-project模块中,定义了两个接口,用来处理异常。
ErrorController
异常处理控制器接口,我们可以自己写一个@Controller控制器实现该接口,来实现异常时,错误处理。
public interface ErrorController {
}
ErrorAttributes
ErrorAttributes错误属性接口,提供了对错误属性的封装。
public interface ErrorAttributes {
String ERROR_ATTRIBUTE = ErrorAttributes.class.getName() + ".error";
// 将错误信息封装为Map,然后可以使用Json 返回给客户端。
default Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
return Collections.emptyMap();
}
// 在webRequest中获取异常
Throwable getError(WebRequest webRequest);
}
DefaultErrorAttributes
DefaultErrorAttributes实现了ErrorAttributes和HandlerExceptionResolver接口。
它的作用主要是将错误信息放在request对象中。
常用的错误属性如下:
属性 | |
---|---|
timestamp | 异常发生的时间 |
status | 状态吗 |
error | 错误原因 |
exception | 异常名 |
message | 异常信息 |
trace | 异常栈信息 |
path | 异常路径 |
spring-boot-autoconfigure
spring-boot-autoconfigure模块也添加了一些异常处理相关的类
ErrorViewResolver
ErrorViewResolver接口,只有一个方法用于解析指定详细信息的错误视图。
@FunctionalInterface
public interface ErrorViewResolver {
ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model);
}
DefaultErrorViewResolver
DefaultErrorViewResolver是ErrorViewResolver 的默认实现类。可以在异常时,搜索异常页面。
如果404异常时,将会在下面这里路径下搜索异常页面并展示:
/templates/error/404.x
/static/error/404.html
/templates/error/4xx.x
/static/error/4xx.html
AbstractErrorController
AbstractErrorController是ErrorController的抽象子类,添加了一些使用ErrorViewResolvers解析的重要方法。
BasicErrorController
BasicErrorController是spring boot 提供的一个控制器方法,是ErrorController的实现类,映射/error路径。
访问路径:
@RequestMapping("${server.error.path:${error.path:/error}}")
成员属性,
private final ErrorProperties errorProperties;
提供了两个重要的控制器方法,当/error请求的MediaType为text/html,也就是页面访问时,会进入到errorHtml,返回ModelAndView 。
当其他方式访问,直接返回ResponseEntity,也就是Json信息。
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
ErrorMvcAutoConfiguration
ErrorMvcAutoConfiguration的作用,主要是:
- 注入BasicErrorController错误控制器
- 注入DefaultErrorViewResolver
- 默认错误页面视图内部类StaticView
异常解析器加载流程
1. 进入DispatcherServlet初始化组件
在DispatcherServlet类中,有个成员属性维护了一个HandlerExceptionResolver集合,用来存放所有的异常解析器。
@Nullable
private List<HandlerExceptionResolver> handlerExceptionResolvers;
之前在DispatcherServlet初始化源码分析中,有分析到DispatcherServlet最后会调用initStrategies方法进行组件的初始化,而处理器异常解析器也是在这里初始化加载的。
2.初始化解析器
initHandlerExceptionResolvers 会初始化DispatcherServlet使用的HandlerExceptionResolver。
private void initHandlerExceptionResolvers(ApplicationContext context) {
this.handlerExceptionResolvers = null;
// 1. 是否检测所有 HandlerExceptionResolver,默认为true
if (this.detectAllHandlerExceptionResolvers) {
// 1.1 在ApplicationContext中查找所有HandlerExceptionResolver,包括parent上下文。
Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils
.beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
if (!matchingBeans.isEmpty()) {
// 1.2. 将查询到的容器添加到handlerExceptionResolvers集合中并排序
this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values());
// We keep HandlerExceptionResolvers in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
}
}
else {
try {
// 2. HandlerExceptionResolver为false
// 2.1 查询名称为handlerExceptionResolver的Bean,并赋值给handlerExceptionResolvers
HandlerExceptionResolver her =
context.getBean(HANDLER_EXCEPTION_RESOLVER_BEAN_NAME, HandlerExceptionResolver.class);
this.handlerExceptionResolvers = Collections.singletonList(her);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, no HandlerExceptionResolver is fine too.
}
}
// 3. handlerExceptionResolvers为null时,则为默认HandlerExceptionResolver。
if (this.handlerExceptionResolvers == null) {
this.handlerExceptionResolvers = getDefaultStrategies(context, HandlerExceptionResolver.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerExceptionResolvers declared in servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}
可以看到最后添加了两个异常解析器,DefaultErrorAttributes、HandlerExceptionResolverComposite(组合解析器)。
异常解析器工作流程
1. DispatchServlet结果处理
可以在DispatchServlet 的doDispatch方法中看到,在执行了控制器方法时,都进行了catch。
异常是在processDispatchResult方法进行处理的,可以看到它的入参传入了异常对象。
processDispatchResult负责处理调用的结果,解析我们返回的结果或者处理异常为ModelAndView。
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception {
boolean errorView = false;
// 1. 如果异常对象不为空
if (exception != null) {
// 1.1 如果是ModelAndViewDefiningException,解析将异常解析为ModelAndView
if (exception instanceof ModelAndViewDefiningException) {
this.logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException)exception).getModelAndView();
} else {
// 2. 如果不是ModelAndViewDefiningException异常,进入异常处理方法,返回ModelAndView。
Object handler = mappedHandler != null ? mappedHandler.getHandler() : null;
mv = this.processHandlerException(request, response, handler, exception); // 2.1 将错误页面标记为存在
errorView = mv != null;
}
}
// 3. 如果 视图存在,处理视图,返回页面
if (mv != null && !mv.wasCleared()) {
this.render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
// others
}
2. DispatchServlet 异常处理
接着进入DispatchServlet 的processHandlerException方法开始对异常进行处理。通过加载的HandlerExceptionResolver结合确定异常视图模型。
processHandlerException只要解析到一个错误视图模型对象,就会跳出循环。
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) throws Exception {
request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
ModelAndView exMv = null;
if (this.handlerExceptionResolvers != null) {
// 1. 循环DispatchServlet 中的异常解析器
Iterator var6 = this.handlerExceptionResolvers.iterator();
while(var6.hasNext()) {
HandlerExceptionResolver resolver = (HandlerExceptionResolver)var6.next();
// 2. 解析异常,返回ModelAndView
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
// others...
}
3. 循环异常解析器
processHandlerException方法会循环DispatchServlet 中的异常解析器,第一个是DefaultErrorAttributes,它的主要作用是将异常设置到request对象的属性中。
接着进入到组合解析器中HandlerExceptionResolverComposite。这个解析器的resolveException方法,会循环其成员属性中的所有解析器。
首先进入ExceptionHandlerExceptionResolver解析器的resolveException方法。
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
// 1. 解析器不支持handler类型,直接返回null
if (!this.shouldApplyTo(request, handler)) {
return null;
} else {
// 2. 如果配置response缓存,则缓存处理,默认false.
this.prepareResponse(ex, response);
// 3. 解析异常
ModelAndView result = this.doResolveException(request, response, handler, ex);
// others....
}
接着进入到doResolveHandlerMethodException方法,正式解析异常,因为我们这里没有配置@ExceptionHandler注解,ExceptionHandlerExceptionResolver解析器返回的结果也是null。
接着进入到组合解析器的第二个解析器ResponseStatusExceptionResolver,对使用了@ResponseStatus响应状态码注解进行解析。
@Nullable
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
try {
// 1. 如果是ResponseStatusException,设置响应消息头,并response.sendError。
if (ex instanceof ResponseStatusException) {
return this.resolveResponseStatusException((ResponseStatusException)ex, request, response, handler);
}
// 2. 根据异常,查询状态码。这里ArithmeticException,未找到状态码,返回null
ResponseStatus status = (ResponseStatus)AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class);
// 3. 获取到了ResponseStatus,则设置响应消息头,并response.sendError。
if (status != null) {
return this.resolveResponseStatus(status, request, response, handler, ex);
}
// others
}
最后进入到DefaultHandlerExceptionResolver解析器,会判断是不是spring MVC中的异常,如果是,直接设置对应状态码,并调用response.sendError(404);,返回一个空的ModelAndView对象。
protected ModelAndView handleNoHandlerFoundException(NoHandlerFoundException ex, HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException {
pageNotFoundLogger.warn(ex.getMessage());
response.sendError(404);
return new ModelAndView();
}
4. 回到processHandlerException
经过上述解析器后,最终我们的ArithmeticException异常,返回的ModelAndView为null。
processHandlerException继续往下走,这里的处理逻辑为:
- 如果解析器ModelAndView为null,直接throw 异常,在控制台打印。
- ModelAndView为空,则会在request中设置异常属性
- ModelAndView不为空,存在View,查看是否有默认异常视图,有则设置View为默认视图。
processDispatchResult执行完毕,最终没有一个解析器处理了我们的异常。
返回null,表明异常仍未解决,如果异常最后还在,则允许冒泡到Servlet容器,交给Tomcat处理。
5. 异常进入StandardHostValve(Tomacat)
在之前DispathServlet已经执行完毕了,但是解析器也没有对异常进行解决,最终由Tomacat来处理。
StandardHostValve是的Tomacat的核心类,其中invoke方法根据Request匹配Context,并将Request传递给匹配到的Context,spring MVC没有解决的异常,最终由它处理。
StandardHostValve会去获取response的status如果response是错误转状态则继续去获取错误码对应的ErrorPage,如果没有定义则获取默认的ErrorPage(/error),并重定向/error请求。
所以/error重定向请求会再次进入到DispathServlet中的=》doService=》doDispatch等方法,直接去访问/error路径。
6. /error访问
/error访问路径会进入到BasicErrorController中。
@RequestMapping(
produces = {
"text/html"}
)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
// 1. 获取HttpStatus 500 SERVER_ERROR
HttpStatus status = this.getStatus(request);
// 2. 获取错误信息
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
// 3. 设置响应状态码
response.setStatus(status.value());
// 4. 调用DefaultErrorViewResolver
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
// 5. 返回视图
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}
调用DefaultErrorViewResolver解析器,会查到相关视图模板,这里没有配置,所以返回了null。所以这里/error访问的控制器,最后返回了 new ModelAndView(“error”, model);。
private ModelAndView resolve(String viewName, Map<String, Object> model) {
// 视图名 error/5xx
String errorViewName = "error/" + viewName;
// 查询可用的模板
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
}
7. 进入processDispatchResult
接着进入到processDispatchResult方法,因为这里返回的ModelAndView为/error,参数exception为null,所以会进入render方法。
render会调用视图解析器,最终调用StaticView视图的render方法。
直接拼接错误信息,然后response直接把信息页面返回个浏览器了。
8. PostMan访问
当使用PostMan时,这里/error 会直接访问到BasicErrorController的error控制器。
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = this.getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity(status);
} else {
Map<String, Object> body = this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity(body, status);
}
}
直接返回了Json信息。
版权声明:本文不是「本站」原创文章,版权归原作者所有 | 原文地址: