Read the Spring Environment in one article

Scan code_Search joint communication style-standard color version.jpg

Running with Spring Boot v2.5.7

Today, Spring Boot, dedicated to helping developers write production-level systems faster with less code, has become the de facto standard for Java application development. Among the many features provided by Spring Boot, automatic configuration is undoubtedly the most significant feature for improving the development experience. Based on this feature, Spring Boot automatically declares a number of beans that can be used out of the box and have a certain function for developers. In most cases, auto-configured beans can just meet your needs, but in some cases, they have to be completely covered. At this time, you only need to redeclare the related types of beans, because most auto-configured beans Beans are all @ConditionalOnMissingBeandecorated with annotations. ServerPropertiesFortunately, if you just want to fine-tune some details, such as changing the port number (server.port) and the data source URL (spring.datasource.url), there is no need to redeclare DataSourcePropertiesthese two beans to override the auto-configured beans. Spring Boot provides more than 1,000 properties for fine-tuning of automatically configured beans. When you need to adjust settings, you only need to specify them in environment variables, command-line parameters, or configuration files (application.properties/application.yml) , which is the Externalized Configuration(configuration externalization) feature of Spring Boot .

Of course, external configuration sources are not limited to environment variables, command line parameters, and configuration files. Interested readers can read the official Spring Boot documentation by themselves. In Spring, it BeanFactoryplays the role of a Bean container, and is Environmentalso positioned as a container, that is, the properties in the external configuration source will be added to the Environment . Today, when microservices are popular, distributed configuration centers such as Disconf , Apollo , and Nacos have been derived from external configuration sources . However, in Spring's site, it is still necessary to do as the locals do, and the properties read from the configuration center will still be added to the Environment . in .

The reason why I wrote this article was jasyptinspired by components. The first time I came into contact with it was in 2018. At that time, I was very curious about how this thing implements encryption and decryption of sensitive attributes; now, to achieve such a thing, not only need to be familiar with the Bean life cycle, IoC container Knowledge of extension points (IoC Container Extension Points) and the startup process of Spring Boot also need to master Environment .

Getting started with jasypt is easy. First jasypt-maven-plugin, generate ciphertext for sensitive property values ​​through this maven plugin, and then ENC(密文)replace the sensitive property values ​​with them. as follows:

jasypt.encryptor.password=crimson_typhoon

spring.datasource.url=jdbc:mysql://HOST:PORT/db_sql_boy?characterEncoding=UTF-8
spring.datasource.hikari.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.hikari.username=root
spring.datasource.hikari.password=ENC(qS8+DEIlHxvhPHgn1VaW3oHkn2twrmwNOHewWLIfquAXiCDBrKwvIhDoqalKyhIF)
复制代码

1 Getting to know the Environmnent

在实际工作中,我们与 Environment 打交道的机会并不多;如果业务 Bean 确实需要获取外部配置源中的某一属性值,可以手动将 Environment 注入到该业务 Bean 中,也可以直接实现EnvironmentAware接口,得到 Environment 类型的 Bean 实例之后可以通过getProperty()获取具体属性值。Environment 接口内容如下所示:

public interface Environment extends PropertyResolver {
    String[] getActiveProfiles();
    String[] getDefaultProfiles();
    boolean acceptsProfiles(Profiles profiles);
}

public interface PropertyResolver {
    boolean containsProperty(String key);
    String getProperty(String key);
    String getProperty(String key, String defaultValue);
    <T> T getProperty(String key, Class<T> targetType);
    <T> T getProperty(String key, Class<T> targetType, T defaultValue);
    String resolvePlaceholders(String text);
}
复制代码

大家不要受 EnvironmentgetProperty() 方法的误导,外部配置源中的属性并不是以单个属性为维度被添加到 Environment 中的,而是以PropertySource为维度PropertySource 是对属性源名称和该属性源中一组属性的抽象,MapPropertySource是一种最简单的实现,它通过 Map<String, Object> 来承载相关的属性。PropertySource 内容如下:

public abstract class PropertySource<T> {
    protected final String name;
    protected final T source;

    public PropertySource(String name, T source) {
        this.name = name;
        this.source = source;
    }

    public String getName() { return this.name; }
    public T getSource() { return this.source; }
    public abstract Object getProperty(String name);
}
复制代码

从上述 PropertySource 内容来看,PropertySource 自身是具备根据属性名获取属性值这一能力的。

getProperty()内部执行逻辑

env_getproperty_sequence.png

