Spring基础知识(24)- Spring Boot (五)


多环境配置(Profile)、配置加载顺序、自动配置原理


1. 多环境配置(Profile)

    在实际的项目开发中,一个项目通常会存在多个环境,例如,开发环境、测试环境和生产环境等。不同环境的配置也不尽相同,例如开发环境使用的是开发数据库,测试环境使用的是测试数据库,而生产环境使用的是线上的正式数据库。

    Profile 为在不同环境下使用不同的配置提供了支持,我们可以通过激活、指定参数等方式快速切换环境。

    1) 多 Profile 文件方式

        Spring Boot 的配置文件共有两种形式:.properties  文件和 .yml 文件,不管哪种形式,它们都能通过文件名的命名形式区分出不同的环境的配置,文件命名格式为:

            application-{profile}.properties 或 application-{profile}.yml
        
        其中,{profile} 一般为各个环境的名称或简称,例如 dev、test 和 prod 等等。

        (1) properties 配置

            在上文 SpringbootBasic 的 src/main/resources 下添加 4 个配置文件:

                application.properties:主配置文件
                application-dev.properties:开发环境配置文件
                application-test.properties:测试环境配置文件
                application-prod.properties:生产环境配置文件

            在 application.properties 文件中,配置如下。

                # 默认环境
                user.username=admin-default
                user.age=10

                # 激活指定的 profile
                spring.profiles.active=prod

            在 application-dev.properties 文件中,配置如下。

                # 开发环境
                user.username=admin-dev
                user.age=11

            在 application-test.properties 文件中,配置如下。

                # 测试环境
                user.username=admin-test
                user.age=12

            在 application-prod.properties 文件中,配置如下。

                # 生产环境
                user.username=admin-prod
                user.age=13
        
        (2) yml 配置

            与 properties 文件类似,我们也可以添加 4 个配置文件:

                application.yml:默认配置
                application-dev.yml:开发环境配置
                application-test.yml:测试环境配置
                application-prod.yml:生产环境配置

            在 application.yml 文件中,配置如下。

                # 默认环境
                user:
                    username: admin-default
                    age: 10

                # 激活指定的 profile
                spring:
                    profiles:
                        active: test

            在 application-dev.yml 文件中,配置如下。

                # 开发环境
                user:
                    username: admin-dev
                    age: 11

            在 application-test.yml 文件中,配置如下。

                # 测试环境
                user:
                    username: admin-test
                    age: 12

            在 application-prod.yml 文件中,配置如下。

                # 生产环境
                user:
                    username: admin-prod
                    age: 13

    2) 多 Profile 文档块模式

        在 YAML 配置文件中,可以使用 “---” 把配置文件分割成了多个文档块,因此我们可以在不同的文档块中针对不同的环境进行不同的配置,并在第一个文档块内对配置进行切换。

        修改 application.yml,配置多个文档块,并在第一文档快内激活测试环境的 Profile,代码如下。

            # 默认环境
            user:
                username: admin-default
                age: 10
            # 激活指定的 profile
            spring:
                profiles:
                    active: test
            ---
            # 开发环境
            user:
                username: admin-dev
                age: 11            
            spring:
                config:
                    activate:
                        on-profile: dev
            ---
            # 测试环境
            user:
                username: admin-test
                age: 12
            spring:
                config:
                    activate:
                        on-profile: test
            ---
            # 生产环境
            user:
                username: admin-prod
                age: 13
            spring:
                config:
                    activate:
                        on-profile: prod

    3) 激活 Profile

        除了可以在配置文件中激活指定 Profile,Spring Boot 还为我们提供了另外 2 种激活 Profile 的方式:

            程序运行参数激活
            虚拟机参数激活

        (1) 程序运行参数激活
           
            这里把 “Spring基础知识(23)- Spring Boot (四)” 里 “2. 默认配置文件” 修改过的 SpringbootBasic 项目打包成 JAR 文件,并在通过命令行运行时,配置命程序运行参数,激活指定的 Profile。

            点击 IDEA 底部 Terminal 标签页,执行如下命令。

                java -jar target/SpringbootBasic-1.0-SNAPSHOT.jar --spring.profiles.active=dev

            Terminal 输出:
            
                User {username = admin-dev, age = 11}

            注:IDEA Run/Debug Configuration 时,可以设置 Program arguments: --spring.profiles.active=dev

        (2) 虚拟机参数激活

            点击 IDEA 底部 Terminal 标签页,执行如下命令。

                java -jar target/SpringbootBasic-1.0-SNAPSHOT.jar -Dspring.profiles.active=prod

            Terminal 输出:

                User {username = admin-prod, age = 13}

            注:IDEA Run/Debug Configuration 时,可以设置 VM options: -Dspring.profiles.active=prod


