【SpringBoot2—Web请求处理源码解析】

源码解析

版本信息:SpringBoot 2.6.1

每一部分的最后,我都放了所有断点信息的截图,只要你跟着debug下来,相信你一定收获满满

1. 请求映射原理

1.1 发送过来的请求是如何找到处理方法的

我们只需要在 @RestController 中通过@xxxMapping 注解 SpringBoot就可以自动帮我们找到处理请求的方法,接下来我们就来深入源码看看是怎么做到的
核心关注的方法:doDispatch() org.springframework.web.servlet.DispatcherServlet#doDispatch

在这里插入图片描述

从下面的类图中我们可以看出 DispatcherServlet 本质上也是继承于 HttpServlet,而Servlet 处理http请求的就是 doGet() 和 doPost() 这类方法, 但是 DispatcherServlet 中并没有重写这两个方法,所以我们去他的父类中寻找,在 FrameworkServlet 中找到了重写的方法如下图所示。
在这里插入图片描述

可以看到在 FrameworkServlet 中这四个方法都共同调用了 processRequest() 进行处理
在这里插入图片描述

查看 processRequest() 可以发现最终它调用了 doService(),是一个抽象方法,需要子类去实现
在这里插入图片描述在这里插入图片描述

最终我们就来到了 DispatcherServletdoService 方法中,最终可以看到它调用了 doDispatch() 方法,这也就是为什么我们一开始说,处理方法的请求是 doDispatcher()
在这里插入图片描述
在这里插入图片描述

1.2 请求映射到相应的处理方法

首先我们先写一个 controller 定义一些方法

@RestController
@Slf4j
public class TestController {

    @GetMapping("/user/{id}")
    public void user(@PathVariable Integer id) {
        log.info("查询用户信息id={}", id);
    }

    @PostMapping("/user")
    public void save(@RequestBody User user) {
        log.info("创建用户");
    }

    @PutMapping("/user")
    public void update(@RequestBody User user) {
        log.info("更新用户");
    }

    @DeleteMapping("/user/{id}")
    public void delete(@PathVariable Integer id) {
        log.info("删除用户");
    }
}

接下来我们使用 postman 发起请求,调用第一个查询用户信息的请求,并开启 debug 模式
在这里插入图片描述

关键方法 this.getHandler() , 这个方法内部帮我们找到了处理请求的方法在这里插入图片描述
进入 getHandler() 方法中我们可以看到在这个里面遍历了一个 handlerMappings 的集合
在这里插入图片描述
由于我引入了 actuator 监控,所以我们这边会有几个和端点相关的 handlerMapping,如果只是最基础的项目你看到的第一个应该就是 RequestMappingHandlerMapping (大家最好也把一些没有用的依赖去掉,只保留最基本的 web 依赖就可以,这样后续调试起来也会更方便,后续的过程中我也会去掉 actuator)
在这里插入图片描述
接下来就是通过一个while循环,最终找到能够处理我们请求的就是RequestMappingHandlerMapping,如下图所示我们可以看到这个里面的 mappingRegistry 中就有我们在 contorller 中定义的方法的类型及路径的映射关系
在这里插入图片描述

找到handle的过程具体如下执行 mapping.getHandler()
在这里插入图片描述
接下来执行 getHandlerInternal()
在这里插入图片描述
通过 initLookupPath() 获取到了我们的请求路径是 /user/1
在这里插入图片描述lookupHandlerMethod() 中首先调用 mappingRegistry.getMappingsByDirectPath() 查找是否有和请求相配置的,这个方法的本质就是 map.get(“path”),因为我们的路径是 /user/1 所以完全匹配的方式没有找到匹配的结果
在这里插入图片描述

接下来进入 addMatchingMappings() ,可以看到不论上一步是否有匹配结果,最终调用的都是同一个方法,只不过是一个传入了匹配到的key,另一个传入了所有的 key 然后再次寻找匹配项
在这里插入图片描述
可以看到通过 getMatchingMapping() 最终找到了匹配的 {GET [/user/{id}]} 最终添加到 matches 匹配集合中
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述

接下来可以看到从 matches 中取出第一个元素,认为是最佳匹配
在这里插入图片描述
如果找的多个匹配,最终就会报错
在这里插入图片描述
最终成功找到了处理方法,就是我们自己写的 TestController 中的方法
在这里插入图片描述
然后创建了 resolveBean
在这里插入图片描述
至此完成了查找 handler 的过程
在这里插入图片描述

本次 debug 过程中涉及到的主要的断点
在这里插入图片描述

2. 请求参数处理原理

我们只需要在方法参数上增加一些注解例如 @RequestParam
@RequestBody @PathVariable SpringBoot 就可以自动帮我们确定好这个参数所对应的值,接下来我们就来深入源码看看是怎么做到的
核心关注的方法: org.springframework.web.servlet.HandlerAdapter#handle在这里插入图片描述

