《Spring Boot 3:高级与架构设计》第 2 章 IOC容器的高级机制 Environment 阅读笔记 4
在入门篇中,接触了大量关于 ApplicationContext 的操作;
在第 1 章中,深入 ApplicationContext 组合的 BeanFactory 中了解了 BeanDefinitionRegistry 等设计。
本章会接触更多 ApplicationContext 中组合的 API 与运行机制,这些机制在深层次开发与定制中往往发挥非常有效的作用。
想要深挖 Spring Framework 内部工作原理的读者一定要仔细学习本章的内容。
2.1 Environment
在入门篇中讲解过使用 properties 文件、YML 文件加载到 ApplicationContext 的内部,当时使用的注解是 @PropertySource。读者在当时的阅读中是否想到过一个问题:这些配置项的键值对都保存到 IOC 容器的什么区域?使用 @Value 完成属性赋值时又是如何从 IOC 容器中获取的?带着这些疑问,开始认识本章第一个重要 API:Environment。
2.1.1 Environment 概述
Environment 是从 Spring Framework 3.1 开始引入的抽象模型,称为 "运行时环境" 。Environment 本身的设计比较特殊,具体可以有以下 3 点理解。
(1)Environment 中包含两个重要元素 profiles 和 properties,这些配置信息会影响 IOC 容器中 Bean 的注册与创建。
(2)Environment 在 ApplicationContext 创建后才创建,所以 Environment 应该是伴随着 ApplicationContext 的存在而存在的。
(3)ApplicationContext 中同时包含 Environment 和组件 Bean,而且从 BeanFactory 的视角来看,Environment 也是一个Bean,只是其地位非常特殊。
基于以上 3 点理解,可以得到 ApplicationContext 与 Environment 等成员的结构关系,如图所示。虽然 Environment 也属于 BeanFactory 内部的一个Bean,但是将 Environment 划分在 IOC 容器的组件以外,是考虑到 Environment 对象本身组合在 ApplicationContext 中,它的地位非常特殊;Environment 对组件 Bean 有提供配置属性值的功能,所以图中有一个从 Environment 指向组件 Bean 的箭头。

Environment 的 javadoc 比较长,本节仅讲解关键信息。读者可以借助 IDE 自行阅读完整的 javadoc。

简单总结,Environment 是 Spring Framework 3.1 引入的抽象的概念,它包含 profiles 和 properties 的信息,可以实现统一的配置存储和注入、配置属性的解析等。其中 profiles 实现了一种基于模式的环境配置,properties 则多应用于外部化配置。
2.1.2 Environment 的结构
了解 Environment 的概念设计后,下面结合 Environment 的结构与继承关系来探索 Environment 在 Spring Framework 中的设计地位。借助IDEA 可以形成 Environment 的类继承结构关系,如图所示。观察图可以得到两个简单的结论:
(1)Environment 不是顶层根接口;
(2)Environment 的落地实现有多种方式,分别适用于不同的场景。


1. PropertyResolver
从类名上看,PropertyResolver 是一个属性解析器,它可以处理 XML 配置文件、注解配置类、普通Bean中使用到的 ${ } 属性配置占位符。Environment 继承了 PropertyResolver,并且 Environment 中存放了 profiles 和 properties 属性配置,
这意味着 PropertyResolver 可以解析 ${ },并提取出 Environment 中的 properties 配置以完成赋值操作。

2. ConfigurableEnvironment
学习过入门篇中的 BeanFactory 与 ApplicationContext 后,读者对 Configurable 这个前缀应该不陌生:Configurable 前缀意味着这个类/接口具备可配置的能力,接口中大概率会有 set/add 之类的方法。
借助 IDE 浏览其核心方法,如代码所示。前 3 个方法不难理解,即可以编程式地设置 activeProfiles 和 defaultProfiles;

最后一个方法 getPropertySources 比较特殊,它可以用来获取所有的 PropertySource 对象,但返回值的类型是 MutablePropertySources。
这个类型内部组合了一个 List,并使用更合理方便的 API 予以封装。


