前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一文读懂Spring Environment

一文读懂Spring Environment

作者头像
程序猿杜小头
发布2022-12-01 21:43:49
1K0
发布2022-12-01 21:43:49
举报
文章被收录于专栏:程序猿杜小头

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(密文)替换敏感属性值即可。如下:

代码语言:javascript
复制
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 接口内容如下所示:

代码语言:javascript
复制
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 内容如下:

代码语言:javascript
复制
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()内部执行逻辑

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

PropertySource内部更新逻辑

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

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

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

代码语言:javascript
复制
@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 BootSpring Boot 发布后,@Profile注解可以扔到九霄云外了。官方开发大佬肯定也意识到 Profile in Spring 3.1 中这种瑕疵,于是在 Spring Boot 第一版本 (1.0.0.RELEASE) 中就支持为 application.propertiesapplication.yml 里的属性项配置 Profile 了。换个口味,一起来感受下这种优雅:

代码语言:javascript
复制
@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 内容如下:

代码语言:javascript
复制
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 内容如下:

代码语言:javascript
复制
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()方法,主要内容如下:

代码语言:javascript
复制
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 喽,废话不多说,上图。

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

2 Environmnent 初始化流程

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

代码语言:javascript
复制
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() 方法很复杂,但与本文主题契合的逻辑只有处:

代码语言:javascript
复制
prepareEnvironment(listeners, bootstrapContext, applicationArguments);

2.1 prepareEnvironment()

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

代码语言:javascript
复制
public class SpringApplication {
    private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
                                                       DefaultBootstrapContext bootstrapContext,
                                                       ApplicationArguments applicationArguments) {
        // 2.2.1
        ConfigurableEnvironment environment = getOrCreateEnvironment();
        // 2.2.2
        configureEnvironment(environment, applicationArguments.getSourceArgs());
        // 2.2.3
        ConfigurationPropertySources.attach(environment);
        // 2.2.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 继承关系如下图所示:

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

代码语言:javascript
复制
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);
    }
}
代码语言:javascript
复制
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);
    }
}
代码语言:javascript
复制
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 优先级是最高的。主要逻辑如下:

代码语言:javascript
复制
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

代码语言:javascript
复制
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 实例。主要逻辑如下:

代码语言:javascript
复制
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 取出这个配置项的值,如下:

代码语言:javascript
复制
@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响应该事件,这应该是典型的观察者模式。主要内容如下:

代码语言:javascript
复制
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的庐山真面目:

代码语言:javascript
复制
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 是一个函数性接口,内容如下:

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

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

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

继续进入SpringFactoriesLoader一探究竟:

代码语言:javascript
复制
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。内容如下:

代码语言:javascript
复制
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,咋有点“脱裤子放屁,多此一举”的感觉呢,哈哈。

代码语言:javascript
复制
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 配置文件中关于数据源的密码是一个加密后的密文,如下:

代码语言:javascript
复制
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中的内容:

代码语言:javascript
复制
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,核心内容如下:

代码语言:javascript
复制
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,内容如下:

代码语言:javascript
复制
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 中一探究竟了:

代码语言:javascript
复制
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()包裹。

代码语言:javascript
复制
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. https://docs.spring.io/spring-boot/docs/2.5.7/reference/html/features.html
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-05-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序猿杜小头 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 认识 Environmnent
  • 2 Environmnent 初始化流程
    • 2.1 prepareEnvironment()
      • 2.1.1 getOrCreateEnvironment()
      • 2.1.2 configureEnvironment()
      • 2.1.3 ConfigurationPropertySources.attach()
      • 2.1.4 listeners.environmentPrepared()
  • 3 jasypt 核心原理解读
  • 4 总结
  • 5 参考文档
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档