什么是多环境?
任何一个应用级框架都会存在多环境问题,何谓多环境?正常来说,我们在项目开发过程中会经历开发环境、测试环境、灰度环境、生产环境等,以数据库配置为例,不同环境的数据库连接配置是不一样的,程序启动时需要根据当前环境获取相应的配置,这就是多环境,同一个资源,在不同的环境下拥有不同的版本,需要程序根据当前环境获取到相应的版本资源。
@Profile 的用法
@Profile 注解可以声明当前 Bean 的适用环境,为一个接口的两个实现分别声明不同的 Profile。
public class Config {
public static interface DSConfig {
void init();
}
@Profile("dev")
@Component
public static class DevDSConfig implements DSConfig {
public void init() {
System.out.println("init dev ds config");
}
}
@Profile("prod")
@Component
public static class ProdDSConfig implements DSConfig {
public void init() {
System.out.println("init prod ds config");
}
}
}
通过 spring.profiles.active 参数设置当前容器的环境,也可以通过启动参数 -Dspring.profiles.active 的方式进行设置。
public class EnvironmentTest {
public static void main(String[] args) {
System.setProperty("spring.profiles.active", "dev");
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.scan(EnvironmentTest.class.getPackage().getName());
context.refresh();
// 使用接口类型获取 Bean
Config.DSConfig bean = context.getBean(Config.DSConfig.class);
// 输出 init dev ds config
bean.init();
}
}
如果两个实现类上不加 @Profile 注解,程序会报 NoUniqueBeanDefinitionException 异常。
SpringBoot 多环境配置
为 dev 及 test 两个不同的环境分别创建一份配置文件 application-dev.yml 及 application-test.yml。
# application-dev.yml
profileName: dev
# application-test.yml
profileName: test
在配置文件 application.yml 中配置 spring.profiles.active 属性,设置当前环境
spring:
profiles:
active: dev
在 SpringBoot 项目中获取属性,可以看到在 SpringBoot 项目中会根据环境自动加载相应配置文件中的属性。
@SpringBootApplication
public class Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);
// 输出 dev
System.out.println(context.getEnvironment().getProperty("profileName"));
}
}
Environment 源码解析
Spring 将环境抽象为 Environment 接口,主要处理两件事情:
- 管理 Profile
- 获取 Property,这部分功能依赖于 PropertySource 配置源,在其基础上增加了对占位符的处理以及类型转换
对 PropertySource 如何加载配置不懂的同学,可以查看 Spring源码学习 | PropertySource配置源加载
Environment 继承体系
Environment 接口继承自 PropertyResolver 接口,PropertyResolver 接口定义的是一个属性解析器,可以获取属性值,并对属性值进行类型转换,同时还定义了对占位符属性的支持。
public interface PropertyResolver {
/**
* 是否包含某个属性
*/
boolean containsProperty(String key);
/**
* 获取某个属性的值
*/
String getProperty(String key);
/**
* 获取某个属性的值,如果不存在,则设置为默认值
*/
String getProperty(String key, String defaultValue);
/**
* 获取某个属性的值,并转换为指定类型
*/
<T> T getProperty(String key, Class<T> targetType);
/**
* 获取某个属性的值,并转换为指定类型,如果不存在,则设置为默认值
*/
<T> T getProperty(String key, Class<T> targetType, T defaultValue);
/**
* 获取某个属性的值
*/
String getRequiredProperty(String key) throws IllegalStateException;
/**
* 获取某个属性的值,并转换为指定类型
*/
<T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException;
/**
* 解析占位符 ${...}
*/
String resolvePlaceholders(String text);
/**
* 解析占位符 ${...}
*/
String resolveRequiredPlaceholders(String text) throws IllegalArgumentException;
}
Environment 接口本身定义了 Profile 相关方法
public interface Environment extends PropertyResolver {
/**
* 获取当前激活的 profile
*/
String[] getActiveProfiles();
/**
* 获取默认的 profile
*/
String[] getDefaultProfiles();
/**
* 获取当前可用的 profile
*/
boolean acceptsProfiles(Profiles profiles);
}
ConfigurablePropertyResolver 接口对 PropertyResolver 接口进行了扩展,提供配置功能,支持设置 ConversionService(用于属性类型转换),另外提供了一些占位符相关的设置方法。
public interface ConfigurablePropertyResolver extends PropertyResolver {
/**
* 获取 ConversionService,用于类型转换
*/
ConfigurableConversionService getConversionService();
void setConversionService(ConfigurableConversionService conversionService);
/**
* 设置占位符开始字符
*/
void setPlaceholderPrefix(String placeholderPrefix);
/**
* 设置占位符结束字符
*/
void setPlaceholderSuffix(String placeholderSuffix);
/**
* 设置占位符分隔符
*/
void setValueSeparator(@Nullable String valueSeparator);
/**
* 在 getProperty 时是否忽略无法解析的占位符
*/
void setIgnoreUnresolvableNestedPlaceholders(boolean ignoreUnresolvableNestedPlaceholders);
/**
* 设置必须的属性,用于 validateRequiredProperties 验证时使用
*/
void setRequiredProperties(String... requiredProperties);
void validateRequiredProperties() throws MissingRequiredPropertiesException;
}
ConfigurableEnvironment 接口继承了 Environment 及 ConfigurablePropertyResolver。
这是 Spring 的一贯设计手法,每个接口只提供一个最小的功能集合,然后逐步扩展,最后由一个统一的接口实现分离出来的全部接口,形成一个完整的功能体,然后为其提供一个抽象实现类,实现基本功能。
public interface ConfigurableEnvironment extends Environment, ConfigurablePropertyResolver {
/**
* 设置当前激活的 profile
*/
void setActiveProfiles(String... profiles);
/**
* 增加一个当前激活的 profile
*/
void addActiveProfile(String profile);
/**
* 设置默认的 profile
*/
void setDefaultProfiles(String... profiles);
/**
* 获取 PropertySource 配置源
*/
MutablePropertySources getPropertySources();
/**
* 获取系统属性:System.getProperties()
*/
Map<String, Object> getSystemProperties();
/**
* 获取系统环境变量:System.getenv()
*/
Map<String, Object> getSystemEnvironment();
/**
* 环境合并,将 activeProfiles、defaultProfiles 及 propertySources 进行合并
*/
void merge(ConfigurableEnvironment parent);
}
AbstractEnvironment 是 ConfigurableEnvironment 接口的默认实现类,为以上接口设计的功能提供了默认实现。
首先看对 Profile 的管理,这部分的实现比较简单,根据代码中的注释就可以理解
public abstract class AbstractEnvironment implements ConfigurableEnvironment {
/**
* 几个属性配置的名称定义
*/
public static final String IGNORE_GETENV_PROPERTY_NAME = "spring.getenv.ignore";
public static final String ACTIVE_PROFILES_PROPERTY_NAME = "spring.profiles.active";
public static final String DEFAULT_PROFILES_PROPERTY_NAME = "spring.profiles.default";
// 默认 profile
protected static final String RESERVED_DEFAULT_PROFILE_NAME = "default";
// 当前激活的 profile 集合
private final Set<String> activeProfiles = new LinkedHashSet<>();
// 默认的 profile 集合
private final Set<String> defaultProfiles = new LinkedHashSet<>(getReservedDefaultProfiles());
protected Set<String> getReservedDefaultProfiles() {
return Collections.singleton(RESERVED_DEFAULT_PROFILE_NAME);
}
public String[] getActiveProfiles() {
return StringUtils.toStringArray(doGetActiveProfiles());
}
/**
* 首先查看本地缓存 activeProfiles 中是否有值
* 如果没有,则从配置文件中解析 spring.profiles.active 属性,并缓存至本地
*/
protected Set<String> doGetActiveProfiles() {
synchronized (this.activeProfiles) {
if (this.activeProfiles.isEmpty()) {
String profiles = getProperty(ACTIVE_PROFILES_PROPERTY_NAME);
if (StringUtils.hasText(profiles)) {
setActiveProfiles(StringUtils.commaDelimitedListToStringArray(
StringUtils.trimAllWhitespace(profiles)));
}
}
return this.activeProfiles;
}
}
// 设置 activeProfiles
public void setActiveProfiles(String... profiles) {
synchronized (this.activeProfiles) {
this.activeProfiles.clear();
for (String profile : profiles) {
validateProfile(profile);
this.activeProfiles.add(profile);
}
}
}
// 增加一个 activeProfile
public void addActiveProfile(String profile) {
validateProfile(profile);
doGetActiveProfiles();
synchronized (this.activeProfiles) {
this.activeProfiles.add(profile);
}
}
// 获取默认 profile
public String[] getDefaultProfiles() {
return StringUtils.toStringArray(doGetDefaultProfiles());
}
/**
* 首先查看本地缓存 defaultProfiles 中是否有值
* 如果没有,则从配置文件中解析 spring.profiles.default 属性,并缓存至本地
* 如果没有,则使用默认的 default
*/
protected Set<String> doGetDefaultProfiles() {
synchronized (this.defaultProfiles) {
if (this.defaultProfiles.equals(getReservedDefaultProfiles())) {
String profiles = getProperty(DEFAULT_PROFILES_PROPERTY_NAME);
if (StringUtils.hasText(profiles)) {
setDefaultProfiles(StringUtils.commaDelimitedListToStringArray(
StringUtils.trimAllWhitespace(profiles)));
}
}
return this.defaultProfiles;
}
}
}
然后看获取属性是如何实现的,AbstractEnvironment 定义了两个变量:
- MutablePropertySources,配置源集合,负责加载属性,只需要向该集合中添加配置源即可
- PropertySourcesPropertyResolver,属性解析器,支持占位符处理,支持属性类型转换
这里使用了一个巧妙的设计,AbstractEnvironment 初始化时会创建一个 MutablePropertySources 配置源集合,但此时该集合是空的,然后该集合又作为参数设置到了 PropertySourcesPropertyResolver 内部。AbstractEnvironment 在构造方法中调用了 customizePropertySources 方法,并传入了 propertySources,子类只需要实现 customizePropertySources 方法就可以往 propertySources 中添加配置源,而配置的读取、转换所有工作都在 PropertySourcesPropertyResolver 中进行实现。
对 PropertySource 如何加载配置不懂的同学,可以查看 Spring源码学习 | PropertySource配置源加载
public abstract class AbstractEnvironment implements ConfigurableEnvironment {
// 配置源
private final MutablePropertySources propertySources = new MutablePropertySources();
// 属性解析
private final ConfigurablePropertyResolver propertyResolver =
new PropertySourcesPropertyResolver(this.propertySources);
public AbstractEnvironment() {
customizePropertySources(this.propertySources);
}
protected void customizePropertySources(MutablePropertySources propertySources) {
}
}
PropertySourcesPropertyResolver 的实现在后面会详细介绍,现在先看看 AbstractEnvironment 的实现类。AbstractEnvironment 的实现类,唯一需要做的事情就是向 MutablePropertySources 中添加需要的配置源,并且保持顺序。
StandardEnvironment
向配置源添加 System.getenv() 及 System.getProperties() 两个配置源
public class StandardEnvironment extends AbstractEnvironment {
// 系统变量,System.getenv()
public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";
// JAVA属性,入参变量,System.getProperties()
public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";
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()));
}
}
StandardServletEnvironment
向配置源添加 ServletConfig 及 ServletContext 配置源,这里设置的是 StubPropertySource 空配置源,只是为了占个位置,方便 ServletContext 启动完成后替换为真正的配置源。
public class StandardServletEnvironment extends StandardEnvironment implements ConfigurableWebEnvironment {
public static final String SERVLET_CONTEXT_PROPERTY_SOURCE_NAME = "servletContextInitParams";
public static final String SERVLET_CONFIG_PROPERTY_SOURCE_NAME = "servletConfigInitParams";
public static final String JNDI_PROPERTY_SOURCE_NAME = "jndiProperties";
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);
}
public void initPropertySources(@Nullable ServletContext servletContext, @Nullable ServletConfig servletConfig) {
WebApplicationContextUtils.initServletPropertySources(getPropertySources(), servletContext, servletConfig);
}
}
PropertySourcesPropertyResolver 源码解析
AbstractPropertyResolver
AbstractPropertyResolver 是 ConfigurablePropertyResolver 接口的默认实现,AbstractEnvironment 也实现了 ConfigurablePropertyResolver 接口,但本身并没有实现功能,而是通过组合的方式委托给了 AbstractPropertyResolver 的实现类。
public abstract class AbstractPropertyResolver implements ConfigurablePropertyResolver {
// 数据类型转换,如果没有设置,则使用 DefaultConversionService
private volatile ConfigurableConversionService conversionService;
private PropertyPlaceholderHelper nonStrictHelper;
private PropertyPlaceholderHelper strictHelper;
private boolean ignoreUnresolvableNestedPlaceholders = false;
// 占位符,默认格式为 ${...}
private String placeholderPrefix = SystemPropertyUtils.PLACEHOLDER_PREFIX;
private String placeholderSuffix = SystemPropertyUtils.PLACEHOLDER_SUFFIX;
// 分隔符,用于设置默认值 ${a:1}
private String valueSeparator = SystemPropertyUtils.VALUE_SEPARATOR;
// 必须的属性,通过 validateRequiredProperties 方法检查属性必须存在
private final Set<String> requiredProperties = new LinkedHashSet<>();
// 不严格解析占位符
public String resolvePlaceholders(String text) {
if (this.nonStrictHelper == null) {
this.nonStrictHelper = createPlaceholderHelper(true);
}
return doResolvePlaceholders(text, this.nonStrictHelper);
}
// 严格解析占位符,如果解析不了占位符,抛异常
public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {
if (this.strictHelper == null) {
this.strictHelper = createPlaceholderHelper(false);
}
return doResolvePlaceholders(text, this.strictHelper);
}
/**
* 解析占位符
* 根据 ignoreUnresolvableNestedPlaceholders 选择是否严格解析
*/
protected String resolveNestedPlaceholders(String value) {
if (value.isEmpty()) {
return value;
}
return (this.ignoreUnresolvableNestedPlaceholders ?
resolvePlaceholders(value) : resolveRequiredPlaceholders(value));
}
// 创建占位符解析器
private PropertyPlaceholderHelper createPlaceholderHelper(boolean ignoreUnresolvablePlaceholders) {
return new PropertyPlaceholderHelper(this.placeholderPrefix, this.placeholderSuffix,
this.valueSeparator, ignoreUnresolvablePlaceholders);
}
// 占位符的解析使用 PropertyPlaceholderHelper 进行处理
private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) {
return helper.replacePlaceholders(text, this::getPropertyAsRawString);
}
/**
* 属性类型转换
*/
protected <T> T convertValueIfNecessary(Object value, @Nullable Class<T> targetType) {
if (targetType == null) {
return (T) value;
}
ConversionService conversionServiceToUse = this.conversionService;
if (conversionServiceToUse == null) {
if (ClassUtils.isAssignableValue(targetType, value)) {
return (T) value;
}
conversionServiceToUse = DefaultConversionService.getSharedInstance();
}
return conversionServiceToUse.convert(value, targetType);
}
// 获取原始值,不进行占位符解析
protected abstract String getPropertyAsRawString(String key);
}
PropertySourcesPropertyResolver
PropertySourcesPropertyResolver 获取属性的过程,可以分为三步:
- 第一步:轮询 PropertySources 获取属性
- 第二步:通过 resolveNestedPlaceholders 方法,解析占位符,解析的过程由 PropertyPlaceholderHelper 类负责
- 第三步:通过 conversionServiceToUse 对属性进行类型转换
public class PropertySourcesPropertyResolver extends AbstractPropertyResolver {
// 数据源,设置为 MutablePropertySources 对象
private final PropertySources propertySources;
public PropertySourcesPropertyResolver(@Nullable PropertySources propertySources) {
this.propertySources = propertySources;
}
public String getProperty(String key) {
return getProperty(key, String.class, true);
}
public <T> T getProperty(String key, Class<T> targetValueType) {
return getProperty(key, targetValueType, true);
}
protected String getPropertyAsRawString(String key) {
return getProperty(key, String.class, false);
}
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);
}
// 类型转换
return convertValueIfNecessary(value, targetValueType);
}
}
}
return null;
}
}