暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

SpringBoot2.x嵌入式Servlet容器的配置和启动原理

糖爸的架构师之路 2021-06-24
987

写在前面

大家还记得在没有SpringBoot之前我们是如何配置项目的么?在引入Spring对应的Maven依赖后,首先需要在web.xml中注册对应的过滤器,监听器以及Servlet(例如SpringMVC中的DispatcherServlet),在Spring的主配置文件中(通常命名为application.xml)注册容器初始化时需要加载的Bean、AOP、拦截器、视图解析器,注解驱动、静态资源换访问以及其他中间件依赖Spring的相关配置等等一大堆,项目构建好后,在服务器安装Tomcat,并将构建好的war包放到对应的目录下,最后通过启动脚本启动Tomcat。这一整套流程下来其实是非常繁琐的,不但容易出错,而且随着项目体积的膨胀,也非常不便于维护。Spring官方也意识到这个问题,于是就有了后来的SpringBoot。

SpringBoot和传统的Spring项目相比优点如下:

  • 快速整合第三方框架,比如redis,mybatis等等
  • 全部采用注解方式,没有繁琐的xml配置
  • 直接嵌入Tomcat、Jetty和Undertow服务器,不需要单独额外的下载集成工作

  • 提供生产就绪功能。例如指标,健康检查和外部化配置。指标和监控检查可以很方便的帮助运维人员在运维期间监控项目运行情况;外部化配置可以很方便的让运维人员快速、方便的外部化配置和部署工作。

今天我们就上述的第三点——嵌入式Servlet进行深入的探究,本文基于SpringBoot最新的GA版本2.4.2进行讨论。官方地址如下:

https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-embedded-container


Web原生组件注入(Servlet、Filter、Listener)

由于SpringBoot默认是以jar包的方式启动,不存在web.xml文件,所以按照之前传统Spring项目配置原生组件的方式是行不通的。这里我们可以参考官方文档,Spring官方给我们提供了两种注册的方式:


通过RegistrationBean方式配置

如果要使用RegistrationBean方式配置,需要两步操作

  • 定义Servlet、Filter和Listener

  • 将定义的Web原生组件注册到Spring容器中,这里就使用到文档中提到的

    • ServletRegistrationBean
    • FilterRegistrationBean
    • ServletListenerRegistrationBean

可以使用配置类的方式将对应的Web原生组件加载的Spring容器中,代码如下:

其中Servlet和Filter需要针对一个或多个路径进行处理,所以在初始化的时候可以通过构造方法来传入想要匹配的路径。


通过Servlet API方式配置

这种方式就更加简单了,根据文档中的描述,我们只需要通过基于Servlet3.0以上规范提供的注解——@WebServlet、@WebFilter、@WebListener来标识我们要注入的Web原生组件就可以,所以我们可以将注册方式改造成如下:

最后,在启动配置类上添加

@ServletComponentScan(可以指定路径,用于扫描Servlet组件对应的包),将Web原生组件加载进Spring容器中。


DispatchServlet的注册流程

熟悉SpringMVC的小伙伴应该都知道DispatchServlet这个组件,它是SpringMVC前端控制器设计模式的实现,提供

SpringWebMVC的集中访问点,而且负责职责的分派,并与Spring Ioc容器无缝集成,从而可以获得Spring的优势。在传统的Spring项目中,DispatchServlet也是需要配置在web.xml文件中的,但在SpringBoot中,我们依然也不需要做额外的配置,那DispatchServlet是如何加载到容器中作为Servlet使用的呢?这里就应用到了上一章节中提到的ServletRegistrationBean

我们知道在SpringBoot启动时会做非常多的自动配置,而每一个自动配置都会对应一个xxxAutoConfiguration,DispatchServlet自然也不例外。
DispatchServletAutoConfiguration这个自动配置类中,首先会将DispatchServlet对象初始化并注册到Spring容器中,初始化对应的属性配置存放在WebMvcProperties

WebMvcProperties类中标注了

@ConfigurationProperties,说明我们可以在对应的application.properties中通过spring.mvc.xx配置SpringMVC的相关属性

我们继续回到
DispatchServletAutoConfiguration中,在加载了DispatchServlet后,又初始化并注册了DispatchServletRegistrationBean

通过查看继承关系链,我们得知

DispatchServletRegistrationBean其实就是一个ServletRegistrationBean

也就是说,这个注册过程,其实也就是通过RegistrationBean的方式注册Servlet的过程,DispatchServlet对应的路径其实就是 “   ”
妙啊~~

嵌入式Servlet容器自动配置原理

既然SpringBoot使用的是嵌入式的Servlet容器,那么就不需要我们额外安装部署Web容器,只需要启动应用就可以启动Web容器,那它内部是如何做到的呢?下面我们来一起分析一下

首先还是先来看一下官方文档

其实上面的文档翻译过来就是说

ServletWebServerApplicationContext作为嵌入式的Servlet容器的Ioc容器可以在启动时去找单例的ServletWebFactory,并且

ServletWebFactory通常有三个容器实现,分别是

  • TomcatServletServerFactory
  • JettyServletServerFactory
  • UndertowServletWebServerFactory
所以我们可以通过
ServletWebServerApplicationContext这个容器类作为突破口一步一步来分析。

当我们执行主配置类的main()方法启动应用时,内部会最终调用

SpringApplication.run(String..args),在方法内部其实做了很多的事情,其中包括注册监听器、创建引导上下文环境、初始化容器等,这里我们只关心和本文相关的,也就是初始化Ioc容器,代码如下:


createApplicationContext()

这里ApplicatioonContextFactory.create()使用了响应式编程,具体的方法如下:

可以看出,switch..case会根据

webApplicationType提供的不同的类型创建出不同的容器,而在前面初始化

SpringApplication对象时,

webApplicationType会根据当前导入的环境判断出是哪种类型的应用,具体代码如下:

所以如果是Web应用,最终创建的容器对象即为
AnnotationConfigServletWebServletApplicationContext,而正如文档中所说,该容器对象继承自ServletWebServerApplicationContext
至此,Ioc容器创建完成。


refreshContext()

容器创建完成后,接下来SpringBoot会调用refreshContext()方法将刚创建的容器进行初始化和刷新操作,具体代码如下:

super.refresh()方法会调用
AbstractApplicationContext.refresh(),这个过程中Ioc容器会进行初始化,并且会扫描、创建、加载所有组件、执行BeanPostProcessor方法等等一系列的操作,这里我们只关注和本文相关的内容,也就是红色框标识出来的onRefresh()方法。
因为ServletWebServerApplicationContext重写了这个方法,所以根据多态的原理,自然会去调用子类的onRefresh()方法

在createWebServer()方法中,首先会去获取WebServerFactory工厂实例

官方文档中也提到,

ServletWebServerFactory有三个容器实例(如上图),那SpringBoot是如何知道加载哪一个WebServerFactory的呢?这里又用到了ServletWebServerFactoryAutoConfiguration,这个自动配置类的主要作用就是根据导入的不同jar包来判断当前应用应该使用哪一种容器

在ServerProperties中标注了

@ConfigurationProperties,也进一步解释了为什么我们可以在对应的application.properties中通过server.xx配置Web容器的相关属性的原因

例如,spring-boot-start-web中默认就使用了Tomcat作为嵌入式Servlet容器,所以Maven中默认导入了Tomcat相关的jar包,如下图

所以当Tomcat实例和Servlet实例都被导入到工程时,TomcatServletWebServerFactory就会被默认加载
Jetty和Undertow原理同上,SpringBoot在这里使用的其实也是Java的SPI机制,即当服务想要由一种容器切换到另一种容器时,只需要将旧的容器对应的jar包替换为新的容器所对应的jar包,就可以平滑的切换而不需要改动其他代码,好家伙,太妙了~~

既然最终加载的是

TomcatServletWebServerFactory,我们就来看下它里面实现的getWebServer()方法

调用构造方法创建TomcatWebServer对象,默认端口8080,并初始化。

启动Tomcat

至此,嵌入式Servlet容器启动并初始化完成~~


这里有一点要格外注意一点:

SpringBoot是在启动Ioc容器时才会创建Servlet容器,而并不是先创建Servlet容器,再去启动Ioc容器,这个和外置Servlet容器是本质上的区别~~


定制Servlet容器

Spring官方文档中提供了两种方式来定制Servlet容器


配置文件

这种方式使用简单,也是Spring官方推荐的一种方式,由于比较常见,这里我只举个小栗子就可以,例如下面
其他的属性具体可以查看ServerProperties类中的属性


程序化定制

直接实现WebServerFactoryCustomizer接口,设置对应的属性

小伙伴们看到这里可能又会有疑问,这个customize是如何被调用的呢?

要搞清楚这个问题,我们得回看一下

ServletWebServerFactoryAutoConfiguration,在导入组件时,除了导入三种容器的WebServerFactory之外,还导入了一个类叫做

ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar,具体实现如下:

这个类只做了一件事情,就是注册后置处理器

WebServerFactoryCustomizerBeanPostProcessor

根据后置处理器的特点,在对象初始化之前会做如下操作:

上面的注释已经比较详细了,这里在着重说明一下,当从容器中获取所有类型为
WebServerFactoryCustomizer时,由于我们自定义的Customizer也实现了
WebServerFactoryCustomizer接口,所以也会被获取到。因此当调用customer()方法时,就会根据我们的需要修改对应的属性。
那当配置文件和程序化定制同时存在时,哪个的优先级比较高呢?答案是对于相同的属性,程序化定制中的配置会覆盖配置文件中的配置。我们可以再看一下源代码

ServletWebServerFactoryAutoConfiguration自动配置类会初始化自定义配置类

ServletWebServerFactoryCustomizer,它和我们自定义的配置类一样,也实现了WebServerFactoryCustomizer接口

这个类做了两件事情,首先是将自己的优先级设置为最高,所以会优先于我们自定义的配置类加载,其次是将配置文件中的属性一次赋值给对应的WebServerFactory,具体代码如下:

也就是说,当后置处理器统一调用时,会优先调用
ServletWebServerFactoryCustomizer的
custom()方法,之后才会调用我们自定义的customizer,而
ServletWebServerFactoryCustomizer的
custom()方法内部获取的属性就是我们配置文件中的属性,所以当调用我们自己定的Customizer时,如果出现相同的属性,就会将前面设置的覆盖掉
以上就是SpringBoot中嵌入式Servlet容器相关的内容,但是SpringBoot也可以外置化Servlet容器,以war包的方式启动,这个放在下一篇内容里介绍。
其实Spring源码真的写的非常优雅,虽然有很多地方调用栈很深,代码也很晦涩,但是不得不说,太牛逼了。有些代码看一遍感觉,写的啥玩意儿,等真正明白其中玄机的时候,惊得只会拍大腿了~
本篇完结啦~撒花
文章转载自糖爸的架构师之路,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论