SpringBoot擴充套件點EnvironmentPostProcessor

huan1993發表於2022-04-25

一、背景

之前專案中用到了Apollo配置中心,對接Apollo配置中心後,配置中心的屬性就可以在程式中使用了,那麼這個是怎麼實現的呢?配置中心的屬性又是何時載入到程式中的呢?那麼我們如果找到了這個是怎麼實現的是否就可以 從任何地方載入配置屬性配置屬性的加解密功能呢

二、需求

需求
從上圖中得知,我們的需求很簡單,即我們自己定義的屬性需要比配置檔案中的優先順序更高。

三、分析

1、什麼時候向SpringBoot中加入我們自己的配置屬性

當我們想在Bean中使用配置屬性時,那麼我們的配置屬性必須在Bean例項化之前就放入到Spring到Environment中。即我們的介面需要在 application context refreshed 之前進行呼叫,而 EnvironmentPostProcessor 正好可以實現這個功能。

2、獲取配置屬性的優先順序

我們知道在 Spring中獲取屬性是有優先順序的。
比如我們存在如下配置屬性 username

├─application.properties
│   >> username=huan
├─application-dev.properties
│   >> username=huan.fu

那麼此時 username 的值是什麼呢?此處借用 Apollo的一張圖來說解釋一下這個問題。

參考連結:https://www.apolloconfig.com/#/zh/design/apollo-design
配置的優先順序
Spring從3.1版本開始增加了ConfigurableEnvironmentPropertySource

ConfigurableEnvironment

  • Spring的ApplicationContext會包含一個Environment(實現ConfigurableEnvironment介面)
  • ConfigurableEnvironment自身包含了很多個PropertySource

PropertySource

  • 屬性源
  • 可以理解為很多個Key - Value的屬性配置

由上方的原理圖可知,key在最開始出現的PropertySource中的優先順序更高,上面的例子在SpringBootusername的值為huan.fu

3、何時加入我們自己的配置

由第二步 獲取配置屬性的優先順序 可知,PropertySource 越靠前越先執行,那麼要我們配置生效,就必須放在越前面越好。
在這裡插入圖片描述
由上圖可知,SpringBoot載入各種配置是通過EnvironmentPostProcessor來實現的,而具體的實現是ConfigDataEnvironmentPostProcessor來實現的。那麼我們自己編寫一個EnvironmentPostProcessor的實現類,然後在ConfigDataEnvironmentPostProcessor後執行,並加入到 Environment中的第一位即可。
保證我們自己的PropertySource在第一位

四、實現

1、引入SpringBoot依賴

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.6</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.huan.springcloud</groupId>
    <artifactId>springboot-extension-point</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-extension-point</name>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
</project>

2、在application.properties中配置屬性

vim application.properties

username=huan

3、編寫自定義屬性並加入Spring Environment中

在這裡插入圖片描述
注意:
1、如果發現程式中日誌沒有輸出,檢查是否使用了slf4j輸出日誌,此時因為日誌系統未初始化無法輸出日誌。解決方法如下:

SpringBoot版本
        >= 2.4 可以參考上圖中的使用 DeferredLogFactory 來輸出日誌
        < 2.4
            1、參考如下連結 https://stackoverflow.com/questions/42839798/how-to-log-errors-in-a-environmentpostprocessor-execution
            2、核心程式碼:
                @Component
                public class MyEnvironmentPostProcessor implements
                        EnvironmentPostProcessor, ApplicationListener<ApplicationEvent> {
                    private static final DeferredLog log = new DeferredLog();
                    @Override
                    public void postProcessEnvironment(
                            ConfigurableEnvironment env, SpringApplication app) {
                        log.error("This should be printed");
                    }
                    @Override
                    public void onApplicationEvent(ApplicationEvent event) {
                        log.replayTo(MyEnvironmentPostProcessor.class);
                    }
                }

4、通過SPI使自定義的配置生效

1、在 src/main/resources下新建META-INF/spring.factories檔案
建立spring.factories檔案
2、配置

org.springframework.boot.env.EnvironmentPostProcessor=\
  com.huan.springcloud.extensionpoint.environmentpostprocessor.CustomEnvironmentPostProcessor

5、編寫測試類,輸出定義的 username 屬性的值

@Component
public class PrintCustomizeEnvironmentProperty implements ApplicationRunner {

    private static final Logger log = LoggerFactory.getLogger(PrintCustomizeEnvironmentProperty.class);

    @Value("${username}")
    private String userName;

    @Override
    public void run(ApplicationArguments args) {
        log.info("獲取到的 username 的屬性值為: {}", userName);
    }
}

6、執行結果

執行結果

五、注意事項

1、日誌無法輸出

參考上方的 3、編寫自定義屬性並加入Spring Environment中提供的解決方案。

2、配置沒有生效

  • 檢查EnvironmentPostProcessor的優先順序,看看是否@Order或者Ordered返回的優先順序值不對。
  • 看看別的地方是否實現了 EnvironmentPostProcessorApplicationContextInitializerBeanFactoryPostProcessorBeanDefinitionRegistryPostProcessor等這些介面,在這個裡面修改了 PropertySource的順序。
  • 理解 Spring 獲取獲取屬性的順序 參考 2、獲取配置屬性的優先順序

3、日誌系統如何初始化

如下程式碼初始化日誌系統

org.springframework.boot.context.logging.LoggingApplicationListener

六、完整程式碼

https://gitee.com/huan1993/spring-cloud-parent/tree/master/springboot/springboot-extension-point/src/main/java/com/huan/springcloud/extensionpoint/environmentpostprocessor

七、參考連結

1、https://github.com/apolloconfig/apollo/blob/master/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/boot/ApolloApplicationContextInitializer.java
2、https://github.com/apolloconfig/apollo/blob/master/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/config/PropertySourcesProcessor.java

3、https://www.apolloconfig.com/#/zh/design/apollo-design

4、解決EnvironmentPostProcessor中無法輸出日誌

5、https://docs.spring.io/spring-boot/docs/2.6.6/reference/htmlsingle/#howto.application.customize-the-environment-or-application-context

相關文章