2. 配置加载顺序

    Spring Boot 不仅可以通过配置文件进行配置,还可以通过环境变量、命令行参数等多种形式进行配置。这些配置都可以让开发人员在不修改任何代码的前提下,直接将一套 Spring Boot  应用程序在不同的环境中运行。

    1) Spring Boot 配置优先级

        以下是常用的 Spring Boot 配置形式及其加载顺序(优先级由高到低):

            (1) 命令行参数
            (2) 来自 java:comp/env 的 JNDI 属性
            (3) Java 系统属性(System.getProperties())
            (4) 操作系统环境变量
            (5) RandomValuePropertySource 配置的 random.* 属性值
            (6) 配置文件(YAML 文件、Properties 文件)
            (7) @Configuration 注解类上的 @PropertySource 指定的配置文件
            (8) 通过 SpringApplication.setDefaultProperties 指定的默认属性

        以上所有形式的配置都会被加载,当存在相同配置内容时,高优先级的配置会覆盖低优先级的配置;存在不同的配置内容时,高优先级和低优先级的配置内容取并集,共同生效,形成互补配置。

    2) 命令行参数

        Spring Boot 中的所有配置,都可以通过命令行参数进行指定,其配置形式如下。

            java -jar {Jar文件名} --{参数1}={参数值1} --{参数2}={参数值2}

        示例,点击 IDEA 底部 Terminal 标签页,执行如下命令。

            java -jar target/SpringbootBasic-1.0-SNAPSHOT.jar --user.username=admin-param --user.age=36  

        Terminal 输出:

            User {username = admin-param, age = 36}

    3) 配置文件

        Spring Boot 启动时,会自动加载 JAR 包内部及 JAR 包所在目录指定位置的配置文件(Properties 文件、YAML 文件),同一位置下,Properties 文件优先级高于 YAML 文件。

        Spring Boot 配置文件的优先级顺序,遵循以下规则:

            (1) 先加载 JAR 包外的配置文件,再加载 JAR 包内的配置文件;
            (2) 先加载 config 目录内的配置文件,再加载 config 目录外的配置文件;
            (3) 先加载 config 子目录下的配置文件,再加载 config 目录下的配置文件;
            (4) 先加载 appliction-{profile}.properties (或 appliction-{profile}.yml),再加载 application.properties(或 application.yml);
            (5) 先加载 .properties 文件,再加载 .yml 文件。


3. 自动配置原理

    Spring Boot 项目创建完成后,即使不进行任何的配置,也能够顺利地运行,这都要归功于 Spring Boot 的自动化配置。

    Spring Boot 默认使用 application.properties 或 application.yml 作为其全局配置文件,我们可以在该配置文件中对各种自动配置属性(server.port、logging.level.* 、spring.config.active.no-profile 等等)进行修改,并使之生效。

    1) Spring Factories 机制

        Spring Boot 的自动配置是基于 Spring `Factories` 机制实现的。

        Spring Factories 机制是 Spring Boot 中的一种服务发现机制,这种扩展机制与 Java SPI 机制十分相似。Spring Boot 会自动扫描所有 Jar 包类路径下 META-INF/spring.factories 文件,并读取其中的内容,进行实例化,这种机制也是 Spring Boot Starter 的基础。

        spring.factories 文件本质上与 properties 文件相似,其中包含一组或多组键值对(key=vlaue),其中,key 的取值为接口的完全限定名;value 的取值为接口实现类的完全限定名,一个接口可以设置多个实现类,不同实现类之间使用 “,” 隔开,例如:

            org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
            org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
            org.springframework.boot.autoconfigure.condition.OnClassCondition,\
            org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition

            注意:文件中配置的内容过长,为了阅读方便而手动换行时,为了防止内容丢失可以使用“\”。

    2) Spring Factories 实现原理

        spring-core 包里定义了 SpringFactoriesLoader 类,这个类会扫描所有 Jar 包类路径下的 META-INF/spring.factories 文件,并获取指定接口的配置。在 SpringFactoriesLoader 类中定义了两个对外的方法,如下表。

