源码解析
版本信息: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(),是一个抽象方法,需要子类去实现
最终我们就来到了 DispatcherServlet 的 doService 方法中,最终可以看到它调用了 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 涉及的断点如下所示