所以spring mvc异常处理工作原理是啥

news2024/12/23 18:13:46

文章目录

  • spring mvc异常处理(源码分析)
    • 概述
    • 原理(源码角度)模拟debug
      • 前期提要
      • 分析
      • 4个map
      • 4个map的初始化
      • 为什么需要基于mappedMethods缓存
    • 总结一下

spring mvc异常处理(源码分析)

概述

spring mvc有下面三种方式实现异常处理:

分别是:

  • 实现handlerExceptionResolver+@Component(上古版本)
  • controller里耦合@ExceptionHandler(优先级最高)
  • @ControllerAdvice+@ExceptionHandler(最常用)

1.在对应类实现spring的异常处理核心组件handlerExceptionResolver+@Component(在多个异常执行时的优先级最低,并且麻烦,最早期的异常处理)

在这里插入图片描述

@ExceptionHandler注解在controller方法上(优先级高于ControllerAdvice,但比较麻烦)

在这里插入图片描述

@ControllerAdvice+@ExceptionHandler统一异常处理(最常用)

在这里插入图片描述

原理(源码角度)模拟debug

从源码角度分析,spring mvc是如何进行统一异常处理的?

为了更好的说明问题,我选择用倒推,先去模拟错误发生

前期提要

1.我定义了一个controller,有一个deleteBook方法,在这个方法中会对传来的bookId检查,非法则会抛出一个runtimeException

在这里插入图片描述

2.我用上述三种spring mvc异常处理方法都加在了项目中:如概述的图片所示

3.向deleteBook发送一个id非法的请求。

4.在processHandlerException方法处打上断点。

分析

可以看到这里马上去执行resolveException(这正是spring mvc的resolveException方法,若用上古处理方法,你需要去重写它。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

而在执行resolveException,会依次走更抽象的顶层方法,然后来到核心方法doResolveHandlerMethodException

在这里插入图片描述

doResolveHandlerMethodException方法中:他会干下面的事情

  • 1.根据HandlerMethod(要处理的方法)和exception获取异常处理的Method(先从exceptionHandlerCache查,再从advice中查)

  • 2.设置异常处理方法的参数解析器和返回值解析器(一般就是之前默认的argumentResolvers和returnValueHandlers)

  • 3.执行具体的异常处理方法(也就是1找到的Method)

  • 4.对返回的视图模型进行处理(这个不用太纠结,因为一般不返回视图了,并且一般在@ExceptionHandler标注的方法中我们会对请求处理的,所以一般会返回new ModelAndView(),表示不对视图进行进一步处理)

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

重点就是如何根据HandlerMethod(要处理的方法)和exception获取异常处理的Method

先理解一下exceptionHandlerCache和exceptionHandlerAdviceCache。因为Method就从他们身上获取的。

4个map

exceptionHandlerCache,exceptionHandlerAdviceCache,mappedMethods,基于mappedMethods的缓存

直接看定义

	private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerCache =
			new ConcurrentHashMap<>(64);

	private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> exceptionHandlerAdviceCache =
			new LinkedHashMap<>();

exceptionHandlerCache存储@RequestMapping对应的ExceptionHandlerMethodResolver(就是本例中在@controller里的@ExceptionHandler)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

exceptionHandlerAdviceCache保存了@ControllerAdvice对应的ExceptionHandlerMethodResolver(在本例中就是global那个类)

还有mappedMethods,封装的异常对应的处理方法。和基于它的mappedMethods缓存

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

为什么需要这个缓存后面说

回到如何根据HandlerMethod(要处理的方法)和exception获取异常处理的Method这个问题上来

直接看详细注释:

@Nullable
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(
      @Nullable HandlerMethod handlerMethod, Exception exception) {

   Class<?> handlerType = null;

   if (handlerMethod != null) {
       //0.获取handlerMethod(deleteBook)的类(BookController)
      handlerType = handlerMethod.getBeanType();
       //1.从exceptionHandlerCache查(@Controller里的@ExpectionHandler)查一个resolver
      ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType);
       //2.第一次一般为null,这时会根据handlerType去new一个resolver,若有handlerType对应的异常方法,这时这个resolver的mappedMethods有这个方法
      if (resolver == null) {
         resolver = new ExceptionHandlerMethodResolver(handlerType);
         this.exceptionHandlerCache.put(handlerType, resolver);
      }
       //3.根据异常解析这个方法,先从mappedMethods的缓存中查,查不出到mappedMethods查,然后写入缓存
      Method method = resolver.resolveMethod(exception);
      if (method != null) {
          //4.解析出方法了,在这个ServletInvocableHandlerMethod会回调具体方法
         return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method, this.applicationContext);
      }
      // 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());
      }
   }
	//5.和上面类似,只是遍历所有的exceptionHandlerAdviceCache(@ControllerAdvice),从每一个entry里找出resolver,试图解析出exception对应的方法,然后调用
   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);
         }
      }
   }

   return null;
}

