『深入学习 Spring Boot』(十) Environment

1,355 阅读5分钟

前言

这一小节,主要学习 Spring Boot 的属性环境配置接口。

Environment 概述

学习 Spring Boot 的属性配置原理,首先我们需要了解 Environment 这个类,来瞄一眼它的类注释。

Interface representing the environment in which the current application is running. Models two key aspects of the application environment: profiles and properties. Methods related to property access are exposed via the {@link PropertyResolver} superinterface.

该接口表示当前应用程序的运行环境。 为应用程序环境的构建两个方面的模型:profiles 和properties。 需要访问属性时,可使用 PropertyResolver 提供的方法。

A profile is a named, logical group of bean definitions to be registered with the container only if the given profile is active. Beans may be assigned to a profile whether defined in XML or via annotations; see the spring-beans 3.1 schema or the {@link org.springframework.context.annotation.Profile @Profile} annotation for syntax details. The role of the {@code Environment} object with relation to profiles is in determining which profiles (if any) are currently {@linkplain #getActiveProfiles active}, and which profiles (if any) should be {@linkplain #getDefaultProfiles active by default}.

profile 是仅在给定 profile 为active时,才向容器注册的Bean定义的命名逻辑组。

与配置文件相关的Environment对象的作用是确定哪些配置文件(如果有)当前处于活动状态,以及哪些配置文件(如果有)在默认情况下应处于活动状态。

Properties play an important role in almost all applications, and may originate from a variety of sources: properties files, JVM system properties, system environment variables, JNDI, servlet context parameters, ad-hoc Properties objects, Maps, and so on. The role of the environment object with relation to properties is to provide the user with a convenient service interface for configuring property sources and resolving properties from them.

Properties 在几乎所有应用程序中都起着重要作用,并且可能源自多种来源:属性文件,JVM系统属性,系统环境变量,JNDI,servlet上下文参数,临时属性对象,地图等。环境对象与属性有关的作用是为用户提供方便的接口,用于配置属性源并从中解析属性。

我们可以了解到:

  • Environment 就是 Spring Boot 启动时环境的抽象表示。

  • Environment 通过继承 PropertyResolver 提供获取属性的方法

  • Environment 还有两个重要的概念

    • profiles

      就是我们日常配置 application-dev、application-test 这种文件名称的后缀(如 dev、test)。

    • Properties

      表示具体的属性,例如 spring.name=lalala。来源有很多种:属性文件,JVM系统属性,系统环境变量等等等。

      需要注意:这里不只是我们自定义的业务属性,还包括当前Jvm属性、系统属性等等。

官网文件-Externalized Configuration 概念

我当前的 Spring Boot 版本是 Spring Boot 2.1.9,我们到官网上查看一下 Properties 来源:

docs.spring.io/spring-boot…

image-20210522182444008

这上面提到了 17 种之多,绝大部分我们都不会用到,其中也就12-15种方式是我们会用到的。

注入 Environment

最后,我们通过注入 Environment 来观察一下,Envitonment 中的属性。

首先查看一下属性源列表:

image-20210522183341887

这里包含了,我们之前创建的三个系统初始化器,应该是 Spring Boot 团队考虑到,用户可能会在系统初始化器中修改系统配置属性内容。

我们还可以查看一下,我们配置的属性:

image-20210522184205228

Environment 加载原理

Environment 的加载是在 SpringApplication 的 run 方法中完成的:

public ConfigurableApplicationContext run(String... args) {
    ....
    ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
    ....
}

prepareEnvironment(listeners, applicationArguments)

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
            ApplicationArguments applicationArguments) {
        // Create and configure the environment
        ConfigurableEnvironment environment = getOrCreateEnvironment();
        configureEnvironment(environment, applicationArguments.getSourceArgs());
        ConfigurationPropertySources.attach(environment);
        listeners.environmentPrepared(environment);
        bindToSpringApplication(environment);
        if (!this.isCustomEnvironment) {
            environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
                    deduceEnvironmentClass());
        }
        ConfigurationPropertySources.attach(environment);
        return environment;
    }

getOrCreateEnvironment()

private ConfigurableEnvironment getOrCreateEnvironment() {
        if (this.environment != null) {
            return this.environment;
        }
        switch (this.webApplicationType) {
        case SERVLET:
            return new StandardServletEnvironment();
        case REACTIVE:
            return new StandardReactiveWebEnvironment();
        default:
            return new StandardEnvironment();
        }
    }

这里我们一般都是 SERVLET 环境,所以会进入 new StandardServletEnvironment()

new StandardServletEnvironment()会加载四种属性来源:

image-20210522185823607

