SpringFramework — Environment抽象

小龙 644 2022-05-17

Environment抽象

SpringFramework加载的 properties 资源配置,以及 ApplicationContext 内部的一些默认配置属性,都放在了哪里?组件又是怎么把配置值注入进去到对象属性中的?

带着问题看文章,效果更好

Environment概述

Environment的是SpringFramework3.1 开始引入的一个抽象模型。
看看spring官方对 Environment的解释

The Environment interface is an abstraction integrated in the container that models two key aspects of the application environment: profiles and properties. 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 with annotations. The role of the Environment object with relation to profiles is in determining which profiles (if any) are currently active, and which profiles (if any) should be active by default. 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, Map objects, 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.

Environment 接口是集成在容器中的抽象,可对应用程序环境的两个关键方面进行建模:Profile 和 properties 。 Profiles 是仅在指定 profile 处于活动状态( active )时才向容器注册 BeanDefinition 的命名逻辑组。它可以将 Bean 分配给不同的 profile (无论是以 XML 定义还是注解配置)。与配置文件相关的 Environment 作用是确定哪些配置文件当前处于活动状态,以及哪些配置文件在默认情况下应处于活动状态。 Properties 在几乎所有应用程序中都起着重要作用,并且可能源自多种来源:属性文件,JVM 系统属性,系统环境变量,JNDI,ServletContext 参数,临时属性对象,Map 对象等。Environment 与属性相关联的作用是为用户提供方便的接口,它可以用于配置属性源,并从 Environment 中解析属性。

第一句【Environment 是集成在容器中的抽象】。按照官方文档的说法,Environment 与工程的结构应该是这样才对:

image-1652767728070

到底是不是这样的,根据个人的理解不同表达出来也会不太一样。

我自己的理解:

  • 官方解释中 Environment 中包含了 profiles 和 properties ,这些配置信息会影响IOC容器中 Bean 的注册与创建

  • Environment 的创建是在 ApplicationContext 创建之后才创建的。所有Environment 应该伴随着 ApplicationContext 的存在而存在。

  • ApplicationContext中同时包含 Environment 和组件 bean,而且从 BeanFactory 视角来看,Environment 只是一个比较特殊的Bean

所以Environment与工程结构图应该如下

image-1652768075523

javadoc中的描述

Environment包含profile于properties

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 PropertyResolver superinterface.

Environment 是表示当前应用程序正在其中运行的环境的接口。它为应用环境制定了两个关键的方面:profile 和 properties。与属性访问有关的方法通过 PropertyResolver 这个父接口公开。

这一段也是总体的概括 Environment 的基本设计和作用,不过它又提到了 PropertyResolver 这个接口,这个接口负责解析占位符( ${…} )对应的值

profile用于区分不同的环境模式

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 @Profile annotation for syntax details. The role of the Environment object with relation to profiles is in determining which profiles (if any) are currently active, and which profiles (if any) should be active by default.

profile 机制保证了仅在给定 profile 处于激活状态时,才向容器注册的 BeanDefinition 的命名逻辑组。无论是用 XML 定义还是通过注解定义,都可以将 Bean 分配给指定的 profile。有关语法的详细信息,请参见 spring-beans 3.1规范文档 或 @Profile 注解。Environment 的作用是决定当前哪些配置文件(如果有)处于活动状态,以及默认情况下哪些配置文件(如果有)应处于活动状态。

Environment 配合 profile 可以完成指定模式的环境的组件装配,以及不同的配置属性注入。

properties用于配置属性和注入值

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,ServletContext 参数,临时属性对象,Map等。Environment 与 Properties 的关系是为用户提供方便的服务接口,以配置属性源,并从中解析属性值。

properties 的最大作用之一是做外部化配置,Environment 中存放了很多 properties ,它们的来源有很多种,而最终的作用都是提供了属性配置,或者给组件注入属性值。

Environment不建议直接使用

Beans managed within an ApplicationContext may register to be EnvironmentAware or @Inject the Environment in order to query profile state or resolve properties directly. In most cases, however, application-level beans should not need to interact with the Environment directly but instead may have to have ${…} property values replaced by a property placeholder configurer such as PropertySourcesPlaceholderConfigurer, which itself is EnvironmentAware and as of Spring 3.1 is registered by default when using <context:property-placeholder/>.