经过测试,在本例中,

若存在三种异常处理方式

第一次执行:走234

第二次执行:走1(exceptionHandlerCache已被写入)

若把@Controller耦合@ExceptionHandler去掉,则走advice的cache(也就是global)。

若把global也去掉,则走最后那个上古版本的resolver,不会到这个getExceptionHandlerMethod方法的。直接执行resolveException了

所以优先级:@Controller耦合@ExceptionHandler > @ControllerAdvice+@ExceptionHandler > 实现handlerExceptionResolver+@Component(上古版本)

4个map的初始化

上面在探究如何根据HandlerMethod(要处理的方法)和exception获取异常处理的Method中,你会好奇这些map什么时候被初始化的

先说结论:

  • exceptionHandlerAdviceCache在ExceptionHandlerExceptionResolver被初始化的过程的afterPropertiesSet方法中赋值

  • exceptionHandlerCache则是在执行异常时,碰到了一场异常要处理了,再去初始化(也就是第二次执行中exceptionHandlerCache被写入)

handlerExceptionResolver作为bean注入容器:

spring mvc通过 WebMvcConfigurationSupport类(configuration类)+@Bean注解的方式来注入handlerExceptionResolver bean

并在上述的#addDefaultHandlerExceptionResolvers方法中,注册了3个处理器:分别处理@ExceptionHandler,@ResponseStatus标注的方法和默认异常解析器。

@Bean
public HandlerExceptionResolver handlerExceptionResolver(
      @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager) {
   List<HandlerExceptionResolver> exceptionResolvers = new ArrayList<>();
    //钩子方法,用于向列表中添加用户自定义的异常处理器。
   configureHandlerExceptionResolvers(exceptionResolvers);
    //向列表中添加Spring MVC的默认异常处理器
   if (exceptionResolvers.isEmpty()) {
      addDefaultHandlerExceptionResolvers(exceptionResolvers, contentNegotiationManager);
   }
    //扩展或修改异常处理器列表
   extendHandlerExceptionResolvers(exceptionResolvers);
    //返回一个HandlerExceptionResolverComposite实例,它是handlerExceptionResolver一种实现
   HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();
   composite.setOrder(0);
   composite.setExceptionResolvers(exceptionResolvers);
   return composite;
}

在addDefaultHandlerExceptionResolvers

protected final void addDefaultHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers,
		ContentNegotiationManager mvcContentNegotiationManager) {
	ExceptionHandlerExceptionResolver exceptionHandlerResolver = createExceptionHandlerExceptionResolver();
	//设置内容协商处理器(确定相应格式),消息转换器(java<->json),
	exceptionHandlerResolver.setContentNegotiationManager(mvcContentNegotiationManager);
	exceptionHandlerResolver.setMessageConverters(getMessageConverters());
	exceptionHandlerResolver.setCustomArgumentResolvers(getArgumentResolvers());
	exceptionHandlerResolver.setCustomReturnValueHandlers(getReturnValueHandlers());
	//若jackson存在,则给ResponseBodyAdvice设置一个JsonViewResponseBodyAdvice实例,用于处理jackson的jsonview
	if (jackson2Present) {
		exceptionHandlerResolver.setResponseBodyAdvice(
				Collections.singletonList(new JsonViewResponseBodyAdvice()));
	}
	if (this.applicationContext != null) {
		exceptionHandlerResolver.setApplicationContext(this.applicationContext);
	}
	//调用afterPropertiesSet,完成初始化
	exceptionHandlerResolver.afterPropertiesSet();
	exceptionResolvers.add(exceptionHandlerResolver);

	ResponseStatusExceptionResolver responseStatusResolver = new ResponseStatusExceptionResolver();
	responseStatusResolver.setMessageSource(this.applicationContext);
	exceptionResolvers.add(responseStatusResolver);

	exceptionResolvers.add(new DefaultHandlerExceptionResolver());
	//添加3个ExceptionResolver:分别处理@ExceptionHandler,@ResponseStatus,默认异常解析器
}

