接上篇【熟练掌握spring框架第三篇】
Spring MVC 的工作流程
MVC架构模式
MVC模式是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)
最早是由施乐研究中心提出的,大名鼎鼎的AspectJ也是他们提出的。
来自维基百科
那么问题来了,为什么要引入这个模式?
MVC要实现的目标是将软件用户界面和业务逻辑分离以使代码可扩展性、可复用性、可维护性、灵活性加强。也就是MVC的核心是把M和V分开,C存在的目的则是确保M和V的同步,一旦M改变,V应该同步更新。传统的mvc架构模式使用模版引擎进行视图的显示。常见的比如jsp
,Thymeleaf
等。但更为合理的是使用rest服务,进行前后端分离,前端专注页面渲染,后端专注业务逻辑和数据支持。我认为前后端分离是MVC架构模式最新的进化成果。前后端分离在我看来有如下几点显而易见的特点:
- 分离之后,前端静态资源文件可以使用
cdn
加速。 - 更易于技术的更新换代。比如说前端想从
react
技术栈切到vue
技术栈,后端想从java
切换到ruby
- 更易于部署,可以单独部署前端和后端。当然这也增加了部署的复杂度。
- 前后端分离更像是拆分为两个不同的子系统,使用http接口进行通信。所以也会引入一些常见问题,比如接口向下兼容问题。
- 更好的用户体验,浏览器只要发送ajax请求进行局部刷新即可。
- 一种的服务拆分方式,拆分之后更有利于后端服务的水平扩展。
我们知道spring mvc是基于servlet技术的。并且内嵌了一个tomcat容器。
@RestController
public class StockController {
@GetMapping("/my-favorites")
public List<Stock> findMyFavorites() {
return Lists.newArrayList(new Stock("Alphabet Inc", "GOOG"));
}
}
这个例子很简单提供了一个查询我的股票收藏的服务。我们看下它的调用栈。
额... 是不是特别的长!不急,我们一步步解析,抽丝剥茧。
开始是一个线程的run方法。既然是线程,那必定有一个线程池。此处不得不啰嗦一下tomcat
的线程模型了。例子中我使用的是spring boot2.4.2
截止发稿前的官方最新稳定版本。使用的是tomcat 9.0
,那么问题来了,tomcat是如何启动的?tomcat启动了哪些线程分别是干了什么?首先看下tomcat是如何启动的。在spring容器启动的时候,如果是starter-web
,加载的是ServletWebServerApplicationContext
,这个类的createWebServer
会注册一个单例bean
WebServerStartStopLifecycle
,它的start
方法会启动webServer
,默认是TomcatWebServer
。它的start
方法里,StandardService
会添加server.port
这个端口的连接,并且启动它。而这个连接的start
方法里,名为Http11NioProtocol
的协议处理器会去初始化线程。
// 来自 NioEndpoint 的startInternal方法
if (getExecutor() == null) {
createExecutor();
}
initializeConnectionLatch();
// Start poller thread
poller = new Poller();
Thread pollerThread = new Thread(poller, getName() + "-ClientPoller");
pollerThread.setPriority(threadPriority);
pollerThread.setDaemon(true);
pollerThread.start();
startAcceptorThread();
createExecutor
创建工作线程池。corePoolSize
是10,最大个数是200,keepAliveTime是60秒。
poller
线程,这个线程的run方法就是就是轮询注册在selector
上的每个SelectionKey
然后逐个进行处理,由于本机是mac环境,此处的selector
对象名为:KQueueSelectorImpl
是mac os下的nio实现。当我使用postman
发送一个请求时。需要处理的SelectionKey
感兴趣的事件是OP_READ
,已经就绪的事件也是OP_READ
。poller
线程封装一个SocketProcessor
,丢给工作线程池就不管了。
那AcceptorThread
线程干啥的呢。我们看下这个线程的run
方法。实际就是调用ServerSocketChannel
的accept
方法啦。
当客户端发起请求时。返回一个SocketChannel
,然后调用setSocketOptions
配置socket
,注意这句
配置成非阻塞的,这样poller
线程就可以工作了。当然最重要的就是执行了poller.register
方法,生成了一个PollerEvent
,poller
线程会去处理这个事件。如果是注册event
,实际是执行了SocketChannel
的register
,将selector
和SocketChannel
建立关联。
说了这么多想必读着对tomcat的线程模型和这些线程之间是如何协作的已经有了一个很清楚的了解了。总结下来就是tomcat也是使用的java nio
,一个Accept
线程负责等待和接收客户端连接
,poller
线程负责获取就绪的SelectionKey
,交给工作线程,工作线程执行真正的业务逻辑。
既然上面那个长长的调用栈的源头我们说清楚了,那么就开始逐步讲解怎么走到我们的controller
的吧。
DispatcherServlet
无疑是spring mvc
的最重要的角色了。那么他是什么时候生成的呢。它是单例的吗?DispatcherServletAutoConfiguration
给了我们答案。这个位于spring-boot自动配置
模块的自动配置类,一旦检测到classpath
中包含DispatcherServlet
这个类,就会往spring容器中注册一个DispatcherServlet
的bean。并且是单例的。
既然这个类已经到了spring
容器了,那么他和tomcat容器又是如何整合了的呢,下面我结合embed-tomcat-9.0
源码,绘制了一张tomcat工作类图。
TomcatWebServer
启动的时候会把DispatcherServlet
添加ServletContext
这个servlet
容器里面。结合这个类图,我们就可以大概了解了这个内嵌的tomcat
服务器是如何工作的了。当我们的ApplicationFilterChain
调用
servlet.service(request, response);
剩下的工作也就随之交给了DispatcherServlet
。而之前长长的调用栈。也变得非常简单。
对照下源码,我们首先看下获取请求的handler
遍历handlerMappings
,调用每个mapping
的getHandler
方法,找到了RequestMappingHandlerMapping
拿到handler
返回。RequestMappingHandlerMapping
在WebMvcConfigurationSupport
中创建。实现了接口InitializingBean,在bean加载完成后会自动调用afterPropertiesSet
方法,在此方法中调用了initHandlerMethods()
来实现初始化,RequestMappingHandlerMapping
是在DispatcherServlet
onRefresh
的阶段进行添加进去的。
简单解读下initHandlerMethods
- 扫描所有除了
ScopedProxy
的bean
- 通过是否有
Controller
或者RequestMapping
注解判断是否是Handler
,所以不用@Controller
注解也是有机会注册Handler
的。 - 检查是否有
HandlerMethod
,如果有,注册到mappingRegistry
- 判断是否是
handlerMethod
是通过AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
有没有进行判断的。
我们再来看看与RequestMappingHandlerMapping
对应的RequestMappingHandlerAdapter
,这个实例也是在WebMvcConfigurationSupport
中创建的。DispatcherServlet
的onRefresh
阶段添加进去的。它的afterPropertiesSet
方法初始化了所需的argumentResolvers
和returnValueHandlers
下面以一个简单的例子说明ArgumentResolver
的工作流程。
@GetMapping("/xxxx")
public void xxxx(LocalDate birthday) {
//do something
}
上面这个例子中,我想要用birthday
接受一个日期类型参数。如果不添加额外配置请求会报错。报错位置如下:
- 调用
Adapter
的handle
方法。 HandlerMethod
的invoke
之前获取方法参数。- 循环每个参数,获取相应的
ArgumentResolver
此处匹配到的是RequestParamMethodArgumentResolver
,匹配原因详见它的supportsParameter
方法。主要是因为birthday
的类型是LocalDate
属于简单类型。拿到resolver
,然后就把工作交给WebDataBinder
进行数据绑定了。 - 调用
conversionService
进行转换 - 根据原类型和目标类型获取
converter
进行转换 - 解析失败。抛出
DateTimeParseException
。
那怎么解决呢,答案是替换日期类型的格式化器。
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setDateFormatter(DateTimeFormatter.ISO_DATE);
registrar.setTimeFormatter(DateTimeFormatter.ISO_TIME);
registrar.setDateTimeFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
registrar.registerFormatters(registry);
}
}
这样配置实际上是修改WebConversionService
这个bean
的formatter
那么这个WebConversionService
到底是不是上面转换过程中用到的conversionService
呢。答案当然是的。我们看下定义RequestMappingHandlerAdapter
的地方。早早的就把这个转换服务
给塞到数据绑定初始化器
里去了。
RequestParamMethodArgumentResolver
说完了,下面再来简单介绍下RequestResponseBodyMethodProcessor
它不仅是ArgumentResolver
,也是ReturnValueHandler
。我们先说下它的ArgumentResolver
功能。
@PostMapping("/new-stock")
public void saveStock(@RequestBody Stock stock) {
System.out.println("保存stock");
}
使用@RequestBody
接受参数。debug发现。它的ArgumentResolver
是RequestResponseBodyMethodProcessor
它的判断逻辑很简单:
parameter.hasParameterAnnotation(RequestBody.class)
核心方法resolveArgument
调用父类AbstractMessageConverterMethodArgumentResolver
的readWithMessageConverters
,遍历messageConverters
,根据http 的content-type
匹配到的converter
是MappingJackson2HttpMessageConverter
。这些messageConverters
都是在创建RequestMappingHandlerAdapter
的时候初始化的。
处理返回值的套路和处理参数的套路很像,都是从一堆的处理器里面找到一个合适的。如果是ResponseBody
那么匹配的就是RequestResponseBodyMethodProcessor
,处理返回值的核心方法是handleReturnValue
,调用父类的writeWithMessageConverters
,仍然是根据mediaType
选中MappingJackson2HttpMessageConverter
。进行序列化。并往输出流中写入。最终调用Http11OutputBuffer
中的socketWrapper
的write
方法进行nio的写入。下面通过写入的调用栈分析下具体的流程。
- jackson向输出流写入数据
- 调用tomcat封装的输出流执行
flush
进行写入 response
持有processor
的引用是通过一个叫ActionHook
的接口进行的。- 调用
processor
的outputBuffer
的socketWrapper
进行冲刷 - 调用
socketWrapper
封装的socketChannel
进行真正的回写。
总结
本篇文章结合了tomcat
和spring mvc
的源码详细的解释了整个rest请求
的全过程。因为涉及到的代码非常多,所以看上去有点凌乱。读者在阅读的时候可以结合源码细细推敲。从中可以吸取tomcat源码
和spring源码
的精华。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。