springboot优雅实现异常处理
前言
在平时的 API 开发过程中,总会遇到一些错误异常没有捕捉到的情况。那有的小伙伴可能会想,这还不简单么,我在 API 最外层加一个 try...catch
不就完事了。
哈哈哈,没错。这种方法简单粗暴。指北君曾经也是这么干的,但是你转过来想一想,你会在每一个 API 入口,都去做 try...catch
吗?这样不是代码非常丑陋的。小伙伴开始思考,突然灵光一现,说我们实现一个 AOP 来做这事不就完了。没错,使用 AOP 来实现是最佳的选择。
现在就给大家来介绍介绍 Spring Boot
怎么通过注解来实现全局异常处理的。
主角 @ControllerAdvice
和 @ExceptionHandler
我们先来介绍一下今天的主角,分别是 @ControllerAdvice
和 @ExceptionHandler
。
@ControllerAdvice
相当于controller
的切面,主要用于@ExceptionHandler
,@InitBinder
和@ModelAttribute
,使注解标注的方法对每一个controller
都起作用。默认对所有controller
都起作用,当然也可以通过@ControllerAdvice
注解中的一些属性选定符合条件的controller
。
源码如下:
package org.springframework.web.bind.annotation;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
@AliasFor("basePackages")
String[] value() default {};
@AliasFor("value")
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Class<?>[] assignableTypes() default {};
Class<? extends Annotation>[] annotations() default {};
}
@ExceptionHandler
用于异常处理的注解,可以通过value
指定处理哪种类型的异常还可以与@ResponseStatus
搭配使用,处理特定的http
错误。标记的方法入参与返回值都有很大的灵活性,具体可以看注释也可以在后边的深度探究。
源码如下:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
Class<? extends Throwable>[] value() default {};
}
案例分析
今天我们就通过几种案例的方式,来给大家分析分析,怎么通过全局异常处理的方式玩转 Spring Boot 的全局异常处理。
案例一
一般的异常处理,所有的API都需要有相同的异常结构。
exception1
在这种情况下,实现是非常简单的,我们只需要创建 GeneralExceptionHandler
类,用 @ControllerAdvice
注解来注解它,并创建所需的 @ExceptionHandler
,它将处理所有由应用程序抛出的异常,如果它能找到匹配的 @ExceptionHandler
,它将相应地进行转换。
@ControllerAdvice
public class GeneralExceptionHandler {
@ExceptionHandler(Exception.class)
protected ResponseEntity<Error> handleException(Exception ex) {
MyError myError = MyError.builder()
.text(ex.getMessage())
.code(ex.getErrorCode()).build();
return new ResponseEntity(myError,
HttpStatus.valueOf(ex.getErrorCode()));
}
}
案例二
我们有一个API,它需要有一个或多个异常以其他格式处理,与其他应用程序的 API 不同。
exception2
我们可以采取两种方式来实现这种情况。我们可以在 OtherController
内部添加 @ExceptionHandler
来处理 OtherException
,或者为 OtherController
创建新的@ControllerAdvice
,以备我们也想在其他 API 中处理 OtherException
。
在 OtherController
中添加 @ExceptionHandler
来处理 OtherException
的代码示例。
@RestController
@RequestMapping("/other")
public class OtherController {
@ExceptionHandler(OtherException.class)
protected ResponseEntity<Error> handleException(OtherException ex) {
MyOtherError myOtherError = MyOtherError.builder()
.message(ex.getMessage())
.origin("Other API")
.code(ex.getErrorCode()).build();
return new ResponseEntity(myOtherError,
HttpStatus.valueOf(ex.getErrorCode()));
}
}
只针对 OtherController
控制器的 @ControllerAdvice
的代码示例
@ControllerAdvice(assignableTypes = OtherController.class)
public class OtherExceptionHandler {
@ExceptionHandler(OtherException.class)
protected ResponseEntity<Error> handleException(OtherException ex) {
MyOtherError myOtherError = MyOtherError.builder()
.message(ex.getMessage())
.origin("Other API")
.code(ex.getErrorCode()).build();
return new ResponseEntity(myOtherError,
HttpStatus.valueOf(ex.getErrorCode()));
}
}
案例三
与案例二类似,我们有一个 API 需要以不同于应用程序中其他 API 的方式对异常进行格式化,但这次所有的异常都需要进行不同的转换。
exception3
为了实现这个案例,我们将不得不使用两个 @ControllerAdvice
,并加上 @Order
注解的注意事项。因为现在我们需要告诉 Spring
,在处理同一个异常时,哪个 @ControllerAdvice
的优先级更高。如果我们没有指定 @Order
,在启动时,其中一个处理程序将自动注册为更高的顺序,我们的异常处理将变得不可预测。例如,我最近看到一个案例,如果你使用 mvn springboot:run
任务启动一个应用程序,OtherExceptionHandler
是主要的,但是当以jar形式启动时,GeneralExceptionHandler
是主要的。
@ControllerAdvice
public class GeneralExceptionHandler {
@ExceptionHandler(Exception.class)
protected ResponseEntity<Error> handleException(Exception ex) {
MyError myError = MyError.builder()
.text(ex.getMessage())
.code(ex.getErrorCode()).build();
return new ResponseEntity(myError,
HttpStatus.valueOf(ex.getErrorCode()));
}
}
@ControllerAdvice(assignableTypes = OtherController.class)
@Order(Ordered.HIGHEST_PRECEDENCE)
public class OtherExceptionHandler {
@ExceptionHandler(Exception.class)
protected ResponseEntity<Error> handleException(Exception ex) {
MyError myError = MyError.builder()
.message(ex.getMessage())
.origin("Other API")
.code(ex.getErrorCode()).build();
return new ResponseEntity(myError,
HttpStatus.valueOf(ex.getErrorCode()));
}
}
@ExceptionHandler 运行原理分析
ExceptionHandler的初始化
上面讲了通过xml配置的方式或者@ExceptionHandler
注解的方式可以得到ExceptionHandler。是如何做到的呢?
Spring容器初始化阶段,在初始化ExceptionHandlerExceptionResolver的时候,会执行afterPropertiesSet()方法,这是Bean生命周期中的一步。
进一步的初始化Bean。在ExceptionHandlerExceptionResolver的afterPropertiesSet()方法中,会调用initExceptionHandlerAdviceCache()
代码如下:
private void initExceptionHandlerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
// 扫描 @ControllerAdvice 注解的Bean,并进行排序
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
// 遍历 ControllerAdviceBean 数组
for (ControllerAdviceBean adviceBean : adviceBeans) {
Class<?> beanType = adviceBean.getBeanType();
if (beanType == null) {
throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
}
// 扫描该 ControllerAdviceBean 对应的类型
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
// 有 @ExceptionHandler 注解,则添加到 exceptionHandlerAdviceCache 中
if (resolver.hasExceptionMappings()) {
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
}
// 如果该 beanType 类型是 ResponseBodyAdvice 子类,则添加到 responseBodyAdvice 中
if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
this.responseBodyAdvice.add(adviceBean);
}
}
if (logger.isDebugEnabled()) {
int handlerSize = this.exceptionHandlerAdviceCache.size();
int adviceSize = this.responseBodyAdvice.size();
if (handlerSize == 0 && adviceSize == 0) {
logger.debug("ControllerAdvice beans: none");
}
else {
logger.debug("ControllerAdvice beans: " +
handlerSize + " @ExceptionHandler, " + adviceSize + " ResponseBodyAdvice");
}
}
}
首先找到加了注解@ControllerAdvice的Bean
遍历找到的所有的Bean,根据Bean类型构建ExceptionHandlerMethodResolver
对象
ExceptionHandlerMethodResolver(beanType)
方法如下:
public ExceptionHandlerMethodResolver(Class<?> handlerType) {
// 遍历 @ExceptionHandler 注解的方法
for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
// 遍历处理的异常集合
for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
// 添加到 mappedMethods 中
addExceptionMapping(exceptionType, method);
}
}
}
逻辑很简单,就是遍历这个类中的所有的方法,找到加了注解@ExceptionHandler的方法。然后遍历这个方法中的异常类型的映射。也就是该方法可以处理的异常类型。将它添加到mappedMethods当中。key是异常类型,value是对应的异常处理的方法。
这样一个类就对应这一个ExceptionHandlerMethodResolver对象,保存在exceptionHandlerAdviceCache缓存当中。就可以做到不仅一个Controller可以使用了。单例池中结果如下:
之后在DispatcherServlet
初始化的时候,会调用initHandlerExceptionResolvers()
,该方法从spring容器中找HandlerExceptionResolver
类型的Bean,添加到成员变量handlerExceptionResolvers
当中。并排序。
对应xml配置的就更为简单了。
容器初始化的时候,扫描xml配置文件,解析Bean标签,构建SimpleMappingExceptionResolver对象。填充xml中配置的属性。就完成了SimpleMappingExceptionResolver的初始化。
ExceptionHandler的触发时机
在doDispatch
中有一个局部变量Exception dispatchException = null
,用于存储catch到的异常。并在调用processDispatchResult的时候,会将这个局部变量传入。processDispatchResult代码如下:
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
boolean errorView = false;
if (exception != null) {//异常视图处理
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);//执行异常处理
errorView = (mv != null);
}
}
// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
render(mv, request, response);//解析视图 分发结果
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No view rendering, null ModelAndView returned.");
}
}
if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}
if (mappedHandler != null) {
// Exception (if any) is already handled..
mappedHandler.triggerAfterCompletion(request, response, null);//触发拦截器完成处理
}
}
调用processHandlerException来调用异常处理器处理异常,代码如下:
public ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
// 判断是否可以应用
if (shouldApplyTo(request, handler)) {
// 阻止缓存
prepareResponse(ex, response);
// 执行解析异常,返回 ModelAndView 对象 由子类实现
ModelAndView result = doResolveException(request, response, handler, ex);
// 如果 ModelAndView 对象非空,则进行返回
if (result != null) {
// Print debug message when warn logger is not enabled.
if (logger.isDebugEnabled() && (this.warnLogger == null || !this.warnLogger.isWarnEnabled())) {
logger.debug("Resolved [" + ex + "]" + (result.isEmpty() ? "" : " to " + result));
}
// Explicitly configured warn logger in logException method.
// 打印异常日志
logException(ex, request);
}
// 返回 ModelAndView 对象
return result;
}
else {
return null;
}
}
真正的异常处理是调用doResolveException,由子类实现,根据不同类型的HandlerExceptionResolver执行不同的逻辑。
根据上面异常处理的时机,可以得出的结论是异常处理拦截的是视图解析之前的逻辑。也就是从getHandler开始到执行了拦截器后置处理的地方。
总结
经过上述的几个案例,指北君觉得大家应该已经能够轻松应对 Spring Boot 中大部分的全局异常处理的情况。
细心的同学也许会觉得为什么不使用 @RestControllerAdvice
呢?如果是用的 @RestControllerAdvice
注解,它会将数据自动转换成JSON格式,不再需要 ResponseEntity
的处理来。这种与 Controller
和 RestController
类似,本质是一样的,所以我们在使用全局异常处理之后可以进行灵活的选择处理。
参考文档
web九大组件之---HandlerExceptionResolver异常处理器使用详解【享学Spring MVC】
ExceptionHandlerExceptionResolver类源码解析
SpringBoot源码解析-ExceptionHandler处理异常的原理