SpringMVC源码分析

think123 2020-11-11 15:51:57
java 分析 源码 springmvc SegmentFault


强大的DispatcherServlet

还记得在web.xml中配置的DispatcherServlet吗?其实那个就是SpringMVC框架的入口,这也是struts2和springmvc不同点之一,struts2是通过filter的,而springmvc是通过servlet的。看下servlet的结构图
结构图
类图

从上面这张图很明显可以看出DispatcherServlet和Servlet以及Spring的关系。而我们今天的重点就从DispatchServlet说起。

在分析之前我用SpringBoot搭建了一个很简单的后台项目,用于分析。代码如下

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
@AllArgsConstructor
public class User {
private Integer id;
private String name;
private Integer age;
private String address;
public User() {
}
}
/**
* @author generalthink
*/
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping(value = "/{id}",method = RequestMethod.GET)
public User getUser(HttpServletRequest request,@PathVariable Integer id) {
//创建一个user,不走数据库只是为了分析springmvc源码
User user = User.builder()
.id(id)
.age(ThreadLocalRandom.current().nextInt(30))
.name("zzz" + id)
.address("成都市").build();
return user;
}
@RequestMapping(value = "/condition",method = RequestMethod.GET)
public User getByNameOrAge(@RequestParam String name,@RequestParam Integer age) {
User user = User.builder().name(name).age(age).address("成都市").id(2).build();
return user;
}
@PostMapping
public Integer saveUser(@RequestBody User user) {
Integer id = user.getName().hashCode() - user.getAge().hashCode();
return id > 0 ? id : -id;
}
}

这里为了方便调试把关注点更多集中在SpringMVC源码中,所以这里的数据都是伪造的。而且这里的关注点也集中到使用注解的Controller(org.springframework.stereotype.Controller),而不是Controller接口(org.springframework.web.servlet.mvc.Controller),这两者的区别主要在意一个只用标注注解,一个需要实现接口,但是它们都能完成处理请求的基本功能。我们都知道访问servlet的时候默认是访问service方法的,所以我们将断点打在HttpServlet的service方法中,此时查看整个调用栈如下
调用栈
从这里我们也知道了请求时如何从servlet到了DispatcherServlet的,我们先来看一下DispatcherServlet的doDiapatch的方法逻辑,这里把核心逻辑列出来了,把其他的一些非核心逻辑移除了

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
//注意这里放回的是HandlerExecutionChain对象
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
ModelAndView mv = null;
Exception dispatchException = null;
//检查是否存在文件上传
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 根据当前request获取handler,handler中包含了请求url,以及最终定位到的controller以及controller中的方法
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 通过handler获取对应的适配器,主要完成参数解析
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 调用Controller中的方法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}

可以看到核心逻辑其实非常简单,首先检查是不是multipart request,如果是则对当前的request进行一定的封装(提取文件等),然后获取对应的handler(保存了请求url对应的controller以及method以及一系列的Interceptor),然后在通过handler获取到对应的handlerAdapter(参数组装),通过它来进行最终方法的调用

解析multipart

那么是如何解析当前请求是文件上传请求呢?这里直接进入到checkMultipart方法看看是如何解析的:

//我精简了下代码,只提取了核心逻辑
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
return this.multipartResolver.resolveMultipart(request);
}
return request;
}

从这里可以看出通过multipartResolver判断当前请求是否是文件上传请求,如果是则返回MultipartHttpServletRequest(继承自HttpServletRequest).不是则返回原本request对象。
那么问题来了multipartResolver是什么时候初始化的呢?

我们在idea中可以直接将断点定位到multipartResolver属性上,进行请求访问这个时候会发现断点直接进入到了initMultipartResolver方法中,接着跟踪整个调用栈,可以发现调用关系如下:
初始化multipartResovler
图上表明了是在初始化servlet的时候对multipartResolver进行了初始化的。

private void initMultipartResolver(ApplicationContext context) {
//从Spring中获取id为multipartResolver的类
this.multipartResolver = context.getBean("multipartResolver", MultipartResolver.class);
}

MultipartResolver接口有CommonsMultipartResolve以及StandardServletMultipartResolver2种实现,CommonsMultipartResolver接口是依赖于commons-upload组件实现的,而 StandardServletMultipartResolver是依赖于Servlet的part(servlet3才存在)实现的.两者判断是否是文件上传请求的方法isMultipart均是通过判定请求方法是否为post以及content-type头是否包含multipart/来进行判定的。