在exceptionHandlerResolver.afterPropertiesSet()中:

1.找到所有@ControllerAdvice注解的类,注册为bean

2.将每一个@ControllerAdvice注解的类与ExceptionHandlerMethodResolver的对应关系 写入exceptionHandlerAdviceCache中。

3.初始化argumentResolvers和returnValueHandlers(一般就是spring默认提供的各种processor和handler)

如returnValueHandlers的HttpEntityMethodProcessor,ModelAndViewMethodReturnValueHandler等等。

如argumentResolvers的**@SessionAttribute**,@RequestAttribute

private void initExceptionHandlerAdviceCache() {
   if (getApplicationContext() == null) {
      return;
   }
   //获取所有@ControllerAdvice的类
   List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
   for (ControllerAdviceBean adviceBean : adviceBeans) {
      Class<?> beanType = adviceBean.getBeanType();
      if (beanType == null) {
         throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
      }
      ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
      if (resolver.hasExceptionMappings()) {
         this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
      }
      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");
      }
   }
}

//RequestMappingHandlerAdapter.java
@Override
public void afterPropertiesSet() {
   // Do this first, it may add ResponseBody advice beans
   initControllerAdviceCache();

   if (this.argumentResolvers == null) {
      List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
      this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
   }
   if (this.initBinderArgumentResolvers == null) {
      List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
      this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
   }
   if (this.returnValueHandlers == null) {
      List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
      this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
   }
}

https://www.cnblogs.com/java-chen-hao/p/11190659.html#_label0

https://blog.csdn.net/qq_26222859/article/details/51320493

https://blog.csdn.net/zzti_erlie/article/details/105746203

为什么需要基于mappedMethods缓存

因为一个异常可能有多个匹配方法,这时需要按照继承优先级对它进行排序,执行最近的那个。

由于最后只执行最近的那个method,那么我们只关心这个method就OK,所以将最近的这个method写入一个map,在每次查询时直接去查这个缓存并返回就好了,避免了排序

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

还是不会写源码分析,小小总结一下

总结一下

  • spring mvc有下面三种方式实现异常处理:

    • 实现handlerExceptionResolver+@Component(上古版本)

    • controller里耦合@ExceptionHandler(优先级最高)

    • @ControllerAdvice+@ExceptionHandler(最常用)

  • 若存在三种异常处理时,优先级为:@Controller耦合@ExceptionHandler > @ControllerAdvice+@ExceptionHandler > 实现handlerExceptionResolver+@Component(上古版本)(执行一次就ok)

    • 为什么:先查ExceptionHandler的cache,尝试解析一个方法,解析不出来再去查advice的cache。
  • spring mvc异常处理如何实现的

    • 当打开webMvcConfiguration后,会注入handlerExceptionResolver(一个bean),这个handlerExceptionResolver会注入@ExceptionHandler,@ResponseStatus,默认异常解析器3个处理器,最值得关注的是ExceptionHandler处理器。
    • 对于ExceptionHandler的处理器,会调用afterPropertiesSet方法完成initExceptionHandlerAdviceCache的注入
    • 当异常发生后,在执行resolveException会一层一层到doResolveHandlerMethodException方法,在这里寻找该异常匹配的方法,并回调。(完成类似aop切面的效果)。
  • 为什么mappedMethods需要缓存

    • 因为一个异常可能有多个匹配方法,这时需要按照继承优先级对它进行排序,执行最近的那个。
    • 由于最后只执行最近的那个method,所以将最近的这个method写入一个map,在每次查询时直接去查这个缓存并返回就好了,避免了排序
      onHandler处理器。
    • 对于ExceptionHandler的处理器,会调用afterPropertiesSet方法完成initExceptionHandlerAdviceCache的注入
    • 当异常发生后,在执行resolveException会一层一层到doResolveHandlerMethodException方法,在这里寻找该异常匹配的方法,并回调。(完成类似aop切面的效果)。
  • 为什么mappedMethods需要缓存

    • 因为一个异常可能有多个匹配方法,这时需要按照继承优先级对它进行排序,执行最近的那个。
    • 由于最后只执行最近的那个method,所以将最近的这个method写入一个map,在每次查询时直接去查这个缓存并返回就好了,避免了排序

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1836139.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ubuntu 18.04 安装vnc

