Spring IOC高级-Environment抽象

从 0 开始深入学习 Spring 专栏目录总览

上一章的最后,小册提出了一个问题:加载的 properties 资源配置,以及 ApplicationContext 内部的一些默认配置属性,都放在哪里了?组件 Bean 又是怎么把配置值注入进去到对象的属性中的?本章就会解答这些问题。

1. Environment概述【理解】

先对 Environment 有一个大体的认识吧。Environment 是从 SpringFramework 3.1 开始引入的一个抽象模型,之前咱在第 4 章 SpringFramework 概述中也提过它的出现时机了。至于抽象模型,和具体的理解,我想小伙伴们可以先自行思索一下。

【以下内容可能比较啰里八嗦,想直接拿来面试的小伙伴请直接移步 1.5 节】

1.1 第一感觉

其实第一眼看到这个名词,我们就应该有一个模糊的猜想了,它应该是基于 SpringFramework 的工程的运行时环境。所以我们可以这样看待我们编写的基于 SpringFramework 的应用程序:

Spring IOC高级-Environment抽象

这个概念的理解,在 SpringBoot 源码的小册中也有提过,当时也是画了这样一个概念图来帮助小伙伴们理解。

这个理解是否正确呢?我们可以去官方文档加以验证。

1.2 官方文档的描述

当我们去翻 SpringFramework 的官方文档时,会发现官方是这样概述 Environment 的:

docs.spring.io/spring/docs…

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 与工程的结构应该是这样才对:

Spring IOC高级-Environment抽象

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

1.3 小册一家之言

作者个人是倾向于如下的结构理解,这样解释起来会相对更合理一些:

  • 首先,Environment 中包含 profiles 和 properties ,这些配置信息会影响 IOC 容器中的 bean 的注册与创建;
  • 其次,Environment 的创建是在 ApplicationContext 创建后才创建的( IOC 原理部分会解释),所以 Environment 应该是伴随着 ApplicationContext 的存在而存在;
  • 第三,ApplicationContext 中同时包含 Environment 和组件 bean ,而且从 BeanFactory 的视角来看,Environment 也是一个 Bean ,只不过它的地位比较特殊。

基于这三点,Environment 与工程的结构应该是下图这样的:

Spring IOC高级-Environment抽象

理解了两者的结构及关系,再来回头看看 Environment 的组成部分:profiles 和 properties ,咱之前也都了解过了,所以 Environment 的整体理解也就相对没有那么难了吧!

1.4 javadoc中的描述

上面并没有引用 Environment 的 javadoc 来阐述 Environment 的概念和定义,原因是 javadoc 并没有对 Application 和 Environment 之间的关系进行描述,所以小册选择在这里再贴出。

由于 javadoc 的篇幅太长,咱们拆解开来看。

1.4.1 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 这个接口,这个接口负责解析占位符( ${…} )对应的值,作用也比较容易理解,这里就不多展开解释啦。

1.4.2 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 的作用是决定当前哪些配置文件(如果有)处于活动状态,以及默认情况下哪些配置文件(如果有)应处于活动状态。

通过前面 18 章条件装配的学习,这段文档注释也就不难理解了吧。Environment 配合 profile 可以完成指定模式的环境的组件装配,以及不同的配置属性注入。

1.4.3 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 ,它们的来源有很多种,而最终的作用都是提供了属性配置,或者给组件注入属性值

1.4.4 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 的获取时,直接使用占位符就可以获取了。

1.4.5 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 应用的启动流程或者运行中进行一些额外的扩展或者修改。

到这里,整个 javadoc 也就读完了,最后总结一下吧,这部分最终还是要理解,并且最好用自己的话概括出来。

1.5 【面试题】面试中如何概述Environment

以下答案仅供参考,可根据自己的理解调整回答内容:

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

2. Environment的结构【了解】

了解了概念和设计的思想,下面咱来看看 Environment 在 SpringFramework 中设计的结构。

借助 IDEA ,可以看到 Environment 的上下级继承和派生关系:

Spring IOC高级-Environment抽象

这里面的几个重要的接口和类关注一下。

2.1 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 可以获取配置元信息,同时也可以解析占位符的信息

2.2 ConfigurableEnvironment

老套路了,看到 Configurable 开头,立马应该想到它又扩展了什么 set 方法吧!果然,可以从接口的方法定义中看到这样的方法名:setActiveProfiles 、addActiveProfile 。。。好了不用说了,这个接口一定可以用编程式设置 profile !

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

    MutablePropertySources getPropertySources();

看方法名可以知道它会返回所有的 PropertySource 对象,可是 MutablePropertySources 是个什么鬼呢?点开它的源码,发现它的内部就是一个 List :

public class MutablePropertySources implements PropertySources {

	private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();

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

2.3 StandardEnvironment

它是 SpringFramework 中默认使用的标准运行时环境的抽象实现,不过它里面的方法实现的非常少,基本都是由 AbstractEnvironment 负责实现。在后面的原理部分,我们还会再见到它的,这里先留意一下就好。

3. Environment的基本使用【熟悉】

虽说 Environment 不建议直接在应用程序中使用,但是部分场景下还是需要直接接触它来操纵。本节小册不会直接介绍 Environment 的实际使用,而是先带小伙伴用最简单的方式用一用,体会一下 Environment 的作用即可。

本章源码均在 com.linkedbear.spring.environment

3.1 获得Environment的API

既然 Environment 存在于 ApplicationContext 中,那么获取 Environment 的方式自然也就可以想到:@Autowired 就可以吧!下面咱来实际操作一下。

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

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

之后直接使用包扫描的方式,驱动 AnnotationConfigApplicationContext ,就可以取到刚写的这个 EnvironmentHolder 了:

public class EnvironmentQuickstartApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
                "com.linkedbear.spring.environment.a_quickstart.bean");
        EnvironmentHolder environmentHolder = ctx.getBean(EnvironmentHolder.class);
        environmentHolder.printEnvironment();
    }
}