DispatchServlet初始化了哪些内容

protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context); //初始化multipartResolver
initLocaleResolver(context);//初始化localeResolver
initThemeResolver(context);//初始化themResolver
initHandlerMappings(context);//初始化handerMappings
initHandlerAdapters(context);//初始化handlerAdapters
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);//初始化试图解析器
initFlashMapManager(context);
}

这些初始化的内容都会在后面被逐一使用,这里先有一个印象。

根据请求获取mapperHandler

还是进入到getHander方法中看看到底做了什么?

@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping hm : this.handlerMappings) {
HandlerExecutionChain handler = hm.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}

根据HandlerMapping来查看对应的handler,那么进入到initHandlerMappings方法中查看如何初始化handlerMappings
初始化handlerMappings

其中获取默认的handlerMappings是去spring-webmvc的org.springframework.web.servlet中的DispatcherServlet.properties中查找,文件内容是这样的
DispatcherServlet.properties
因为detechAllhanderMappings默认为true,所以会获取到所有HanderMapping的实现类,来看看它的类图结构是怎样的
HandlerMapping类图
this.handlerMappings的值
这几个HandlerMapping的作用如下:
SimpleUrlHandlerMapping : 允许明确指定URL模式和Handler的映射关系,内部维护了一个urlMap来确定url和handler的关系
BeanNameUrlHandlerMapping: 指定URL和bean名称的映射关系,不常用,我们的关注点也主要集中在RequestMappingHandlerMapping

这里也基本明确了HandlerMapping的作用:帮助DispatcherServlet进行Web请求的URL到具体类的匹配,之所以称为HandlerMapping是因为在SpringMVC中并不局限于
必须使用注解的Controller我们也可以继承Controller接口,也同样可以使用第三方接口,比如Struts2中的Action
RequestMappingHandlerMapping
接着看下getHandler的实现:

@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping hm : this.handlerMappings) {
HandlerExecutionChain handler = hm.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}

返回的handler是HandlerExecutionChain,这其中包含了真实的handler以及拦击器,可以在执行前,执行后,执行完成这三个阶段处理业务逻辑。
RequestMappingHandlerMapping的getHandler的调用逻辑如下:
调用逻辑

会遍历所有Controller的url查看是否有符合条件的match(head,url,produce,consume,method都要满足要求),采用antMatcher的方式来进行url匹配,如果匹配上了则返回对应的handler,否则返回null,如果映射发现有重复的映射(url映射相同,请求方法相同,参数相同,请求头相同,consume相同,produce相同,自定义参数相同),则会抛出异常。

而SimpleUrlHandlerMapping的调用逻辑如下:
SimpleUrlHandlerMapping调用逻辑
其中维护了url到handler的映射,先通过url到urlMap中找对应的handler,如果没有找到则尝试pattenMatch,成功则返回对应的handler,未匹配则返回null。

会发现处理HandlerMapping这里运用了模板方法,在抽象类中定义好了业务逻辑,具体实现只需要实现自己的业务逻辑即可。同时也符合开闭原则,完全是面向接口编程,不得不让人叹服这里的涉及逻辑。

分析到这里的时候我们会发现我们之前定义的Controller明显是符合RequestMappingHandlerMapping的策略的,所以返回的HandlerExecutionChain已经包含了需要访问的方法的全路径了。

关于HandlerAdapter

HandlerMapping会通过HandlerExecutionChain返回一个Object类型的Handler对象,用于Web请求处理,这里的Handler并没有限制具体是什么类型,一般来说任何类型的Handler都可以在
SpringMVC中使用,只要它是用于处理Web请求的处理对象就行。

不过对于DispatcherServlet来说就存在问题了,它无法判断到底使用的是什么类型的Handler,也无法知道是调用Handler的哪个方法来处理请求,为了以同意的方式来调用各种类型的Handler,
DispatcherServlet将不同Handler的调用职责转交给了一个成为HandlerAdapter的角色。

先看一下HandlerAdpter接口的定义

public interface HandlerAdapter {
boolean supports(Object handler);
@Nullable
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
long getLastModified(HttpServletRequest request, Object handler);
}