一般,Environment 实现类中会持有一个PropertyResolver类型的成员变量,进而交由 PropertyResolver 负责执行 getProperty() 逻辑。PropertyResolver 实现类中又会持有两个成员变量,分别是:ConversionServicePropertySources;首先,PropertyResolver 遍历 PropertySources 中的 PropertySource,获取原生属性值;然后委派 ConversionService 对原生属性值进行数据类型转换 (如果有必要的话)。虽然 PropertySource 自身是具备根据属性名获取属性值这一能力的,但不具备占位符解析与类型转换能力,于是在中间引入具备这两种能力的 PropertyResolver, 这也印证了一个段子:在计算机科学中,没有什么问题是在中间加一层解决不了的,如果有,那就再加一层

PropertySource内部更新逻辑

propertysources_crud.png

Environment 实现类中除了持有PropertyResolver类型的成员变量外,还有一个MutablePropertySources类型的成员变量,但并不提供直接操作该 MutablePropertySources 的方法,我们只能通过getPropertySources()方法获取 MutablePropertySources 实例,然后借助 MutablePropertySources 中的addFirst()addLast()replace()等方法去更新 PropertySourceMutablePropertySourcesPropertySources 唯一一个实现类,如下图所示:

property_sources_uml.png

总的来说,Environment 是对 PropertySourceProfile 的顶级抽象,下面介绍 Profile 的概念。当应用程序需要部署到不同的运行环境时,一些属性项通常会有所不同,比如,数据源 URL 在开发环境和测试环境就会不一样。Spring 从3.1版本开始支持基于 Profile 的条件化配置。

Profile in Spring 3.1

在 Spring 发布3.1版本时,Spring Boot 还未问世,可以说此时的 Profile 特性还是有些瑕疵的,但瑕不掩瑜。主要体现在:针对同一类型的 Bean,必须声明多次。一起来感受下这种小瑕疵:

@Configuration(proxyBeanMethods = false)
public class DataSourceConfig {
    @Bean
    @Profile("dev")
    public DataSource devDataSource () {
        return DataSourceBuilder.create()
                .driverClassName("com.mysql.jdbc.Driver")
                .url("jdbc:mysql://DEV_HOST:PORT/db_sql_boy?characterEncoding=UTF-8")
                .username("dev")
                .password("dev")
                .build();
    }

    @Bean
    @Profile("test")
    public DataSource testDataSource () {
        return DataSourceBuilder.create()
                .driverClassName("com.mysql.jdbc.Driver")
                .url("jdbc:mysql://TEST_HOST:PORT/db_sql_boy?characterEncoding=UTF-8")
                .username("test")
                .password("test")
                .build();
    }
}
复制代码

Profile in Spring Boot

Spring Boot 发布后,@Profile注解可以扔到九霄云外了。官方开发大佬肯定也意识到 Profile in Spring 3.1 中这种瑕疵,于是在 Spring Boot 的第一版本 (1.0.0.RELEASE) 中就迫不及待地支持为 application.propertiesapplication.yml 里的属性项配置 Profile 了。换个口味,一起来感受下这种优雅:

@Configuration(proxyBeanMethods = false)
public class DataSourceConfig {
    @Bean
    public DataSource devDataSource (DataSourceProperties dataSourceProperties) {
        return DataSourceBuilder.create()
                .driverClassName(dataSourceProperties.getDriverClassName())
                .url(dataSourceProperties.getUrl())
                .username(dataSourceProperties.getUsername())
                .password(dataSourceProperties.getPassword())
                .build();
    }
}
复制代码

application-dev.properties 内容如下:

spring.datasource.url=jdbc:mysql://DEV_HOST:PORT/db_sql_boy?characterEncoding=UTF-8
spring.datasource.hikari.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.hikari.password=dev
spring.datasource.hikari.username=dev
复制代码

application-test.properties 内容如下:

spring.datasource.url=jdbc:mysql://TEST_HOST:PORT/db_sql_boy?characterEncoding=UTF-8
spring.datasource.hikari.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.hikari.password=test
spring.datasource.hikari.username=test
复制代码

在原生 Spring 3.1 和 Spring Boot 中,均是通过spring.profiles.active来为 Environment 指定激活的 Profile,否则Environment 中默认激活的 Profile 名称为default。写到这里,笔者脑海中闪现一个问题:一般,@Profile 注解主要与 @Configuration 注解或 @Bean 注解搭配使用,如果 spring.profiles.active 的值为 dev 时,那么那些由 @Configuration@Bean 注解标记 (但没有@Profile注解的身影哈) 的 Bean 还会被解析为若干BeanDefinition实例吗?答案是会的。ConfigurationClassPostProcessor负责将 @Configuration 配置类解析为 BeanDefinition,在此过程中会执行ConditionEvaluatorshouldSkip()方法,主要内容如下:

public class ConditionEvaluator {
    public boolean shouldSkip(AnnotatedTypeMetadata metadata, ConfigurationCondition.ConfigurationPhase phase) {
        if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
            return false;
        }

        if (phase == null) {
            if (metadata instanceof AnnotationMetadata &&
                    ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) {
                return shouldSkip(metadata, ConfigurationCondition.ConfigurationPhase.PARSE_CONFIGURATION);
            }
            return shouldSkip(metadata, ConfigurationCondition.ConfigurationPhase.REGISTER_BEAN);
        }

        List<Condition> conditions = new ArrayList<>();
        for (String[] conditionClasses : getConditionClasses(metadata)) {
            for (String conditionClass : conditionClasses) {
                Condition condition = getCondition(conditionClass, this.context.getClassLoader());
                conditions.add(condition);
            }
        }

        AnnotationAwareOrderComparator.sort(conditions);

        for (Condition condition : conditions) {
            ConfigurationCondition.ConfigurationPhase requiredPhase = null;
            if (condition instanceof ConfigurationCondition) {
                requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
            }
            if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
                return true;
            }
        }

        return false;
    }
}
复制代码

shouldSkip()方法第一行 if 语句就是答案,@Profile注解由@Conditional(ProfileCondition.class)修饰,那如果一个配置类头上没有Condition的身影,直接返回false了,那就是不跳过该配置类的意思喽!

Environment 中的这些 PropertySource 究竟有啥用啊?当然是为了填充 Bean 喽,废话不多说,上图。

propertysource_bean_population.png

笔者以前都是用 visio 和 processOn 画图,第一次体验 draw.io,没想到如此优秀,强烈安利一波!

2 Environmnent 初始化流程

本节主要介绍 Spring Boot 在启动过程中向 Environmnt 中究竟注册了哪些 PropertySource。启动入口位于SpringApplication中的run(String... args)方法,如下:

public class SpringApplication {
    public ConfigurableApplicationContext run(String... args) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        DefaultBootstrapContext bootstrapContext = createBootstrapContext();
        ConfigurableApplicationContext context = null;
        configureHeadlessProperty();
        SpringApplicationRunListeners listeners = getRunListeners(args);
        listeners.starting(bootstrapContext, this.mainApplicationClass);
        try {
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
            ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
            configureIgnoreBeanInfo(environment);
            Banner printedBanner = printBanner(environment);
            context = createApplicationContext();
            context.setApplicationStartup(this.applicationStartup);
            prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
            refreshContext(context);
            afterRefresh(context, applicationArguments);
            stopWatch.stop();
            if (this.logStartupInfo) {
                new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
            }
            listeners.started(context);
            callRunners(context, applicationArguments);
        } catch (Throwable ex) {
            handleRunFailure(context, ex, listeners);
            throw new IllegalStateException(ex);
        }

        try {
            listeners.running(context);
        } catch (Throwable ex) {
            handleRunFailure(context, ex, null);
            throw new IllegalStateException(ex);
        }
        return context;
    }
}
复制代码

可以明显看出,Environmnt 的初始化是在refreshContext(context)之前完成的,这是毫无疑问的。run() 方法很复杂,但与本文主题契合的逻辑只有处:

prepareEnvironment(listeners, bootstrapContext, applicationArguments);
复制代码

下面分别分析这两处核心逻辑。

2.1 prepareEnvironment()

显然,核心内容都在prepareEnvironment()方法内,下面分小节逐一分析。

public class SpringApplication {
    private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
                                                       DefaultBootstrapContext bootstrapContext,
                                                       ApplicationArguments applicationArguments) {
        // 2.1.1
        ConfigurableEnvironment environment = getOrCreateEnvironment();
        // 2.1.2
        configureEnvironment(environment, applicationArguments.getSourceArgs());
        // 2.1.3
        ConfigurationPropertySources.attach(environment);
        // 2.1.4
        listeners.environmentPrepared(bootstrapContext, environment);
        DefaultPropertiesPropertySource.moveToEnd(environment);
        bindToSpringApplication(environment);
        ConfigurationPropertySources.attach(environment);
        return environment;
    }
}
复制代码

2.1.1 getOrCreateEnvironment()

getOrCreateEnvironment()主要负责构建 Environment 实例。如果当前应用是基于同步阻塞I/O模型的,则 Environment 选用ApplicationServletEnvironment;相反地,如果当前应用是基于异步非阻塞I/O模型的,则 Environment 选用ApplicationReactiveWebEnvironment。我们工作中基本都是基于 Spring MVC 开发应用,Spring MVC 是一款构建于Servlet API之上、基于同步阻塞 I/O 模型的主流 Java Web 开发框架,这种 I/O 模型意味着一个 HTTP 请求对应一个线程,即每一个 HTTP 请求都是在各自线程上下文中完成处理的。ApplicationServletEnvironment 继承关系如下图所示:

environment_uml.png

从上图可以看出 ApplicationServletEnvironment 家族相当庞大,在执行 ApplicationServletEnvironment 构造方法的时候必然会触发各级父类构造方法中的逻辑,依次为

public abstract class AbstractEnvironment implements ConfigurableEnvironment {
    public AbstractEnvironment() {
        this(new MutablePropertySources());
    }
    
    protected AbstractEnvironment(MutablePropertySources propertySources) {
        this.propertySources = propertySources;
        // createPropertyResolver(propertySources)
        // |___ ConfigurationPropertySources.createPropertyResolver(propertySources)
        //      |___ new ConfigurationPropertySourcesPropertyResolver(propertySources)
        this.propertyResolver = createPropertyResolver(propertySources);
        customizePropertySources(propertySources);
    }
}
复制代码
public class StandardServletEnvironment extends StandardEnvironment implements ConfigurableWebEnvironment {
    @Override
    protected void customizePropertySources(MutablePropertySources propertySources) {
        propertySources.addLast(new StubPropertySource("servletConfigInitParams"));
        propertySources.addLast(new StubPropertySource("servletContextInitParams"));
        super.customizePropertySources(propertySources);
    }
}
复制代码
public class StandardEnvironment extends AbstractEnvironment {
    @Override
    protected void customizePropertySources(MutablePropertySources propertySources) {
        propertySources.addLast(
                new PropertiesPropertySource("systemProperties", (Map) System.getProperties()));
        propertySources.addLast(
                new SystemEnvironmentPropertySource("systemEnvironment", (Map) System.getenv()));
    }
}
复制代码

随着 ApplicationServletEnvironment 构造方法的执行,此时在 EnvironmentMutablePropertySources 类型的成员变量propertySources中已经有了PropertySource 了,名称依次是:servletConfigInitParamsservletContextInitParamssystemPropertiessystemEnvironment。此外,也要记住 ApplicationServletEnvironment 中的两个重要成员变量,即MutablePropertySourcesConfigurationPropertySourcesPropertyResolver

2.1.2 configureEnvironment()

configureEnvironment()方法中的逻辑也很简单哈。首先,为 Environment 中的 PropertySourcesPropertyResolver 设定 ConversionService;然后,向 Environment 中的 MutablePropertySources 追加一个名称为commandLineArgsPropertySource 实例,注意使用的是addFirst()方法哦,这意味着这个名称为commandLineArgsPropertySource 优先级是最高的。主要逻辑如下:

public class SpringApplication {
    protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
        if (this.addConversionService) {
            environment.getPropertyResolver().setConversionService(new ApplicationConversionService());
        }
        if (this.addCommandLineProperties && args.length > 0) {
            MutablePropertySources sources = environment.getPropertySources();
            sources.addFirst(new SimpleCommandLinePropertySource(args));
        }
    }
}
复制代码

继续SimpleCommandLinePropertySource

public class SimpleCommandLinePropertySource extends CommandLinePropertySource<CommandLineArgs> {
    public SimpleCommandLinePropertySource(String... args) {
        // 其父类构造方法为:super("commandLineArgs", source)
        super(new SimpleCommandLineArgsParser().parse(args));
    }
}
复制代码

命令行参数还是比较常用的,比如我们在启动 Spring Boot 应用时会这样声明命令行参数:java -jar app.jar --server.port=8088

2.1.3 ConfigurationPropertySources.attach()

attach()方法主要就是在 EnvironmentMutablePropertySources 的头部位置插入加一个名称为configurationPropertiesPropertySource 实例。主要逻辑如下:

public final class ConfigurationPropertySources {
    public static void attach(org.springframework.core.env.Environment environment) {
        MutablePropertySources sources = ((ConfigurableEnvironment) environment).getPropertySources();
        PropertySource<?> attached = getAttached(sources);
        if (attached != null && attached.getSource() != sources) {
            sources.remove(ATTACHED_PROPERTY_SOURCE_NAME);
            attached = null;
        }
        if (attached == null) {
            sources.addFirst(new ConfigurationPropertySourcesPropertySource("configurationProperties", new SpringConfigurationPropertySources(sources)));
        }
    }

    static PropertySource<?> getAttached(MutablePropertySources sources) {
        return (sources != null) ? sources.get("configurationProperties") : null;
    }
}
复制代码

