Spring Boot3.0自动配置原理详解

Java技术 潘老师 9个月前 (07-29) 365 ℃ (0) 扫码查看

前言

近年来,Spring Boot在许多项目中得到了广泛采用。引入Spring Boot的starter(启动器)可以简化项目的配置过程,比如使用spring-boot-starter-web可以快速启动一个Web应用程序,并自动启动Tomcat Web服务器来接收HTTP请求。但是,你可能会好奇,Spring Boot是如何实现这一切的?又是如何知道要启动Tomcat而不是Undertow呢?如果我想使用Undertow,又该如何切换呢?接下来,我将深入探讨背后的原理。

首先,让我们了解一下Spring Boot的自动配置机制。简而言之,Spring Boot的自动配置是一种机制:当项目引入一个包含自动配置的jar包时,根据特定的条件和规则,它会注册不同的Bean到Spring容器中,从而启动不同的功能特性。

那么,什么是自动配置?它是如何工作的呢?有哪些条件和规则?这些条件和规则又是如何匹配和应用的?本文将分三个部分帮你全面了解自动配置的工作原理:

  • 核心概念:@AutoConfiguration(自动配置类)和@Conditional注解(条件匹配)
  • 案例分析:Spring Boot是怎么自动启动Tomcat服务器的?
  • 常见问题和FAQ

本文基于Spring Boot 3.0.x版本,同时也适用于Spring Boot 2.7.x版本。

核心概念 – 自动配置类@AutoConfiguration

什么是自动配置类

使用过Spring框架的开发者应该对@Configuration注解非常熟悉了。在项目中,我们经常使用它来进行自定义的Bean配置。

@AutoConfiguration是专门用于自动配置类的注解,而这些加了AutoConfiguration注解的自动配置类就是自动配置的入口。@AutoConfiguration本身也使用了@Configuration注解,表明自动配置类也是一个标准的配置类。

与标准的配置类相同,自动配置类的核心内容也是配置Bean,但是它会在此基础上,添加各种条件和规则,只有满足特定的条件和规则,这些Bean才会生效。另外,这些条件规则也可以应用到自动配置类本身,控制整个自动配置类的开启与否。

通常一个特定的自包含特性功能会对应一个自动配置类,但是配置本身不一定要都要全写在这一个类里,可以分解为多个普通的@Configuration配置类,然后通过@Import引入。

例如,Servlet Web服务器相关功能的自动配置,入口就是一个自动配置类ServletWebServerFactoryAutoConfiguration,它将每个可选的Web服务器配置都拆分到各自的@Configuration配置类中,部分代码如下所示:

@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配置 ...
}

我们来详细分析下这个自动配置类上的注解。

@AutoConfiguration(after = SslAutoConfiguration.class):告诉Spring框架这个类是用于自动配置的。有些自动配置的初始化是有先后依赖关系,可以通过afterbefore来声明这种依赖关系。

@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE):设置自动配置类加载的顺序。

@ConditionalOnClass@ConditionalOnWebApplication:这两个注解就是自动配置生效的条件和规则,后面会详细说明。

@EnableConfigurationProperties(ServerProperties.class):自动配置提供的自定义参数,比如server.port等。

@Import({...}):自动配置类一般是作为入口,简单的配置可以直接写在自动配置类里。而复杂的配置建议按功能或范围拆分成子配置,然后通过@Import引入。注意,引入的顺序会影响条件的匹配,尤其是选项类的配置(比如选择Tomcat,Jetty还是Undertow)。

查找自动配置类

我们现在有了自动配置类,那么Spring Boot是如何知道要加载这个自动配置类的呢?要知道,我们只是单纯引入了一个jar包而已,并没有做任何设置。

答案是,Spring定义了一套自动配置专用的发现机制,就是jar包里的META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件。该文件的每一行就是一个自动配置类的完全限定名,比如下面是spring-boot-autoconfigure包里该文件的部分内容:

## 其它自动配置类
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-webspring-boot-starter-jdbc等。这些starter没有任何Java代码,唯一作用是引入所有需要的依赖。

核心概念 – 条件@Conditional

自动配置的核心是条件匹配,不同的条件加载不同的Bean,从而启用不同的功能特性。在Spring Boot中,使用了一系列的@ConditionalXXX注解来定义条件。其中,最常用的包括:

类条件:@ConditionalOnClass@ConditionalOnMissingClass,用于检测类的存在与否。简单的说就是,应用程序有没有直接或者间接的引用了包含这个类的jar包。比如,你要开启Undertow服务器的自动配置,就要引入Undertow相关的jar包。

Bean条件:@ConditionalOnBean@ConditionalOnMissingBean,用于检测Spring容器中是否已经注册了指定的Bean。通过使用这些条件注解,开发者可以根据需要注册自定义的Bean,以覆盖默认的配置。比如Spring提供了多种DataSource,不过不包含Druid,你就可以自定义一个基于DruidDataSource Bean,覆盖Spring默认提供的DataSource实现。

属性条件:@ConditionalOnProperty,用于检测当前的Environment中是否配置了指定的属性,这些属性可以来自配置文件,JVM系统属性,操作系统的环境变量等。比如Hikari的其中一个条件是@ConfigurationProperties(prefix = "spring.datasource.hikari")

资源条件:@ConditionalOnResource,用于检查是否存在特定的资源,比如是否存在某个配置文件,这种条件用到的很少。

Web特定条件:@ConditionalOnWebApplication@ConditionalOnNotWebApplication,用于检测应用类型是否为Web应用。@ConditionalOnWarDeployment@ConditionalOnNotWarDeployment注解用于判断是否是一个部署在Servlet容器上的传统WAR应用,而使用内嵌的web服务器的应用就不符合此条件。

SpEL表达式条件:@ConditionalOnExpression可以用SpEL表达式指定条件规则。要注意, 如果在表达式中引入了其它bean,会导致提早初始化这些bean。此时,这些Bean的状态可能是不完整的,因为它还没有经过Post Processor(比如属性绑定)的处理。建议先用上面的几种条件,无法满足再考虑这种。

我们分析一个实际案例,ServletWebServerFactoryAutoConfiguration条件注解如下:

@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配置 ...
}

@ConditionalOnClass(ServletRequest.class):要求类路径下必须存在ServletRequest类,这个很好理解,如果都没用到Servlet相关的类和库,说明你不需要Servlet Web服务相关的功能,也就没必要启动相关配置了。

@ConditionalOnWebApplication(type = Type.SERVLET):只是引入了Servlet相关类和库,也不能表明这就是一个Servlet Web服务应用。这个条件就能确保当前启动的应用是一个Servlet Web服务。

这两个条件注解是应用在自动配置类上的,是一种总开关,如果不满足,这个自动配置类就会被完全禁用。

如果满足了类级别上的条件,就会继续加载具体的配置,包括自动配置类里定义的@Bean方法和@Import的配置类。

假设自动配置类的开关条件满足了,我们看下Tomcat的具体配置,也就是@Import里的ServletWebServerFactoryConfiguration.EmbeddedTomcat配置类,它的核心代码如下:

@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;  
    }
}

我们详细分析下这个配置类:

@Configuration(proxyBeanMethods = false):表明它是一个配置类,proxyBeanMethods=false表示这个配置类不需要用CGLIB增强@Bean方法,CGLIB增强后,可以以直接调用@Bean方法的方式,定义Bean之间的依赖关系。

@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class }) :这些都是Tomcat的核心类。简单的说,就是要求你引入Tomcat相关的jar包。同理,EmbeddedUndertow的条件就要求引入Jetty的核心类。

@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT) :字面意思是,只有当前Spring容器中没有ServletWebServerFactory类型的Bean,才会注册这个Bean。换种说法就是,目前还没有加载其它Web服务器。其它可选的服务器配置类,比如EmbeddedJettyEmbeddedUndertow也是这个条件。你也可以注册自定义的ServletWebServerFactory,覆盖Spring Boot自带的Web服务器。

tomcatServletWebServerFactory:这个配置类只有这一个@Bean方法,返回的是一个工厂类Bean,它的作用是实例化,初始化一个Tomcat Web Server。只有EmbeddedTomcat类上的条件注解都满足之后,这个@Bean方法才会生效。

案例分析:Spring Boot是怎么自动启动Tomcat服务器的?

上面讲述了自动配置的基本原理和概念,接下来我们来回答文章开头提出的问题:”我们只是引入了spring-boot-starter-web包,Spring Boot是怎么知道要自动启动Tomcat服务器的?具体是如何启动的呢?”

第一个问题其实简单,因为spring-boot-starter-web引入了spring-boot-starter-tomcat

