1. 为什么要统一异常处理
在之前我们会在Controller层会有大量的异常处理代码,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Slf4j @RestController @RequestMapping("/user") public class UserController {
@PostMapping("add") public ResponseEntity<String> add(@Valid @RequestBody UserParam userParam) { try { } catch(Exception e) { return ResponseEntity.fail("error"); } return ResponseEntity.ok("success"); } }
|
但是这种处理方式需要我们编写大量的代码,而且异常信息不易于统一维护,增加了开发工作量,甚至可能还会出现异常没有被捕获的情况。为了能够高效的处理好各种系统异常,我们需要在项目中统一集中处理我们的异常
2. 统一异常处理
在Spring项目中,我们可以通过三种常见的方案来实现全局统一异常处理
基于Springboot的全局统一异常处理,需要实现ErrorController
接口
基于Spring AOP实现全局统一异常处理
基于@ControllerAdvice
注解实现全局统一异常处理
使用统一异常处理的优点:
标准统一的返回结果,系统交互更加友好
有效防止业务异常没有被捕获的情况
代码更加干净整洁,不需要开发者自己定义维护异常
2.1 基于ErrorController实现
通过实现ErrorController
接口,来实现自定义错误异常返回,支持返回JSON字符串、自定义错误页面,可做到根据不同status跳转不同的页面,代码示例如下:
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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
| @Slf4j @RestController @EnableConfigurationProperties({ServerProperties.class}) public class ExceptionController implements ErrorController { private static final String ERROR_PATH = "/error"; private ErrorAttributes errorAttributes; @Autowired private ServerProperties serverProperties;
@Override public String getErrorPath() { return ERROR_PATH; }
@Autowired public ExceptionController(ErrorAttributes errorAttributes) { this.errorAttributes = errorAttributes; }
@RequestMapping(value = ERROR_PATH, produces = "text/html") @ResponseBody public ModelAndView errorHtml404(HttpServletRequest request) { ModelAndView modelAndView; ServletWebRequest requestAttributes = new ServletWebRequest(request); Map<String, Object> model = getErrorAttributes(requestAttributes, isIncludeStackTrace(request)); model.put("queryString", request.getQueryString()); HttpStatus status = getStatus(request); if (status.value() == HttpStatus.INTERNAL_SERVER_ERROR.value()) { modelAndView = new ModelAndView("500", model); } else if (status.value() == HttpStatus.NOT_FOUND.value()) { modelAndView = new ModelAndView("404", model); } else { modelAndView = new ModelAndView("error", model); } return modelAndView; }
@RequestMapping(value = ERROR_PATH) @ResponseBody public ResponseEntity errorApiHandler(HttpServletRequest request) { ServletWebRequest requestAttributes = new ServletWebRequest(request); Map<String, Object> attr = getErrorAttributes(requestAttributes, isIncludeStackTrace(request)); HttpStatus status = getStatus(request); log.error("异常编码:{}", status.value()); return ResponseEntity.badRequest().body(attr.get("message").toString()); }
protected boolean isIncludeStackTrace(HttpServletRequest request) { ErrorProperties.IncludeStacktrace include = this.serverProperties.getError().getIncludeStacktrace(); if (include == ErrorProperties.IncludeStacktrace.ALWAYS) { return true; } return include == ErrorProperties.IncludeStacktrace.ON_TRACE_PARAM && getTraceParameter(request); }
private Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) { return this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace); }
private boolean getTraceParameter(HttpServletRequest request) { String parameter = request.getParameter("trace"); return parameter != null && !"false".equalsIgnoreCase(parameter); }
private HttpStatus getStatus(HttpServletRequest request) { Integer statusCode = (Integer) request .getAttribute("javax.servlet.error.status_code"); if (statusCode == null) { return HttpStatus.INTERNAL_SERVER_ERROR; } try { return HttpStatus.valueOf(statusCode); } catch (Exception ex) { log.error("获取当前 HttpStatus 发生异常", ex); return HttpStatus.INTERNAL_SERVER_ERROR; } } }
|
我们可以根据不同异常状态码跳转不同页面,并且对于非web端请求可以返回JSON谁,但是它无法获取到异常的具体错误代码,同时也无法根据异常类型进行不同的响应。
2.2 基于Spring AOP实现
首先使用@Aspect
来声明一个切面,使用@Pointcut
来定义切入点位置,然后使用@Around
环绕通知来处理方法请求,当请求方法跑出异常后,
使用catch捕获异常并通过handlerException
方法处理异常信息
通过上面的操作我们就可以是心啊异常的统一处理以及通过切面获取接口信息等
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
| @Slf4j @Component @Aspect public class AspectException { @Pointcut("execution(* org.example.demo.controller.*.*(..))") public void pointCut() { }
@Around("pointCut()") public ResponseResult handleControllerMethod(ProceedingJoinPoint pjp) { Stopwatch stopwatch = Stopwatch.createStarted(); ResponseResult r; try { log.info("执行Controller开始: " + pjp.getSignature() + " 参数:" + Lists.newArrayList(pjp.getArgs()).toString()); r = (ResponseResult) pjp.proceed(pjp.getArgs()); log.info("执行Controller结束: " + pjp.getSignature() + ", 返回值:" + r.toString()); log.info("耗时:" + stopwatch.stop().elapsed(TimeUnit.MILLISECONDS) + "(毫秒)."); } catch (Throwable throwable) { r = handlerException(pjp, throwable); } return r; }
private ResponseResult handlerException(ProceedingJoinPoint pjp, Throwable e) { ResponseResult r = null; if (e instanceof BusinessException) { BusinessException businessException = (BusinessException) e; log.error("BusinessException{方法:" + pjp.getSignature() + ", 参数:" + pjp.getArgs() + ",异常:" + businessException.getMessage() + "}", e); r = ResponseResult.fail(businessException.getCode(), businessException.getMessage()); } else if (e instanceof RuntimeException) { log.error("RuntimeException{方法:" + pjp.getSignature() + ", 参数:" + pjp.getArgs() + ",异常:" + e.getMessage() + "}", e); r = ResponseResult.fail("400", "未知异常"); } else { log.error("异常{方法:" + pjp.getSignature() + ", 参数:" + pjp.getArgs() + ",异常:" + e.getMessage() + "}", e); r = ResponseResult.fail("500", "系统异常"); } return r; } }
|
其中ResponseResult
代码可以在Springboot统一接口封装
基于AOP原理实现的异常捕获,可以有效的捕获controller、service中出现的异常,而且可以统一打印接口请求入参和返回结果日志,打印接口访问性能日志等,但是无法处理计入controller前出现的异常以及参数校验异常等情况
2.3 基于@ControllerAdvice实现
基于@ControllerAdvice
注解实现的controller全局统一异常处理,同时还需要配置ExceptionHandler
注解一起使用。
2.3.1 @ControllerAdvice
作用于类上,使用该注解可以实现三个方面的功能
在项目中使用这个注解可以帮我们简化很多工作,他是SpringMVC提供的功能,并且在springboot中也可以使用。
在进行全局异常处理是,需要配合ExceptionHandler
注解使用
2.3.2 @RestControllerAdvice
同样也是作用于类上,它是 @ControllerAdvice 和 @ResponesBody 的合体,可以支持返回 JSON 格式的数据。在后面的代码示例中就会使用这个注解。
2.3.3 @ExceptionHandler
作用于方法上,顾明思议,它就是一个异常处理器,作用是统一处理某一类异常,可以很大程度的减少代码重复率和复杂度。该注解的 value 属性可以用于指定具体的拦截异常类型。
如果有多个 @ExceptionHandler
存在,则需要指定不同的 value
类型,由于异常类拥有继承关系,所以 @ExceptionHandler
会首先执行在继承树中靠前的异常类型。基于这个特性,我们可以使用 @ExceptionHandler
来处理程序中各种具体异常了,比如处理:
ServletException: 即进入 Controller 前的异常,如: NoHandlerFoundException、HttpRequestMethodNotSupportedException、HttpMediaTypeNotSupportedException等
基于特定业务的自定义业务异常,如:BusinessException、BaseException
参数校验异常,如:BindException、 MethodArgumentNotValidException、ConstraintViolationException
未知异常,当上面的异常处理无法捕获某个异常时,统一使用 Throwable 来捕获,并响应为未知异常
2.3.4 统一异常处理类
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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
| @Slf4j @RestControllerAdvice public class UnifiedExceptionHandler {
@ExceptionHandler(value = Throwable.class) public ResponseResult handleException(Throwable t) { log.error("未知异常,{},异常类型:{}", t.getMessage(), t.getClass()); return ResponseResult.fail("500", t.getMessage()); }
@ExceptionHandler(value = BusinessException.class) public ResponseResult handleBusinessException(BusinessException e) { log.error("业务处理异常,{}", e.getMessage(), e); return ResponseResult.fail(e.getCode(), e.getMessage()); }
@ExceptionHandler({ NoHandlerFoundException.class, HttpRequestMethodNotSupportedException.class, HttpMediaTypeNotSupportedException.class, HttpMediaTypeNotAcceptableException.class, MissingPathVariableException.class, MissingServletRequestParameterException.class, TypeMismatchException.class, HttpMessageNotReadableException.class, HttpMessageNotWritableException.class, ServletRequestBindingException.class, ConversionNotSupportedException.class, MissingServletRequestPartException.class, AsyncRequestTimeoutException.class }) @ResponseBody public ResponseResult handleServletException(Exception e) { log.error("网络请求异常,{}", e.getMessage(), e); return ResponseResult.fail(CommonResponseEnum.SERVLET_ERROR); }
@ExceptionHandler(value = {MethodArgumentNotValidException.class}) public ResponseResult handleValidException(MethodArgumentNotValidException e) { log.error("数据校验异常,{},异常类型:{}", e.getMessage(), e.getClass()); BindingResult bindingResult = e.getBindingResult(); Map<String, String> errorMap = getErrorMap(bindingResult); return ResponseResult.fail(CommonResponseEnum.PARAM_ERROR).put("data", errorMap); }
@ExceptionHandler(value = {BindException.class}) public ResponseResult handleValidException(BindException e) { log.error("数据校验异常,{},异常类型:{}", e.getMessage(), e.getClass()); BindingResult bindingResult = e.getBindingResult(); Map<String, String> errorMap = getErrorMap(bindingResult); return ResponseResult.fail(CommonResponseEnum.PARAM_ERROR).put("data", errorMap); }
@ExceptionHandler(value = {ConstraintViolationException.class}) public ResponseResult handleValidException(ConstraintViolationException e) { log.error("数据校验异常,{},异常类型:{}", e.getMessage(), e.getClass()); List<String> violations = e.getConstraintViolations().stream() .map(ConstraintViolation::getMessage).collect(Collectors.toList()); String error = violations.get(0); return ResponseResult.fail(CommonResponseEnum.PARAM_ERROR).put("data", error); }
private Map<String, String> getErrorMap(BindingResult result) { return result.getFieldErrors().stream().collect( Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage, (k1, k2) -> k1) ); }
@InitBinder private void activateDirectFieldAccess(DataBinder dataBinder) { dataBinder.initDirectFieldAccess(); } }
|
从上面的代码中可以看出,在不同异常情况下会响应不同的异常错误码,根据异常错误码我们可以快速定位系统问题
完成上面的工作后,在默认配置情况下,我发现404异常 (NoHandlerFoundException ),不会被统一异常处理器处理,经过翻阅相关资料发现,需要在项目配置文件中增加如下配置:
1 2 3
| spring: mvc: throw-exception-if-no-handler-found: true
|
3. 总结
上面介绍了目前主流的三种全局统一异常处理方案,每种处理方案都有各自的优缺点和适合场景,最后总结:
ErrorController
接口实现类 虽然可以对全局错误进行处理,但是它无法获取到异常的具体错误码,同时也无法根据异常类型进行不同的响应。
使用 AOP 方案不仅可以做全局统一异常处理,还可以统一打印接口请求入参和返回结果日志,打印接口访问性能日志等。但是无法处理进入 controller 前出现的异常以及参数校验异常。
@ControllerAdvice
配合 @ExceptionHandler
一起使用可以捕获全部异常情况,包括ServletException、参数校验异常、自定义业务异常、其他未知异常等,但是在默认情况下无法捕获 404 异常,需要在项目配置中进行额外处理。