由此又可以得出一个小结论:Mutable 开头的类名通常是一个类型的 List 组合封装。
PropertySource 可以简单理解为一个 PropertySource 对象对应了一个配置源,这个配置源可能来自 properties 文件,可能来自系统环境变量、项目信息,也可能是自定义的加载方式(如数据库)。但无论其来自哪里,最终都会被封装为 key-value 的形式,存储在一个 PropertySource 对象中,统一归 Environment 管理。
3. AbstractEnvironment
所有 Environment 落地实现的抽象父类是 AbstractEnvironment,其部分核心成员如代码所示。之所以看抽象类不看实现类,是因为实现类中的代码量相当少,而且没有很重要的逻辑,大多数实现类都在 AbstractEnvironment 中体现。此处,了解 AbstractEnvironment 的核心设计即可。

代码所列的是 AbstractEnvironment 的部分核心成员,其存储默认的 profile 和声明激活的所有 profile,以及所有的 PropertySource。
此外,请读者注意另外一个很重要的成员,即 PropertySourcesPropertyResolver,它也是一个 PropertyResolver。
AbstractEnvironment 在此处组合了一个 PropertyResolver 的实现类,意图也很明显:只要是 PropertyResolver 接口下的方法要做的工作,全部交予 PropertySourcesPropertyResolver 代替完成。
这种设计称为"委派",它与代理、装饰者不同:委派仅仅是将方法的工作执行转移给另一个对象,而代理可能会在此做额外的处理,装饰者也会在方法执行前后做增强处理。
在笔者的另一本图书《Spring Boot源码解读与原理分析》的第 12 章中,讲解 DispatcherServlet 的工作流程时也反复提到过委派机制,感兴趣的读者可以移步阅读。
2.1.3 Environment 的基本使用
虽然不建议直接在应用程序中使用 Environment,但是在少部分场景下还是需要直接接触并操纵它。此处我们先了解 Environment 的简单使用方式,然后在 2.1.4 节进行扩展。
1. 获得 Environment 的API
既然 Environment 存在于 ApplicationContext 中,那么获取 Environment 的方式之一就是使用 @Autowired。下面通过一个简单示例来获取Environment。先编写一个 Environment 的持有器 EnvironmentHolder,使用 @Autowired 声明注入 Environment,随后使用注解驱动的 IOC 容器进行组件扫描并获取 EnvironmentHolder,如代码所示。

java
package com.yangjunbo.spring.environment.examplea.bean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
@Component
public class EnvironmentHolder {
@Autowired
private Environment environment;
public void printEnvironment() {
System.out.println(environment);
}
}
执行 main 方法后,控制台打印了 Environment 的基本信息,从输出的内容可以直观地了解到,Environment 中存放的基本信息包括 profiles 的信息、PropertySource 的组合信息。

java
package com.yangjunbo.spring.environment.examplea;
import com.yangjunbo.spring.environment.examplea.bean.EnvironmentHolder;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
/**
* ClassName: environmentAApplication
* Package: com.yangjunbo.spring.environment.examplea
* Description:
*
* @Author 杨钧博
* @Create 2026/9/8 9:20
* @Version 1.0
*/
public class environmentAApplication {
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
"com.yangjunbo.spring.environment.examplea.bean");
EnvironmentHolder environmentHolder = ctx.getBean(EnvironmentHolder.class);
environmentHolder.printEnvironment();
}
}

此外,由于 BeanFactory、ApplicationContext 的注入方式还有回调注入,作为 Spring Framework 的内置 API,Environment 自然也有一个配套的回调注入接口 EnvironmentAware。
2. 使用 Environment 获取配置属性的值
上一小节中已经获取了 Environment,接下来就可以调用 Environment 的方法。为了方便获取 properties 的配置信息,首先编写一个配置类,将入门篇 4.7 节中演示的 jdbc.properties 文件加载进来;随后从 EnvironmentHolder 中获取 Environment,并打印 jdbc.url 的值,如代码所示。

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

java
package com.yangjunbo.spring.environment.exampleb.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@Configuration
@ComponentScan("com.yangjunbo.spring.environment.exampleb.bean")
@PropertySource("environment/exampleb/jdbc.properties")
public class EnvironmentPropertyConfiguration {
}

