Springboot基础知识(05)- 多环境配置(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 类中定义了两个对外的方法,如下表。

方法 描述
<T>
List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader)
静态方法;根据接口获取其实现类的实例;该方法返回的是实现类对象列表。
List<String> 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<Entry> 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<WebListenerRegistrar> 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> 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 <T> void registerSyntheticBeanIfMissing(BeanDefinitionRegistry registry, 
55                             String name, Class<T> beanClass, Supplier<T> instanceSupplier) {
56                             ...
57                         }
58                     }
59                 } 
复制代码


            该类使用了以下注解:

                @Configuration:用于定义一个配置类,可用于替换 Spring 中的 xml 配置文件;
                @Bean:被 @Configuration 注解的类内部,可以包含有一个或多个被 @Bean 注解的方法,用于构建一个 Bean,并添加到 Spring 容器中;该注解与 spring 配置文件中 <bean> 等价,方法名与 <bean> 的 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 使用。

posted @   垄山小站  阅读(4219)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 上周热点回顾(2.17-2.23)
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
点击右上角即可分享
微信分享提示