在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
注解,然后使用这个注解的属性进行处理。
andlerExceptionResolver使用@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状态代码。
测试方法
如这里指定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); } }; }