方法 描述

List loadFactories(Class factoryType, @Nullable ClassLoader classLoader)
静态方法;根据接口获取其实现类的实例;该方法返回的是实现类对象列表。
List loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) 公共静态方法;根据接口l获取其实现类的名称;该方法返回的是实现类的类名的列表


        以上两个方法的关键都是从指定的 ClassLoader 中获取 spring.factories 文件,并解析得到类名列表。

            (1) loadFactories() 方法能够获取指定接口的实现类对象;
            (2) loadFactoryNames() 方法能够根据接口获取其实现类类名的集合;

    3) 自动配置的加载

        Spring Boot 自动化配置也是基于 Spring Factories 机制实现的,在 spring-boot-autoconfigure-xxx.jar 类路径下的 META-INF/spring.factories 中设置了 Spring Boot 自动配置的内容 ,如下。

 1             # Auto Configure
 2             org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
 3             org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
 4             org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
 5             org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
 6             org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
 7             org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
 8             org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
 9 
10             ...


        以上配置中,value 取值是由多个 *.xxxAutoConfiguration (使用逗号分隔)组成,每个 xxxAutoConfiguration 都是一个自动配置类。Spring Boot 启动时,会利用 Spring-Factories 机制,将这些 xxxAutoConfiguration 实例化并作为组件加入到容器中,以实现 Spring Boot 的自动配置

        (1) @SpringBootApplication 注解

            所有 Spring Boot 项目的主启动程序类上都使用了一个 @SpringBootApplication 注解,该注解是 Spring Boot 中最重要的注解之一 ,也是 Spring Boot 实现自动化配置的关键。

            @SpringBootApplication 是一个组合元注解,其主要包含两个注解:@SpringBootConfiguration 和 @EnableAutoConfiguration,其中 @EnableAutoConfiguration 注解是 SpringBoot 自动化配置的核心所在。

        (2) @EnableAutoConfiguration 注解

            @EnableAutoConfiguration 注解用于开启 Spring Boot 的自动配置功能,它使用 Spring 框架提供的 @Import 注解通过 AutoConfigurationImportSelector类(选择器)给容器中导入自动配置组件。

        (3) AutoConfigurationImportSelector 类

            AutoConfigurationImportSelector 类实现了 DeferredImportSelector 接口,AutoConfigurationImportSelector 中还包含一个静态内部类 AutoConfigurationGroup,它实现了 DeferredImportSelector 接口的内部接口 Group(Spring 5 新增)。

            AutoConfigurationImportSelector 类中包含 3 个方法,如下表。

方法 描述
Class<? extends Group> getImportGroup() 该方法获取实现了 Group 接口的类,并实例化

void process(AnnotationMetadata annotationMetadata,DeferredImportSelector deferredImportSelector)

该方法用于引入自动配置的集合
Iterable selectImports() 遍历自动配置类集合(Entry 类型的集合),并逐个解析集合中的配置类


            各方法执行顺序如下。

                a) getImportGroup() 方法

                    AutoConfigurationImportSelector 类中 getImportGroup() 方法主要用于获取实现了 DeferredImportSelector.Group 接口的类

                b) process() 方法

                    静态内部类 AutoConfigurationGroup 中的核心方法是 process(),该方法通过调用 getAutoConfigurationEntry() 方法读取 spring.factories 文件中的内容,获得自动配置类的集合。

                    getAutoConfigurationEntry() 方法通过调用 getCandidateConfigurations() 方法来获取自动配置类的完全限定名,并在经过排除、过滤等处理后,将其缓存到成员变量中。

                    在 getCandidateConfigurations() 方法中,根据 Spring Factories 机制调用 SpringFactoriesLoader 的 loadFactoryNames() 方法,根据 EnableAutoConfiguration.class (自动配置接口)获取其实现类(自动配置类)的类名的集合。

                c) selectImports() 方法

                    以上所有方法执行完成后,AutoConfigurationImportSelector.AutoConfigurationGroup#selectImports() 会将 process() 方法处理后得到的自动配置类,进行过滤、排除,最后将所有自动配置类添加到容器中。

    4) 自动配置的生效和修改

        spring.factories 文件中的所有自动配置类(xxxAutoConfiguration),都是必须在一定的条件下才会作为组件添加到容器中,配置的内容才会生效。这些限制条件在 Spring Boot 中以 @Conditional 派生注解的形式体现,如下表。

