赞
踩
现在许多项目都广泛采用了Spring Boot,你只需要引入相应的starter,例如spring-boot-starter-web,然后启动应用程序,就会自动启动Tomcat Web服务器并开始接收HTTP请求。那么,这是如何实现的呢?它是如何知道要启动Tomcat而不是Undertow? 另外,如果我希望使用Undertow吗,要如何切换?本文将深入剖析背后的原理。
简单的说,就是Spring Boot提供了一种自动配置(auto-configuration)机制:当项目引入一个包含自动配置的jar包时,根据特定的条件和规则,它会注册不同的Bean到Spring容器中,从而启动不同的功能特性。
那么,具体什么是自动配置,它是如何工作的?有哪些条件和规则?这些条件和规则又是如何匹配和应用的?本文将分三个部分帮你全面了解自动配置的工作原理:
本文基于Spring Boot 3.0.x版本,同时也适用于Spring Boot 2.7.x版本。
使用过Spring框架的开发者应该对@Configuration注解非常熟悉了。在项目中,我们经常使用它来进行自定义的Bean配置。
而@AutoConfiguration是专门用于自动配置类的注解,而这些加了AutoConfiguration注解的自动配置类就是自动配置的入口。@AutoConfiguration本身也使用了@Configuration注解,表明自动配置类也是一个标准的配置类。
与标准的配置类相同,自动配置类的核心内容也是配置Bean,但是它会在此基础上,添加各种条件和规则,只有满足特定的条件和规则,这些Bean才会生效。另外,这些条件规则也可以应用到自动配置类本身,控制整个自动配置类的开启与否。
通常一个特定的自包含特性功能会对应一个自动配置类,但是配置本身不一定要都要全写在这一个类里,可以分解为多个普通的@Configuration配置类,然后通过@Import引入。
例如,Servlet Web服务器相关功能的自动配置,入口就是一个自动配置类ServletWebServerFactoryAutoConfiguration,它将每个可选的Web服务器配置都拆分到各自的@Configuration配置类中,部分代码如下所示:
- Java复制代码@AutoConfiguration(after = SslAutoConfiguration.class)
- @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
- @ConditionalOnClass(ServletRequest.class)
- @ConditionalOnWebApplication(type = Type.SERVLET)
- @EnableConfigurationProperties(ServerProperties.class)
- @Import({
- ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
- ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
- ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
- ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
- public class ServletWebServerFactoryAutoConfiguration {
- // ... 其它Bean配置 ...
- }
我们来详细分析下这个自动配置类上的注解。
我们现在有了自动配置类,那么Spring Boot是如何知道要加载这个自动配置类的呢?要知道,我们只是单纯引入了一个jar包而已,并没有做任何设置。
答案是,Spring定义了一套自动配置专用的发现机制,就是jar包里的META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件。该文件的每一行就是一个自动配置类的完全限定名,比如下面是spring-boot-autoconfigure包里该文件的部分内容:
- shell复制代码## 其它自动配置类
- org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration
- org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration
- org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
- ## 其它自动配置类
可以看到ServletWebServerFactoryAutoConfiguration就在这个文件里。具体的加载逻辑可以查看源码AutoConfigurationImportSelector#getCandidateConfigurations。
imports文件是Spring 2.7新引入的发现机制,之前版本使用的是spring.factories文件。实际上,Spring 2.7.x版本两种方式都支持,而在Spring 3.0中完全删除了对spring.factories文件的兼容支持。
【自动配置的模块组织】一般简单的自动配置模块,只有一个starter模块。而复杂的配置,都会拆成两个模块:starter和autoconfigure。例如,Spring Boot将其内置的所有自动配置类都放在了spring-boot-autoconfigure包里,包括imports文件。而为每个单独的功能特性提供了独立的starter包,比如spring-boot-starter-web,spring-boot-starter-jdbc等。这些starter没有任何Java代码,唯一作用是引入所有需要的依赖。
自动配置的核心是条件匹配,不同的条件加载不同的Bean,从而启用不同的功能特性。在Spring Boot中,使用了一系列的@ConditionalXXX注解来定义条件。其中,最常用的包括:
我们分析一个实际案例,ServletWebServerFactoryAutoConfiguration条件注解如下:
- Java复制代码@AutoConfiguration(after = SslAutoConfiguration.class)
- @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
- @ConditionalOnClass(ServletRequest.class) // (1)
- @ConditionalOnWebApplication(type = Type.SERVLET) // (2)
- @EnableConfigurationProperties(ServerProperties.class)
- @Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
- ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
- ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
- ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
- public class ServletWebServerFactoryAutoConfiguration {
- // ... 其它Bean配置 ...
- }
这两个条件注解是应用在自动配置类上的,是一种总开关,如果不满足,这个自动配置类就会被完全禁用。
如果满足了类级别上的条件,就会继续加载具体的配置,包括自动配置类里定义的@Bean方法和@Import的配置类。
假设自动配置类的开关条件满足了,我们看下Tomcat的具体配置,也就是@Import里的ServletWebServerFactoryConfiguration.EmbeddedTomcat配置类,它的核心代码如下:
- Java复制代码@Configuration(proxyBeanMethods = false) // (1)
- @ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class }) // (2)
- @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT) // (3)
- static class EmbeddedTomcat {
- @Bean // (4)
- TomcatServletWebServerFactory tomcatServletWebServerFactory(...) {
- TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
- // 其它初始化代码
- return factory;
- }
- }
我们详细分析下这个配置类:
上面讲述了自动配置的基本原理和概念,接下来我们来回答文章开头提出的问题:”我们只是引入了spring-boot-starter-web包,Spring Boot是怎么知道要自动启动Tomcat服务器的?具体是如何启动的呢?”
第一个问题其实简单,因为spring-boot-starter-web引入了spring-boot-starter-tomcat。
而关于其中的决策和启动过程,上面讲原理的时候其实已经提到了核心部分,无非就是条件匹配,不过前面部分侧重原理,知识点比较分散,这里通过案例分析的方式,把整个过程串起来,再详细说明下Spring Boot的整个决策过程。
这是所有Spring Boot启动的标准步骤,这里没有什么特殊的地方。只需要知道自动配置类的解析和加载是在用户自定义的Bean配置之后的。只有这样,自动配置才能根据用户的自定义配置做调整。
在这一步,Spring会扫描类路径下的所有jar包,查找自动配置类的注册文件META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,然后加载文件里的自动配置类。
我们的应用只引入了spring-boot-starter-web包,但是这个包引入了spring-boot-starter,继而引入了spring-boot-autoconfigure,我们可以从spring-boot-autoconfigure包下找到这个imports文件,该文件配置了Spring Boot内置的大量自动配置类,这里我们只关心Servlet Web服务器相关的自动配置类org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration。
我们再回顾下这个类的源码,后续会解析具体的条件匹配过程。
- Java复制代码@AutoConfiguration(after = SslAutoConfiguration.class)
- @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
- @ConditionalOnClass(ServletRequest.class)
- @ConditionalOnWebApplication(type = Type.SERVLET)
- @EnableConfigurationProperties(ServerProperties.class)
- @Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
- ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
- ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
- ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
- public class ServletWebServerFactoryAutoConfiguration {
- // ... 其它Bean配置 ...
- }
spring-boot-starter-web包引入了spring-boot-starter-tomcat,继而引入了tomcat-embed-core,这个包打包了JavaEE(Spring Boot 3.x之后是Jakarta EE)的类,其中就包含了ServletRequest类,这样就满足了该条件。
SpringApplication类的构造函数会调用下面这段代码,判断Web应用的类型。
- Java复制代码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;
- }
从这段代码可以看出,当前应用是Servlet Web服务的前提,是存在相关的类SERVLET_INDICATOR_CLASSES,这个值在Spring Boot 3.x和之前的版本有些微差别,具体如下:
- Java复制代码// Spring Boot 3.x
- String[] SERVLET_INDICATOR_CLASSES = { "jakarta.servlet.Servlet",
- "org.springframework.web.context.ConfigurableWebApplicationContext" };
-
- // Spring Boot 2.7.x
- String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet",
- "org.springframework.web.context.ConfigurableWebApplicationContext" };
差异就是Servlet类的包名改了,因为Spring 3.x从JavaEE升级到了Jakarta EE,Servlet跟第三步要求的ServletRequest类在同一个包下,因此这个条件也满足了。剩下的就是org.springframework.web.context.ConfigurableWebApplicationContext类。从包名可以看出,它是spring-web中的一个类,我们分析下包的依赖关系,发现spring-web包是由spring-boot-starter-web包引入的。它其实是个接口,具体的实现类是ServletWebServerApplicationContext。
至此,ServletWebServerFactoryAutoConfiguration的两个条件注解都满足了。Spring Boot就会开始加载这个配置类以及它@Import的配置类。
我们先看下EmbeddedTomcat的源码:
- Java复制代码@Configuration(proxyBeanMethods = false)
- @ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
- @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
- static class EmbeddedTomcat {
- @Bean
- TomcatServletWebServerFactory tomcatServletWebServerFactory(...) {
- // ...
- }
- }
spring-boot-starter-web引入了spring-boot-starter-tomcat,继而引入了Tomcat相关的依赖包,因此满足了第一个条件@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })。
由于我们只是引入了spring-boot-starter-web包,没有做任何配置,此时容器肯定没有ServletWebServerFactory类型的Bean,因此满足了第二个条件@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)。
自此,EmbeddedTomcat配置的所有条件满足,配置生效,@Bean方法tomcatServletWebServerFactory会被注册到Spring容器中,在合适的阶段用于创建TomcatServletWebServerFactory类型的Bean实例。
此外,剩下两个被@Import的EmbeddedJetty和EmbeddedUndertow也还是会被处理的,但是由于我们没有引入相应的Jetty或Undertow的包,因此条件都不满足,它们的配置也就不会生效。其实,就算引入了需要的jar包,由于EmbeddedTomcat已经注册了ServletWebServerFactory,这两个配置类也不会生效,它们的源码如下:
- Java复制代码@Configuration(proxyBeanMethods = false)
- @ConditionalOnClass({ Servlet.class, Server.class, Loader.class, WebAppContext.class }) // 这些类都是Jetty核心包下的类
- @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT) // EmbeddedTomcat已经注册了,这个条件无法满足
- static class EmbeddedJetty {
- @Bean
- JettyServletWebServerFactory jettyServletWebServerFactory(...) {
- // ...
- }
- }
-
- @Configuration(proxyBeanMethods = false)
- @ConditionalOnClass({ Servlet.class, Undertow.class, SslClientAuthMode.class }) // 这些类都是Undertow核心包下的类
- @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT) // EmbeddedTomcat已经注册了,这个条件无法满足
- static class EmbeddedUndertow {
- @Bean
- UndertowServletWebServerFactory undertowServletWebServerFactory(...) {
- // ...
- }
- }

