熟练掌握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"));
    }
}

这个例子很简单提供了一个查询我的股票收藏的服务。我们看下它的调用栈。

image-20210509135609710

额... 是不是特别的长!不急,我们一步步解析,抽丝剥茧。

开始是一个线程的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的协议处理器会去初始化线程。

image-20210509153749499

// 来自 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

Selector、SelectionKey和Channel 源码联合解析_Nicholas.S的博客-CSDN博客

然后逐个进行处理,由于本机是mac环境,此处的selector对象名为:KQueueSelectorImpl是mac os下的nio实现。当我使用postman发送一个请求时。需要处理的SelectionKey感兴趣的事件是OP_READ,已经就绪的事件也是OP_READpoller线程封装一个SocketProcessor,丢给工作线程池就不管了。

AcceptorThread线程干啥的呢。我们看下这个线程的run方法。实际就是调用ServerSocketChannelaccept方法啦。

image-20210509163600734

当客户端发起请求时。返回一个SocketChannel,然后调用setSocketOptions配置socket,注意这句

image-20210509164024398

配置成非阻塞的,这样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。并且是单例的。

image-20210510165301991

既然这个类已经到了spring容器了,那么他和tomcat容器又是如何整合了的呢,下面我结合embed-tomcat-9.0源码,绘制了一张tomcat工作类图。

image-20210511214155940

TomcatWebServer启动的时候会把DispatcherServlet添加ServletContext 这个servlet容器里面。结合这个类图,我们就可以大概了解了这个内嵌的tomcat服务器是如何工作的了。当我们的ApplicationFilterChain调用

servlet.service(request, response);

剩下的工作也就随之交给了DispatcherServlet。而之前长长的调用栈。也变得非常简单。

image-20210510170633663

对照下源码,我们首先看下获取请求的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接受一个日期类型参数。如果不添加额外配置请求会报错。报错位置如下:

image-20210511154849046
  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

image-20210511162722043

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

image-20210511163920674

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

image-20210511172249743

它不仅是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的时候初始化的。

image-20210511195131680

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

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

总结

本篇文章结合了tomcatspring mvc的源码详细的解释了整个rest请求的全过程。因为涉及到的代码非常多,所以看上去有点凌乱。读者在阅读的时候可以结合源码细细推敲。从中可以吸取tomcat源码spring源码的精华。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容