使用 Spring Boot 集成 Web 环境是很方便的,只要在依赖中加入如下依赖,就默认使用 Tomcat 作为 servlet 容器,不需要额外的代码。这个特性极大的方便了 Web 项目的开发。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
那 Spring Boot 是如何自动完成 servlet 容器初始化的呢?这个问题的关键点在于两点,第一点是 servlet 容器是怎么初始化的,第二点是 Spring Boot 是如何自动完成这个过程的。特别是第二点,涉及到 Spring Boot 的自动装配机制。
Servlet 容器初始化流程
Spring Boot 应用在启动的时候,会判断当前应用类型。有 NONE,SERVLET 和 REACTIVE 三种。NONE 表示普通的 JAVA 程序,不涉及 web 容器。SERVLET 表示使用 servlet 作为 web 容器,我们一般使用的就是这种类型。REACTIVE 是在 Spring 5 之后新增的一种应用类型,也是一种 web 框架,但并不是基于 servlet,而是基于 reactive。
如何判断当前应用是哪种类型呢?不同的应用类型引入的 jar 包是不一样的,SERVLET 引入的是 spring-boot-starter-web,REACTIVE 引入的是 spring-boot-starter-webflux。 Spring Boot 通过判断当前的应用中是否存在特定的类来确定应用类型。
deduceFromClasspath 方法用于判断应用类型。如下代码列出几个特定的类,比如使用 webflux 的时候,一定会存在 org.springframework.web.reactive.DispatcherHandler,一定不存在 org.springframework.web.servlet.DispatcherServlet。
private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet";
private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler";
private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet",
"org.springframework.web.context.ConfigurableWebApplicationContext" };
private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";
static WebApplicationType deduceFromClasspath() {
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
for (String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
return WebApplicationType.NONE;
}
}
return WebApplicationType.SERVLET;
}
方法中使用 ClassUtils.isPresent() 方法来判断是否存在某个类,该方法内部最终调用的是 Class.forName() 方法。lass.forName(xxx.xx.xx) 的作用就是要求 JVM 查找并加载指定的类。
有了应用类型后,就能根据应用类型生成特定的 ApplicationContext,这些 ApplicationContext 的不同之处体现在执行上下文刷新时候操作不同。我们以 SERVLET 类型的应用为例,该类型的应用使用 AnnotationConfigServletWebServerApplicationContext 作为应用上下文。
ApplicationContextFactory DEFAULT = (webApplicationType) -> {
try {
switch (webApplicationType) {
case SERVLET:
return new AnnotationConfigServletWebServerApplicationContext();
case REACTIVE:
return new AnnotationConfigReactiveWebServerApplicationContext();
default:
return new AnnotationConfigApplicationContext();
}
}
};
ApplicationContext 初始化完毕后,会刷新上下文,也就是执行最关键的 refresh() 方法。这个阶段 Spring 会获得 BeanFactory,由 BeanFactory 生成 Bean,我们忽略中间复杂的流程,refresh() 方法中有一个方法叫做 onRefresh(),这个方法就是不同类型 ApplicationContext 实现个性化的扩展的地方。如下是 ServletWebServerApplicationContext(AnnotationConfigServletWebServerApplicationContext 继承了这个类) 的 onRefresh 方法。
可以看出,onRefresh 中方法最重要的就是创建服务器。
@Override
protected void onRefresh() {
// super 并没有做任何事
super.onRefresh();
// 创建 web server
try {
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}
创建服务器的流程是这样的:WebServerFactory -> WebServer -> initPropertySources。这也是 Spring 一贯的套路,首先获得工厂类,然后通过工厂类来完成 Bean 的创建。
private void createWebServer() {
...
if (webServer == null && servletContext == null) {
...
// 创建 WebServerFactory
ServletWebServerFactory factory = getWebServerFactory();
// 通过 WebServerFactory 创建 WebServer
this.webServer = factory.getWebServer(getSelfInitializer());
createWebServer.end();
...
}
...
// WebServer 初始化配置
initPropertySources();
}
获得工厂类的方式很简单,直接从当前 Spring 容器中获得类型为 ServletWebServerFactory 的 Bean 名称。这里有一个问题就是,为什么 Spring 容器中会有类型为 ServletWebServerFactory 的 Bean,先放一放,后续详细分析。
总之,这里会从 Spring 容器中获得一个 TomcatServletWebServerFactory 的对象。这个对象不仅包含了创建 TomcatSercer 的方法,还包含了用户的一些配置参数。有了这些参数才能正确的创建 Web 服务。
protected ServletWebServerFactory getWebServerFactory() {
// Use bean names so that we don't consider the hierarchy
String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
...
return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}
调用该对象的 getWebServer 方法。该方法中创建了一个 Tomcat 对象。并启动该 Tomcat 实例。
@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
if (this.disableMBeanRegistry) {
Registry.disableRegistry();
}
// 创建 Tomcat 对象
Tomcat tomcat = new Tomcat();
// 省略对 Tomcat 属性赋默认值的代码
...
// 启动 Tomcat,返回一个 TomcatWebServer
return getTomcatWebServer(tomcat);
}
Spring Boot 中内置了 Tomcat 的代码,其实不止 Tomcat,它还内置了 jetty,netty 和 Undertow 的代码。只不过默认使用 tomcat。
总结一下 Web 环境启动过程,Spring Boot 根据当前引入的 jar 包(特定类)判断应用类型,根据应用类型创建不同的 ApplicationContext,应用刷新上下文时,不同的 ApplicationContext 会有不同的行为,比如 SERVLET 类型的应用就是创建一个 WebServer。Spring Boot 会直接从当前容器中获得类型为 ServletWebServerFactory 的 Bean,作为 WebServer 的工厂类,然后调用该类生成一个 WebServer,最后启动这个 Server。
Spring Boot 自动装配特性
现在还有一个问题就是,ServletWebServerFactory 的实现类是如何加入容器的?为什么会是 TomcatServletWebServerFactory?
这就涉及到 Spring Boot 的自动装配特性。也就是 @EnableAutoConfiguration 注解。该注解集成在了 @SpringBootApplication 注解中,每个 Spring Boot 应用都会申明。
该注解中需要关注 @Import(AutoConfigurationImportSelector.class)。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
...
}
我们之前在Spring Boot 是如何解析配置类的一文中详细的分析了配置类的执行阶段,以及解析流程。@Import 注解就是在解析配置类的过程中生效的。
之前讲过,@Import 有三种方式,其中有一种就是 @Import 一个 ImportSelector。其实 ImportSelector 还有一个子类是 DeferredImportSelector,实现了这个类表示这些导入的 Bean 需要延后处理,本质上是执行的时机不同。
AutoConfigurationImportSelector 就是一个 DeferredImportSelector。忽略该类的调用流程,直接看 AutoConfigurationImportSelector 最终为容器中加入了怎样的类。如下是核心代码。
@Override
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// 使用 SPI 的方式获得 EnableAutoConfiguration 实现类
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
// 去重
configurations = removeDuplicates(configurations);
// 从注解的 exclude/excludeName 属性中获取排除项
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
...
return new AutoConfigurationEntry(configurations, exclusions);
}
Spring Boot 使用 SPI 的方式获得所有包下的 META-INF/spring.factories 文件,并读取其中的 EnableAutoConfiguration 实现类。
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());
return configurations;
}
protected Class<?> getSpringFactoriesLoaderFactoryClass() {
return EnableAutoConfiguration.class;
}
在 spring-boot-autoconfigure jar 包下,就定义了 129 个实现类(自动配置类基本都在这里),我随便列举几个。其它的包下还有一些调试会用到的,不关键,就不列举了。
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
...
这写类并不会全部加载到容器中,除了需要去重,根据注解传参排除一些外,还会在加载之前进行一次过滤,负责过滤的类也是通过 SPI 的方式加载到 Spring 中的。这部分就不赘述了。总之,此时我们获得了很多 EnableAutoConfiguration 的实现类,这些类会被 Import 到 Spring 容器中,它们就是实现自动装配的而基础。
EnableAutoConfiguration 的实现中有一个是 ServletWebServerFactoryAutoConfiguration,是负责集成 Web 环境的,这个类也是一个配置类,这种情况下会进行递归 Import。
ServletWebServerFactoryAutoConfiguration 中再次使用 @Import 导入一些类。我们关注 EmbeddedTomcat,EmbeddedJetty 和 EmbeddedUndertow。这三个配置类会分别检测 classpath 上存在的类,从而判断当前应用使用的 WebServer 的类型,从而决定加载哪一个 WebServer 的工厂类。
@Configuration(proxyBeanMethods = false)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
// ServletRequest 存在时才生效
@ConditionalOnClass(ServletRequest.class)
// 应用类型是 SERVLET 时才生效
@ConditionalOnWebApplication(type = Type.SERVLET)
// 前缀为 server 的配置参数加载到 bean ServerProperties
// 确保 server.port 等配置正确
@EnableConfigurationProperties(ServerProperties.class)
// 判断 WebServer 类型,定义加载 Web 服务器的工厂 Bean。
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
public class ServletWebServerFactoryAutoConfiguration {
...
}
以 EmbeddedTomcat 为例,它是 ServletWebServerFactoryConfiguration 的内部类,这里规定了 EmbeddedTomcat 存在的一些条件,以及如果 EmbeddedTomcat 符合条件,那么工厂类就是 TomcatServletWebServerFactory。
@Configuration(proxyBeanMethods = false)
class ServletWebServerFactoryConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
static class EmbeddedTomcat {
@Bean
TomcatServletWebServerFactory tomcatServletWebServerFactory(
ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers,
ObjectProvider<TomcatContextCustomizer> contextCustomizers,
ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) {
...
return factory;
}
}
...
}
饶了这么大一大圈,TomcatServletWebServerFactory 终于加载到容器中了,后续的流程正好就接上了第一部分的内容。以点见面,Spring Boot 自动装配的底层原理其实就是判断当前 classpath 是否存在特定类。我们只是替换了一个 jar 包,Spring 帮我们干了很多的脏活累活:根据条件加载不同的配置类,工厂类,生成最终的 Bean,将配置属性赋给 Bean 等等。少让用户写代码是 Spring Boot 能火起来的一个重要原因。
总结
-
Spring Boot 根据当前引入的 jar 包(特定类)判断应用类型,根据应用类型创建不同的 ApplicationContext,应用刷新上下文时,Spring Boot 会直接从当前容器中获得类型为 ServletWebServerFactory 的 Bean,作为 WebServer 的工厂类,然后调用该类生成一个 WebServer,最后启动这个 Server。
-
Spring Boot 自动装配特性极大的减少了配置量。自动装配的原理是:Spring Boot 通过 SPI 的方式加载 EnableAutoConfiguration 的实现类,这些类也是一些配置类,它们会通过判断当前 classpath 是否存在特定类,在容器中加载不同的 Bean。除了集成 Web 环境外,该特性还可以用于集成 elasticsearch,mongo, neo4j,oauth2 等等。
-
Spring Boot 自定集成 Web 环境践行了 Spring Boot 约定大于配置 的理念。Spring Boot 通过对加载路径,加载方式的约定,实现了对 Spring 容器的灵活控制。也实现了很多高级功能,比如自动装配。我们在阅读 Spring Boot 源码的过程中,可以不看细节,但是要理解 Spring Boot 到底是如何约定的,以及实现了怎样的功能,这样才能对整体框架有一个清晰的认识。
如果您觉得有所收获,就请点个赞吧!