到目前位置,Web服务器的选择决策部分已经结束了,剩下的就是其它依赖Bean的配置,这里就不再详细展开了。
在容器初始化完毕后,会调用AbstractApplicationContext#onRefresh方法,而ServletWebServerApplicationContext会重写该方法,在重写的方法中调用createWebServer方法来创建一个WebServer实例。而具体要创建哪个WebServer实例,就是看容器中注册的ServletWebServerFactory类型Bean。具体代码如下:
- scss复制代码protected ServletWebServerFactory getWebServerFactory() {
- String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
- return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
- }
从实际效果来看,就是调用了第五步注册的tomcatServletWebServerFactory创建的工厂Bean,然后用这个工厂Bean创建了真正的Tomcat实例。
需要提一下,此时还只是创建和初始化Tomcat实例,并没有真正启动服务。在SpringApplication启动的最后一步,会触发WebServerStartStopLifecycle的start()回调,这个回调触发WebServer.start()方法,从而真正启动一个Web服务器,开始接收请求。
1. 如何排除特定的自动配置类?
我们以排除自动数据源配置类为例,第一种方法是通过@SpringBootApplication的exclude字段:
- Java复制代码// 第一种方法,用exclude字段。
- // 如果不想对类有依赖,可以用excludeName字段。
- @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class)
- public class Application {
-
- public static void main(String[] args) {
- SpringApplication.run(PayPalApplication.class, args);
- }
- }
第二种方法是在配置文件中排除:
Java复制代码spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
2. 不想用Tomcat,如何换成Undertow?
只需要排除spring-boot-starter-tomcat,并引入spring-boot-starter-undertow。
- xml复制代码<dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- <exclusions>
- <!-- 排除Tomcat的依赖 -->
- <exclusion>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-tomcat</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
- <!-- 替换成Undertow的依赖 -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-undertow</artifactId>
- </dependency>