注解 生效条件
@ConditionalOnJava 应用使用指定的 Java 版本时生效
@ConditionalOnBean 容器中存在指定的  Bean 时生效
@ConditionalOnMissingBean 容器中不存在指定的 Bean 时生效
@ConditionalOnExpression 满足指定的 SpEL 表达式时生效
@ConditionalOnClass 存在指定的类时生效
@ConditionalOnMissingClass 不存在指定的类时生效
@ConditionalOnSingleCandidate 容器中只存在一个指定的 Bean 或这个 Bean 为首选 Bean 时生效
@ConditionalOnProperty 系统中指定属性存在指定的值时生效
@ConditionalOnResource 类路径下存在指定的资源文件时生效
@ConditionalOnWebApplication 当前应用是 web 应用时生效
@ConditionalOnNotWebApplication 当前应用不是 web 应用生效


        下面我们以 ServletWebServerFactoryAutoConfiguration 为例,介绍 Spring Boot 自动配置是如何生效的。

        (1) ServletWebServerFactoryAutoConfiguration

            ServletWebServerFactoryAutoConfiguration 代码如下。

 1                 // 表示这是一个配置类,与 xml 配置文件等价,也可以给容器中添加组件
 2                 @Configuration(proxyBeanMethods = false) 
 3                 @AutoConfigureOrder(-2147483648)
 4                 // 判断当前项目有没有 ServletRequest 这个类
 5                 @ConditionalOnClass({ServletRequest.class}) 
 6                 // 判断当前应用是否是 web 应用,如果是,当前配置类生效 
 7                 @ConditionalOnWebApplication(type = Type.SERVLET)
 8                 // 将配置文件中对应的值和 ServerProperties 绑定起来
 9                 @EnableConfigurationProperties({ServerProperties.class}) 
