大家好!用过spring boot的开发者会有种很直观的感受,就是发现servlet容器“消失了”,从而来带一丝丝的空虚感。之前开发web项目,都是把程序写完后部署到servlet容器(比如Tomcat),但是使用spring boot开发项目,写完程序后直接就能运行了,好神奇!
其实呢,这是因为spring boot使用了嵌入式servlet容器的关系,话说Jetty好多年前就支持嵌入式servlet了。所以开发者在默认情况下不需要做任何配置,是因为spring boot提供了一个叫EmbeddedServletContainerAutoConfiguration的配置类 (所谓配置类就是被@Configuration注解的类),spring boot能实现“零配置”,是因为spring定义了好多这样的配置类(配置没有少,只是以前交由开发者的工作spring帮你完成了)。今天,我们就来揭秘EmbeddedServletContainerAutoConfiguration。
一. 创建嵌入式servlet容器的入口在哪里?
在介绍EmbeddedServletContainerAutoConfiguration之前,我想先告诉大家在spring启动过程中的哪个步骤完成对嵌入式servlet容器的创建。一个处于web环境的spring boot项目,AC的类型是AnnotationConfigEmbeddedWebApplicationContext,它派生自EmbeddedWebApplicationContext。而EmbeddedWebApplicationContext重写了AbstractApplicationContext的onRefresh()方法,所以就是在这个onRefresh()方法中创建嵌入式servlet容器的,在方法中从bean工厂获取嵌入式serlvet容器工厂,然后调用工厂方法获取servlet容器,然后配置servlet容器。onRefresh()方法在AbstractApplicationContext中是一个空方法,定义这个方法的目的就是留给派生容器执行特定的初始化操作。
spring boot是如何知道当前处于web环境的呢?判断ConfigurableWebApplicationContext和javax.servlet.Servlet这两个类是否存在。
二. 嵌入式servlet容器工厂
在EmbeddedServletContainerAutoConfiguration这个配置类中嵌套定义了三个配置类,分别是“EmbeddedTomcat”,“EmbeddedJetty”,“EmbeddedUndertow”(源代码我就不贴了,太长)。然后这三个配置类中又分别定义了三个被@Bean注解的方法,用于往bean工厂注册嵌入式servlet容器工厂,比如tomcat对应的是TomcatEmbeddedServletContainerFactory。我之前的博客中提到过,spring boot是否引入某配置类判断的依据是其依赖的jar包是否存在。
问大家个问题,如果你的项目中同时存在tomcat、jetty、undertow三个项目的jar包,spring boot会注册三个嵌入式servlet容器工厂吗?答案是否定的,spring很聪明,在往bean工厂注册嵌入式serlvet容器工厂前,会先判断是否存在,存在就不注册了。我测试过,如果同时存在tomcat、jetty、undertow的jar包,被注册的是jetty,只是一种巧合随机行为。默认情况下spring boot引入的是tomct,也就说TomcatEmbeddedServletContainerFactory会被注册到bean工厂。
三. 如何把用户定义的filter、servlet、listener注册到servletContext
一种方法是在自己的filter、servlet、listener的类上添加@WebFilter,@WebServlet注解,然后在启动类上加上@ServletComponentScan注解。另一种方法是把filter、servlet、listener定义成bean。在嵌入式servlet容器工厂配置servlet容器的阶段,会从bean工厂获取用户定义的这些对象,然后添加到servletContext对象。spring boot会默认配置一个servlet和四个filter,分别是“dispatcherServlet”,“characterEncodingFilter”,“hiddenHttpMethodFilter”,“httpPutFormContentFilter”,“requestContextFilter”。这些都是通过spring默认提供的配置类注册到 bean工厂的,详细内容以后博客在讨论。
四. 如何做到优雅地自定义嵌入式servlet容器
1. 最简单的方法就是把相关配置定义在application.*中,能定义哪些key大家可以参看文档,这些key有相同的前缀“server.”。
2. 还有一种方法,通过bean后处理器(EmbeddedServletContainerCustomizerBeanPostProcessor),它的作用是从bean工厂获取嵌入式servlet容器自定义器列表(EmbeddedServletContainerCustomizer),然后调用每个元素的customize(ConfigurableEmbeddedServletContainer)方法对象嵌入式servlet容器执行自定义配置。这个行为发生在创建bean对象过程中的初始化bean对象之前。所以用户只需要定义一些实现EmbeddedServletContainerCustomizer接口的类,比如你可以编写一个ServerProperties的派生类,然后把它们注册到bean工厂即可。
3. 最暴力的方法就是自己写一个EmbeddedServletContainerFactory接口实现了,把它注册到bean工厂,这样默认的嵌入式servlet容器工厂就不会被使用的。