而关于其中的决策和启动过程,上面讲原理的时候其实已经提到了核心部分,无非就是条件匹配,不过前面部分侧重原理,知识点比较分散,这里通过案例分析的方式,把整个过程串起来,再详细说明下Spring Boot的整个决策过程。

第一步:扫描和注册用户自定义的Bean配置

这是所有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

我们再回顾下这个类的源码,后续会解析具体的条件匹配过程。

@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配置 ...
}

第三步:条件匹配@ConditionalOnClass(ServletRequest.class)

spring-boot-starter-web包引入了spring-boot-starter-tomcat,继而引入了tomcat-embed-core,这个包打包了JavaEE(Spring Boot 3.x之后是Jakarta EE)的类,其中就包含了ServletRequest类,这样就满足了该条件。

第四步:条件匹配@ConditionalOnWebApplication(type = Type.SERVLET)

SpringApplication类的构造函数会调用下面这段代码,判断Web应用的类型。

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和之前的版本有些微差别,具体如下:

// 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的配置类。

第五步:加载@Import的EmbeddedTomcat

我们先看下EmbeddedTomcat的源码:

@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实例。

此外,剩下两个被@ImportEmbeddedJettyEmbeddedUndertow也还是会被处理的,但是由于我们没有引入相应的Jetty或Undertow的包,因此条件都不满足,它们的配置也就不会生效。其实,就算引入了需要的jar包,由于EmbeddedTomcat已经注册了ServletWebServerFactory,这两个配置类也不会生效,它们的源码如下:

@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的配置,这里就不再详细展开了。

第六步:启动内嵌的Tomcat服务器

在容器初始化完毕后,会调用AbstractApplicationContext#onRefresh方法,而ServletWebServerApplicationContext会重写该方法,在重写的方法中调用createWebServer方法来创建一个WebServer实例。而具体要创建哪个WebServer实例,就是看容器中注册的ServletWebServerFactory类型Bean。具体代码如下:

protected ServletWebServerFactory getWebServerFactory() {
    String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
    return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
    }

从实际效果来看,就是调用了第五步注册的tomcatServletWebServerFactory创建的工厂Bean,然后用这个工厂Bean创建了真正的Tomcat实例。

需要提一下,此时还只是创建和初始化Tomcat实例,并没有真正启动服务。在SpringApplication启动的最后一步,会触发WebServerStartStopLifecyclestart()回调,这个回调触发WebServer.start()方法,从而真正启动一个Web服务器,开始接收请求。

FAQ

1. 如何排除特定的自动配置类?

我们以排除自动数据源配置类为例,第一种方法是通过@SpringBootApplicationexclude字段:

// 第一种方法,用exclude字段。
// 如果不想对类有依赖,可以用excludeName字段。
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class)
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(PayPalApplication.class, args);
    }
}

第二种方法是在配置文件中排除:

spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration

2. 不想用Tomcat,如何换成Undertow?

只需要排除spring-boot-starter-tomcat,并引入spring-boot-starter-undertow

dependency>
    groupId>org.springframework.bootgroupId>
    artifactId>spring-boot-starter-webartifactId>
    exclusions>
        
        exclusion>
            groupId>org.springframework.bootgroupId>
            artifactId>spring-boot-starter-tomcatartifactId>
        exclusion>
    exclusions>
dependency>

dependency>
    groupId>org.springframework.bootgroupId>
    artifactId>spring-boot-starter-undertowartifactId>
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

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的核心思想在于将自动配置类与条件匹配相结合,使得我们能够快速集成各种功能和组件,无需手动进行繁琐的配置。

对于使用者而言,通过引入或排除特定的依赖,配置属性和Bean,就可以影响条件的匹配,从而灵活地配置和定制特定功能的开关和选项。

所有Spring Boot支持的自动配置类都存储在spring-boot-autoconfigure包的META-INF/spring.factories文件中。当引入一个新的Starter包时,强烈建议查看相关的自动配置类,这有助于了解它所提供的功能和默认配置。

Spring Boot的自动配置机制让开发者能够更专注于业务逻辑,简化了依赖管理和配置方式,使项目结构更清晰、维护更容易。

如果你对Spring Boot的自动配置机制感兴趣,建议查阅官方文档和源码,深入学习该机制,从而更全面地了解和掌握Spring Boot的强大功能。


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/java/7491.html
喜欢 (1)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】