首先,new StandardServletEnvironment() 时,会先调用父类的构造方法 StandardEnvironment()StandardEnvironment()又会去调用他的父类构造方法 :

public AbstractEnvironment() {
   customizePropertySources(this.propertySources);
}

由于 customizePropertySources()方法被子类覆写了,所以会直接调用StandardServletEnvironment#customizePropertySources`:

protected void customizePropertySources(MutablePropertySources propertySources) {
        propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME));
        propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME));
        if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
            propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME));
        }
        super.customizePropertySources(propertySources);
    }

这里,StandardServletEnvironment # customizePropertySources 又去调用了 super.customizePropertySources(propertySources):

@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
        propertySources.addLast(
                new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
        propertySources.addLast(
                new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
    }

所以这样,就添加了四种属性来源。这玩意儿,整的还挺绕,可能这就是大佬吧。

configureEnvironment(environment, applicationArguments.getSourceArgs())

模板方法以该顺序委派给configurePropertySources(ConfigurableEnvironment, String[])和configureProfiles(ConfigurableEnvironment, String[]) 。 重写此方法以完全控制环境自定义,或者重写以上方法之一以分别对属性源或配置文件进行细粒度控制。

    protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
        if (this.addConversionService) {
            ConversionService conversionService = ApplicationConversionService.getSharedInstance();
            environment.setConversionService((ConfigurableConversionService) conversionService);
        }
        configurePropertySources(environment, args);
        configureProfiles(environment, args);
    }

configurePropertySources(environment, args);

在此应用程序的环境中添加,删除或重新排序任何PropertySource 。

protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) {
    // 获取属性源
        MutablePropertySources sources = environment.getPropertySources();
    // 设置defaultProperties
        if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {
            sources.addLast(new MapPropertySource("defaultProperties", this.defaultProperties));
        }
    // 就是我们启动时jar包时,携带的命令行参数
        if (this.addCommandLineProperties && args.length > 0) {
            String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
            if (sources.contains(name)) {
                PropertySource<?> source = sources.get(name);
                CompositePropertySource composite = new CompositePropertySource(name);
                composite.addPropertySource(
                        new SimpleCommandLinePropertySource("springApplicationCommandLineArgs", args));
                composite.addPropertySource(source);
                sources.replace(name, composite);
            }
            else {
                sources.addFirst(new SimpleCommandLinePropertySource(args));
            }
        }
    }

configureProfiles(environment, args);

配置该配置文件环境中哪些配置文件处于活动状态(或默认情况下处于活动状态)。 其他配置文件可以在配置文件处理期间通过spring.profiles.active属性激活。

    protected void configureProfiles(ConfigurableEnvironment environment, String[] args) {        // 确保它们已初始化        // ensure they are initialized        environment.getActiveProfiles();        // But these ones should go first (last wins in a property key clash)        // 但是这些应该排在最前面(属性键冲突中的最后胜利)        Set<String> profiles = new LinkedHashSet<>(this.additionalProfiles);        profiles.addAll(Arrays.asList(environment.getActiveProfiles()));        environment.setActiveProfiles(StringUtils.toStringArray(profiles));    }

active 属性指定配置文件。后面我们也会单独来一章学习的。

暂且跳过。

获取属性

我们改造一下,系统加载器。

@Component@Order(10)public class MyOneRunner implements CommandLineRunner {    @Autowired    Environment environment;    @Override    public void run(String... args) throws Exception {        System.out.println("我的第一个启动加载器");        String property = environment.getProperty("spring.application.name");        System.out.println(property);    }}

如此方便我们 debug,一路下来,我们会来到这个方法:

protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {        if (this.propertySources != null) {            for (PropertySource<?> propertySource : this.propertySources) {                Object value = propertySource.getProperty(key);                if (value != null) {                    if (resolveNestedPlaceholders && value instanceof String) {                        value = resolveNestedPlaceholders((String) value);                    }                    logKeyFound(key, propertySource, value);                    return convertValueIfNecessary(value, targetValueType);                }            }        }        ......        return null;    }

这个方法其实挺简单的,遍历属性源,如果找到了属性返回,不再遍历。

这意味着,在属性源 List 中,排位在前的 key-value 才会生效。

做一个简单的验证:

# 这是 application.properties 的配置spring.application.name=lololo # 这是 application.yml 的配置spring:  application:    name: lalala

这是属性源列表:image-20210522183341887

所以打印出来的应该是 lololo

image-20210525211719090

总结

这一节,重点理解 Enviroment,以及常用的配置源。

后面,我们会继续学习,关于 active 属性配置相关的内容。