likes
comments
collection
share

应用启动过程——Environment

作者站长头像
站长
· 阅读数 16

1 Environment使用示例

  • 新建四个properties文件备用

应用启动过程——Environment

application.properties

app.key1=app-value1

application-dev.properties

app.key1=app-dev-value1

application-prd.properties

app.key1=app-prd-value1

my.properties

my.key1=my-value1
  • 在配置类上使用@PropertySource注解导入my.properties属性源
@PropertySource(value = {"classpath:my.properties"})
@Configuration
public class DemoAutoConfiguration {
}
  • 新增AppConfigurationProperties类,利用@ConfigurationProperties注解绑定app.*的属性配置
  • 编写测试类,尝试获取属性值
@Component
public class EnvironmentService extends TestService {

    @Override
    public void test(ConfigurableApplicationContext applicationContext) {
        Environment environment = applicationContext.getBean(Environment.class);
        AppConfigurationProperties appConfigurationProperties = applicationContext.getBean(AppConfigurationProperties.class);
        System.out.println("my.key1: " + environment.getProperty("my.key1"));
        System.out.println("app.key1: " + appConfigurationProperties.getKey1());
    }
}
  • 指定profile配置,spring.profiles.active=dev,并启动程序

应用启动过程——Environment

运行结果如图,可见my.properties的属性值可以正常读取到,而且因为指定了profile为dev,所以app.key1生效的值是来自application-dev.properties

应用启动过程——Environment

如果你按照上面的程序自己写代码走一遍,你就基本上已经了解到了足够的Environment相关用法。如果你希望更深入地了解Environment背后的原理,对于以上代码,你很可能会产生类似以下几个疑问:

  • 为什么my.properties需要手动引入,而application*.properties不需要?
  • @PropertySource注解是如何工作的
  • spring.profiles.active是如何起作用的

2 Environment初始化过程

先来看下在SpringApplication的启动过程中,对Environment都做了哪些事,从SpringApplication#run中提取了和Environment有关系的步骤,制图如下:应用启动过程——Environment

(1)是创建Environment及一些初始化工作,这个部分比较复杂,稍后单独展开。

(2)是配置spring.beaninfo.ignore,一般默认为true,表示忽略BeanInfo类,比较简单无需深究

(3)是根据环境信息打印banner,就是下图这个东西,也没必要深究

应用启动过程——Environment

(4)是在初始化ApplicationContext过程中,将Environment与其绑定,就是一个set方法,知道从这里开始应用上下文可以获取到Environment了

(5)是刷新ApplicationContext,在这个过程中,会把我们自定义的properties等属性源加载到Environment,因此也需要详细读一下源码

接下来,先就(1)步骤内容展开

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
      DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
   		// Create and configure the environment
(1.1) ConfigurableEnvironment environment = getOrCreateEnvironment();    				 
(1.2) configureEnvironment(environment, applicationArguments.getSourceArgs());   
(1.3) ConfigurationPropertySources.attach(environment);
(1.4) listeners.environmentPrepared(bootstrapContext, environment);
(1.5) DefaultPropertiesPropertySource.moveToEnd(environment);
   		Assert.state(!environment.containsProperty("spring.main.environment-prefix"),
    		     "Environment prefix cannot be set via properties.");
(1.6) bindToSpringApplication(environment);
(1.7) if (!this.isCustomEnvironment) {
      		EnvironmentConverter environmentConverter = new EnvironmentConverter(getClassLoader());
      		environment = environmentConverter.convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
   		}
(1.8) ConfigurationPropertySources.attach(environment);
   		return environment;
}

(1.1)根据webApplicationType类型创建ConfigurableEnvironment,具体地,代码遍历了所有ApplicationContextFactory,根据webApplicationType类型,最终创建的是ApplicationServletEnvironment,创建代码位置为AnnotationConfigServletWebServerApplicationContext.Factory#createEnvironment

(1.2)configureEnvironment代码如下所示

protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
1.2.1)if (this.addConversionService) {
						environment.setConversionService(new ApplicationConversionService());
   			 }
1.2.2configurePropertySources(environment, args);
1.2.3configureProfiles(environment, args);
}

其中,

(1.2.1)为environment配置ConversionService,打开ApplicationConversionService的代码可以看到,类中添加了大量的Converter和Formatter,不难推断其用途是做类型转换和格式化的

(1.2.2)这段代码在前面章节有分析过,是将应用启动参数args包装成一种PropertySource,并添加到Environment

(1.2.3)configureProfiles为空实现,不作分析,实际和Profiles相关的处理并不是在这里

(1.3)回到prepareEnvironment代码,attach方法是给Environment添加了一个名为“configurationProperties”的ConfigurationPropertySourcesPropertySource,该PropertySource中,Environment的propertySources被封装为ConfigurationPropertySource

public static void attach(Environment environment) {
   Assert.isInstanceOf(ConfigurableEnvironment.class, environment);
   MutablePropertySources sources = ((ConfigurableEnvironment) environment).getPropertySources();
   PropertySource<?> attached = getAttached(sources);
   if (attached == null || !isUsingSources(attached, sources)) {
      attached = new ConfigurationPropertySourcesPropertySource(ATTACHED_PROPERTY_SOURCE_NAME,
            new SpringConfigurationPropertySources(sources));
   }
   sources.remove(ATTACHED_PROPERTY_SOURCE_NAME);
   sources.addFirst(attached);
}