如何在Ubuntu 18.04安装VNC | myfreax sudo apt install xfce4 xfce4-goodies xorg dbus-x11 x11-xserver-utils sudo apt install tigervnc-standalone-server tigervnc-common vncserver sudo apt install xfce4 xfce4-goodies xorg dbus-x11 x11-xserver-utils sudo apt ins…

kotlin数组

1、kotlin中的数组与java数组比较&#xff1a; 2、创建 fun main() {// 值创建val a intArrayOf(1,2,3)// 表达式创建val b IntArray(3){println("it: ${it}")it1}println("a数组&#xff1a;${a.contentToString()}, 长度&#xff1a;${a.size}")prin…

乡村养老服务管理系统的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;医疗人员管理&#xff0c;乡村志愿者管理&#xff0c;文娱活动管理&#xff0c;活动报名管理&#xff0c;医疗保健管理 前台账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;文娱活…

【vue baidu-map】解决更新数据,bm-marker显示不完全问题

实现效果&#xff1a; 问题&#xff1a;切换上面基地tab键&#xff0c;导致地图图标展示不完全&#xff1b;刷新页面就可以正常展示。判断是<bm-marker>标记元素没有动态刷新dom元素引起的问题。 方案&#xff1a;this.$nextTick({}) this.$nextTick(()>{this.equipm…

多网页登录Cookie免登通俗理解

背景&#xff0c;现在有A、B两个系统&#xff0c;其中B是乾坤框架的微前端&#xff0c;里面又有若干可以单独运行的系统C、D、E、F&#xff0c;现在的目标是&#xff0c;如果没有登录过其中任一系统&#xff0c;则需要跳转登录页登录&#xff0c;登录后&#xff0c;所有的A-F都…

threejs材质的贴图(四)

效果 代码实现 import ./style.css import * as THREE from three import { OrbitControls } from three/examples/jsm/controls/OrbitControls.js//相机轨道控制器 import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js"//加载hdr文件作为环境贴…

如何训练AI大模型?熬夜爆肝整理大全

随着人工智能技术的快速发展&#xff0c;大型预训练模型在自然语言处理、计算机视觉、语音识别等领域取得了显著成果。这些模型通过在海量数据上进行预训练&#xff0c;能够捕捉到丰富的特征信息&#xff0c;为各种下游任务提供强大的支持。然而&#xff0c;训练AI大模型面临着…

船舶能源新纪元:智能管理引领绿色航运潮流

在蓝色的大海上&#xff0c;无数船只乘风破浪&#xff0c;为全球的贸易和文化交流贡献着力量。然而&#xff0c;随着环保意识的提升和可持续发展的要求&#xff0c;船舶的能源消耗和排放问题逐渐成为了人们关注的焦点。在这个关键时刻&#xff0c;船舶能源管理系统应运而生&…

山体滑坡监测利器:传感器与智能监测平台的应用

山体滑坡&#xff0c;这一地质灾害的代名词&#xff0c;指的是山坡上的土体或岩体在重力作用下&#xff0c;因自然或人为因素而向下滑动的现象。滑坡具有突发性、隐蔽性、危害性和破坏性等特征&#xff0c;因此&#xff0c;对于山体滑坡的监测工作显得尤为重要。本文将探讨山体…