java
package com.yangjunbo.spring.environment.exampleb.bean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
@Component
public class EnvironmentHolder {
@Autowired
private Environment environment;
public void printEnvironment() {
System.out.println(environment.getProperty("jdbc.url"));
}
}
重新驱动 IOC 容器,并取出 EnvironmentHolder,执行 printEnvironment 方法,控制台可以正确打印 properties 文件中 jdbc.url 对应的值,这就是 Environment 的基本使用方式。有关 Environment 中其他的 API 方法,读者可以自己动手测试和体验,本节不过多展开。

java
package com.yangjunbo.spring.environment.exampleb;
import com.yangjunbo.spring.environment.exampleb.bean.EnvironmentHolder;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
/**
* ClassName: EnvironmentBApplication
* Package: com.yangjunbo.spring.environment.exampleb
* Description:
*
* @Author 杨钧博
* @Create 2026/9/8 9:34
* @Version 1.0
*/
public class EnvironmentBApplication {
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext("com.yangjunbo.spring.environment.exampleb");
EnvironmentHolder environmentHolder = ctx.getBean(EnvironmentHolder.class);
environmentHolder.printEnvironment();
}
}
2.1.4 Environment 深入探讨
请读者观察 2.1.3 节第一个示例中控制台打印的默认 profiles,可见其中有一个默认值是 default,这个值没有显式声明,它由何而来?另外,为什么在没有定义其他内容的前提下,Environment 中还会保存两套 PropertySource?针对这些问题,接下来将逐一进行深入探究。

1. Environment 的默认 profiles
若想知道 Environment 中 profiles 的默认配置,就要进入 Environment 的抽象实现 AbstractEnvironment 中一探究竟。
从代码所示的 AbstractEnvironment 源码片段中可以得知,获取默认 profiles 使用的 getDefaultProfiles 方法底层调用了 doGetDefaultProfiles 方法。

这种设计在 Spring Framework 中大量出现和应用。
在 Spring Framework 的框架源码中,如果一个方法以 do 开头,并且去掉 do 后能找到一个与剩余名称一样的方法,
则其代表如下含义 "不以 do 开头的方法通常负责前置校验处理、返回结果封装,以 do 开头的方法才是真正执行逻辑的方法"。
下面继续深入探究 doGetDefaultProfiles 方法。结合源码与注释可以发现,该方法实现的整体逻辑并不复杂,关键是它取框架默认的 profiles,可知获取的就是 AbstractEnvironment 中内置的常量:RESERVED_DEFAULT_PROFILE_NAME,即 "default"。




需要补充的是,如果在应用启动时主动声明 spring.profiles.default 参数的配置属性值,则可以覆盖 Spring Framework 中原有的默认 profiles。一个比较常用的方法是在 JVM 的启动项中添加该参数,即使用图所示的方式覆盖,在 IDEA 的启动配置中声明 VMoptions。同理,指定激活的profiles 也可以使用相同的方式,只需配置属性名为 spring.profiles.active。

2. Environment 解析 properties 的底层
2.1.2 节在介绍 Environment 的结构时已经指出,Environment 继承了父接口 PropertyResolver,拥有解析配置元信息的能力。AbstractEnvironment 中组合了一个 PropertySourcesPropertyResolver,它与 Environment 实现了相同的 PropertyResolver 接口,以实现 "委派"的效果。


结合源码片段也可以看出,凡是 Environment 要处理配置属性获取的动作,就会转交给其内部的 PropertySourcesPropertyResolver 来完成。

3. Environment 在开发中的应用场景
简单分析源码后回到实际项目开发中考虑:Environment 在实际开发中有什么应用场景?
由于 Environment 本身保存了配置属性,因此它可被看作一个应用级或业务级配置库,用于存储一些与应用运行相关的配置参数(业务无关参数,如线程池大小、全局响应最大时间等),以及一些在应用运行期间通常不会改变的业务参数(如当前应用的版权信息、网站信息等)。
由于大多数 Web 应用开发的业务场景下都能直接或间接地获取 IOC 容器,自然也就可以获取 Environment,因此将这些配置属性存放到Environment 后,应用运行的大多数位置都可以获取这些配置属性值,从而支撑业务或功能运行。