接上篇【熟练掌握spring框架第三篇】

Spring MVC 的工作流程

MVC架构模式

MVC模式是软件工程中的一种软件架构模式,把软件系统分为三个根本局部:模型(Model)、视图(View)和控制器(Controller)
最早是由施乐钻研核心提出的,赫赫有名的AspectJ也是他们提出的。
来自维基百科

那么问题来了,为什么要引入这个模式?
MVC要实现的指标是将软件用户界面和业务逻辑拆散以使代码可扩展性、可复用性、可维护性、灵活性增强。也就是MVC的外围是把M和V离开,C存在的目标则是确保M和V的同步,一旦M扭转,V应该同步更新。传统的mvc架构模式应用模版引擎进行视图的显示。常见的比方jspThymeleaf等。但更为正当的是应用rest服务,进行前后端拆散,前端专一页面渲染,后端专一业务逻辑和数据反对。我认为前后端拆散是MVC架构模式最新的进化成绩。前后端拆散在我看来有如下几点不言而喻的特点:

  1. 拆散之后,前端动态资源文件能够应用cdn减速。
  2. 更易于技术的更新换代。比如说前端想从react技术栈切到vue技术栈,后端想从java切换到ruby
  3. 更易于部署,能够独自部署前端和后端。当然这也减少了部署的复杂度。
  4. 前后端拆散更像是拆分为两个不同的子系统,应用http接口进行通信。所以也会引入一些常见问题,比方接口向下兼容问题。
  5. 更好的用户体验,浏览器只有发送ajax申请进行部分刷新即可。
  6. 一种的服务拆分形式,拆分之后更有利于后端服务的程度扩大。

咱们晓得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_READpoller线程封装一个SocketProcessor,丢给工作线程池就不论了。

AcceptorThread线程干啥的呢。咱们看下这个线程的run办法。理论就是调用ServerSocketChannelaccept办法啦。

当客户端发动申请时。返回一个SocketChannel,而后调用setSocketOptions配置socket,留神这句

配置成非阻塞的,这样poller线程就能够工作了。当然最重要的就是执行了poller.register办法,生成了一个PollerEventpoller线程会去解决这个事件。如果是注册event,理论是执行了SocketChannelregister,将selectorSocketChannel建设关联。

说了这么多想必读着对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,调用每个mappinggetHandler办法,找到了RequestMappingHandlerMapping 拿到handler返回。RequestMappingHandlerMappingWebMvcConfigurationSupport中创立。实现了接口InitializingBean,在bean加载实现后会主动调用afterPropertiesSet办法,在此办法中调用了initHandlerMethods()来实现初始化,RequestMappingHandlerMapping是在DispatcherServlet onRefresh的阶段进行增加进去的。

简略解读下initHandlerMethods

  1. 扫描所有除了ScopedProxybean
  2. 通过是否有Controller或者RequestMapping注解判断是否是Handler,所以不必@Controller注解也是有机会注册Handler的。
  3. 查看是否有HandlerMethod,如果有,注册到mappingRegistry
  4. 判断是否是handlerMethod是通过AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);有没有进行判断的。

咱们再来看看与RequestMappingHandlerMapping对应的RequestMappingHandlerAdapter,这个实例也是在WebMvcConfigurationSupport中创立的。DispatcherServletonRefresh阶段增加进去的。它的afterPropertiesSet办法初始化了所需的argumentResolversreturnValueHandlers

上面以一个简略的例子阐明ArgumentResolver的工作流程。

@GetMapping("/xxxx")public void xxxx(LocalDate birthday) {    //do something      }

下面这个例子中,我想要用birthday承受一个日期类型参数。如果不增加额定配置申请会报错。报错地位如下:

  1. 调用Adapterhandle办法。
  2. HandlerMethodinvoke之前获取办法参数。
  3. 循环每个参数,获取相应的ArgumentResolver 此处匹配到的是RequestParamMethodArgumentResolver,匹配起因详见它的supportsParameter办法。次要是因为birthday的类型是LocalDate属于简略类型。拿到resolver,而后就把工作交给WebDataBinder进行数据绑定了。
  4. 调用conversionService进行转换
  5. 依据原类型和指标类型获取converter 进行转换
  6. 解析失败。抛出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这个beanformatter

那么这个WebConversionService到底是不是下面转换过程中用到的conversionService呢。答案当然是的。咱们看下定义RequestMappingHandlerAdapter的中央。早早的就把这个转换服务给塞到数据绑定初始化器里去了。

RequestParamMethodArgumentResolver说完了,上面再来简略介绍下RequestResponseBodyMethodProcessor

它不仅是ArgumentResolver,也是ReturnValueHandler。咱们先说下它的ArgumentResolver性能。

@PostMapping("/new-stock")public void saveStock(@RequestBody Stock stock) {        System.out.println("保留stock");}

应用@RequestBody承受参数。debug发现。它的ArgumentResolverRequestResponseBodyMethodProcessor它的判断逻辑很简略:

parameter.hasParameterAnnotation(RequestBody.class)

外围办法resolveArgument调用父类AbstractMessageConverterMethodArgumentResolverreadWithMessageConverters,遍历messageConverters,依据http 的content-type匹配到的converterMappingJackson2HttpMessageConverter。这些messageConverters都是在创立RequestMappingHandlerAdapter的时候初始化的。

解决返回值的套路和解决参数的套路很像,都是从一堆的处理器外面找到一个适合的。如果是ResponseBody那么匹配的就是RequestResponseBodyMethodProcessor,解决返回值的外围办法是handleReturnValue,调用父类的writeWithMessageConverters,依然是依据mediaType选中MappingJackson2HttpMessageConverter。进行序列化。并往输入流中写入。最终调用Http11OutputBuffer中的socketWrapperwrite办法进行nio的写入。上面通过写入的调用栈剖析下具体的流程。

  1. jackson向输入流写入数据
  2. 调用tomcat封装的输入流执行flush进行写入
  3. response持有processor的援用是通过一个叫ActionHook的接口进行的。
  4. 调用processoroutputBuffersocketWrapper进行冲刷
  5. 调用socketWrapper封装的socketChannel进行真正的回写。

总结

本篇文章联合了tomcatspring mvc的源码具体的解释了整个rest申请的全过程。因为波及到的代码十分多,所以看上去有点凌乱。读者在浏览的时候能够联合源码细细斟酌。从中能够汲取tomcat源码spring源码的精髓。