笔者盯着这玩意儿看了好久,压根没看出这个名称为configurationPropertiesPropertySource 究竟有啥用。最后,还是在官方文档中关于Relaxed Binding (宽松绑定) 的描述中猜出了些端倪。还是通过代码来解读比较直接。首先,在 application.properties 中追加一个配置项:a.b.my-first-key=hello spring environment;然后,通过 Environment 取出这个配置项的值,如下:

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext configurableApplicationContext = SpringApplication.run(DemoApplication.class, args);
        ConfigurableWebEnvironment environment = (ConfigurableWebEnvironment)
                configurableApplicationContext.getBean(Environment.class);
        System.out.println(environment.getProperty("a.b.my-first-key"));
    }
}
复制代码

启动应用后,控制台打印出了 hello spring environment 字样,这与预期是相符的。可当我们通过environment.getProperty("a.b.myfirstkey")或者environment.getProperty("a.b.my-firstkey")依然能够获取到配置项的内容。a.b.myfirstkeya.b.my-firstkey并不是配置文件中的属性名称,只是相似而已,这的确很宽松啊,哈哈。感兴趣的读者可以自行 DEBUG 看看其中的原理。

2.1.4 listeners.environmentPrepared()

敲黑板,各位大佬,这个要考的 !environmentPrepared()方法会广播一个ApplicationEnvironmentPreparedEvent事件,接着由EnvironmentPostProcessorApplicationListener响应该事件,这应该是典型的观察者模式。主要内容如下:

public class SpringApplicationRunListeners {
    private final List<SpringApplicationRunListener> listeners;

    void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
        doWithListeners("spring.boot.application.environment-prepared",
                (listener) -> listener.environmentPrepared(bootstrapContext, environment));
    }

    private void doWithListeners(String stepName, Consumer<SpringApplicationRunListener> listenerAction) {
        StartupStep step = this.applicationStartup.start(stepName);
        this.listeners.forEach(listenerAction);
        step.end();
    }
}

public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {
    @Override
    public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext,
                                    ConfigurableEnvironment environment) {
        this.initialMulticaster.multicastEvent(
                new ApplicationEnvironmentPreparedEvent(bootstrapContext, this.application, this.args, environment));
    }
}

public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster {
    @Override
    public void multicastEvent(ApplicationEvent event) {
        multicastEvent(event, resolveDefaultEventType(event));
    }

    @Override
    public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
        ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
        Executor executor = getTaskExecutor();
        for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
            if (executor != null) {
                executor.execute(() -> invokeListener(listener, event));
            } else {
                invokeListener(listener, event);
            }
        }
    }
}
复制代码

下面来看一下EnvironmentPostProcessorApplicationListener的庐山真面目:

public class EnvironmentPostProcessorApplicationListener implements SmartApplicationListener, Ordered {
    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ApplicationEnvironmentPreparedEvent) {
            onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
        }
        if (event instanceof ApplicationPreparedEvent) {
            onApplicationPreparedEvent();
        }
        if (event instanceof ApplicationFailedEvent) {
            onApplicationFailedEvent();
        }
    }
    private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
        ConfigurableEnvironment environment = event.getEnvironment();
        SpringApplication application = event.getSpringApplication();
        for (EnvironmentPostProcessor postProcessor : getEnvironmentPostProcessors(application.getResourceLoader(), event.getBootstrapContext())) {
            postProcessor.postProcessEnvironment(environment, application);
        }
    }
}
复制代码

EnvironmentPostProcessor是 Spring Boot 为 Environment 量身打造的扩展点。这里引用官方文档中比较精炼的一句话:Allows for customization of the application's Environment prior to the application context being refreshedEnvironmentPostProcessor 是一个函数性接口,内容如下:

public interface EnvironmentPostProcessor {
    void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application);
}
复制代码

在上述 EnvironmentPostProcessorApplicationListener 事件处理逻辑中,getEnvironmentPostProcessors负责加载出所有的 EnvironmentPostProcessor 。看一下内部加载逻辑:

public interface EnvironmentPostProcessorsFactory {
    static EnvironmentPostProcessorsFactory fromSpringFactories(ClassLoader classLoader) {
        return new ReflectionEnvironmentPostProcessorsFactory(
                classLoader, 
                SpringFactoriesLoader.loadFactoryNames(EnvironmentPostProcessor.class, classLoader)
        );
    }
}
复制代码

继续进入SpringFactoriesLoader一探究竟:

public final class SpringFactoriesLoader {

    public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

    public static List<String> loadFactoryNames(Class<?> factoryType, ClassLoader classLoader) {
        ClassLoader classLoaderToUse = classLoader;
        if (classLoaderToUse == null) {
            classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
        }
        String factoryTypeName = factoryType.getName();
        return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
    }

