接上篇【熟练掌握 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 源码
的精髓。