10                 @Import({ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
11                         EmbeddedTomcat.class, EmbeddedJetty.class, EmbeddedUndertow.class})
12                 public class ServletWebServerFactoryAutoConfiguration {
13 
14                     public ServletWebServerFactoryAutoConfiguration() {
15                     }
16 
17                     @Bean // 给容器中添加一个组件,这个组件的某些值需要从 properties 中获取
18                     public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProperties serverProperties, 
19                         ObjectProvider webListenerRegistrars) {
20                         ...
21                     }
22 
23                     @Bean
24                     @ConditionalOnClass(name = {"org.apache.catalina.startup.Tomcat"})
25                     public TomcatServletWebServerFactoryCustomizer tomcatServletWebServerFactoryCustomizer(
26                         ServerProperties serverProperties) {
27                         ...
28                     }
29 
30                     @Bean
31                     @ConditionalOnMissingFilterBean({ForwardedHeaderFilter.class})
32                     @ConditionalOnProperty(value = {"server.forward-headers-strategy"}, havingValue = "framework")
33                     public FilterRegistrationBean forwardedHeaderFilter() {
34                         ...
35                     }
36 
37                     public static class BeanPostProcessorsRegistrar implements BeanFactoryAware,
38                         ImportBeanDefinitionRegistrar {
39                         
40                         private ConfigurableListableBeanFactory beanFactory;
41 
42                         public BeanPostProcessorsRegistrar() {
43                         }
44 
45                         public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
46                             ...
47                         }
48 
49                         public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
50                                                             BeanDefinitionRegistry registry) {
51                             ...
52                         }
53 
54                         private  void registerSyntheticBeanIfMissing(BeanDefinitionRegistry registry, 
55                             String name, Class beanClass, Supplier instanceSupplier) {
56                             ...
57                         }
58                     }
59                 } 


            该类使用了以下注解:

                @Configuration:用于定义一个配置类,可用于替换 Spring 中的 xml 配置文件;
                @Bean:被 @Configuration 注解的类内部,可以包含有一个或多个被 @Bean 注解的方法,用于构建一个 Bean,并添加到 Spring 容器中;该注解与 spring 配置文件中 等价,方法名与 的 id 或 name 属性等价,方法返回值与 class 属性等价;

            除了 @Configuration 和 @Bean 注解外,该类还使用 5 个 @Conditional 衍生注解:

                @ConditionalOnClass({ServletRequest.class}):判断当前项目是否存在 ServletRequest 这个类,若存在,则该配置类生效。
                @ConditionalOnWebApplication(type = Type.SERVLET):判断当前应用是否是 Web 应用,如果是的话,当前配置类生效。
                @ConditionalOnClass(name = {"org.apache.catalina.startup.Tomcat"}):判断是否存在 Tomcat 类,若存在则该方法生效。
                @ConditionalOnMissingFilterBean({ForwardedHeaderFilter.class}):判断容器中是否有 ForwardedHeaderFilter 这个过滤器,若不存在则该方法生效。
                @ConditionalOnProperty(value = {"server.forward-headers-strategy"},havingValue = "framework"):判断配置文件中是否存在 server.forward-headers-strategy = framework,若不存在则该方法生效。

        (2) ServerProperties

            ServletWebServerFactoryAutoConfiguration 类还使用了一个 @EnableConfigurationProperties 注解,通过该注解导入了一个 ServerProperties 类,其部分源码如下。

 1                 @ConfigurationProperties(
 2                     prefix = "server",
 3                     ignoreUnknownFields = true
 4                 )
 5                 public class ServerProperties {
 6                     private Integer port;
 7                     private InetAddress address;
 8                     @NestedConfigurationProperty
 9                     private final ErrorProperties error = new ErrorProperties();
10                     private ServerProperties.ForwardHeadersStrategy forwardHeadersStrategy;
11                     private String serverHeader;
12 
13                     ...
14 
15                     public ServerProperties() {
16                         ...
17                     }
18 
19                     ...
20                 }


            ServletWebServerFactoryAutoConfiguration 使用了一个 @EnableConfigurationProperties 注解,而 ServerProperties 类上则使用了一个 @ConfigurationProperties 注解。这其实是 Spring Boot 自动配置机制中的通用用法。

            Spring Boot 中为我们提供了大量的自动配置类 XxxAutoConfiguration 以及 XxxProperties,每个自动配置类 XxxAutoConfiguration 都使用了 @EnableConfigurationProperties 注解,而每个 XxxProperties 上都使用 @ConfigurationProperties 注解。

            @ConfigurationProperties 注解的作用,是将这个类的所有属性与配置文件中相关的配置进行绑定,以便于获取或修改配置,但是 @ConfigurationProperties 功能是由容器提供的,被它注解的类必须是容器中的一个组件,否则该功能就无法使用。而 @EnableConfigurationProperties 注解的作用正是将指定的类以组件的形式注入到 IOC 容器中,并开启其 @ConfigurationProperties 功能。因此,@ConfigurationProperties + @EnableConfigurationProperties 组合使用,便可以为 XxxProperties 类实现配置绑定功能。

            自动配置类 XxxAutoConfiguration 负责使用 XxxProperties 中属性进行自动配置,而 XxxProperties 则负责将自动配置属性与配置文件的相关配置进行绑定,以便于用户通过配置文件修改默认的自动配置。也就是说,真正“限制”我们可以在配置文件中配置哪些属性的类就是这些 XxxxProperties 类,它与配置文件中定义的 prefix 关键字开头的一组属性是唯一对应的。

            注意:XxxAutoConfiguration 与 XxxProperties 并不是一一对应的,大多数情况都是多对多的关系,即一个 XxxAutoConfiguration 可以同时使用多个 XxxProperties 中的属性,一个 XxxProperties 类中属性也可以被多个 XxxAutoConfiguration 使用。