在dispatchServlet处理过程中,有时会抛出异常。SpringMVC对此进行了处理,如下图所示在processDispatchResult中提供了入口processHandlerException(request, response, handler, exception);
来处理异常。
具体如何处理呢?SpringMVC提供了一个处理控制器方法执行过程中所出现的异常的接口:HandlerExceptionResolver
HandlerExceptionResolver接口的实现类
- 蓝色实线表示的是继承关系
- 绿色虚线表示的是接口实现关系
- 绿色实线表示的是接口与接口的关系
每个类主要方法示意图
【1】概述
SpringMVC通过HandlerExceptionResolver处理程序的异常,包括handler映射、数据绑定以及目标方法的执行时发生的异常。
① 异常处理顺序
-
如果是特定异常使用
DefaultHandlerExceptionResolver
; -
如果存在
@ExceptionHandler({RuntimeException.class})
注解的方法,将执行。 -
如果同时存在
@ExceptionHandler({RuntimeException.class})
和类似于@ExceptionHandler({ArithmeticException.class})
,则近者优先! -
如果不存在
@ExceptionHandler
,则异常尝试使用ResponseStatusExceptionResolver
。 -
最后使用
SimpleMappingExceptionResolver
。
② HandlerExceptionResolver接口
由对象实现的接口,可以解决在处理程序映射或执行期间抛出的异常,在典型情况下是会跳到一个错误视图。实现者通常在应用程序上下文中注册为bean。
错误视图类似于错误页面JSP,但可以用于任何类型的异常,包括任何已检查的异常,以及特定处理程序的潜在细粒度映射(某个controller的某个方法)。
package org.springframework.web.servlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;
public interface HandlerExceptionResolver {
//尝试解析给定的异常并返回一个ModelAndView (一个合适的错误页面)
//返回的ModelAndView 可能为空,表示异常已经成功解决但是不需一个错误页面,比如设置STATUS CODE
@Nullable
ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
}
其实现类如下图所示(可以通过Navigate -> Type Hierarchy
打开,快捷键是F4):
③ DispatcherServlet默认装配的ExceptionResolver
① 没有配置<mvc:annotation-driven/>
② 配置了<mvc:annotation-driven/>
【2】ExceptionHandlerExceptionResolver
主要处理handler中用@ExceptionHandler注解定义的方法。
AbstractHandlerMethodExceptionResolver
可以通过@ExceptionHandler
注解的方法解决异常。可以通过setCustomArgumentResolver
和setCustomReturnValueHandlers
添加对自定义参数和返回值类型的支持。或者使用setArgumentResolvers
和setReturnValueHandlers
重新配置所有参数和返回值类型。
相关说明如下
-
在
@ExceptionHandler
方法的入参中可以加入 Exception 类型的参数, 该参数即对应发生的异常对象; -
@ExceptionHandler
方法的入参中不能传入 Map。若希望把异常信息传到页面上,需要使用ModelAndView
作为返回值;若想直接返回JSON可以使用@ResponseBody
注解; -
@ExceptionHandler
方法标记的异常有优先级的问题. -
@ControllerAdvice
: 如果在当前 Handler 中找不到@ExceptionHandler
方法来处理当前方法出现的异常, 则将去@ControllerAdvice
标记的类中查找@ExceptionHandler
标记的方法来处理异常.
测试代码如下:
@ExceptionHandler({ArithmeticException.class})
public ModelAndView handleArithmeticException(Exception ex){
System.out.println("出异常了: " + ex);
ModelAndView mv = new ModelAndView("error");
mv.addObject("exception", ex);
return mv;
}
@RequestMapping("/testExceptionHandlerExceptionResolver")
public String testExceptionHandlerExceptionResolver(@RequestParam("i") int i){
System.out.println("result: " + (10 / i));
return "success";
}
当 i = 0时,抛出异常:
[FirstInterceptor] preHandle
出异常了: java.lang.ArithmeticException: / by zero
[FirstInterceptor] afterCompletion
handleArithmeticException
方法捕捉该异常,并跳到error.jsp
页面(同时可注意到,因为目标方法抛出异常,故拦截器不执行postHandle
方法)。
如果想方法捕捉的范围更广一点,可以使用如下配置
ArithmeticException
改为RuntimeException
@ExceptionHandler({RuntimeException.class})
public ModelAndView handleArithmeticException2(Exception ex){
System.out.println("[出异常了-运行时异常]: " + ex);
ModelAndView mv = new ModelAndView("error");
mv.addObject("exception", ex);
return mv;
}
如果两个方法同时存在,则不会执行第二个方法(@ExceptionHandler({RuntimeException.class})
)–近者优先;
如果两个方法都不存在,则将去 @ControllerAdvice
标记的类中查找 @ExceptionHandler
标记的方法来处理异常.
@ControllerAdvice
public class SpringMVCTestExceptionHandler {
@ExceptionHandler({ArithmeticException.class})
public ModelAndView handleArithmeticException(Exception ex){
System.out.println("----> 出异常了: " + ex);
ModelAndView mv = new ModelAndView("error");
mv.addObject("exception", ex);
return mv;
}
}
【3】ResponseStatusExceptionResolver
① 响应状态码异常解析器
在异常或异常父类中找到@ResponseStatus
注解,然后使用这个注解的属性进行处理。
HandlerExceptionResolver
使用@ResponseStatus
注解将异常映射到HTTP STATUS CODE
。默认在DispatcherServlet、MVC java配置以及MVCnamespace,都可以使用该异常解析器。从4.2开始,该解析器还递归查找原因异常中存在的@ResponseStatus
,从4.2.2开始,该解析器支持自定义组合注解中@ResponseStatus
的属性重写。从5.0开始,该解析器支持ResponseStatusException。
ResponseStatus注解
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseStatus {
@AliasFor("code")
HttpStatus value() default HttpStatus.INTERNAL_SERVER_ERROR;
@AliasFor("value")
HttpStatus code() default HttpStatus.INTERNAL_SERVER_ERROR;
String reason() default "";
}
该注解使用status code
和异常原因reason
来标记一个方法或者异常类。当调用处理程序方法时,状态代码将应用于HTTP响应,并通过其他方式覆盖状态信息,如 ResponseEntity
或请求重定向redirect:
。当在异常类上使用该注解或者设置注解属性reason时,将会使用HttpServletResponse.sendError
方法。对于HttpServletResponse.sendError
,响应被视为已完成,不应写入任何其他文件。此外,Servlet容器通常会编写一个HTML错误页面,因此使用reason
不适合REST API。对于这种情况,最好使用org.springframework.http.ResponseEntity
作为返回类型,并避免使用@ResponseStatus
注意,控制器类也可以用@ResponseStatus
注解,然后由所有@RequestMapping
注解的方法继承。
② 自定义异常类
① 异常类上使用注解@ResponseStatus
注意value
和reason
值与下面网页对比。
@ResponseStatus(value=HttpStatus.FORBIDDEN, reason="用户名和密码不匹配!")
public class UserNameNotMatchPasswordException extends RuntimeException{
private static final long serialVersionUID = 1L;
//...
}
在当前 Handler 中找不到 @ExceptionHandler
方法来处理当前异常, 且@ControllerAdvice
标记的类中找不到 @ExceptionHandler
标记的方法处理当前异常.
测试方法
@RequestMapping("/testResponseStatusExceptionResolver")
public String testResponseStatusExceptionResolver(@RequestParam("i") int i){
if(i == 13){
throw new UserNameNotMatchPasswordException();
}
System.out.println("testResponseStatusExceptionResolver...");
return "success";
}
测试结果
② 在方法上使用注解@ResponseStatus
@ResponseStatus(reason="测试",value=HttpStatus.NOT_FOUND)
@RequestMapping("/testResponseStatusExceptionResolver")
public String testResponseStatusExceptionResolver(@RequestParam("i") int i){
if(i == 13){
throw new UserNameNotMatchPasswordException();
}
System.out.println("testResponseStatusExceptionResolver...");
return "success";
}
方法正常调用,但是页面显示404
如果在当前 Handler
中找到 @ExceptionHandler
方法来处理当前异常, 或@ControllerAdvice
标记的类中找到 @ExceptionHandler
标记的方法处理当前异常。则将会调用对应方法处理该异常,不会使用ResponseStatusExceptionResolver
。
【4】DefaultHandlerExceptionResolver
HandlerExceptionResolver的默认实现,解析标准Spring MVC异常并将其转换为相应的HTTP状态代码。
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) |
测试方法
如这里指定post方法。
@RequestMapping(value="/testDefaultHandlerExceptionResolver",method=RequestMethod.POST)
public String testDefaultHandlerExceptionResolver(){
System.out.println("testDefaultHandlerExceptionResolver...");
return "success";
}
测试结果
【5】SimpleMappingExceptionResolver
如果想统一管理异常,就使用SimpleMappingExceptionResolver
,它将异常类名映射到对应视图名。发送异常时 ,会跳转对应页面。
SimpleMappingExceptionResolver
是org.springframework.web.servlet.HandlerExceptionResolver
实现,该实现允许将异常类名映射到视图名,无论是对于一组给定的处理程序还是DispatcherServlet
中的所有处理程序。错误视图类似于错误页面JSP,但可以用于任何类型的异常,包括任何选中的异常,以及特定处理程序的细粒度映射。
springmvc.xml配置
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<!-- 定义默认的异常处理页面 -->
<property name="defaultErrorView" value="default/error"/>
<property name="exceptionAttribute" value="exception"></property>
<!--特殊异常的处理方法-->
<property name="exceptionMappings">
<props>
<!--数组越界异常,视图--error.jsp -->
<prop key="java.lang.ArrayIndexOutOfBoundsException">error</prop>
</props>
</property>
</bean>
后台测试代码
@RequestMapping("/testSimpleMappingExceptionResolver")
public String testSimpleMappingExceptionResolver(@RequestParam("i") int i){
String [] vals = new String[10];
System.out.println(vals[i]);
return "success";
}
测试结果
如果在当前 Handler 中找到 @ExceptionHandler
方法来处理当前异常, 或@ControllerAdvice
标记的类中找到 @ExceptionHandler
标记的方法处理当前异常。则将会调用对应方法处理该异常,不会使用SimpleMappingExceptionResolver
。
【6】ControllerAdvice捕捉异常直接返回JSON
@ControllerAdvice
public class ControllerExceptionHandler {
private final static Logger log = LoggerFactory.getLogger(ControllerExceptionHandler.class);
@ExceptionHandler(value = {BindException.class})
@ResponseBody
public String bindExceptionHandler(HttpServletRequest request, Exception e) {
log.error(e.getMessage());
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", "false");
jsonObject.put("message","非法的日期格式!");
return jsonObject.toJSONString();
}
....
}
【7】EmbeddedServletContainerCustomizer注入自定义错误页面
在SpringBoot中,可以通过如下配置注入自定义的errorPage。这样当出现对应的HttpStatus时,就会跳到指定页面。
@Bean
public EmbeddedServletContainerCustomizer containerCustomizer() {
return new EmbeddedServletContainerCustomizer() {
@Override
public void customize(ConfigurableEmbeddedServletContainer container) {
ErrorPage error400Page = new ErrorPage(HttpStatus.BAD_REQUEST, "/error/400");
ErrorPage error401Page = new ErrorPage(HttpStatus.UNAUTHORIZED, "/error/401");
ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/error/404");
ErrorPage error500Page = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error/500");
container.addErrorPages(error400Page,error401Page, error404Page, error500Page);
}
};
}