参考官方文档:https://docs.spring.io/spring/docs/5.2.7.RELEASE/spring-framework-reference/web.html#mvc-exceptionhandlers
如果异常在请求映射期间发生或从请求处理程序(例如@Controller)抛出,则DispatcherServlet委托给HandlerExceptionResolver Beans 来解决该异常并提供替代处理,通常是错误响应。
下表列出了可用的HandlerExceptionResolver
实现:
HandlerExceptionResolver |
描述 |
---|---|
SimpleMappingExceptionResolver |
异常类名称和错误视图名称之间的映射。对于在浏览器应用程序中呈现错误页面很有用。 |
DefaultHandlerExceptionResolver |
解决Spring MVC引发的异常,并将它们映射到HTTP状态代码。另请参见替代ResponseEntityExceptionHandler 和REST API异常。 |
ResponseStatusExceptionResolver |
使用@ResponseStatus 注释解决异常,并根据注释中的值将其映射到HTTP状态代码。 |
ExceptionHandlerExceptionResolver |
通过调用@Controller或@ControllerAdvice类中的@ExceptionHandler方法来解决异常。请参见@ExceptionHandler方法。 |
支持的Exceptions
javax.servlet.http.HttpServletResponse
Exception | HTTP Status Code |
---|---|
HttpRequestMethodNotSupportedException | 405 (SC_METHOD_NOT_ALLOWED) |
HttpMediaTypeNotSupportedException | 415 (SC_UNSUPPORTED_MEDIA_TYPE) |
HttpMediaTypeNotAcceptableException | 406 (SC_NOT_ACCEPTABLE) |
MissingPathVariableException | 500 (SC_INTERNAL_SERVER_ERROR) |
MissingServletRequestParameterException | 400 (SC_BAD_REQUEST) |
ServletRequestBindingException | 400 (SC_BAD_REQUEST) |
ConversionNotSupportedException | 500 (SC_INTERNAL_SERVER_ERROR) |
TypeMismatchException | 400 (SC_BAD_REQUEST) |
HttpMessageNotReadableException | 400 (SC_BAD_REQUEST) |
HttpMessageNotWritableException | 500 (SC_INTERNAL_SERVER_ERROR) |
MethodArgumentNotValidException | 400 (SC_BAD_REQUEST) |
MissingServletRequestPartException | 400 (SC_BAD_REQUEST) |
BindException | 400 (SC_BAD_REQUEST) |
NoHandlerFoundException | 404 (SC_NOT_FOUND) |
AsyncRequestTimeoutException | 503 (SC_SERVICE_UNAVAILABLE) |
您可以通过HandlerExceptionResolver
在Spring配置中声明多个bean并order
根据需要设置它们的属性来形成异常解析器链。order属性越高,异常解析器的定位就越晚。
HandlerExceptionResolver
接口定义了方法:
ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
HandlerExceptionResolver中定义指定它可以返回:
MVC Config 默认自动声明了内置的 spring MVC 异常解析器,对于带注释@ResponseStatus的异常, 和@ExceptionHandler方法的支持。您可以自定义列表或取代它。
如果任何HandlerExceptionResolver异常仍未得到解决,因此,造成传播或如果响应状态设置为一个错误状态( 4xx, 5xx), Servlet容器可以在HTML渲染一个默认的错误页面。自定义容器的默认错误页面,您可以在web . xml中声明一个错误页面映射。下面的例子显示了如何这样做:
<error-page>
<location>/errorlocation>
error-page>
鉴于前面的例子,当一个异常冒泡或响应错误状态时,Servlet容器将产生的错误发送到容器内配置的URL(例如,/error)。然后由DispatcherServlet处理,可能映射到一个@controller,可实现返回一个错误视图的名字与一个model或呈现JSON响应,如以下示例所示:
@RestController
public class ErrorController {
@RequestMapping(path = "/error")
public Map<String, Object> handle(HttpServletRequest request) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("status", request.getAttribute("javax.servlet.error.status_code"));
map.put("reason", request.getAttribute("javax.servlet.error.message"));
return map;
}
}
注意:
Servlet API没有提供在Java中创建错误页面映射的方法。但是,您可以同时使用WebApplicationInitializer
和web.xml
。
在Spring中常见的全局异常处理,主要有三种:
(1)注解ExceptionHandler
(2)继承HandlerExceptionResolver接口
(3)注解ControllerAdvice
具体可参考:https://www.cnblogs.com/yixinjishu/p/10863529.html
这里我们以HTTP Status Code 400 和 500 作为例子进行演示,我们创建一个Controller类,如下
@RestController
@RequestMapping("echo")
public class EchoController {
/**
* 请求路径 http://localhost:端口/echo/get
*
* @param name
* @return
*/
@RequestMapping("get")
public String get(@RequestParam("name") String name) {
return "response:" + name;
}
/**
* 请求路径 http://localhost:端口/echo/cal
*
* @return
*/
@RequestMapping("cal")
public int calculate() {
int val = 100 / 0;
return val;
}
}
我们分别请求get和cal 返回的错误分别对应400 和 500 如下:
注解ExceptionHandler作用对象为方法,最简单的使用方法就是放在controller文件中,详细的注解定义不再介绍。如果项目中有多个controller文件,通常可以在baseController中实现ExceptionHandler的异常处理,而各个contoller继承basecontroller从而达到统一异常处理的目的。因为比较常见,简单代码在上面添加如下代码:
@ExceptionHandler(Exception.class)
public String exception(Exception e) {
return this.getClass().getSimpleName() + ":" + e.getMessage();
}
这时候在分别请求get和cal 返回的信息如下:
EchoController:Required String parameter 'name' is not present
EchoController:/ by zero
这时候发现添加ExceptionHandler之后的结果
优点:ExceptionHandler简单易懂,并且对于异常处理没有限定方法格式;
缺点:由于ExceptionHandler仅作用于方法,对于多个controller的情况,仅为了一个方法,所有需要异常处理的controller都继承这个类,不太好。
@ControllerAdvice注解,其实是其与ExceptionHandler的组合使用。在上文中可以看到,单独使用@ExceptionHandler时,其必须在一个Controller中,然而当其与ControllerAdvice组合使用时就完全没有了这个限制。换句话说,二者的组合达到的全局的异常捕获处理,我们创建一个类GlobalExceptionHandler,然后把上面EchoController的exception方法整个注释掉,这时候在请求get和cal 返回的信息和单独用注解ExceptionHandler一致。
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
@ResponseBody
public String exception(Exception e) {
return this.getClass().getSimpleName() + ":" + e.getMessage();
}
}
通过上面结果可以看到,异常处理确实已经变更为GlobalExceptionHandler类。这种方法将所有的异常处理整合到一处,去除了Controller中的继承关系,并且达到了全局捕获的效果,推荐使用此类方式。
HandlerExceptionResolver本身是SpringMVC内部的接口,其内部只有resolveException一个方法,通过实现该接口我们可以达到全局异常处理的目的。
我们创建一个类:MyHandlerExceptionResolver,同时将GlobalExceptionHandler类上的注解@ControllerAdvice注释掉。
@Component
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
String message = this.getClass().getSimpleName() + ":" + ex.getMessage();
PrintWriter pw = null;
try {
pw = response.getWriter();
pw.write(message);
pw.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != pw) {
pw.close();
}
}
return new ModelAndView();
}
}
为什么500的异常处理已经生效了,但是400的异常处理却没有生效。这是怎么回事呢?不是说可以达到了全局捕获的效果吗?这里只好跟踪Spring的源码进行分析。
在DispatcherServlet 类中的方法doDispatch 中调用了processDispatchResult方法,processDispatchResult调用了processHandlerException方法,如下图1:
看方法processHandlerException的代码this.handlerExceptionResolvers包含3个HandlerExceptionResolver【DefaultErrorAttributes,HandlerExceptionResolverComposite,MyHandlerExceptionResolver】。在循环到HandlerExceptionResolverComposite时跳出了循环,说明了异常被Spring自带的异常给处理掉了,所以就跳过了MyHandlerExceptionResolver类的处理,从而出现400的返回结果。而对于add请求,中间没有阻拦,所以就达到了预期效果。
先把结论写上在进行分析:@ExceptionHandler优先于@ControllerAdvice优先于HandlerExceptionResolver。
我们将EchoController、GlobalExceptionHandler类上面的注释的内容全部放开。
下面去Spring源码去找结论,上面提到了HandlerExceptionResolverComposite 它里面有3个HandlerExceptionResolver分别是【ExceptionHandlerExceptionResolver,ResponseStatusExceptionResolver,DefaultHandlerExceptionResolver】,如下图3:
图3
ExceptionHandlerExceptionResolver中的doResolveHandlerMethodException方法中调用了getExceptionHandlerMethod方法如下图4:
图4
通过跟进,发现有两个变量可能就是问题的关键:exceptionHandlerCache和exceptionHandlerAdviceCache。首先,exceptionHandlerCache上面的注释的翻译过来意思为:
// Local exception handler methods on the controller class itself.
// 控制器类本身上的本地异常处理程序方法。
// To be invoked through the proxy, even in case of an interface-based proxy.
// 即使是基于接口的代理,也要通过代理调用。
handlerType = handlerMethod.getBeanType();
ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType);
在看代码上面的method正是@ExceptionHandler标注的EchoController方法exception相吻合。而在往下的代码
是针对@ControllerAdvice 注解进行处理的逻辑,然后在结合上面分析的HandlerExceptionResolver所处HandlerExceptionResolver的索引已知,参考图1。所以得出接口@ExceptionHandler优先于@ControllerAdvice优先于HandlerExceptionResolver。
for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
ControllerAdviceBean advice = entry.getKey();
if (advice.isApplicableToBeanType(handlerType)) {
ExceptionHandlerMethodResolver resolver = entry.getValue();
Method method = resolver.resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(advice.resolveBean(), method);
}
}
}