在 ApplicationContext 中管理的 Bean 可以注册为 EnvironmentAware 或使用 @Inject 标注在 Environment 上,以便直接查询 profile 的状态或解析 Properties。 但是,在大多数情况下,应用程序级 Bean 不必直接与 Environment 交互,而是通过将 ${…} 属性值替换为属性占位符配置器进行属性注入(例如 PropertySourcesPlaceholderConfigurer),该属性本身是 EnvironmentAware,当配置了 context:property-placeholder/> 时,默认情况下会使用 Spring 3.1 的规范注册。

这一段的描述主要讲了两件事情:Environment 可以注入到组件中,用于获取当前环境激活的所有 profile 模式;但是又不推荐开发者直接使用它,而是通过占位符注入配置属性的值。为什么会这么说呢,其实这个又要说回 Environment 设计的原始意图。Environment 的设计本身就应该是一个不被应用程序接触到的 “环境” ,我们只能从环境中获取一些它已经有的信息,但不应该获取它本身。所以,在处理 properties 的获取时,直接使用占位符就可以获取了。

ApplicationContext获取到的是ConfigurableEnvironment

Configuration of the environment object must be done through the ConfigurableEnvironment interface, returned from all AbstractApplicationContext subclass getEnvironment() methods. See ConfigurableEnvironment Javadoc for usage examples demonstrating manipulation of property sources prior to application context refresh() .

必须通过从所有 AbstractApplicationContext 子类的 getEnvironment() 方法返回的 ConfigurableEnvironment 接口完成环境对象的配置。请参阅 ConfigurableEnvironment 的 javadoc 以获取使用示例,这些示例演示在应用程序上下文 refresh() 方法被调用之前对属性源进行的操作。

注意这里,ApplicationContext 的根实现类 AbstractApplicationContext 获取到的是 ConfigurableEnvironment ,它具有 “可写” 的特征,换言之我们可以修改它内部的属性值 / 数据。不过话又说回来,通常情况下我们都不会直接改它,除非要对 SpringFramework 应用的启动流程或者运行中进行一些额外的扩展或者修改。

面试题总结

面试中如何概述Environment

Environment 是 SpringFramework3.1 引入的抽象概念,它包含了 profilesproperties 的信息,可以实现统一的配置存储注入配置属性的解析等。其中 profiles 实现了一种基于模式的环境配置properties 则应用于外部化配置

Environment的结构

image-1652769721789

PropertyResolver

从接口名就知道它应该是处理占位符 ${} 的。观察接口的方法定义,直接实锤了它就是做配置属性值的获取和解析的:(下面是 PropertyResolver 的部分方法定义)

public interface PropertyResolver {

    // 检查所有的配置属性中是否包含指定key
    boolean containsProperty(String key);

    // 以String的形式返回指定的配置属性的值
    String getProperty(String key);

    // 带默认值的获取
    String getProperty(String key, String defaultValue);

    // 指定返回类型的配置属性值获取
    <T> T getProperty(String key, Class<T> targetType);

    // ......

    // 解析占位符
    String resolvePlaceholders(String text);

    // ......
}

从 Environment 父接口 propertyResolve 可以看出,Environment 可以获取配置元信息,同时也可以解析占位符的信息。

ConfigurableEnvironment

看到 configuration开头,立刻就能想到它又扩展了什么 set 方法。可以从接口的方法定义中看到这样的方法名:setActiveProfilesaddActiveProfile。可以看出这个接口一定可以用编程式设置 profile !

除此之外,这个接口中还有一个方法比较值得注意:

 MutablePropertySources getPropertySources();

看方法名可以知道它会返回所有的 PropertySource 对象;MutablePropertySources内部就是一个 List

public class MutablePropertySources implements PropertySources {
	private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();
}

由此可以总结出一个小小的结论:Mutable 开头的类名,通常可能是一个类型的 List 组合封装。

StandarEnvironment

它是SpringFramework 中默认使用的标准运行时环境的抽象实现,不够它里面的方法实现非常的少,基本都是由 AbstractEnvironment 负责实现。

Environment的基本使用

获得Environment的API

既然 Environment 存在于 ApplicationContext 中,那么获取 Environment 的方式自然也就可以想到:@Autowired 就可以。

任意编写一个 Bean ,并声明注入 Environment :

@Component
public class EnvironmentHolder {
    
    @Autowired
    Environment environment;
    
