Spring 中的 Environment 对象

可参考官网:Environment Abstraction

核心概念

Environment 对象对两个关键方面进行建模:profiles属性(properties)

Profile

简单来说:profile 机制在 IOC 容器中提供了一种机制:允许在不同的环境中注册不同的bean。即对于一个bean,如果你想在情况A中注册它的一种类型,而在情况B中注册另一种类型,这时你就可以使用 profile

使用 @Profile

考虑一下实际应用,我们需要一个 DataSource。在 开发环境 中,我们希望IOC容器中它的类型是 DataSourceImpl1,而在 生产环境 中,我们希望IOC容器中它的类型是 DataSourceImpl2。

我们可以定义如下两个配置类:

java 复制代码
@Configuration
@Profile("development")
public class DevelopmentDataConfig {
    
    @Bean
    public DataSource dataSource1() {
        return new DataSourceImpl1(); // 伪代码
    }
}

@Configuration
@Profile("production") 
public class ProductionDataConfig {

    @Bean
    public DataSource dataSource2() {
        return new DataSourceImpl2(); // 伪代码
    }
}

@Profile 注解可以在 @Bean 方法上使用

激活一个 Profile

激活一个 profile 可以通过几种方式进行,但最直接的是 以编程方式 对环境API进行激活,该API可以通过 ApplicationContext 获得。下面的例子显示了如何做到这一点:

java 复制代码
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development"); // 激活 "development" 对应的 profile(可以指定多个)
ctx.register(AppConfig.class, DevelopmentDataConfig.class, ProductionDataConfig.class); 
ctx.refresh();

此外,你还可以通过 spring.profiles.active 属性以声明方式激活配置文件,该属性可以通过 系统环境变量、JVM 系统属性、web.xml 中的 servlet 上下文参数 (这些参数其实都会以 "属性源PropertySource" 的形式保存在 Environment 中,包括 SpringBoot 应用中 yml 中配置的参数) 来指定

如,通过 JVM系统属性指定:

powershell 复制代码
-Dspring.profiles.active="development"   #可以指定多个profile

这样一来,IOC 容器在启动时,就会注入 development 环境所需要的 DataSourceImpl1 bean对象

默认 Profile

如果没有活动的 profile,那么 Spring 底层会启用默认的 profile("default")

java 复制代码
@Configuration
@Profile("default") // 系统默认会启用"default"对应的profile
public class DefaultDataConfig {

    @Bean
    public DataSource dataSource() {
        return new DataSourceImpl3();
    }
}

以上配置表明:即使你没有激活任何 profile ,DataSourceImpl3 也会被注入到容器中

如果任何 profile 被启用,默认的profile就不应用。

你可以通过在环境中使用 setDefaultProfiles() 来改变默认配置文件的名称,或者通过声明性地使用 spring.profiles.default 属性。

属性(properties)

在 Spring 中,使用 PropertySource 对象对属性进行抽象。PropertySource 是对 "任何键值对源" 的简单抽象,提供了统一存储外部化配置数据的功能,例如上面配置的JVM系统属性就会被封装为 PropertySource 对象(可参考其 javadoc)

Environment 对象中保存了一系列 PropertySource 集合,可以使用 Environment 对 PropertySource 进行搜索获取:

java 复制代码
ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
boolean flag = env.containsProperty("my-property");
System.out.println(flag); // 当前环境中是否定义了 my-property 属性?

Spring 的 StandardEnvironment 对象保存了两个 PropertySource 对象------ 一个表示 JVM 系统属性集合(System.getProperties()) ,另一个表示系统环境变量集合(System.getenv())

这些默认属性源存在于 StandardEnvironment 中,用于独立的应用程序中。

而其子类 StandardServletEnvironment 中填充了其他默认属性源,包括 Servlet config、Servlet context 参数和 JndiPropertySource(如果 JNDI 可用)。

执行的搜索是分层次的。默认情况下,系统属性(system properties)优先于环境变量(environment variables)

因此,如果在调用 env.getProperty("my-property") 时,my-property 属性恰好在两个地方都被设置了,那么系统属性值 "胜出" 并被返回。请注意,属性值不会合并,而是被前面的条目完全覆盖。

我们可以定义自己的 PropertySource 加入到当前 Environment 中,并且可以指定其位置(搜索优先级)

java 复制代码
ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());  // 加入到最前面(优先级最高)

源码解析

先看 Environment 的体系架构:

我们可以从 AbstractEnvironment 类入手:

当一个组件Xxx(接口)体系很复杂时,从它具体的落地实现类开始分析是十分复杂的,可以考虑从它的抽象实现类:AbstractXxx 入手(内部一定定义了某些重要方法的抽象实现,和一些重要的成员属性)