运行 main 方法,控制台会打印出 Environment 的对象信息:

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

除此之外,联想到 BeanFactory 、ApplicationContext 的注入方式还有回调注入,作为 SpringFramework 的内置 API ,估计也会有一个 Aware 回调注入的接口吧!那自然是必须的,EnvironmentAware 就是回调注入的接口,小伙伴们可以自行实现操作一下,小册这里就不示范了(实在是简单的一批)。

注:使用 @Autowired 的方式在某些情况下会注入失败,所以对于小伙伴们而言,注入是否能成功需要亲手测试运行检验才能知道。在后面的后置处理器部分,会演示一种无法使用 @Autowired 注入 Environment 的方式,小伙伴们到时候可以留意一下。

3.2 使用Environment获取配置属性的值

既然上面已经获取到 Environment 了,那操作 Environment 的方法自然也就不是问题了。

为了方便获取 properties 的配置信息,这里编写一个配置类,把上一章 PropertySource 的 jdbc.properties 加载进去:

@Configuration
@ComponentScan("com.linkedbear.spring.environment.b_api.bean")
@PropertySource("propertysource/jdbc.properties")
public class EnvironmentPropertyConfiguration {
    
}

之后使用该配置类驱动 IOC 容器即可。

EnvironmentHolder 中,这次我们取一下默认的 profiles ,以及 jdbc.properties 中的配置属性值:

@Component
public class EnvironmentHolder {
    
    @Autowired
    Environment environment;
    
    public void printEnvironment() {
        System.out.println(Arrays.toString(environment.getDefaultProfiles()));
        System.out.println(environment.getProperty("jdbc.url"));
    }
}

重新驱动 IOC 容器,并取出 EnvironmentHolder 执行 printEnvironment 方法,控制台打印如下信息:

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

至此,Environment 的功能已正常使用。

Environment 中其他的 API ,小伙伴都可以自己动手使用一下,小册就不展开举例了。下面我们来向更深层次研究几个问题。

4. Environment深入探讨【原理】

先注意一下上面控制台打印的默认 profiles ,发现它有一个默认值是 default ,它是从哪来的呢?我们又没声明呀!

4.1 Environment的默认profiles

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

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

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

4.1.1 SpringFramework中的方法命名规范【重要】

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

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

看这个方法的实现,整体逻辑也不算复杂,关键是看它取框架默认的 profiles ,它其实就是取的 AbstractEnvironment 中内置的常量:

protected static final String RESERVED_DEFAULT_PROFILE_NAME = "default";

protected Set<String> getReservedDefaultProfiles() {
    return Collections.singleton(RESERVED_DEFAULT_PROFILE_NAME);
}

由此,可知默认的 default 来源。

4.1.3 覆盖默认的profiles方法

上面的源码中,也可以看到,我们可以通过声明 spring.profiles.default 的配置,来覆盖 SpringFramework 中原有的默认 profiles ,一个比较常用的方法是在 jvm 的启动参数上添加:

Spring IOC高级-Environment抽象

在 IDEA 的启动配置中,声明 VM options 就可以指定默认的 profiles 了。

同理,指定激活的 profiles 也可以像这样指定,只不过它的参数名称为 spring.profiles.active 。

4.2 Environment解析properties的底层

前面看 Environment 的结构,我们已经知道 Environment 继承了父接口 PropertyResolver ,自然它拥有解析配置元信息的能力,它的底层是如何实现的呢?是 Environment 自己干活,还是…有其人?

4.2.1 PropertyResolver的实现类

借助 IDE ,翻看 PropertyResolver 的子接口和实现类,发现仅仅就这么几个而已:

Spring IOC高级-Environment抽象

自然地,我们要去找 Environment 的实现类,StandardEnvironment ,看它是如何解析配置属性值的。

4.2.2 getProperty的实现是委派

翻看 StandardEnvironment ,发现 getProperty 方法并没有在此实现,而是父类 AbstractEnvironment 中,但是实现类中发现它是直接调用了自身组合的一个 ConfigurablePropertyResolver 来处理(果然环境本身不适合干这个事,得让专门的组件来干):

private final ConfigurablePropertyResolver propertyResolver =
        new PropertySourcesPropertyResolver(this.propertySources);

@Override
@Nullable
public String getProperty(String key) {
    return this.propertyResolver.getProperty(key);
}

一般的,我们称这种方式叫做 “委派” ,它与代理、装饰者不同:委派仅仅是将方法的执行转移给另一个对象,而代理可能会在此做额外的处理,装饰者也会在方法执行前后做增强

继续往里看,就要进入 PropertySourcesPropertyResolver 的底层来研究了,小册认为再往里研究的性价比相对就不高了:这里面的解析逻辑相对复杂,但搞明白后的收益并不大,综合来看不太适合再深入研究。小伙伴们只需要了解 Environment 的解析配置属性值的底层是交给 PropertySourcesPropertyResolver 来处理就好啦。

免责声明:
1.本站所有内容由本站原创、网络转载、消息撰写、网友投稿等几部分组成。
2.本站原创文字内容若未经特别声明,则遵循协议CC3.0共享协议,转载请务必注明原文链接。
3.本站部分来源于网络转载的文章信息是出于传递更多信息之目的,不意味着赞同其观点。
4.本站所有源码与软件均为原作者提供,仅供学习和研究使用。
5.如您对本网站的相关版权有任何异议,或者认为侵犯了您的合法权益,请及时通知我们处理。
火焰兔 » Spring IOC高级-Environment抽象