一文读懂 Spring Environment

3,050 阅读16分钟

Running with Spring Boot v2.5.7

如今,致力于帮助开发者用更少的代码、更快地写出生产级系统的 Spring Boot 已然成为 Java 应用开发的事实标准。在 Spring Boot 提供的众多特性中,自动配置无疑是对提升开发体验最显著的一个特性,Spring Boot 基于这一特性为开发人员自动声明了若干开箱即用、具备某一功能的 Bean。大多数情况下,自动配置的 Bean 刚好能满足大家的需求,但在某些情况下,不得不完整地覆盖它们,这个时候只需要重新声明相关类型的 Bean 即可,因为绝大多数自动配置的 Bean 都会由@ConditionalOnMissingBean注解修饰。幸运的是,如果只是想微调一些细节,比如改改端口号 (server.port) 和数据源 URL (spring.datasource.url) ,那压根没必要重新声明ServerPropertiesDataSourceProperties这俩 Bean 来覆盖自动配置的 Bean。 Spring Boot 为自动配置的 Bean 提供了1000多个用于微调的属性,当需要调整设置时,只需要在环境变量、命令行参数或配置文件 (application.properties/application.yml) 中进行指定即可,这就是 Spring Boot 的Externalized Configuration (配置外化) 特性。

当然,外部配置源并不局限于环境变量、命令行参数和配置文件这三种,感兴趣的读者可以自行阅读 Spring Boot 官方文档。在 Spring 中,BeanFactory扮演着 Bean 容器的角色,而Environment同样定位为一个容器,即外部配置源中的属性都会被添加到 Environment 中。在微服务大行其道的今天,外部配置源又衍生出了DisconfApolloNacos 等分布式配置中心,但在 Spring 的地盘,还是要入乡随俗,从配置中心中读取到的属性依然会被追加到 Environment

笔者之所以写这篇文章,是受jasypt组件的启发。第一次接触它是在2018年,当时就很好奇这玩意儿究竟是如何实现对敏感属性加解密的;现在来看,要想实现这么一个东东,不仅需要熟悉 Bean 的生命周期、IoC 容器拓展点 (IoC Container Extension Points) 和 Spring Boot 的启动流程等知识,还需要掌握 Environment

jasypt 上手十分简单。首先通过jasypt-maven-plugin这一 maven 插件为敏感属性值生成密文,然后用ENC(密文)替换敏感属性值即可。如下:

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 认识 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,那这个肯定可以实现密文解密,必须的!

继续,跟进EncryptablePropertySourceWrapper,内容如下:

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

失望!没看出啥解密逻辑,但从其 getProperty 方法来看,将具体解析逻辑委派给了CachingDelegateEncryptablePropertySource

没办法,只能到 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);
        }
    }
}

终于,跟进到EncryptablePropertySource中看到了解密的最终逻辑。其中,EncryptablePropertyDetector负责探测相关属性是否需要对其解密,主要通过判断该属性值是否由ENC()包裹。

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 总结

总结性的文字就不再说了,笔者现在文思泉涌,否则又能水300字。最后,希望大家记住在当前 Spring Boot 版本中,由ApplicationServletEnvironment扮演 Environment,其最终将委派ConfigurationPropertySourcesPropertyResolver去获取属性值。

5 参考文档

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