主要关注supports和handle方法。先看下DispatcherServlethandlerAdapters的初始化过程,和handlerMappings的初始化过程是类似的
初始化HandlerAdapters
接着在看一下HandlerAdapter的类关系
HandlerAdapter类图
同样的,仍然通过合适的策略寻找对应的Adapter,我们主要关注的是RequestMappingHandlerAdapter(其他的用得很少),所以这里就主要讲解它。查看它support的实现代码:
supports方法
上面关于handler的说明中说了其实Object handler实际上是HandlerMethod,所以这里对应的HandlerAdapter就是RequestMappingHandlerAdapter

找到对应的适配器之后,这个时候就可以调用真正的逻辑了。在这之前使用者可以通过拦截器做一些事儿,比如记录日志,打印执行时间等,所以如果想要在执行的方法之前添加一条语句,我们只需要配置自己的拦击器即可。
执行拦截器方法
接下来我们重点分析handle方法,看看它到底会做什么?,先看一下handle方法的执行流程,同样的adapter同样使用了模板方法,先在父类里面定义流程,子类只需要实现逻辑即可,所以这里首先会调用AbstracthandlerMethodAdapter的invokeHadlerMethod方法,其中对HandlerMethod进行了封装。
invokeHandle
invokeAndHandle
我们进入到第一步,看看invokeForRequest方法中主要做了什么
invokeForRequest

发现这个方法的调用逻辑实际上很简单,就是解析参数,然后调用方法。我们来看一下如何进行参数解析的呢?
参数解析
可以看到几乎所有的核心逻辑都集中到了argumentResovlers中去,那么支持的arguementResolver有哪些?又是在哪里初始化的呢?

首先需要定位到这个属性是从哪里过来的,RequestMappingHandlerAdapter实现了InitializingBean,所以在初始化的时候会执行afterPropertiesSet方法,在这其中对arguementResolvers以及returnValueHandlers进行了初始化。
不同的resovler支持的参数解析不一样,比如说有支持HttpServletRequest注入的,有支持HttpServletREsponse注入的还有支持body体注入的等等。
arguementResovler初始化

returnValueHandlers初始化
经过参数解析之后就得到了反射需要的数据了,class,method以及参数,最后通过java的反射api调用即可。
反射调用真实方法

至此,springmvc的整个调用流程基本就清晰了。
但是到了这里问题仍然没有结束,因为我们还不知道参数具体是如何解析的。比如get方式提交的数据?post方式提交的数据?如何转换成对象的?这写问题都还存在,那我们继续研究。
这里我使用postman工具来发起请求,首先访问 Get http://localhost:8080/user/condition?name=zhangsan&age=25,定位到resolveArgument方法
如何获取具体的arguementResolver

接着又执行revolver.resolveArgument方法,同样的这里还是使用的模板方法,在抽象类AbstractNamedValueMethodArgumentResolver中定义流程,各个子类只需要实现自己的逻辑即可。RequestParamMethodArgumentResolver的参数就是通过request.getParameter来获取到的。获取到了参数之后就执行反射调用,这个时候就执行了我们写的UserController的对应方法,获取到了User对象,接下来就是处理返回值了,通过returnValueHandlers进行处理
处理返回值

handler会根据返回的类型对数据进行处理,比如说这里就通过response向请求方输出数据,输出数据也是通过messageConverter来实现的
处理数据输出
最后获取ModalAndView对象,但是这里由于没有modalAndView所以返回的null.最后在DispatcherServlet的processDispatchResult方法的调用逻辑如下
最后的处理

么对于这样的请求又时如何解析的呢?

@PostMapping
public Integer saveUser(@RequestBody User user) {
Integer id = user.getName().hashCode() - user.getAge().hashCode();
return id > 0 ? id : -id;
}

同样我们聚焦在解析参数的时候,在上一个get请求的示例中我说了会先访问AbstractNamedValueMethodArgumentResolver,但是在处理@RequestBody的参数中它使用的是RequestResponseBodyMethodProcessor,它复写了resolveArgument方法。所以不会去执行父类的逻辑。

参数解析

这里最后会定位到jakson的objectMapper中, 在spring boot中,默认使用Jackson来实现java对象到json格式的序列化与反序列化。当然是可以配置messageConvert的,只需要实现Spring的HttpMessageConverter即可。

