SpringMVC 源码总结篇

一丶处理器映射器HandlerMapping#

Spring支持我们自己定义HandlerMapping,通过Order注解 可以让我们自己定义的HandlerMapping 在默认的HandlerMapping之前生效

1.我的理解#

定义请求和处理程序对象之间映射的对象——用户请求A/B 那么由哪一个程序去处理请求呢

2.SpringMVC重要的两个处理器映射器#

2.1 BeanNameUrlHandlerMapping#

将URL映射到名称以斜杠(“”)开头的bean,类似于Struts将URL映射到操作名称的方式,支持通配符——用户请求A/B 可能会被A/*处理(也可能会被A/B处理,SpringMVC会进行一个匹配程度的比较从而使用最合适的HandlerMapping)

其实基本上很少用到这个HandlerMapping

2.2 RequestMappingHandlerMapping#

@Controller类中的类型和方法级别@RequestMapping注解创建RequestMappingInfo实例

我们最常使用的方式,类上面使用@Controller 注解,使用@RequestMapping来描述方法对应请求的url

3.上述HandlerMapping怎么维护url和处理程序的关系#

3.1 BeanNameUrlHandlerMapping#

继承自 AbstractUrlHandlerMapping ,AbstractUrlHandlerMapping维护了一张Map,key‘是String也就是url value是Object 也就是url对应的处理程序(可能是Bean的名称,可能是Bean的实例)

3.2 RequestMappingHandlerMapping#

RequestMappingHandlerMapping 继承自AbstractHandlerMethodMapping<RequestMappingInfo>

它使用一个MappingRegistry类型的对象维护url和对应的处理程序

维护了
RequestMappingInfo 到 MappingRegistration的Map 
RequestMappingInfo 到 HandlerMethod 的map
url 到 List<RequestMappingInfo>的map
url 到 List<HandlerMethod>的map
HandlerMethod 到 CorsConfiguration 的map

具体有什么用后面使用到这个类再探讨下

4.RequestMappingHandlerMapping 是怎么初始化MappingRegistry的#

RequestMappingHandlerMapping 继承了AbstractHandlerMethodMapping,AbstractHandlerMethodMapping有实现了InitializingBean,其中afterPropertiesSet会调用initHandlerMethods方法

4.1 getCandidateBeanNames方法#

拿所有候选bean的名称,那么什么样的bean是候选的bean,其实是拿到容器中的所有的bean

4.2 processCandidateBean方法#

从容器中拿到这个bean名称对应的类型,isHandler 在 RequestMappingHandlerMapping 中的实现是

检查类上面是否存在Controller注解 或者 RequestMapping注解

存在Controller 没有 RequestMapping 意味着 方法对应的请求就是方法上注解的内容
没有Controller 但是存在 RequestMapping 而且可以进入这个方法 说明是在容器内的bean 说明是通过其他方式注入的bean

4.3 detectHandlerMethods 方法#

4.3.1 扫描Controller 获取处理请求的方法#

ClassUtils.getUserClass 判断如果是一个CGLIB的代理类那么返回其父类(CGLIB是基于继承的动态代理)
getMappingForMethod方法由RequestMappingHandlerInfo实现 主要逻辑是获取类上面的RequestMappingInfo信息 获取方法上面的RequestMappingInfo信息 将二者合并

其中createRequestMappingInfo方法是 获取RequestMapping注解的内容,如果标注的是PostMapping 那么获取到的RequestMapping 实例中的method 是post
这个功能的实现是通过工具类AnnotatedElementUtils.findMergedAnnotation实现的,该方法支持@AiasFor注解 可以把别名信息进行赋值
4.3.2注册到mappingRegistry#
#
4.3.5.1 根据控制层的bean 和方法 实例化一个HandlerMethod#

HandlerMethod 是SpringMvc用来包装bean对象 和方法 以及方法参数的一个对象

它会把bean 对象 方法,方法的参数包装在一起 方便后续根据请求实现参数映射反射调用方法

validateMethodMapping 方法负责检查是否存在相同的两个接口,如果存在抛出异常,因为无法根据请求拿到对应的方法,接口存在歧义
4.3.5.2 将非通配符类型的url 和对应的RequestMappingInfo关联#

因为可以直接利于map的get方法,通过url 那么对应的RequestMappingInfo

4.3.5.3 将bean和方法通过命名策略命名#

默认策略:

如果存在控制层类叫UserController 方法为login 那么命名时U#login 类名大写部分+#+方法名称

4.3.5.4获取跨域配置,实现HandlerMethod和跨域配置的关联#

4.3.5.5实现RequestMapping 和 MappingRegistration 的关联#
MappingRegistration 包含 RequestMapping HandlerMethod,直接匹配的url集合,和通过命名策略名的名称
实在不知道这个有啥用

5.根据请求获取对应的处理器调用链#

根据【4 RequestMappingHandlerMapping 是怎么初始化MappingRegistry的】 我们知道了 springMvc是如何维护url和处理器的关系

那么SpringMvc是如何根据请求知道,到底要走哪个controller的方法,且哪些拦截器。跨域配置等需要执行呢

DispatcherServlet 根据请求遍历每一个HandlerMapping,调用其getHandler 如果返回不为null视为找到该请求对应的处理器执行链

5.1 根据请求获取处理请求的程序#

上面我们提到一个名词 处理器执行链,其实是将拦截器和处理器映射器进行包装的一个类,那么获取处理器执行链,首先需要获得处理器映射器

可以看到这里如果没有拿到合适从处理器程序 可以返回默认的处理程序,接下来我们看下两种处理器映射器分别怎么拿到处理程序对象的

5.1.1 BeanNameUrlHandlerMapping#

首先根据url直接从handlerMap(url 和 对于控制层bean(也许是beand的名称)的map)如果拿到了说明存在一个bean直接和请求的url完全匹配上了,直接返回这个bean

如果没有找到说明可能是通配符类型的,比如请求的url是A/B,但是控制层是A/*,这时候会循环遍历每一个key,看通配符是否可以匹配上,把可以匹配上的bean名称放入到一个list里面,然后使用一个比较工具找到最合适的bean名称,然后返回对应的bean

5.1.2 RequestMappingHandlerMapping#

RequestMappingHandlerMapping 继承自RequestMappingInfoHandlerMapping,获取处理程序的代码如下

主要逻辑在lookupHandlerMethod方法中,大致逻辑是:

  1. 获取和当前url 直接匹配的RequestMappingInfo 可能是多个

  2. 如果1没有找到直接和url 匹配的 那么遍历所有

  3. 如果1 2 找到了多个合适的RequestMappingInfo 那么比较谁更合适,利用比较器(springmvc提供了比较器)排序

    1. 如果有多个最合适的,抛出异常,表示存在歧义
    2. 反之返回最合适的
  4. 如果本来只有一个匹配那么,直接返回对应的handlerMethod

    注意这里最终都是根据最合适,最匹配的RequestMappingInfo 返回HandlerMethod
    
5.1.2.1 获取和当前url 直接匹配的HandlerMethod#

其实是调用mappingRegistry的getMappingsByUrl方法,实际是调用urlLookup的get方法

urlLookup 是一个 url到List<RequestMappingInfo>的map
RequestMappingInfo,存储了控制层接口方法的url,请求方法,参数,请求头,接口consumes(处理请求的提交类型 content-type)接口的produces(指定接口的返回值类型),还有名称(一般为空白)
5.1.2.2 如果存在多个匹配的RequestMappingInfo 那么比较#

比较就必然存在比较的优先级

如果请求的方法是Head请求(HEAD方法跟GET方法相同,只不过服务器响应时不会返回消息体。一个HEAD请求的响应中,HTTP头中包含的元信息应该和一个GET请求的响应消息相同。这种方法可以用来获取请求中隐含的元信息,而不用传输实体本身。也经常用来测试超链接的有效性、可用性和最近的修改)那么请求方法指定为head的接口获胜

反之依次比较

  1. url(比如A/B/* ,A/* 在请求url是A/B/C的时候前者获胜),
  2. 参数,这里见RequestMapping注解的params
  3. 请求头,这里见RequestMapping注解的headers
  4. 处理请求的提交类型 content-type
  5. 接口的返回值类型
5.1.2.3 根据RequestMappingInfo 拿到对应的HandlerMethod#

从MappingRegistry 里面的mappingLookup 中根据RequestMappingInfo get到HandlerMethod

mappingLookup 是一个RequestMappingInfo  到 HandlerMethod的Map
HandlerMethod 包含了controller bean,方法,参数

至此,我们已经拿到了将url 作为bean名称来处理请求的bean,或者通过RequetMapping注解指定控制层方法处理器请求的HandlerMethod的包装类

5.2将处理请求的程序和拦截器以及跨域配置组合在一起形成处理器执行链条#

所谓的处理器执行联调其实包含两个关键属性:拦截器数组,处理请求的程序

5.2.1 将拦截器和处理请求的程序组装到一起#

5.2.2将跨域配置封装成拦截器,放在拦截器数组第一个位置#
5.2.2.1 如何判断是否是一个预检请求#

请求方式为Options 且 请求头包含了origin,且请求头包含Access-Control-Request-Method

Options请求向服务器询问支持那种请求方式,origin表示请求来源,Access-Control-Request-Method表示后续会发起请求的类型

同源是指,域名,协议,端口相同
跨域则是三者存在任何一个不同
预检请求:预检请求会向服务器确认跨域是否允许,服务返回的响应头里有对应字段Access-Control-Allow-Origin来给浏览器判断:如果允许,浏览器紧接着发送实际请求;不允许,报错并禁止客户端脚本读取响应相关的任何东西。
5.2.2.2跨域配置封装成拦截器#

这里把跨域拦截器 放在第一个位置,保证其先于其他拦截器执行

最后无论是否是预检请求都会进入如下逻辑代码:

二丶处理器适配器#

由于我们可以通过实现Controller,HttpRequestHandler以及使用注解的方式实现控制层,SpringMVC为了适配这多种处理器映射器,衍生出处理器适配器HandlerAdapter

HandlerAdapter接口 存在两个比较关键的方法

// 入参处理器 返回是否可以适配当前处理器映射器
boolean supports(Object handler);
// 入参 请求,响应,处理器 执行方法modelView
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;

1.拦截器前置执行#

上面我们指定,会把拦截器 和 跨域 配置组装成执行链条,在处理器执行之前先进行拦截器的前置

拦截器定义了三个方法

//拦截处理程序的执行。在 HandlerMapping 确定适当的处理程序对象之后,但在 HandlerAdapter 调用处理程序之前调用
boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
    
// 拦截处理程序的执行。在 HandlerAdapter 实际调用处理程序之后,但在 DispatcherServlet 呈现视图之前调用。
void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception
   
//请求处理完成后的回调,即渲染视图后。将在处理程序执行的任何结果上调用,从而允许适当的资源清理。注意:仅当此拦截器的 preHandle 方法已成功完成并返回 true 时才会调用
void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,Exception ex) 


在处理器映射器执行之前,会让所有的拦截器进行preHandle 如果存在任何一个拦截器返回false那么将无法进入处理器映射器执行,且返回false之前的拦截器 会执行afterCompletion

2.找到当前处理器映射器对应的适配器#

2.1适配实现了Controller or HttpRequestHandler 接口的处理器映射器#

这两种,我们很少用,但是看下代码可以加深我们对这种适配器设计的理解,对于这两种都是简单的强转然后调用接口

下列入参handler 其实是将bean 名称映射为url的控制层bean,需要实现两个接口中任意一个才能让springmvc你要如何处理一个请求

其实SpringMvc还存在一个Servlet的是配置,也是强转调用对应方法

2.2适配基于@RequestMapping注解的处理器映射器#

2.2.1 support方法#

RequestMappingHandlerAdapter 实现对基于@RequestMapping注解的控制层方法进行支持,其父类覆盖了supports方法

RequestMappingHandlerAdapter 的supportsInternal 是直接返回true

HandlerMethod是SpringMvc用来包装bean对象 和方法 以及方法参数的一个对象
它会把bean 对象 方法,方法的参数包装在一起 方便后续根据请求实现参数映射反射调用方法
2.2.2 反射调用控制层方法#
SpringMvc 会将HandlerMethod 的执行委托给 InvocableHandlerMethod 执行
  1. 将请求中的内容映射成控制层方法对应的参数

    使用的是InvocableHandlerMethod中的getMethodArgumentValues方法

我们知道jdk8之前是无法获取到参数名称的,jdk8之后要开启一个设置才能拿到参数名字,那么SpringMvc 是怎么拿到参数名字的————字节码技术,直接读class 文件解析字节码获取到方法上定义的参数名称

这里的resolvers 参数解析器 是一个HandlerMethodArgumentResolverComposite 对象

它里面使用list包装了所有参数解析器,解析参数的时候就是找到一个具备解析能力的进行解析,老门面模式了

//HandlerMethodArgumentResolver 参数 解析器 是一个接口 定义了两个方法

//是否具备能力解析这个参数
boolean supportsParameter(MethodParameter parameter);

// 从请求中解析参数 并且返回
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory)

接下来我们分析下 常使用的一些参数解析器

  • RequestHeaderMethodArgumentResolver

    对应解析@RequestHeader注解标注的参数,它支持标注此注解且不为Map类型的参数(如果是Map类型那么会使用 RequestHeaderMapMethodArgumentResolver 处理)

    1.从RequestHeader 注解上拿到标注的参数名称,如果拿不到就分析字节码获取参数名称(这里还支持占位符的方式 获取配置中的内容,比如dev环境 你要拿请求头中的dev,st要拿st 可以卸载配置中,利用占位符拿对应的头)
    2.调用 request.getHeaderValues(参数名称) 拿到请求头中的数据
    3.请求头中的数据是String 类型 如果参数类型不是String 会进行转换,转换失败抛出异常
    //如果require为true 但是没有 抛出异常
    
  • ServletCookieValueMethodArgumentResolver

    对应解析@CookieValue 标注的参数

    逻辑和RequestHeaderMethodArgumentResolver 差不多
    无非是遍历所有的cookie 看注解指定的名称(如果没有指定那么使用参数名称)过来对应的cookie
    如果参数本身就是Cookie类型 那么直接返回符合的Cookie
    如果参数不是Cookie 那么拿出cookie的value并且进行转换
    //如果require为true 但是没有 抛出异常
    
  • ServletRequestMethodArgumentResolver

    支持转换实体类参数类型是ServletRequest,HttpSession,InputStream,Reader,HttpMethod,Locale等类的子类

    逻辑很简单,如果是ServletRequest 会把请求作为参数传入
    如果是HttpSession 从请求拿Session
    如果是InputStream or Reader 从请求拿流 or Reader
    //是否以为这个导入excel 这种类型的请求可以直接使用InputStream 接收即可
    
  • ServletResponseMethodArgumentResolver

    支持转换实体类型 ServletResponse OutputStream,Writer,

    处理逻辑很简单,就是直接返回响应,从响应中获取 OutputStream or Writer
    
    //是否以为这个导出 excel 这种类型的请求可以直接使用 OutputStream 接收即可
    
  • RequestParamMethodArgumentResolver

    • 支持标注了@RequestParam的注解,如果参数是一个Map那么必须指定name属性(@RequestParam(name="name")

    • 如果没有标注@RequestParam注解

      • 如果标注了RequestPart注解那么不处理 会使用对于的解析器处理
      • 支持处理MultipartFile或者MultipartFile的集合 数组
      • 支持解析基本类型和基本类型的包装类型,可以解析枚举,CharSequence,Number,Date,Temporal ,URI,URL,Locale,Class等类型及其子类型, 以及这些类型对应的数组类型
    //还是先获取注解中的name属性,如果没有获取参数名称,
    1.对于 MultipartFile 类型的请求,请求中指定了contentType是以multipart/开头的请求,且参数是 MultipartFile ,MultipartFile数组,MultipartFile集合,解析其中文件(如果指定名称 会获取对应名称的文件)
    2.反之调用 request.getParameterValues(名称)的方式获取参数
    3.进行必要的转换 request.getParameterValues拿到的数据是String 类型 如果参数类型不是String 会进行转换,转换失败抛出异常 //这也意味着 你不能@RequestParam A a 企图让SpringMVC把表单的对象给你转换成对象
    //如果require为true 但是没有 抛出异常 
    
  • RequestResponseBodyMethodProcessor

    这个类比较特殊 它负责@RequestBody的注解参数解析,还负责@ResponseBody的写回,我们这里分析它解析参数的流程

    支持标注了RequestBody类型参数

    1.读取请求body中的数据,利用JackSon 进行序列化,(扫描方法的时候可以拿到参数类型,那怕是泛型类型 也可以转换)但是在转换之前,会调用所有RequestBodyAdvice的前置,读取之后调用所有RequestBodyAdvice的后置(如果这个RequestBodyAdvice 支持当前方法的化),如果读到的是null 会调用其handleEmptyBody
    2.进行参数校验 
    
  • PathVariableMethodArgumentResolver

    支持参数上标注PathVariable的参数解析,如果参数是map 那么必须指定value(路径内容是key:value 形式)

    获取参数名称 如果没有在注解上指定参数名称 那么字节码解析获取参数名称,获取参数应该是类似于字符串切分,但是做法有点看不明白
    

  • RequestPartMethodArgumentResolver

    1. 支持标注 RequestPart注解
    2. 如果没有标注RequestPart 但是标注了@RequestParam 那么不处理
      1. 处理MultipartFile 类型,MultipartFile数组 or 集合
    首先获取参数名称,如果注解没有说明name 那么解析字节码获取参数名称
    如果是文件那么解析文件,反之会走RequestBodyAdvice 的前置,然后用json读取body数据 然后走RequestBodyAdvice后置,如果没有body 走RequestBodyAdvice的handleEmptyBody,
    然后进行参数校验
    
  1. 参数构建完了 执行反射,调用Controller bean 对应的方法

  2. 返回值处理器处理返回值

    返回值处理器接口HandlerMethodReturnValueHandler有两个方法

    //能否处理当前返回类型
    boolean supportsReturnType(MethodParameter returnType);
    
    
    //处理返回值
    //如果ModelAndViewContainer setRequestHandled 设置为true 后续视图解析器不会进行处理
    void handleReturnValue( Object returnValue, MethodParameter returnType,
    			ModelAndViewContainer mavContainer, NativeWebRequest webRequest) 
    
    • RequestResponseBodyMethodProcessor

      最常用,用来处理@ResponseBody标注的接口

      1. 处理标注了@ResponseBody 的接口,可以是类上面标注了,可以是方法上面标注

        对于RestController这种合成注解AnnotatedElementUtils.hasAnnotation返回true

      2. 使用jackSon序列化返回值到请求

        调用ModelAndViewContainer.setRequestHandled为true

        在这之前会走ResponseBodyAdvice的beforeBodyWrite方法,然后序列化数据到响应中

3.拦截器后置执行#

所有拦截器依次执行applyPostHandle

posted @   Cuzzz  阅读(474)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~
点击右上角即可分享
微信分享提示
主题色彩