AbstractEnvironment 中几个比较重要的成员属性:

java 复制代码
private final Set<String> activeProfiles = new LinkedHashSet<>(); // 保存激活的Profile
private final Set<String> defaultProfiles = new LinkedHashSet<>(getReservedDefaultProfiles()); // 保存默认的 Profile
private final MutablePropertySources propertySources;   // 存放一系列 PropertySource
private final ConfigurablePropertyResolver propertyResolver; // 属性解析器(如解析占位符${})

其中 MutablePropertySources 对象维护着一个 PropertySource 列表,外部可以通过 get、contains、addFirst 等方法对其进行访问

对外暴露的与 profile 相关的 API 方法(可自行查看方法内部逻辑):

java 复制代码
getDefaultProfiles(); // 获取默认的 Profile
setDefaultProfiles(); // 设置默认的 Profile
getActiveProfiles(); // 获取激活的 Profile
setActiveProfiles(); // 设置激活的 Profile

所以说:Environment 对象对两个关键方面进行建模:Profile 和 PropertySource(属性源)

当我们使用 SpringBoot 进行传统的 Web 开发时,应用环境为 SERVLET ,默认使用的 Environment 实现为:ApplicationServletEnvironment

源码位置:run() ---> prepareEnvironment() ---> getOrCreateEnvironment()

java 复制代码
	private ConfigurableEnvironment getOrCreateEnvironment() {
		if (this.environment != null) {
			return this.environment;
		}
		switch (this.webApplicationType) {
			case SERVLET:
				return new ApplicationServletEnvironment();
			case REACTIVE:
				return new ApplicationReactiveWebEnvironment();
			default:
				return new ApplicationEnvironment();
		}
	}

就是直接使用无参构造创建其对象:new ApplicationServletEnvironment(),分析这个动作都发生了什么

ApplicationServletEnvironment 的无参构造中会调父类的无参构造(默认有super.())... 一直会调用到 AbstractEnvironment 的无参构造:

java 复制代码
public AbstractEnvironment() {
    this(new MutablePropertySources()); 
}

protected AbstractEnvironment(MutablePropertySources propertySources) {
    this.propertySources = propertySources; // 新 new 的 MutablePropertySources
    this.propertyResolver = createPropertyResolver(propertySources);
    customizePropertySources(propertySources); // 模板方法,提供给子类实现(子类向propertySources里面添加一些PropertySource)
}

套路:在分析父类预留给子类实现的方法时,要定位到此方法 "最后一次的重写位置"

子类 StandardServletEnvironment # customizePropertySources 的实现:(customizePropertySources 方法最后的重写位置(StandardServletEnvironment 一系中))

java 复制代码
@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
    // 向 propertySources 中加入了几个 PropertySource :"servletConfigInitParams"、"servletContextInitParams"、"jndiProperties"(根据情况) 
    propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME));
    propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME));
    if (jndiPresent && JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
        propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME));
    }
    // 回调父类 StandardEnvironment # customizePropertySources (父类 StandardEnvironment再向propertySources中加入几个PropertySource)
    super.customizePropertySources(propertySources);
}

StandardEnvironment # customizePropertySources

java 复制代码
@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
    // 向 propertySources 中加入了几个 PropertySource :"systemProperties"、"systemEnvironment"
    propertySources.addLast(new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
    propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
}

所以在 ApplicationServletEnvironment 对象创建完成之后,它所持有的 propertySources 中至少保存了 "servletContextInitParams"、"servletConfigInitParams"、"systemEnvironment"、"systemProperties" 四个 PropertySource

(待...)

相关推荐
Amarantine、沐风倩✨5 分钟前
设计一个监控摄像头物联网IOT(webRTC、音视频、文件存储)
java·物联网·音视频·webrtc·html5·视频编解码·七牛云存储
路在脚下@1 小时前
spring boot的配置文件属性注入到类的静态属性
java·spring boot·sql
啦啦右一1 小时前
Spring Boot | (一)Spring开发环境构建
spring boot·后端·spring
森屿Serien1 小时前
Spring Boot常用注解
java·spring boot·后端
苹果醋32 小时前
React源码02 - 基础知识 React API 一览
java·运维·spring boot·mysql·nginx
Hello.Reader3 小时前
深入解析 Apache APISIX
java·apache
盛派网络小助手3 小时前
微信 SDK 更新 Sample,NCF 文档和模板更新,更多更新日志,欢迎解锁
开发语言·人工智能·后端·架构·c#
菠萝蚊鸭3 小时前
Dhatim FastExcel 读写 Excel 文件
java·excel·fastexcel
旭东怪3 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
007php0073 小时前
Go语言zero项目部署后启动失败问题分析与解决
java·服务器·网络·python·golang·php·ai编程