源码分析到这里就结束了,当然其中还存在一些没有讲的地方,比如View的渲染呀,一般视图是多种多样的,又html,xml,jsp等等,所以springmvc也提供了接口供用户选择自己需要的模板,只需要实现ViewResolver接口即可。还有关于Theme,MessageResource,Exception的处理等等,如果铺开来讲篇幅实在是太长了,我更相信掌握了核心流程看其他的处理就会很简单了,所以这里也就不对其他枝节内容做分析了。

一图胜千言

SpringMVC流程图

版权声明
本文为[think123]所创,转载请带上原文链接,感谢
https://segmentfault.com/a/1190000037800265

  1. 【计算机网络 12(1),尚学堂马士兵Java视频教程
  2. 【程序猿历程,史上最全的Java面试题集锦在这里
  3. 【程序猿历程(1),Javaweb视频教程百度云
  4. Notes on MySQL 45 lectures (1-7)
  5. [computer network 12 (1), Shang Xuetang Ma soldier java video tutorial
  6. The most complete collection of Java interview questions in history is here
  7. [process of program ape (1), JavaWeb video tutorial, baidu cloud
  8. Notes on MySQL 45 lectures (1-7)
  9. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  10. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  11. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  12. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  13. 【递归,Java传智播客笔记
  14. [recursion, Java intelligence podcast notes
  15. [adhere to painting for 386 days] the beginning of spring of 24 solar terms
  16. K8S系列第八篇(Service、EndPoints以及高可用kubeadm部署)
  17. K8s Series Part 8 (service, endpoints and high availability kubeadm deployment)
  18. 【重识 HTML (3),350道Java面试真题分享
  19. 【重识 HTML (2),Java并发编程必会的多线程你竟然还不会
  20. 【重识 HTML (1),二本Java小菜鸟4面字节跳动被秒成渣渣
  21. [re recognize HTML (3) and share 350 real Java interview questions
  22. [re recognize HTML (2). Multithreading is a must for Java Concurrent Programming. How dare you not
  23. [re recognize HTML (1), two Java rookies' 4-sided bytes beat and become slag in seconds
  24. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  25. RPC 1: how to develop RPC framework from scratch
  26. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  27. RPC 1: how to develop RPC framework from scratch
  28. 一次性捋清楚吧,对乱糟糟的,Spring事务扩展机制
  29. 一文彻底弄懂如何选择抽象类还是接口,连续四年百度Java岗必问面试题
  30. Redis常用命令
  31. 一双拖鞋引发的血案,狂神说Java系列笔记
  32. 一、mysql基础安装
  33. 一位程序员的独白:尽管我一生坎坷,Java框架面试基础
  34. Clear it all at once. For the messy, spring transaction extension mechanism
  35. A thorough understanding of how to choose abstract classes or interfaces, baidu Java post must ask interview questions for four consecutive years
  36. Redis common commands
  37. A pair of slippers triggered the murder, crazy God said java series notes
  38. 1、 MySQL basic installation
  39. Monologue of a programmer: despite my ups and downs in my life, Java framework is the foundation of interview
  40. 【大厂面试】三面三问Spring循环依赖,请一定要把这篇看完(建议收藏)
  41. 一线互联网企业中,springboot入门项目
  42. 一篇文带你入门SSM框架Spring开发,帮你快速拿Offer
  43. 【面试资料】Java全集、微服务、大数据、数据结构与算法、机器学习知识最全总结,283页pdf
  44. 【leetcode刷题】24.数组中重复的数字——Java版
  45. 【leetcode刷题】23.对称二叉树——Java版
  46. 【leetcode刷题】22.二叉树的中序遍历——Java版
  47. 【leetcode刷题】21.三数之和——Java版
  48. 【leetcode刷题】20.最长回文子串——Java版
  49. 【leetcode刷题】19.回文链表——Java版
  50. 【leetcode刷题】18.反转链表——Java版
  51. 【leetcode刷题】17.相交链表——Java&python版
  52. 【leetcode刷题】16.环形链表——Java版
  53. 【leetcode刷题】15.汉明距离——Java版
  54. 【leetcode刷题】14.找到所有数组中消失的数字——Java版
  55. 【leetcode刷题】13.比特位计数——Java版
  56. oracle控制用户权限命令
  57. 三年Java开发,继阿里,鲁班二期Java架构师
  58. Oracle必须要启动的服务
  59. 万字长文!深入剖析HashMap,Java基础笔试题大全带答案
  60. 一问Kafka就心慌?我却凭着这份,图灵学院vip课程百度云