    private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
        Map<String, List<String>> result = cache.get(classLoader);
        if (result != null) {
            return result;
        }

        result = new HashMap<>();
        try {
            Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
            while (urls.hasMoreElements()) {
                URL url = urls.nextElement();
                UrlResource resource = new UrlResource(url);
                Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                for (Map.Entry<?, ?> entry : properties.entrySet()) {
                    String factoryTypeName = ((String) entry.getKey()).trim();
                    String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
                    for (String factoryImplementationName : factoryImplementationNames) {
                        result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
                                .add(factoryImplementationName.trim());
                    }
                }
            }
            result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
                    .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
            cache.put(classLoader, result);
        } catch (IOException ex) {
            throw new IllegalArgumentException("Unable to load factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex);
        }
        return result;
    }
}
复制代码

Spring SPI

SpringFactoriesLoader 这一套逻辑就是 Spring 中的SPI机制;直白点说,就是从classpath下的META-INF/spring.factories 文件中加载 EnvironmentPostProcessor ,如果大家有需求就将自己实现的 EnvironmentPostProcessor 放到该文件中就行了。其实与JDK中的SPI机制很类似哈。

在当前版本,Spring Boot 内置了7个 EnvironmentPostProcessor 实现类。接下来挑几个比较典型的分析下。

RandomValuePropertySourceEnvironmentPostProcessor

RandomValuePropertySourceEnvironmentPostProcessorEnvironment 中追加了一个名称为randomPropertySource,即RandomValuePropertySource。内容如下:

public class RandomValuePropertySourceEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {
    public static final int ORDER = Ordered.HIGHEST_PRECEDENCE + 1;
    private final Log logger;
    
    public RandomValuePropertySourceEnvironmentPostProcessor(Log logger) {
        this.logger = logger;
    }

    @Override
    public int getOrder() {
        return ORDER;
    }

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        RandomValuePropertySource.addToEnvironment(environment, this.logger);
    }
}
复制代码

那么这个 RandomValuePropertySource 有啥作用呢?主要就是用于生成随机数,比如:environment.getProperty("random.int(5,10)")可以获取一个随机数。以random.int为属性名可以获取一个 int 类型的随机数;以random.long为属性名可以获取一个 long 类型的随机数;以random.int(5,10)为属性名可以获取一个 [5, 10} 区间内 int 类型的随机数,更多玩法大家自行探索。

SystemEnvironmentPropertySourceEnvironmentPostProcessor

当前,Environment 中已经存在一个名称为systemEnvironmentPropertySource,即SystemEnvironmentPropertySourceSystemEnvironmentPropertySourceEnvironmentPostProcessor用于将该 SystemEnvironmentPropertySource 替换为OriginAwareSystemEnvironmentPropertySource,咋有点“脱裤子放屁,多此一举”的感觉呢,哈哈。

public class SystemEnvironmentPropertySourceEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {
    public static final int DEFAULT_ORDER = SpringApplicationJsonEnvironmentPostProcessor.DEFAULT_ORDER - 1;
    private int order = DEFAULT_ORDER;

    @Override
    public int getOrder() {
        return this.order;
    }

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        String sourceName = "systemEnvironment";
        PropertySource<?> propertySource = environment.getPropertySources().get(sourceName);
        if (propertySource != null) {
            replacePropertySource(environment, sourceName, propertySource, application.getEnvironmentPrefix());
        }
    }
    private void replacePropertySource(ConfigurableEnvironment environment, String sourceName,
                                       PropertySource<?> propertySource, String environmentPrefix) {
        Map<String, Object> originalSource = (Map<String, Object>) propertySource.getSource();
        SystemEnvironmentPropertySource source = new OriginAwareSystemEnvironmentPropertySource(sourceName, originalSource, environmentPrefix);
        environment.getPropertySources().replace(sourceName, source);
    }
}
复制代码

SpringApplicationJsonEnvironmentPostProcessor

我们在通过java -jar -Dspring.application.json={"name":"duxiaotou"} app.jar启动 Spring Boot 应用的时候,该属性会被自动添加到 JVM 系统属性中 (其实 -Dkey=value 这种形式的属性均是如此),其等效于System.setProperty(key, value);而当存在SPRING_APPLICATION_JSON这一系统变量时,自然也会在System.getenv()中出现。前面曾经提到过System.getProperties()代表的是systemProperties这一 PropertySource,而System.getenv()则代表的是systemEnvironment这一 PropertySourceSpringApplicationJsonEnvironmentPostProcessor就是用于从这两个 PropertySource 中抽取出 spring.application.jsonSPRING_APPLICATION_JSONJSON 串,进而单独向 Environment 中追加一个名称为spring.application.jsonPropertySource,即JsonPropertySource