还是使用 postman 发起请求,进入 doDispatch() 方法
在第一部分的内容中我们已经获取到了 mappedHandler
接下来就进入到获取请求适配器并执行方法的阶段

在这里插入图片描述
可以看到这里有4个适配器,接下来就是通过一个while循环找到可以处理请求的适配器,这里就和第一部分中 getHandler() 的过程很像了
在这里插入图片描述
调用 supports 方法判断是否可以处理当前请求
在这里插入图片描述
这里使用反射判断是否为 HandlerMethod 的实例,并调用了 supportInternal() 方法,可以看到 RequestMappingHandlerAdapter中这个方法直接返回了true
在这里插入图片描述在这里插入图片描述
找到第一个支持处理该请求的适配器就会结束循环,最终我们找到了可以处理当前请求的 RequestMappingHandlerAdapter
在这里插入图片描述
找到适配器后,接下来会判断是否有缓存,有缓存直接就返回304响应,这是 lastModified 相关的知识,有兴趣的可以自行百度。然后就是去执行前置拦截器,这里不是本次的重点
在这里插入图片描述

接下来就是本次的重点了 handle() 方法
在这里插入图片描述
接下来会调用 handleInternal()
在这里插入图片描述
接下来会调用 checkRequest() 判断当前方法是否可以支持处理,是否需要 session
在这里插入图片描述在这里插入图片描述

接下来就是执行方法 invokeHandlerMethod()
在这里插入图片描述
接下来就是获取到请求参数处理的核心:请求参数解析器 argumentResolvers ,可以看到目前有27个,我简单说明了两个,其他的大家也可以类比一下,大致就可以猜出是用于解析什么形式的参数时使用的。可以处理多少种参数就取决于有多少种请求参数解析器。
在这里插入图片描述
接下来时获取到处理返回值的核心:返回值处理器 returnValueHandlers,目前可以看到有15个,同样的可以处理多少种返回值,就取决于有多少种返回值处理器

在这里插入图片描述

接下来我们直接来到 invokeAndHandle() ,上面都是一些设置属性之类的操作我们就不看了
在这里插入图片描述
接下来就是执行 invokeForRequest() 方法
在这里插入图片描述
接下来执行 getMethodArgumentValues() 获取方法的参数
在这里插入图片描述
接下来调用 getMethodParameters() 获取方法参数的详细信息(此时还没有确定参数的值是什么)。如下图所示,目前我们只有一个参数,详细信息中也可以看到他是由@PathVariable 标注
在这里插入图片描述
如果方法没有任何参数就直接返回,有参数的话,就通过下面的 for 循环确定每个参数的值
在这里插入图片描述

接下来判断参数解析器(之前看到的27个解析器)是否支持解析当前这个参数,调用 this.resolvers.supportsParameter()
在这里插入图片描述在这里插入图片描述
可以看到最终在 getArgumentResolver() 中通过while循环,调用 resolver.supportParameter() 依次判断每一个解析器是否能够解析,找到第一个可以解析的解析器就结束循环。每一个解析器都有自己的判断逻辑,比如判断一下这个参数上标注的注解是什么,参数是什么类型等等,从而判断自己是否能够解析。
注意: 这里会将参数和对应的解析器缓存起来,这样下一次请求就会很快
在这里插入图片描述

接下来就是解析出参数的值,调用 resolvers.resolveArgument()在这里插入图片描述
可以看到这里再次调用了 getArgumentResolver(),和上面调用的是同一个方法,
在这里插入图片描述
不过这一次直接从缓存中就可以拿到,因为在上面判断解析器是否能够解析参数时,就已经就参数对应的解析器存入了缓存中
在这里插入图片描述
拿到解析器后就可以真正调用解析器的 resolver.resolveArgument(),获取到参数的值
在这里插入图片描述
在 resolveArgument() 中,首先获得参数的名称和详细信息,详细信息包括参数上标注的注解、参数类型等等。接下来调用 resolveName() 解析出参数的值
在这里插入图片描述
具体的解析方法就由这些解析器自己实现,对于@PathVariable 的解析器来说,就是把路径参数映射成一个map,然后根据参数名去map中获取
在这里插入图片描述
中间的过程就不需要看了,最终返回了解析出来的值
在这里插入图片描述

解析好所有的参数后就是使用反射调用方法,也就是我们在controller中所写的逻辑
在这里插入图片描述
放行断点,然后就会来到我们所写的方法中
在这里插入图片描述
执行完我们的逻辑之后,接下来就是处理返回值
在这里插入图片描述

至此对于请求参数的解析过程我们就 debug 结束了,本次debug 涉及的断点如下所示
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值