(1.4)环境准备完成的回调逻辑,也就是在这里设置了Environment的activeProfiles,以及增加了一些新的PropertySource,如application.properties等

(1.5)把defaultProperties移到最后,优先级最低

(1.6)把属性源的配置内容绑定到SpringApplication,主要是前缀为“spring.main”的配置属性,比如在application.properties配置了如下属性,会在此处绑定到SpringApplication类的对应field上

spring.main.allow-circular-references=true
spring.main.web-application-type=servlet
spring.main.register-shutdown-hook=true

(1.7)判断environment的实例类型是否需要做转换,一般类型都是吻合的,不需执行转换

(1.8)再次执行attach方法,实际作用是将configurationProperties属性源优先级提到最高

如前所述,(2、3、4)代码比较简单明确,这里不再赘述,(5)的代码无法直接找到分析入口,初步的分析到此为止

3 Environment的局部分析

第2小节从代码上直观可见地分析了Environment的初始化过程,但有些实现细节并不太好直接从遍历代码的过程中找到,这里我们从第1小节的问题出发,分析Environment的原理

3.1 问题1 2 3

为什么my.properties需要手动引入,而application*.properties不需要?

通过debug代码发现,application.properties的加载时机是在(1.4)

应用启动过程——Environment

之前分析过,这个地方的listener是EventPublishingRunListener,然后发布了ApplicationEnvironmentPreparedEvent事件,通过debug代码,可以找到EnvironmentPostProcessorApplicationListener监听了该事件,至此代码可以定位到EnvironmentPostProcessorApplicationListener#onApplicationEnvironmentPreparedEvent,

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

onApplicationEnvironmentPreparedEvent方法中又是找了一些EnvironmentPostProcessor执行postProcessEnvironment方法,继续debug代码可以发现是ConfigDataEnvironmentPostProcessor处理了application.properties,具体代码可以定位到ConfigDataEnvironmentPostProcessor#processAndApply方法

void processAndApply() {
   ConfigDataImporter importer = new ConfigDataImporter(this.logFactory, this.notFoundAction, this.resolvers,
         this.loaders);
   registerBootstrapBinder(this.contributors, null, DENY_INACTIVE_BINDING);
   ConfigDataEnvironmentContributors contributors = processInitial(this.contributors, importer);
   ConfigDataActivationContext activationContext = createActivationContext(
         contributors.getBinder(null, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE));
   contributors = processWithoutProfiles(contributors, importer, activationContext);
   activationContext = withProfiles(contributors, activationContext);
   contributors = processWithProfiles(contributors, importer, activationContext);
   applyToEnvironment(contributors, activationContext, importer.getLoadedLocations(),
         importer.getOptionalLocations());
}

在方法中,通过processWithoutProfiles和processWithProfiles处理,application*.properties被导入进来,效果如图所示

应用启动过程——Environment

而具体获取application这个名字的代码就更深了,这里直接给出代码位置org.springframework.boot.context.config.StandardConfigDataLocationResolver#DEFAULT_CONFIG_NAMES

	private static final String[] DEFAULT_CONFIG_NAMES = { "application" };

至于profile是如何拼到application文件名后缀的,可以参考StandardConfigDataReference的构造方法

StandardConfigDataReference(ConfigDataLocation configDataLocation, String directory, String root, String profile,
      String extension, PropertySourceLoader propertySourceLoader) {
   this.configDataLocation = configDataLocation;
   String profileSuffix = (StringUtils.hasText(profile)) ? "-" + profile : "";
   this.resourceLocation = root + profileSuffix + ((extension != null) ? "." + extension : "");
   this.directory = directory;
   this.profile = profile;
   this.propertySourceLoader = propertySourceLoader;
}

说完了application*.properties的引入时机,再来看下其他自定义properties的处理时机,这个代码同样很深,我直接给出代码路径:

SpringApplication#refreshContext ->

SpringApplication#refresh ->

AbstractApplicationContext#refresh ->

AbstractApplicationContext#invokeBeanFactoryPostProcessors ->

PostProcessorRegistrationDelegate#invokeBeanFactoryPostProcessors ->

PostProcessorRegistrationDelegate#invokeBeanDefinitionRegistryPostProcessors ->

ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry ->

ConfigurationClassPostProcessor#processConfigBeanDefinitions ->

ConfigurationClassParser#parse(Set) ->

ConfigurationClassParser#parse(org.springframework.core.type.AnnotationMetadata, java.lang.String) ->

ConfigurationClassParser#processConfigurationClass ->

ConfigurationClassParser#doProcessConfigurationClass ->

ConfigurationClassParser#processPropertySource

所以以上分析已经可以解答问题1、2、3

application 是默认配置文件名称,无需手动引入,而在refreshContext时,通过@PropertySource注解导入了自定义的properties文件。同时,profile作为文件名后缀的处理只发生在application.properties文件加载期,因此可以识别application-dev、application-prd等文件,而自定义properties文件不能通过文件名后缀的方式按profile生效。