导航
返回软件工程分类
本篇介绍Java的异常处理机制。
什么是全局异常处理器
软件开发SpringBoot项目过程中,不可避免的需要处理各种异常,Spring MVC架构中各层会出现大量的try{…} catch{…} finally{…}代码块,不仅有大量的冗余代码,而且还影响代码的可读性。这样就需要定义个全局统一异常处理器,以便把面向客户端的异常响应集中处理;业务层仍需负责可以恢复的错误和事务语义。
Spring在3.2版本增加了一个注解@ControllerAdvice,可以与@ExceptionHandler、@InitBinder、@ModelAttribute等注解配套使用。不过跟异常处理相关的只有注解@ExceptionHandler,从字面上看,就是异常处理器的意思。
为什么需要全局异常
不用强制写try-catch,由全局异常处理器统一捕获处理。
自定义异常也可以由局部try-catch或Controller内的@ExceptionHandler处理;全局处理器只是集中转换响应的一种方式。
参数校验可能在Controller方法调用前失败,此时方法体内的try-catch捕获不到。可以通过异常处理器统一响应,也可以在支持的参数后声明BindingResult处理校验结果,见Spring MVC校验文档。
原理和目标
简单的说,@ControllerAdvice注解可以把异常处理器应用到所有控制器,而不是单个控制器。借助该注解,我们可以实现:在独立的某个地方,比如单独的一个类,定义一套对各种异常的处理机制,然后在类的签名加上注解@ControllerAdvice,统一对不同阶段的,不同异常进行处理。这就是统一异常处理的原理。
对异常按阶段进行分类,大体可以分成:进入Controller前的异常和Service层异常
目标是减少重复的响应处理代码,并保留必要的业务校验与局部恢复逻辑。
编码实现全局异常处理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| package com.wliafe.common.exception;
import com.wliafe.common.domain.MyResult; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.http.HttpStatus;
@Slf4j @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(value = RuntimeException.class) @ResponseBody @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public MyResult RuntimeExceptionHandler(RuntimeException e) { log.error("未处理的服务端异常", e); return MyResult.error("服务器内部错误"); }
@ExceptionHandler(value = NullPointerException.class) @ResponseBody @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public MyResult exceptionHandler(NullPointerException e) { log.error("未处理的服务端异常", e); return MyResult.error("服务器内部错误"); }
@ExceptionHandler(value = Exception.class) @ResponseBody @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public MyResult exceptionHandler(Exception e) { log.error("未处理的服务端异常", e); return MyResult.error("服务器内部错误"); } }
|
上面的兜底异常返回HTTP 500,不把内部异常消息暴露给客户端;参数错误和已知业务异常应另写处理器,返回合适的4xx状态和可公开的提示。
处理filter中抛出的异常
简介
@ControllerAdvice不会自动捕获在DispatcherServlet之外抛出的Filter异常。Spring Security认证和授权失败通常应交给AuthenticationEntryPoint、AccessDeniedHandler或认证失败处理器。
分析
我们在过滤器中抛出了异常,抛出异常以后程序不会继续向下执行,也就是请求并没有到达controller层。而全局异常拦截的注解@ControllerAdvice只针对controller层出现的异常才会处理。
解决方法
定义一个过滤器,专门捕获我们过滤器抛出的异常,如果捕获到异常,那么我们就把这个异常传递到指定的controller来处理,并把异常也传递过去,这个controller拿到传递过来的异常,然后再把异常抛出去,这样全局异常处理器就可以感知到这个异常了。
代码实现
定义过滤器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class ExceptionFilter implements Filter {
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { try { filterChain.doFilter(request, response); } catch (UserException e) { e.printStackTrace(); request.setAttribute("filterError", e); request.getRequestDispatcher(CommonConstant.ERROR_CONTROLLER_PATH).forward(request, response); } } }
|
下面的转发仅用于响应尚未提交的情况;转发目标不能再次进入同一段抛错逻辑,否则会循环。UserException、MyResult和ResultCode是项目自定义类型。
定义异常处理controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @RestController public class ExceptionController {
@RequestMapping(CommonConstant.ERROR_CONTROLLER_PATH) public void handleException(HttpServletRequest request){ throw (UserException) request.getAttribute("filterError"); } }
public class CommonConstant {
public static final String ERROR_CONTROLLER_PATH = "/error/throw"; }
|
注册过滤器(顺序必须早于需要捕获异常的目标过滤器,-1只是示例,不保证早于Spring Security过滤器链):
1 2 3 4 5 6 7 8 9
| @Bean public FilterRegistrationBean exceptionFilter() { FilterRegistrationBean registrationBean = new FilterRegistrationBean(); ExceptionFilter filter = new ExceptionFilter(); registrationBean.setFilter(filter); registrationBean.setName("exceptionFilter"); registrationBean.setOrder(-1); return registrationBean; }
|
开测,很幸运的我们拿到了预想的结果
1 2 3 4 5
| { "code": 500, "msg": "用户信息获取异常", "data": null }
|
全局异常处理器的相关代码
1 2 3 4 5 6 7 8 9 10 11
| @RestControllerAdvice public class CustomExceptionHandler {
@ExceptionHandler(UserException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ResultCode HandleException(UserException e) { return ResultCode.fail(e.getCode(), e.getMsg()); } }
|