    public void printEnvironment() {
        System.out.println(environment);
    }
}

打印结果

 StandardEnvironment {activeProfiles=[], defaultProfiles=[default], propertySources=[PropertiesPropertySource {name='systemProperties'}, SystemEnvironmentPropertySource {name='systemEnvironment'}]}

除了使用 @Autowired 方式注入,还通过回调的方式注入,作为 SpringFramework 的内置 API ,也会有一个 Aware 回调注入的接口 EnvironmentAware 就是回调注入的接口

使用Environment获取配置属性的值

已经能获取到 Environment 对象,那操作 Environment 的方法自然也就不是问题了。

为了方便获取 properties 的配置信息,这里编写一个配置类,加载 jdbc.properties 配置文件:

jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.driver-class-name=com.mysql.jdbc.Driver
jdbc.username=root
jdbc.password=123456

配置类

@Configuration
@ComponentScan("com.rsthe.environment")
@PropertySource("classpath:propertySource/jdbc.properties")
public class EnvironmentConfig {
}

现在我们就可以在 EnvironmentHolder 中操作了

获取默认的 profiles ,以及 jdbc.properties 中的配置属性值:

@Component
public class EnvironmentHolderAware implements EnvironmentAware {
	private Environment environment;

	public void printEnvironment() {
		System.out.println(Arrays.toString(environment.getDefaultProfiles()));
		System.out.println(environment.getProperty("jdbc.url"));
	}

	@Override
	public void setEnvironment(Environment environment) {
		this.environment = environment;
	}
}

执行结果

[default]
jdbc:mysql://localhost:3306/test

控制台打印的默认 profiles:是一个默认值 default,这个值我们没有声明过,那它是从哪里来的呢?

Environment的默认profiles

想知道 profiles 的默认配置,就要进入到 Environment 的抽象实现 AbstractEnvironment 中了:

public String[] getDefaultProfiles() {
		return StringUtils.toStringArray(doGetDefaultProfiles());
}

这里有一个很有意思的操作:getDefaultProfiles 调用了 doGetDefaultProfiles 方法,这个设计在 SpringFramework 中大量出现和使用!

SpringFramework中的方法命名规范

在 SpringFramework 的框架编码中,如果有出现一个方法是 do 开头,并且去掉 do 后能找到一个与剩余名称一样的方法,则代表如下含义:不带 do 开头的方法一般负责前置校验处理、返回结果封装带 do 开头的方法是真正执行逻辑的方法(如 getBean 方法的底层会调用 doGetBean 来真正的寻找 IOC 容器的 bean ,createBean 会调用 doCreateBean 来真正的创建一个 bean )

doGetDefaultProfiles的实现

public static final String DEFAULT_PROFILES_PROPERTY_NAME = "spring.profiles.default";

protected Set<String> doGetDefaultProfiles() {
    synchronized (this.defaultProfiles) {
        // 取框架默认的profiles,并与当前的对比
        if (this.defaultProfiles.equals(getReservedDefaultProfiles())) {
            // 如果一致,则尝试从Environment中获取显式声明的profiles
            String profiles = getProperty(DEFAULT_PROFILES_PROPERTY_NAME);
            // 如果有显式声明,则覆盖原有的默认值
            if (StringUtils.hasText(profiles)) {
                setDefaultProfiles(StringUtils.commaDelimitedListToStringArray(
                        StringUtils.trimAllWhitespace(profiles)));
            }
        }
        return this.defaultProfiles;
    }
}

Environment 的解析配置属性值的底层是交给 PropertySourcesPropertyResolver

思考

  1. Environment 是什么?它与 ApplicationContext 的关系是什么?

    Environment是SpringFramework内置的一种环境抽象,主要用来描述 ApplicationContext 运行时的一些配置信息和构件信息,比如profiles 和 properties

  2. Environment 都具有什么功能?

    通过 ${} 占位符可以获取Properties的属性信息,配合 profiles 可以完成指定模式的环境的组件装配,以及不同的配置属性注入

  3. Environment 与 profile 的关系是什么?如何获取和设置 profile ?

    Environment 包含了 profile。通过ConfigurableEnvironment 子接口的setActiveProfiles()方法可以设置profile,通过Environment 接口的 getDefaultProfiles() 方法可以获取默认的 profiles,getActiveProfiles()方法可以获取活动的profile


# IOC