乐趣区

关于java:熟练掌握spring框架第四篇

接上篇【熟练掌握 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 容器。

@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_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

那怎么解决呢,答案是替换日期类型的格式化器。

@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 这个 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 源码 的精髓。

退出移动版