当前位置: 首页 > news >正文

SpringBoot 全局异常处理用法及原理

SpringBoot 全局异常处理用法及原理

Springboot或springMVC项目中, 我们一般会设置一个全局异常处理, 来对异常进行兜底。 业务代码执行过程中抛出的异常, 如果业务逻辑没有主动捕获,那么异常就会一直往上抛,最后进入全局异常处理逻辑。

本文和大家探讨SpringBoot 全局异常处理用法及原理, 整体分为三个部分:第一部分,讲spring如何处理一个http请求异常;第二部分, 讲全局异常处理的几种方法; 第三部分,探究一下全局异常处理的底层原理。

1、springboot如何处理一个http请求异常

SpingBoot中,web请求由DispatcherServlet类的doDispatch方法来处理,如果处理过程抛出了异常,processDispatchResult方法会对异常进行处理。 此处省略了一些无关的代码。

	protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {HttpServletRequest processedRequest = request;HandlerExecutionChain mappedHandler = null;boolean multipartRequestParsed = false;WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);try {ModelAndView mv = null;Exception dispatchException = null;try {processedRequest = checkMultipart(request);multipartRequestParsed = (processedRequest != request);// Determine handler for the current request.mappedHandler = getHandler(processedRequest);if (mappedHandler == null || mappedHandler.getHandler() == null) {noHandlerFound(processedRequest, response);return;}// Determine handler adapter for the current request.HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());// code omitted// Actually invoke the handler.mv = ha.handle(processedRequest, response, mappedHandler.getHandler());if (asyncManager.isConcurrentHandlingStarted()) {return;}applyDefaultViewName(processedRequest, mv);mappedHandler.applyPostHandle(processedRequest, response, mv);}catch (Exception ex) {dispatchException = ex;}catch (Throwable err) {// As of 4.3, we're processing Errors thrown from handler methods as well,// making them available for @ExceptionHandler methods and other scenarios.dispatchException = new NestedServletException("Handler dispatch failed", err);}// this is where thrown exception is handled  处理异常的地方processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);}// code omitted}}

processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException)方法是异常处理的入口,然后委派给processHandlerException(request, response, handler, exception)方法来处理。

	/*** Handle the result of handler selection and handler invocation, which is* either a ModelAndView or an Exception to be resolved to a ModelAndView.* */private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {boolean errorView = false;if (exception != null) {if (exception instanceof ModelAndViewDefiningException) {logger.debug("ModelAndViewDefiningException encountered", exception);mv = ((ModelAndViewDefiningException) exception).getModelAndView();}else {// get handler from handlerchain, in most cases the hanlder is a HanlderMethodObject handler = (mappedHandler != null ? mappedHandler.getHandler() : null);// delegate to this method  委派给processHandlerExceptionmv = processHandlerException(request, response, handler, exception);errorView = (mv != null);}}// code omitted
	protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,Object handler, Exception ex) throws Exception {// Check registered HandlerExceptionResolvers...ModelAndView exMv = null;// 重点代码for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);if (exMv != null) {break;}}if (exMv != null) {if (exMv.isEmpty()) {request.setAttribute(EXCEPTION_ATTRIBUTE, ex);return null;}// We might still need view name translation for a plain error model...if (!exMv.hasView()) {exMv.setViewName(getDefaultViewName(request));}if (logger.isDebugEnabled()) {logger.debug("Handler execution resulted in exception - forwarding to resolved error view: " + exMv, ex);}WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());return exMv;}throw ex;}

可以看出, 这里有一组异常解析器,按照优先级由高到低排列。遍历异常解析器, 逐一调用resolveException方法来处理异常, 异常一旦得到处理,就break,这样就保证了,优先级高的处理器优先处理异常。 关于springboot如何处理http请求异常,了解这么多就够了。

至于这些handlerExceptionResolvers是怎么注册的? 第三部分会解释。

2、自定义全局异常处理的三种方法

2.1 实现HandlerExceptionResolver接口(不推荐)

看过第一部分,我们知道springboot的http请求异常由一组异常解析器来处理, 那么我们自然可以创建自己的异常解析器,然后把它加到现有的解析器中。
Spring已经为我们提供了抽象类AbstractHandlerExceptionResolver(Abstract base class for HandlerExceptionResolver implementations), 我们可以选择继承AbstractHandlerExceptionResolver来创建自己的全局异常解析器。

例如,

@Component  //autowired
public class MyExceptionResolver extends AbstractHandlerExceptionResolver {@Override   //异常解析器的顺序, 数值越小,表示优先级越高public int getOrder() {return -1;}@Override  // write your exception-handle codeprotected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {System.out.println("hello from MyExceptionResolver");if(ex.instanceOf(BusinessException)){// ...// business code// ...}}
}

这里MyExceptionResolver继承了AbstractHandlerExceptionResolver, 并重写了getOrder和doResolveException方法。在doResolveException中, 我根据异常的类型, 做相应的处理。

从图片可以看出来MyExceptionResolver注册进来了, 而且因为让getOrder方法返回了-1, 所以MyExceptionResolver排在了最前面,可以优先处理异常。

但在实际应用中,几乎没见过使用这种方法,这里提出来主要是加深大家对原理的理解。

2.2. @ExceptionHandler + BaseController(不推荐)

第二种方法, 是使用@ExceptionHandler注解 + BaseController。
例如,

@Controller
public class HelloWorldController {private final Logger logger = LoggerFactory.getLogger(HelloWorldController.class);@Autowiredprivate HelloWorldService helloWorldService;@RequestMapping(value = "/hello/{name:.+}", method = RequestMethod.GET)public ModelAndView hello(@PathVariable("name") String name) throws Exception {logger.debug("hello--> {}", name);throw new BusinessException();}@ExceptionHandler({BusinessException.class})  // 这里可以定义要处理的一组异常类型private ModelAndView handleException(Exception e){System.out.println("hello from in  controller");// handle BusinessException}@ExceptionHandler({BusinessException1.class, BusinessException2.class})  // 这里可以定义要处理的异常类型private ModelAndView handleException1(Exception e){System.out.println("hello from in  controller");// handle BusinessException1 and BusinessException2}
}

这里,在HelloWorldController中, 定义了两个由@ExceptionHandler注释的方法来处理异常。如果HelloWorldController的hello方法(或者其他方法)执行过程中, 抛出BusinessException类型的异常, 那么会被handleException方法捕获到; 如果抛出了BusinessException1类型或是BusinessException2类型的异常,则会被handleException1方法捕获到。

通过@ExceptionHandler注解的方式, 我们可以通过让不同的方法处理不同的异常。

但是这种方式是不是意味着, 我们需要在每个controller中都要定义自己@ExceptionHandler方法呢? 如果各自处理自己的异常, 那叫什么全局异常处理呢?

为此, 我们其实可以写一个BaseController, 将全局、通用的异常处理方法写在BaseController里, 需要进行全局异常处理的Controller继承BaseController。而个性化的异常处理,则写在具体的controller里。

最后看一下ExceptionHandler的代码

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {/*** Exceptions handled by the annotated method. If empty, will default to any* exceptions listed in the method argument list.* 是一个数组, 表示要处理的一组异常类型。 如果为空的话, 会处理注解所修饰方法参数代表的异常类型。*/Class<? extends Throwable>[] value() default {};}

方法二, 不如方法三灵活,几乎也没有人用。

2.3 @ExceptionHandler + @ControllerAdvice(推荐)

第三种方法是使用@ExceptionHandler注解 + @ControllerAdvice注解。通过给ExceptionHandler注解传入参数或者给方法添加异常类型的参数,可以让方法处理指定类型的一组异常。

@ControllerAdvice
public class GlabalExceptionHandler {@ExceptionHandler(BusinessException.class)private void handleException(Exception e){System.out.println("hello from glabal exception handler");// handle BusinessException}@ExceptionHandler({BusinessException1.class, BusinessException2.class})  // 这里可以定义要处理的一组异常类型private ModelAndView handleException1(Exception e){System.out.println("hello from glabal exception handler");// handle BusinessException1 and BusinessException2}
}

这里对于所有controller,如果有方法抛出BusinessException类型的异常, 会走到handleException方法; 如果抛出了BusinessException1类型或是BusinessException2类型的异常,则会被handleException1方法处理。

ControllerAdvice提供了灵活的方式, 来指定对哪些controller来进行异常处理。
看一下ControllerAdvice的代码

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {/*** basePackages的别名,功能同下。*/@AliasFor("basePackages")String[] value() default {};/*** 指定一组包名。* 可以指定一组包名。 这些包(包括子包)里的所有controller,都会拥有异常处理的能力*/@AliasFor("value")String[] basePackages() default {};.../**** 指定一组类, controller如果可以赋值给其中任意一个类(controller就是该类或者是该类的子类,对于接口来说也是一样), 则该controller会拥有异常处理的能力。*/Class<?>[] assignableTypes() default {};...}

实际应用中,多采用这种方法。

3、原理

我们进一步讨论第二部分的三种用法的原理。

3.1. 实现HandlerExceptionResolver接口的原理

我们在第一部分中已经提到,在org.springframework.web.servlet.DispatcherServlet#processHandlerException方法中,会有一组异常解析器来解析异常,而且第一部分末尾留下了一个问题:这一组handlerExceptionResolvers是怎么注册的?

/*** This implementation calls {@link #initStrategies}.*/@Overrideprotected void onRefresh(ApplicationContext context) {initStrategies(context);}/*** Initialize the strategy objects that this servlet uses.* <p>May be overridden in subclasses in order to initialize further strategy objects.*/protected void initStrategies(ApplicationContext context) {initMultipartResolver(context);initLocaleResolver(context);initThemeResolver(context);initHandlerMappings(context);initHandlerAdapters(context);//调用初始异常处理器方法initHandlerExceptionResolvers(context);initRequestToViewNameTranslator(context);initViewResolvers(context);initFlashMapManager(context);}
/*** Initialize the HandlerExceptionResolver used by this class.* <p>If no bean is defined with the given name in the BeanFactory for this namespace,* we default to no exception resolver.*/private void initHandlerExceptionResolvers(ApplicationContext context) {this.handlerExceptionResolvers = null;if (this.detectAllHandlerExceptionResolvers) {// Find all HandlerExceptionResolvers in the ApplicationContext, including ancestor contexts.// 最关键一段代码,这里Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);if (!matchingBeans.isEmpty()) {this.handlerExceptionResolvers = new ArrayList<HandlerExceptionResolver>(matchingBeans.values());// We keep HandlerExceptionResolvers in sorted order.AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);}}else {try {HandlerExceptionResolver her =context.getBean(HANDLER_EXCEPTION_RESOLVER_BEAN_NAME, HandlerExceptionResolver.class);this.handlerExceptionResolvers = Collections.singletonList(her);}catch (NoSuchBeanDefinitionException ex) {// Ignore, no HandlerExceptionResolver is fine too.}}// Ensure we have at least some HandlerExceptionResolvers, by registering// default HandlerExceptionResolvers if no other resolvers are found.if (this.handlerExceptionResolvers == null) {this.handlerExceptionResolvers = getDefaultStrategies(context, HandlerExceptionResolver.class);if (logger.isDebugEnabled()) {logger.debug("No HandlerExceptionResolvers found in servlet '" + getServletName() + "': using default");}}}

最终是在initHandlerExceptionResolvers方法完成异常解析器的初始化, 关键代码用中文注释标出。 这里首先会从spring容器中找出所有HandlerExceptionResolver类型(包括子类)的bean, 然后按照order进行排序。这样,我们自定义的异常解析器就会被spring注册并放到指定的位置。

3.2. @ExceptionHandler + Controller、@ExceptionHandler + @ControllerAdvice的原理

我们把方法二和方法三放到一起说。
Spring是依靠自带的ExceptionHandlerExceptionResolver这个异常解析器来支持这两种方式的。DispatcherServlet初始化时,会把ExceptionHandlerExceptionResolver注册到handlerExceptionResolvers中。弄清楚了ExceptionHandlerExceptionResolver就清楚了方法二和方法三的原理。

以下是ExceptionHandlerExceptionResolver的关键代码:

@Override
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,HttpServletResponse response, HandlerMethod handlerMethod, Exception exception) {//这里是关键ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);if (exceptionHandlerMethod == null) {return null;}//省略其它代码...
}

实际调用代码

protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception) {Class<?> handlerType = null;if (handlerMethod != null) {// Local exception handler methods on the controller class itself.// To be invoked through the proxy, even in case of an interface-based proxy.handlerType = handlerMethod.getBeanType();//首先尝试从exceptionHandlerCache里找ExceptionHandlerMethodResolverExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType);if (resolver == null) {resolver = new ExceptionHandlerMethodResolver(handlerType);this.exceptionHandlerCache.put(handlerType, resolver);}Method method = resolver.resolveMethod(exception);if (method != null) {return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method);}// For advice applicability check below (involving base packages, assignable types// and annotation presence), use target class instead of interface-based proxy.if (Proxy.isProxyClass(handlerType)) {handlerType = AopUtils.getTargetClass(handlerMethod.getBean());}}//遍历ControllerAdvice去找异常处理方法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);}}}return null;
}

可以看出来:
首先会从exceptionHandlerCache中去找handlerMethod所属bean的class对应的ExceptionHandlerMethodResolver, 如果找不到则new一个ExceptionHandlerMethodResolver并缓存起来。 然后从ExceptionHandlerMethodResolver去找该exception对应的异常处理方法。
先上一张图,直观感受一下,来自第二部分方法二的例子:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZE6KRKbq-1676102871044)(/img/image-20230211160403740.png)]

ExceptionHandlerMethodResolver中存的是各个Exception到各个异常处理方法映射。

我们再看一下new ExceptionHandlerMethodResolver(handlerType)的实现, 不详细说了, 简单说一下关键点, 直接写在代码注释里。

/*** A constructor that finds {@link ExceptionHandler} methods in the given type.* @param handlerType the type to introspect*/
public ExceptionHandlerMethodResolver(Class<?> handlerType) {//找出handlerType(这里就是对应的Controller)里所有有@ExceptionHandler注解的方法,然后遍历方法for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {//	从一个异常处理方法中提取出处理的异常,然后遍历异常for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {//添加异常到方法的映射addExceptionMapping(exceptionType, method);}}
}
/*** Extract exception mappings from the {@code @ExceptionHandler} annotation first,* and then as a fallback from the method signature itself.*/
@SuppressWarnings("unchecked")
private List<Class<? extends Throwable>> detectExceptionMappings(Method method) {List<Class<? extends Throwable>> result = new ArrayList<Class<? extends Throwable>>();//先从注解中找detectAnnotationExceptionMappings(method, result);if (result.isEmpty()) {// 如果注解中没有, 才会从参数中找。 也就是说, 如果注解中设置了异常的话, 那么异常参数就没有意义。for (Class<?> paramType : method.getParameterTypes()) {if (Throwable.class.isAssignableFrom(paramType)) {result.add((Class<? extends Throwable>) paramType);}}}if (result.isEmpty()) {//	有 @Exceptionhandler但是找不到任何异常类型, 会抛出异常throw new IllegalStateException("No exception types mapped to " + method);}return result;
}
private void addExceptionMapping(Class<? extends Throwable> exceptionType, Method method) {Method oldMethod = this.mappedMethods.put(exceptionType, method);//如果之前已经有该异常类型的映射, 会抛异常。if (oldMethod != null && !oldMethod.equals(method)) {throw new IllegalStateException("Ambiguous @ExceptionHandler method mapped for [" +exceptionType + "]: {" + oldMethod + ", " + method + "}");}
}

看到这里, 相信方法二的原理大家应该清楚了。

接着往下看getExceptionHandlerMethod方法。 简单说就是遍历所有被@ControllerAdvice注解的bean, 如果该bean适用于本Controller,则去匹配异常处理方法。
关键看一下exceptionHandlerAdviceCache是如何初始化的?

private void initExceptionHandlerAdviceCache() {if (getApplicationContext() == null) {return;}if (logger.isDebugEnabled()) {logger.debug("Looking for exception mappings: " + getApplicationContext());}//找出所有被@controllerAdvice注解的bean, 然后排序List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());AnnotationAwareOrderComparator.sort(adviceBeans);for (ControllerAdviceBean adviceBean : adviceBeans) {//遍历bean, 然后解析其中的异常处理方法。 上面已经介绍过了。ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(adviceBean.getBeanType());if (resolver.hasExceptionMappings()) {this.exceptionHandlerAdviceCache.put(adviceBean, resolver);if (logger.isInfoEnabled()) {logger.info("Detected @ExceptionHandler methods in " + adviceBean);}}if (ResponseBodyAdvice.class.isAssignableFrom(adviceBean.getBeanType())) {this.responseBodyAdvice.add(adviceBean);if (logger.isInfoEnabled()) {logger.info("Detected ResponseBodyAdvice implementation in " + adviceBean);}}}
}

写到这里方法三的原理大家也应该明白了吧。

总结一下, 对于某一个controller,如果既有采用方法二配置的异常处理,也有方法三的全局异常配置, 那么方法二优先。也就是说, 如果在controller里已经找到了异常处理方法,则不会再去controlleradvicebean中找。 只有当controller里没有对应的处理方法,才会去 controlleradvicebean找。

http://www.lryc.cn/news/2876.html

相关文章:

  • 浏览器中HTTP请求流程是如何处理的
  • 【Pytorch项目实战】之语义分割:U-Net、UNet++、U2Net
  • 七、插件机制
  • kmp算法
  • 【Python】正则表达式简单教程
  • SAP ABAP Odata
  • Android native ASAN 排查内存泄漏
  • Django项目开发
  • Debezium系列之:深入理解Debezium Server和Debezium Server实际应用案例详解
  • IDE2022源码编译tomcat
  • 214 情人节来袭,电视剧 《点燃我温暖你》李峋同款 Python爱心表白代码,赶紧拿去用吧
  • 数据库范式
  • CUDA中的底层驱动API
  • 【博客616】prometheus staleness对PromQL查询的影响
  • 多传感器融合定位十三-基于图优化的建图方法其二
  • linux 服务器线上问题故障排查
  • Sandman:一款基于NTP协议的红队后门研究工具
  • 【SSL/TLS】准备工作:HTTPS服务器部署:Nginx部署
  • 微搭低代码从入门到精通11-数据模型
  • 【算法基础】前缀和与差分
  • LTD212次升级 | 官网社区支持PC端展示 • 官网新增证件查询应用,支持条形码扫码查询
  • 【安全】nginx反向代理+负载均衡上传webshell
  • 线程池框架
  • 【TCP的拥塞控制】基于窗口的拥塞控制
  • STP协议基础
  • Linux上面配置Apache2支持Https(ssl)具体方案实现
  • [Linux]进程替换
  • 常见的锁策略面试题
  • 设计师一定要知道这几个网站,解决你80%的设计素材。
  • QT基础入门