ConfigDataEnvironmentPostProcessor

ConfigDataEnvironmentPostProcessor负责将optional:classpath:/optional:classpath:/config/optional:file:./optional:file:./config/optional:file:./config/*/这些目录下的 application.properties 配置文件加载出来;如果还指定了 spring.profiles.active的话,同时也会将这些目录下的 application-{profile}.properties 配置文件加载出来。最终,ConfigDataEnvironmentPostProcessor 将会向 Environment 中追加两个OriginTrackedMapPropertySource,这俩 PropertySource 位于 Environment 的尾部;其中 application-{profile}.properties 所代表的 OriginTrackedMapPropertySource 是排在 application.properties 所代表的 OriginTrackedMapPropertySource 前面的,这一点挺重要。

3 jasypt 核心原理解读

jasypt基础组件库与jasypt-spring-boot-starter是不同作者写的,后者只是为 jasypt 组件开发了 Spring Boot 的起步依赖组件而已。本文所分析的其实就是这个起步依赖组件。

application.properties 配置文件中关于数据源的密码是一个加密后的密文,如下:

spring.datasource.hikari.password=ENC(4+t9a5QG8NkNdWVS6UjIX3dj18UtYRMqU6eb3wUKjivOiDHFLZC/RTK7HuWWkUtV)
复制代码

HikariDataSource完成属性填充操作后,该 Bean 中 password 字段的值咋就变为解密后的 qwe@1234 这一明文了呢?显然,Spring Boot 为 Environment 提供的EnvironmentPostProcessor这一拓展点可以实现偷天换日!但作者没有用它,而是使用了 Spring 中的一个 IoC 拓展点,即BeanFactoryPostProcessor,这也是完全可以的,因为当执行到 BeanFactoryPostProcessor 中的postProcessBeanFactory()逻辑时,只是完成了所有BeanDefinition的加载,但还没有实例化 BeanDefinition 各自所对应的 Bean。

下面看一下EnableEncryptablePropertiesBeanFactoryPostProcessor中的内容:

public class EnableEncryptablePropertiesBeanFactoryPostProcessor implements BeanFactoryPostProcessor, Ordered {

    private final ConfigurableEnvironment environment;
    private final EncryptablePropertySourceConverter converter;

    public EnableEncryptablePropertiesBeanFactoryPostProcessor(ConfigurableEnvironment environment, EncryptablePropertySourceConverter converter) {
        this.environment = environment;
        this.converter = converter;
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        MutablePropertySources propSources = environment.getPropertySources();
        converter.convertPropertySources(propSources);
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE - 100;
    }
}
复制代码

上述源码表明该 BeanFactoryPostProcessor 借助EncryptablePropertySourceConverterMutablePropertySources 做了一层转换,那么转换成啥了呢?

接着,跟进 EncryptablePropertySourceConverter,核心内容如下:

public class EncryptablePropertySourceConverter {
    
    public void convertPropertySources(MutablePropertySources propSources) {
        propSources.stream()
                .filter(ps -> !(ps instanceof EncryptablePropertySource))
                .map(this::makeEncryptable)
                .collect(toList())
                .forEach(ps -> propSources.replace(ps.getName(), ps));
    }
    
    public <T> PropertySource<T> makeEncryptable(PropertySource<T> propertySource) {
        if (propertySource instanceof EncryptablePropertySource 
                || skipPropertySourceClasses.stream().anyMatch(skipClass -> skipClass.equals(propertySource.getClass()))) {
            return propertySource;
        }
        PropertySource<T> encryptablePropertySource = convertPropertySource(propertySource);
        return encryptablePropertySource;
    }

    private <T> PropertySource<T> convertPropertySource(PropertySource<T> propertySource) {
        PropertySource<T> encryptablePropertySource;
        if (propertySource instanceof SystemEnvironmentPropertySource) {
            encryptablePropertySource = (PropertySource<T>) new EncryptableSystemEnvironmentPropertySourceWrapper((SystemEnvironmentPropertySource) propertySource, propertyResolver, propertyFilter);
        } else if (propertySource instanceof MapPropertySource) {
            encryptablePropertySource = (PropertySource<T>) new EncryptableMapPropertySourceWrapper((MapPropertySource) propertySource, propertyResolver, propertyFilter);
        } else if (propertySource instanceof EnumerablePropertySource) {
            encryptablePropertySource = new EncryptableEnumerablePropertySourceWrapper<>((EnumerablePropertySource) propertySource, propertyResolver, propertyFilter);
        } else {
            encryptablePropertySource = new EncryptablePropertySourceWrapper<>(propertySource, propertyResolver, propertyFilter);
        }
        return encryptablePropertySource;
    }
}
复制代码

显然,它将相关原生 PropertySource 转换为了一个EncryptablePropertySourceWrapper,那这个肯定可以实现密文解密,必须的!

Go ahead, follow up EncryptablePropertySourceWrapper, as follows:

public class EncryptablePropertySourceWrapper<T> extends PropertySource<T> implements EncryptablePropertySource<T> {
    private final CachingDelegateEncryptablePropertySource<T> encryptableDelegate;

    public EncryptablePropertySourceWrapper(PropertySource<T> delegate, EncryptablePropertyResolver resolver, EncryptablePropertyFilter filter) {
        super(delegate.getName(), delegate.getSource());
        encryptableDelegate = new CachingDelegateEncryptablePropertySource<>(delegate, resolver, filter);
    }

    @Override
    public Object getProperty(String name) {
        return encryptableDelegate.getProperty(name);
    }

    @Override
    public PropertySource<T> getDelegate() {
        return encryptableDelegate;
    }
}
复制代码

Disappointed! I don't see any decryption logic, but from its getProperty method, the specific parsing logic is delegated to it CachingDelegateEncryptablePropertySource.

No way, only to find out in CachingDelegateEncryptablePropertySource :

public class CachingDelegateEncryptablePropertySource<T> extends PropertySource<T> implements EncryptablePropertySource<T> {
    private final PropertySource<T> delegate;
    private final EncryptablePropertyResolver resolver;
    private final EncryptablePropertyFilter filter;
    private final Map<String, Object> cache;

    public CachingDelegateEncryptablePropertySource(PropertySource<T> delegate, EncryptablePropertyResolver resolver, EncryptablePropertyFilter filter) {
        super(delegate.getName(), delegate.getSource());
        this.delegate = delegate;
        this.resolver = resolver;
        this.filter = filter;
        this.cache = new HashMap<>();
    }

    @Override
    public PropertySource<T> getDelegate() {
        return delegate;
    }

    @Override
    public Object getProperty(String name) {
        if (cache.containsKey(name)) {
            return cache.get(name);
        }
        synchronized (name.intern()) {
            if (!cache.containsKey(name)) {
                Object resolved = getProperty(resolver, filter, delegate, name);
                if (resolved != null) {
                    cache.put(name, resolved);
                }
            }
            return cache.get(name);
        }
    }
}
复制代码

Finally, follow up to EncryptablePropertySourcesee the final logic of decryption. Among them, it EncryptablePropertyDetectoris responsible for detecting whether the relevant attribute needs to be decrypted, mainly by judging whether the attribute value is ENC()packaged.

public interface EncryptablePropertySource<T> extends OriginLookup<String> {
    default Object getProperty(EncryptablePropertyResolver resolver, EncryptablePropertyFilter filter, PropertySource<T> source, String name) {
        Object value = source.getProperty(name);
        if (value != null && filter.shouldInclude(source, name) && value instanceof String) {
            String stringValue = String.valueOf(value);
            return resolver.resolvePropertyValue(stringValue);
        }
        return value;
    }
}

public class DefaultPropertyResolver implements EncryptablePropertyResolver {

    private final Environment environment;
    private StringEncryptor encryptor;
    private EncryptablePropertyDetector detector;

    @Override
    public String resolvePropertyValue(String value) {
        return Optional.ofNullable(value)
                .map(environment::resolvePlaceholders)
                .filter(detector::isEncrypted)
                .map(resolvedValue -> {
                    try {
                        String unwrappedProperty = detector.unwrapEncryptedValue(resolvedValue.trim());
                        String resolvedProperty = environment.resolvePlaceholders(unwrappedProperty);
                        return encryptor.decrypt(resolvedProperty);
                    } catch (EncryptionOperationNotPossibleException e) {
                        throw new DecryptionException("Unable to decrypt property: " + value + " resolved to: " + resolvedValue + ". Decryption of Properties failed,  make sure encryption/decryption " +
                                "passwords match", e);
                    }
                })
                .orElse(value);
    }
}
复制代码

4 Summary

The concluding words will not be said any more. The author is now thinking of writing, otherwise he can write 300 words. Finally, I hope you remember that in the current Spring Boot version, the ApplicationServletEnvironmentrole of the Environment , which will eventually delegate ConfigurationPropertySourcesPropertyResolverto get the property value.

5 Reference documents

  1. docs.spring.io/spring-boot…

Guess you like

Origin juejin.im/post/7098299623759937543