spring官方解读(一文读懂SpringEnvironment)

如今,致力于帮助开发者用更少的代码、更快地写出生产级系统的 Spring Boot 已然成为 Java 应用开发的事实标准。在 Spring Boot 提供的众多特性中,自动配置无疑是对提升开发体验最显著的一个特性,Spring Boot 基于这一特性为开发人员自动声明了若干开箱即用、具备某一功能的 Bean。大多数情况下,自动配置的 Bean 刚好能满足大家的需求,但在某些情况下,不得不完整地覆盖它们,这个时候只需要重新声明相关类型的 Bean 即可,因为绝大多数自动配置的 Bean 都会由@ConditionalOnMissingBean注解修饰。幸运的是,如果只是想微调一些细节,比如改改端口号 (Server.port) 和数据源 URL (spring.DataSource.url) ,那压根没必要重新声明ServerProperties和DataSourceProperties这俩 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()内部执行逻辑

spring官方解读(一文读懂SpringEnvironment)(1)

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

PropertySource内部更新逻辑

spring官方解读(一文读懂SpringEnvironment)(2)

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

spring官方解读(一文读懂SpringEnvironment)(3)

总的来说,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,在此过程中会执行ConditionEvaluator的shouldSkip()方法,主要内容如下:

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 喽,废话不多说,上图。

spring官方解读(一文读懂SpringEnvironment)(4)

笔者以前都是用 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 继承关系如下图所示:

spring官方解读(一文读懂SpringEnvironment)(5)

从上图可以看出 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 了,名称依次是:servletConfigInitParams、servletContextInitParams、systemProperties和systemEnvironment。此外,也要记住 ApplicationServletEnvironment 中的两个重要成员变量,即MutablePropertySources和ConfigurationPropertySourcesPropertyResolver。

2.1.2 configureEnvironment()

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

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 的头部位置插入加一个名称为configurationProperties的 PropertySource 实例。主要逻辑如下:

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

笔者盯着这玩意儿看了好久,压根没看出这个名称为configurationProperties的 PropertySource 究竟有啥用。最后,还是在官方文档中关于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.myfirstkey和a.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

RandomValuePropertySourceEnvironmentPostProcessor向 Environment 中追加了一个名称为random的 PropertySource,即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 中已经存在一个名称为systemEnvironment的 PropertySource,即SystemEnvironmentPropertySource。SystemEnvironmentPropertySourceEnvironmentPostProcessor用于将该 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这一 PropertySource。SpringApplicationJsonEnvironmentPostProcessor就是用于从这两个 PropertySource 中抽取出 spring.application.jsonSPRING_APPLICATION_JSONJSON 串,进而单独向 Environment 中追加一个名称为spring.application.json的 PropertySource,即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 借助EncryptablePropertySourceConverter对 MutablePropertySources 做了一层转换,那么转换成啥了呢?

接着,跟进 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去获取属性值。

作者:程序猿杜小头链接:https://juejin.cn/post/7098299623759937543来源:稀土掘金

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页