SpringMVC中异常处理与ControllerAdvice捕捉全局异常

简介: SpringMVC中异常处理与ControllerAdvice捕捉全局异常

在dispatchServlet处理过程中,有时会抛出异常。SpringMVC对此进行了处理,如下图所示在processDispatchResult中提供了入口processHandlerException(request, response, handler, exception);来处理异常。

20fcc989824549918c1202c7a5409d5c.png

具体如何处理呢?SpringMVC提供了一个处理控制器方法执行过程中所出现的异常的接口:HandlerExceptionResolver

HandlerExceptionResolver接口的实现类


dcc22ccaddfa447dbc065e6177a618eb.png

  • 蓝色实线表示的是继承关系
  • 绿色虚线表示的是接口实现关系
  • 绿色实线表示的是接口与接口的关系

每个类主要方法示意图


【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/>


aHR0cDovL2ltZy5ibG9nLmNzZG4ubmV0LzIwMTcwNTA0MTE0OTUyNDEx.png

② 配置了<mvc:annotation-driven/>


aHR0cDovL2ltZy5ibG9nLmNzZG4ubmV0LzIwMTcwNTA0MTE1MDE0Mzg1.png

【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方法)。


aHR0cDovL2ltZy5ibG9nLmNzZG4ubmV0LzIwMTcwNTA0MTE0NDAxMzMx.png

如果想方法捕捉的范围更广一点,可以使用如下配置

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

注意valuereason值与下面网页对比。

@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状态代码。

image.png

测试方法

如这里指定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";
}

aHR0cDovL2ltZy5ibG9nLmNzZG4ubmV0LzIwMTcwNTA0MTUxNDUzNDc4.png

如果在当前 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);
        }
    };
}







目录
相关文章
|
3月前
|
运维 Devops
全局异常处理
全局异常处理
34 0
SpringBoot 如何使用 @ExceptionHandler 注解进行局部异常处理
SpringBoot 如何使用 @ExceptionHandler 注解进行局部异常处理
|
6月前
|
前端开发
Nestjs(五)异常处理方式(异常过滤器)
Nestjs(五)异常处理方式(异常过滤器)
127 0
|
6月前
|
前端开发 IDE Java
使用aop实现全局异常处理
使用aop实现全局异常处理
144 0
|
JSON 安全 Java
Spring Boot之全局异常处理:404异常为何捕获不到?
Spring Boot之全局异常处理:404异常为何捕获不到?
1573 0
Spring Boot之全局异常处理:404异常为何捕获不到?
|
监控 前端开发 Java
SpringMVC 的三种异常处理方式详解
SpringMVC 的三种异常处理方式详解
185 0
|
前端开发 Java Spring
SpringMVC-异常
SpringMVC-异常
54 0
定义全局异常和全局异常处理器
定义全局异常和全局异常处理器
|
安全 Java 微服务
SpringBoot 中如何优雅地处理异常,包括异常处理机制、全局异常处理器、自定义异常?
SpringBoot 中如何优雅地处理异常,包括异常处理机制、全局异常处理器、自定义异常?
306 0