鸿蒙开发网络管理:【@ohos.request (上传下载)】

上传下载 说明&#xff1a; 本模块首批接口从API version 6开始支持。后续版本的新增接口&#xff0c;采用上角标单独标记接口的起始版本。 导入模块 import request from ohos.request;限制与约束 默认支持https&#xff0c;如果要支持http&#xff0c;需要在config.json里…

C++ 21 之 将成员属性设置为私有

c21将成员属性设置为私有.cpp #include <iostream> using namespace std; #include <string>class person08{ private:string p_name;int p_age;int p_pwd; public:// 设置名字void setName(string name){p_name name;}// 获取名字string getName(){return p_nam…

数据结构习题

第一章 绪论 与数据元素本身的形式、内容、相对位置、个数无关的是数据的 逻辑结构。 第二章 线性表 在一个有127个元素的顺序表中插入一个新元素并保持原来顺序不变&#xff0c;平均要移动的元素个数为 63.5。 n/2 单链表的存储密度 小于1。 创建一个包括n个结点的有序单链…

AI在线免费视频工具2:视频配声音

1、视频配声音 https://deepmind.google/discover/blog/generating-audio-for-video/ https://www.videotosoundeffects.com/ &#xff08;免费在线使用&#xff09;

6-18作业

作业1&#xff1a; mywidget.h #ifndef MYWIDGET_H #define MYWIDGET_H#include <QWidget> #include <QLabel> #include <QMessageBox>QT_BEGIN_NAMESPACE namespace Ui { class myWidget; } QT_END_NAMESPACEclass myWidget : public QWidget {Q_OBJECTpu…

2024/06/18--代码随想录算法8/17| 股票问题

121.买卖股票的最佳时机 力扣链接 动规五部曲 确定dp数组&#xff08;dp table&#xff09;以及下标的含义 dp[i][0] 表示第i天持有股票所得最多现金,dp[i][1] 表示第i天不持有股票所得最多现金确定递推公式 dp[i][0] max(dp[i-1][0], -price[i]) dp[i][1]max(dp[i-1][1], …

高压防触碰预警装置,工期重要还是命重要?

“说了多少遍了&#xff0c;不要在高压线下赶工期”吊车违规施工碰撞到高压线&#xff0c;导致供电线路跳闸停电事故&#xff0c;现场火花四溅及其危险&#xff0c; 高压线路被外力破坏的情况&#xff0c;违规施工、赶工期、视觉盲区导致线路外破等情况&#xff0c;想必大家也…

【小白专用24.6.18】C# SqlSugar:连接数据库实现简单的,增、删、改、查

【小白专用 已验证24.6.18】C# SqlSugar操作MySQL数据库实现增删改查-CSDN博客 通过NuGet包管理器搜索SqlSugar&#xff08;MySql还要安装MySql.Data、Newtonsoft.Json&#xff09;包并安装 SqlSugarClient db new SqlSugarClient(new ConnectionConfig(){ConnectionString …

范式(上)-第一范式(1NF)、第二范式(2NF)、第三范式(3NF)、可用关系模式

一、范式的作用 根据关系模式间属性的数据依赖来评价关系模式的好坏 以下我们将基于函数依赖的范围内来讨论范式 二、范式的定义 1、数据依赖满足一定约束的关系模式是范式 2、范式是符合某一级别的关系模式的集合&#xff0c;关系模式R为第几范式可记为 三、第一范式&am…

【Java】已解决com.mysql.cj.jdbc.exceptions.CommunicationsException异常

文章目录 一、分析问题背景二、可能出错的原因三、错误代码示例四、正确代码示例五、注意事项 已解决com.mysql.cj.jdbc.exceptions.CommunicationsException异常 一、分析问题背景 com.mysql.cj.jdbc.exceptions.CommunicationsException是Java程序在使用MySQL Connector/J与…

Docker(三)-Docker常用命令

1.run run命令执行流程:2.帮助启动类命令 2.1 启动docker systemctl start docker2.2 停止docker systemctl stop docker2.3 重启docker systemctl restart docker2.4查看docker状态 systemctl status docker2.5开机启动 systemctl enable docker2.6查看docker概要信息 …