前言

在平时的 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 的处理来。这种与 ControllerRestController 类似,本质是一样的,所以我们在使用全局异常处理之后可以进行灵活的选择处理。

参考文档

Spring异常处理@ExceptionHandler

web九大组件之---HandlerExceptionResolver异常处理器使用详解【享学Spring MVC】

ExceptionHandlerExceptionResolver类源码解析

SpringBoot源码解析-ExceptionHandler处理异常的原理

SpringMVC源码解析---ExceptionHandler异常处理实现原理

SpringMVC重要注解(二)@ControllerAdvice

springboot优雅实现异常处理的更多相关文章

  1. springboot优雅的异常处理

    springboot全局异常处理 @ControllerAdvice 尽管springboot会对一些异常进行处理,不过对于开发者来说,这还不太便于维护,因此我们需要自己来对异常进行统一的捕获与处理. ...

  2. SpringBoot优雅的全局异常处理

    前言 本篇文章主要介绍的是SpringBoot项目进行全局异常的处理. SpringBoot全局异常准备 说明:如果想直接获取工程那么可以直接跳到底部,通过链接下载工程代码. 开发准备 环境要求 JD ...

  3. springboot 常用的异常处理方式

    springboot常用的异常处理推荐: 一.创建一个异常控制器,并实现ErrorController接口: package com.example.demo.controller; import o ...

  4. Springboot项目统一异常处理

    Springboot项目统一异常处理 一.接口返回值封装 1. 定义Result对象,作为通用返回结果封装 2. 定义CodeMsg对象,作为通用状态码和消息封装 二.定义全局异常类 三.定义异常处理 ...

  5. SpringBoot实战 之 异常处理篇

    在互联网时代,我们所开发的应用大多是直面用户的,程序中的任何一点小疏忽都可能导致用户的流失,而程序出现异常往往又是不可避免的,那该如何减少程序异常对用户体验的影响呢?其实方法很简单,对异常进行捕获,然 ...

  6. SpringBoot中对于异常处理的提供的五种处理方式

    1.自定义错误页面 SpringBoot 默认的处理异常机制:SpringBoot默认的已经提供了一套处理异常的机制.一旦程序中出现了异常,SpringBoot会向/error的url发送请求.在Sp ...

  7. SpringBoot整合全局异常处理&SpringBoot整合定时任务Task&SpringBoot整合异步任务

    ============整合全局异常=========== 1.整合web访问的全局异常 如果不做全局异常处理直接访问如果报错,页面会报错500错误,对于界面的显示非常不友好,因此需要做处理. 全局异 ...

  8. Springboot 优雅停止服务的几种方法

    在使用Springboot的时候,都要涉及到服务的停止和启动,当我们停止服务的时候,很多时候大家都是kill -9 直接把程序进程杀掉,这样程序不会执行优雅的关闭.而且一些没有执行完的程序就会直接退出 ...

  9. SpringBoot优雅地配置日志

    本文主要给大家介绍SpringBoot中如何通过sl4j日志组件优雅地记录日志.其实,我们入门 JAVA 的第一行代码就是一行日志,那你现在还在使用System.out.println("H ...

随机推荐

  1. PTA 列车调度 (25分)

    PTA 列车调度 (25分) [程序实现] #include<bits/stdc++.h> using namespace std; int main(){ int num,n; cin& ...

  2. Java踩坑之List的removeAll方法

    最近在公司写东西,发现List的removeAll方法报错 Demo代码如下: List<Long> ids1 = Arrays.asList(1L, 3L, 2L); List<L ...

  3. 常见的yaml写法-CronJob

    CronJob其实就是在Job的基础上加上了时间调度,我们可以:在给定的时间点运行一个任务,也可以周期性地在给定时间点运行.这个实际上和我们Linux中的crontab就非常类似了.一个CronJob ...

  4. [loj3146]路灯

    显然,能从$l$到$r$当且仅当$[l,r)$中的灯全部都亮,以下不妨令询问的$r$全部减1 当修改节点$x$时,找到包含$x$的极大的灯(除$x$以外)全部都亮的区间$[l,r]$,即令$l_{0} ...

  5. OAuth 2.1 带来了哪些变化

    OAuth 2.1 是 OAuth 2.0 的下一个版本, OAuth 2.1 根据最佳安全实践(BCP), 目前是第18个版本,对 OAuth 2.0 协议进行整合和精简, 移除不安全的授权流程, ...

  6. 使用postman对elasticsearch接口调用

    post 新增 get 查询 put更新 post http://127.0.0.1:9200/index4/type1 {"node":0} { "_index&quo ...

  7. layui增加转圈效果

    var loadix = layer.load(1, {shade: [0.1,'#fff']}); layer.close(loadix);

  8. MemoryMappedFile 在IIS与程序跨程序交互数据的权限问题

    使用IIS 与程序交互时,发布到IIS上获取不到数据提供方的数据(VSF5运行可以获取到数据),MemoryMappefFile基本使用不做介绍 数据方 static void Main(string ...

  9. HDU 6987 - Cycle Binary(找性质+杜教筛)

    题面传送门 首先 mol 一发现场 AC 的 csy 神仙 为什么这题现场这么多人过啊啊啊啊啊啊 继续搬运官方题解( 首先对于题目中的 \(k,P\)​,我们有若存在字符串 \(k,P,P'\)​ 满 ...

  10. Atcoder Grand Contest 013 E - Placing Squares(组合意义转化+矩阵快速幂/代数推导,思维题)

    Atcoder 题面传送门 & 洛谷题面传送门 这是一道难度 Cu 的 AGC E,碰到这种思维题我只能说:not for me,thx 然鹅似乎 ycx 把题看错了? 首先这个平方与乘法比较 ...