Spring Boot全局异常处理

在Spring Boot中应用运行时可能会引起各种各样的异常,如果直接让这些异常返回给前端,就会影响用户体验。因此我们可以自定义 全局异常处理 来提前捕获异常,并对返回结果进行处理,让前端收到更加友好的提示。

Spring Boot自定义全局异常处理

首先是一个最简单的ControllerAdvice实例,捕获Exception异常类并对结果进行处理。

@RestControllerAdvice
public class ExceptionControllerAdvice {
	@ExceptionHandler(Exception.class)
	public String exceptionHandler(Exception exception) {
		return "程序运行发生异常";
	}
}
@RestControllerAdvice  
class ExceptionControllerAdvice {  
    @ExceptionHandler(Exception::class)
    fun exceptionHandler(exception: Exception): String {  
        return "程序运行发生异常"
    }  
}

在上面两段代码中,我们首先定义了异常处理的类,并添加了 RestControllerAdvice 注解,表明这是一个用于异常处理的类。然后在里面定义了方法 exceptionHandler ,并添加了 ExceptionHandler 注解,表明这个方法处理 Exception 类型的异常;并在方法的参数中捕获这个异常,并对其进行处理,将结果作为响应返回。

RestControllerAdvice 的定义如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)  
@Documented  
@ControllerAdvice  
@ResponseBody
public @interface RestControllerAdvice {
	// 属性定义省略
}

可以看到, RestControllerAdvice 同时继承了 ControllerAdvice 注解和 ResponseBody 注解,因此在使用时不必额外添加 ResponseBody 注解。

ExceptionHandler 的定义如下:

@Target(ElementType.METHOD)  
@Retention(RetentionPolicy.RUNTIME)  
@Documented  
@Reflective(ExceptionHandlerReflectiveProcessor.class)  
public @interface ExceptionHandler {  
	Class<? extends Throwable>[] value() default {};
}

它的 value 属性的类型是一个数组,意味着我们可以让一个方法处理多个异常类。

结论:对于自定义Spring Boot的全局异常处理,有以下步骤:

  1. 定义异常处理的类,并在类上添加注解 RestControllerAdvice
  2. 定义异常处理的方法,在上面添加注解 ExceptionHandler 并在注解中标注想要捕获的异常类。
  3. 在异常处理方法的参数中捕获异常,对异常进行处理,将处理后的结果返回给前端。

多个ControllerAdvice的执行顺序

考虑以下场景,有一个多模块应用,其中Common模块定义了一个全局异常处理类,里面有一个方法,捕获 Exception 类的异常。另一个Authentication模块又定义了一个全局异常处理类,立面有一个方法,捕获 UsernameNotFoundException 类型的异常。想当然地思考,这两个异常肯定会被分别处理,但是实际情况确是所有的异常处理都走了Common模块的异常处理类,按照 Exception 异常处理逻辑执行,而没有按照 UsernameNotFoundException 异常的处理逻辑执行。这其实就是ControllerAdvice和ExceptionHandler的执行顺序问题。为了解决这个问题,我们可以在异常处理类上面添加 Order 注解,将Authentication模块的异常处理类设置高优先级,代码示例如下:

// UsernameNotFound的异常处理
// 两条注解二选一即可
@Order(Ordered.HIGHEST_PRECEDENCE)
@Priority(Ordered.HIGHEST_PRECEDENCE)
@RestControllerAdvice
public class AuthenticationControllerAdvice {
	@ExceptionHandler(UsernameNotFoundException.class)
	public String exceptionHandler(UsernameNotFoundException exception) {
		return "Username Not Found";
	}
}

// Exception的异常处理
@RestControllerAdvice
public class CommonControllerAdvice {
	@ExceptionHandler(Exception.class)
	public String exceptionHandler(Exception exception) {
		return "Exception";
	}
}
// 两条注解二选一即可
@Order(Ordered.HIGHEST_PRECEDENCE)
@Priority(Ordered.HIGHEST_PRECEDENCE)
@RestControllerAdvice
class AuthenticationControllerAdvice {
	@ExceptionHandler(UsernameNotFoundException::class)
	fun exceptionHandler(exception: UsernameNotFoundException): String {
		return "Username Not Found";
	}
}

// Exception的异常处理
@RestControllerAdvice
class CommonControllerAdvice {
	@ExceptionHandler(Exception::class)
	fun exceptionHandler(exception: Exception): String {
		return "Exception";
	}
}

分析思路

点击 ControllerAdvice ,查看它的使用情况,发现它实际上被一个 ControllerAdviceBean 封装了起来,继续下去,可以看到被保存在了 ExceptionHandlerExceptionResolver 之中,保存的变量定义如下:

public class ExceptionHandlerExceptionResolver {
	private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> exceptionHandlerAdviceCache =  
       new LinkedHashMap<>();
}

这里需要记住, LinkedHashMap 是有序集合,它的Key是ControllerAdviceBean,Value是ExceptionHandlerResolver,保存了这个ControllerAdviceBean所能够处理的所有异常类型和对应的方法。其初始化过程如下:

public class ExceptionHandlerExceptionResolver {
	private void initExceptionHandlerAdviceCache() {
		// 省略无关代码
		// 加载所有标注ControllerAdvice的注解
		List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
		
		// 遍历列表
		for (ControllerAdviceBean adviceBean : adviceBeans) {
			Class<?> beanType = adviceBean.getBeanType();
			
			// 获取其中的异常处理方法,如果里面定义了方法,就添加到缓存中
			ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);  
			if (resolver.hasExceptionMappings()) {  
			    this.exceptionHandlerAdviceCache.put(adviceBean, resolver);  
			}
		}
	}
}

查看 ControllerAdviceBean.findAnnotatedBeans 方法,其定义如下:

public class ControllerAdviceBean implements Ordered {
	public static List<ControllerAdviceBean> findAnnotatedBeans(ApplicationContext context) {
		// 获取BeanFactory过程省略
		List<ControllerAdviceBean> adviceBeans = new ArrayList<>();
		for (String name : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, Object.class)) {  
          // 省略部分代码,这个ControllerAdviceBean的构造方法是关键
          adviceBeans.add(new ControllerAdviceBean(name, beanFactory, controllerAdvice));  
       }  
    }  
}
		OrderComparator.sort(adviceBeans)  // 排序过程在这里
		return adviceBeans;
	}
}

在添加完 adviceBeans 之后对其进行了排序。继续追踪,发现它调用了 OrderComparator.sort 方法,定义如下:

public class OrderComparator implements Comparator<Object> {
	public static void sort(List<?> list) {  
	    if (list.size() > 1) {  
	       list.sort(INSTANCE);  // INSTANCE就是OrderComparator的单例实例
	    }  
	}
	
	// 在Java中,list.sort会调用Comparator的compare方法
	@Override  
	public int compare(@Nullable Object o1, @Nullable Object o2) {  
	    return doCompare(o1, o2, null);  
	}
	
	// doCompare方法的定义,省略无关内容
	private int doCompare(Object o1, Object o2, OrderSourceProvider sourceProvider) {
		int i1 = getOrder(o1, sourceProvider);  
		int i2 = getOrder(o2, sourceProvider);  
		return Integer.compare(i1, i2);
	}
	
	// getOrder方法,由于传入的参数是null,因此我们省略无关内容
	private int getOrder(Object obj, OrderSourceProvider sourceProvider) {
		Integer order = null;
		// 这里省略if判断
		order = findOrder(obj);
		return (order != null ? order : getOrder(obj));
	}
	
	// getOrder的另一个重载
	protected int getOrder(Object obj) {
		if (obj != null) {  
	    Integer order = findOrder(obj);  
		    if (order != null) {  
		       return order;  
		    }  
		}  
		return Ordered.LOWEST_PRECEDENCE;
	}
	
	// 我们可以看到两个getOrder方法最终都调用了findOrder方法
	protected Integer findOrder(Object obj) {  
	    return (obj instanceof Ordered ordered ? ordered.getOrder() : null);  
	}

如果这个Bean是 Ordered 类型的,那么就调用其中的 getOrder 方法获取它的顺序,这样我们就要回到 ControllerAdviceBean 中,这个方法太过复杂,不过幸好作者给我们留下了足够的文档(使用IntelliJ Idea的Download Source功能即可实现)。

在Spring Framework 5.3之后,Bean的加载顺序是通过以下算法(即getOrder方法)获取并缓存起来。但是 ControllerAdvice 被配置为一个具有作用域的Bean(例如每次请求都构造一个新的Bean),它的顺序不会被立即解析。因此使用 Ordered 接口将不会生效。因此就会通过注解来获取顺序,主要是 OrderPriority 这两个注解。注意: Primary 注解不会改变同一个Bean类型的集合中各个Bean的顺序,因此使用 Primary 注解也不会生效。

对于 OrderPrimary 注解,Spring Boot会使用 OrderUtils.getOrder 方法。如果没有定义顺序,则 getOrder 方法将返回最低的优先级。至此,ControllerAdvice的顺序来源已经明确。在这之后,我们需要分析其调用过程。

ExceptionHandlerExceptionResolver.getExceptionHandlerMethod 方法负责根据异常返回对应的ExceptionHandler,第一部分是Controller内部定义的ExceptionHandler,因此我们跳过。直接查看后面的遍历过程:

for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {  
    ControllerAdviceBean advice = entry.getKey();  
    if (advice.isApplicableToBeanType(handlerType)) {  
       ExceptionHandlerMethodResolver resolver = entry.getValue();  
       // 这里是关键
       Method method = resolver.resolveMethod(exception);  
       if (method != null) {  
          return new ServletInvocableHandlerMethod(advice.resolveBean(), method, this.applicationContext);  
       }  
    }  
}

这里一路追踪,最后找到一个名为 resolveMethodByExceptionType 的方法中。这个方法首先从缓存中查找,如果在缓存中没找到,就在之前初始化ControllerAdviceBean中定义的ExceptionHandlerMethodResolver中寻找。这里按照两个异常的深度排序,找出深度最低的一个。这里的 深度最低 指的是和对应异常的类型最为接近,即它的本身会比它的父类更为接近。找到方法之后将其封装好并返回。至此,ControllerAdvice执行顺序问题得到了确定。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

OriginCoding

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值