接上篇【熟练掌握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容器。
@RestControllerpublic 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 threadpoller = 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
。
那怎么解决呢,答案是替换日期类型的格式化器。
@Configurationpublic 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源码
的精髓。