3. 如果同时直接或间接地引入了Tomcat,Jetty和Undertow的依赖包,最终启动的是哪个?
实际测试发现,三个都引入的话,最终启动的是Tomcat。而如果只有Jetty和Undertow,实际启动的是Jetty。没有找到官方的优先级文档,我猜测这跟@Import的顺序有关,@Import就是按照Tomcat,Jetty和Undertow的顺序引用的,Spring先看到了import的EmbeddedTomcat配置类,发现满足条件,于是注册了ServletWebServerFactory类型的Bean TomcatServletWebServerFactory,然后继续检查Jetty和Undertown,此时由于已经注册了TomcatServletWebServerFactory,就不满足条件@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)了。
4. 某个配置类为什么没生效?要怎么排查?
启动应用的时候,加上-Ddebug参数,Spring就会打印出每个配置类的条件匹配的细节。
作为案例,我们看下没有exclude掉tomcat,同时又引入undertow的情况下,看看为什么undertow没有生效。从下面这个输出可以看出,虽然匹配了@ConditionalOnClass条件,但是没有匹配到@ConditionalOnMissingBean条件,具体原因是已经存在了tomcatServletWebServerFactory。
- Java复制代码ServletWebServerFactoryConfiguration.EmbeddedUndertow:
- Did not match:
- - @ConditionalOnMissingBean (types: org.springframework.boot.web.servlet.server.ServletWebServerFactory; SearchStrategy: current) found beans of type 'org.springframework.boot.web.servlet.server.ServletWebServerFactory' tomcatServletWebServerFactory (OnBeanCondition)
- Matched:
- - @ConditionalOnClass found required classes 'jakarta.servlet.Servlet', 'io.undertow.Undertow', 'org.xnio.SslClientAuthMode' (OnClassCondition)
Spring Boot自动配置的核心思想就是将自动配置类和条件匹配相结合,使得我们能够快速集成各种功能和组件,而无需手动进行繁琐的配置。
而从使用者的角度看,就是通过引入或者排除特定jar包的依赖,配置特定属性和Bean,来影响条件的匹配,从而灵活地配置和定制特定功能的开关和选项。
Spring Boot支持的所有自动配置类都配置在了spring-boot-autoconfigure包的META-INF\spring\org.springframework.boot.autoconfigure.AutoConfiguration.imports,引入一个新的Starter包的时候,强烈建议去看下相关的自动配置类。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。