前言
前段时间有个业务需求,需要区分服务部署环境,来执行不同的代码逻辑。虽然之前使用过 Spring profile 提供的环境切换功能,但没有深入了解,所以也踩了许多坑,这篇主要是对 Spring profile 机制的分析总结。
分为两部分,源码分析及使用总结。本文基于 SpringBoot 源码(版本1.5.9.RELEASE)进行的分析。
源码分析
分析之前,先来回忆下 Spring-Boot 之前的 java web 工程结构,应该都会有一个 web.xml 这样的文件。这是旧版本 Servlet 规范的部署文件,管理着初始化参数、Servlet、Filter、Listener等主要组件的配置。
Servlet3 问世之后,支持注解方式配置以上组件。SpringBoot 便在自己的标准工程结构中移除了 web.xml 文件,取而代之的是使用代码、注解的配置组件。利用的就是 Servlet3 提供的扩展接口:
package javax.servlet;
import java.util.Set;
public interface ServletContainerInitializer {
void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}
Spring 对它进行实现:
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
List<WebApplicationInitializer> initializers = new LinkedList<WebApplicationInitializer>();
// 初始化:WebApplicationInitializer实现类
if (webAppInitializerClasses != null) {
for (Class<?> waiClass : webAppInitializerClasses) {
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
initializers.add((WebApplicationInitializer) waiClass.newInstance());
} catch (Throwable ex) {
throw new ServletException(
"Failed to instantiate WebApplicationInitializer class", ex);
}
}
}
}
...// 判空处理
servletContext.log(initializers.size() +
" Spring WebApplicationInitializers detected on classpath");
AnnotationAwareOrderComparator.sort(initializers);
// 遍历调用 WebApplicationInitializer.onStartup
// 将 Servelt容器上下文作为入参传入
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
}
}
这一步主要遍历调用所有 WebApplicationInitializer.onStartup 方法。该接口就是 Spring 提供的对旧版本 web.xml 内组件的代码配置支持。比如 “将SpringBoot由jar启动转为war部署”。来看下 SpringBoot 对该接口的实现:
public abstract class SpringBootServletInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
this.logger = LogFactory.getLog(getClass());
// 创建Spring上下文,spring-boot启动逻辑
WebApplicationContext rootAppContext = createRootApplicationContext(
servletContext);
...// 省略
}
protected WebApplicationContext createRootApplicationContext(
ServletContext servletContext) {
// new SpringApplicationBuilder,建造者模式创建 SpringApplication
SpringApplicationBuilder builder = createSpringApplicationBuilder();
// 继承自 AbstractEnvironment,会触发 customizePropertySources方法调用
StandardServletEnvironment environment = new StandardServletEnvironment();
// 调用 WebApplicationContextUtils.initServletPropertySources
environment.initPropertySources(servletContext, null);
builder.environment(environment);
builder.main(getClass());
ApplicationContext parent = getExistingRootWebApplicationContext(servletContext);
if (parent != null) {
this.logger.info("Root context already created (using as parent).");
servletContext.setAttribute(
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, null);
builder.initializers(new ParentContextApplicationContextInitializer(parent));
}
builder.initializers(new ServletContextApplicationContextInitializer(servletContext));
builder.contextClass(AnnotationConfigEmbeddedWebApplicationContext.class);
// jar转 war部署就是扩展此方法,将原本的入口类传入
builder = configure(builder);
// 进行一些列属性设置后创建 SpringApplication
SpringApplication application = builder.build();
// 如果没有设置 Sources,会默认搜索该类是否被 @Configuration注解标识(间接标记也算)
if (application.getSources().isEmpty() && AnnotationUtils
.findAnnotation(getClass(), Configuration.class) != null) {
application.getSources().add(getClass());
}
Assert.state(!application.getSources().isEmpty(),
"No SpringApplication sources have been defined. Either override the "
+ "configure method or add an @Configuration annotation");
// Ensure error pages are registered
if (this.registerErrorPageFilter) {
application.getSources().add(ErrorPageFilterConfiguration.class);
}
// 调用 SpringApplication.run
return run(application);
}
}
方法的最后会调用 SpringApplication.run 方法,分析它之前我们先看看影响 Profile 取值的 StandardServletEnvironment 是如何创建的。
环境预加载
该类作用是管理已生效的 Profile 以及全局加载的 PropertySource 集合。父类(AbstractEnvironment)的构造器会调用 customizePropertySources 方法。
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";
@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
// 对应 web.xml servlet <init-param>
propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME));
// 对应 web.xml <context-param>
propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME));
if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
// 支持 JNDI,例如:数据源
propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME));
}
super.customizePropertySources(propertySources);
}
@Override
public void initPropertySources(ServletContext servletContext, ServletConfig servletConfig) {
// 初始化 Servlet相关参数
WebApplicationContextUtils.initServletPropertySources(
getPropertySources(), servletContext, servletConfig);
}
}
public class StandardEnvironment extends AbstractEnvironment {
public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";
public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";
@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
// 通过 System.getProperties(),加载 JVM参数
propertySources.addLast(new MapPropertySource(
SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
// 通过 System.getenv(),加载系统环境变量
propertySources.addLast(new SystemEnvironmentPropertySource(
SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
}
}
从加载顺序可以看出,init-param > context-param > jvm参数 > 环境变量。这将会影响到属性取值的优先级。环境信息加载完成后,继续来看 SpringApplication.run 中是如何选择生效的 Profile 逻辑。
容器启动
该类承载了SpringBoot启动及相关的逻辑。
public class SpringApplication {
public ConfigurableApplicationContext run(String... args) {
.....
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(
args);
// 关注此方法
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
....// 省略
return context;
} catch (Throwable ex) {
handleRunFailure(context, listeners, analyzers, ex);
throw new IllegalStateException(ex);
}
}
private ConfigurableEnvironment prepareEnvironment(
SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) {
// 获取到刚才创建的 ConfigurableEnvironment
ConfigurableEnvironment environment = getOrCreateEnvironment();
// 配置环境
configureEnvironment(environment, applicationArguments.getSourceArgs());
listeners.environmentPrepared(environment);
if (!this.webEnvironment) {
environment = new EnvironmentConverter(getClassLoader())
.convertToStandardEnvironmentIfNecessary(environment);
}
return environment;
}
protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
// 加入 SpringApplication.setDefaultProperties设置的参数
// 加入 main方法入参
configurePropertySources(environment, args);
// profile生效
configureProfiles(environment, args);
}
protected void configureProfiles(ConfigurableEnvironment environment, String[] args) {
// 初始化调用一次
environment.getActiveProfiles();
// 设置 SpringApplication.setAdditionalProfiles方法设置的 Profile
Set<String> profiles = new LinkedHashSet<String>(this.additionalProfiles);
profiles.addAll(Arrays.asList(environment.getActiveProfiles()));
// 最后汇总所有生效的 Profile
environment.setActiveProfiles(profiles.toArray(new String[profiles.size()]));
}
}
Spring 提供了许多设置 Profile 的入口。包含了最传统的配置文件设置、SpringApplication对象方法设置、main方法入参设置等等。这些数据都会被整合进 ConfigurableEnvironment,以供后面的取值。
占位符解析
从这一步开始,就开始了获取生效的 spring profile 值的逻辑,包含占位符的解析、占位符嵌套处理、默认值等。
public abstract class AbstractEnvironment implements ConfigurableEnvironment {
// 缓存
private final Set<String> activeProfiles = new LinkedHashSet<String>();
private final ConfigurablePropertyResolver propertyResolver =
new PropertySourcesPropertyResolver(this.propertySources);
@Override
public String[] getActiveProfiles() {
return StringUtils.toStringArray(doGetActiveProfiles());
}
protected Set<String> doGetActiveProfiles() {
// 尝试获取缓存
synchronized (this.activeProfiles) {
if (this.activeProfiles.isEmpty()) {
// 获取:spring.profiles.active
String profiles = getProperty(ACTIVE_PROFILES_PROPERTY_NAME);
if (StringUtils.hasText(profiles)) {
// 设置缓存
setActiveProfiles(StringUtils.commaDelimitedListToStringArray(
StringUtils.trimAllWhitespace(profiles)));
}
}
return this.activeProfiles;
}
}
@Override
public String getProperty(String key) {
return this.propertyResolver.getProperty(key);
}
}
public class PropertySourcesPropertyResolver extends AbstractPropertyResolver {
public PropertySourcesPropertyResolver(PropertySources propertySources) {
this.propertySources = propertySources;
}
@Override
public String getProperty(String key) {
// 获取指定键的 String类型值
return getProperty(key, String.class, true);
}
// 参数:resolveNestedPlaceholders为 true时,说明需要以解析嵌套占位符
protected <T> T getProperty(String key, Class<T> targetValueType,
boolean resolveNestedPlaceholders) {
if (this.propertySources != null) {
for (PropertySource<?> propertySource : this.propertySources) {
if (logger.isTraceEnabled()) {
logger.trace("Searching for key '" + key + "' in PropertySource '" +
propertySource.getName() + "'");
}
// 从之前加载的属性键值对中寻找
Object value = propertySource.getProperty(key);
if (value != null) {
if (resolveNestedPlaceholders && value instanceof String) {
// 解析占位符
value = resolveNestedPlaceholders((String) value);
}
logKeyFound(key, propertySource, value);
return convertValueIfNecessary(value, targetValueType);
}
}
}
if (logger.isDebugEnabled()) {
logger.debug("Could not find key '" + key + "' in any property source");
}
return null;
}
}
默认的 resolveNestedPlaceholders 属性为 true,即需要解析嵌套占位符。比如:指定的 spring.profiles.active = ${test},它的值又指向了另一个属性键,Spring 需要一层一层解析下去。来看解析逻辑:
public abstract class AbstractPropertyResolver implements ConfigurablePropertyResolver {
protected String resolveNestedPlaceholders(String value) {
// 根据属性判断是否忽略不能解析的占位符
// 创建 PropertyPlaceholderHelper时指定 ignoreUnresolvablePlaceholders
// 最终都会调用 doResolvePlaceholders方法
return (this.ignoreUnresolvableNestedPlaceholders ?
resolvePlaceholders(value) : resolveRequiredPlaceholders(value));
}
private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) {
// 调用 PropertyPlaceholderHelper.replacePlaceholders
return helper.replacePlaceholders(text, new PropertyPlaceholderHelper.PlaceholderResolver() {
@Override
public String resolvePlaceholder(String placeholderName) {
return getPropertyAsRawString(placeholderName);
}
});
}
}
默认 ignoreUnresolvablePlaceholders 指定的为 true,即忽略不能解析的属性值。比如指定了 spring.profiles.active = ${test},如果没有找到另一个指定 test 的配置,那最终 spring.profiles.active 的值就是 '${test}'了。
doResolvePlaceholders方法里继续调用 PropertyPlaceholderHelper.replacePlaceholders,传入了匿名内部类,主要用于方法回调。下面会讲,先来看下 replacePlaceholders 方法内部实现:
public class PropertyPlaceholderHelper {
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
Assert.notNull(value, "'value' must not be null");
return parseStringValue(value, placeholderResolver, new HashSet<String>());
}
protected String parseStringValue(
String value, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) {
StringBuilder result = new StringBuilder(value);
// 获取占位符前置索引
int startIndex = value.indexOf(this.placeholderPrefix);
// 如果未找到,说明不存在需要替换的,直接跳过
while (startIndex != -1) {
// 找到占位符后置索引
int endIndex = findPlaceholderEndIndex(result, startIndex);
if (endIndex != -1) {
// 把占位符的属性key截取出来
String placeholder = result.substring(
startIndex + this.placeholderPrefix.length(), endIndex);
String originalPlaceholder = placeholder;
// 这个为了防止循环解析
if (!visitedPlaceholders.add(originalPlaceholder)) {
throw new IllegalArgumentException(
"Circular placeholder reference '"
+ originalPlaceholder + "' in property definitions");
}
// 递归调用,层层解析(因为占位符会嵌套)
placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
// 回调,下面会讲解
String propVal = placeholderResolver.resolvePlaceholder(placeholder);
// 如果没有找到属性对应的值,尝试解析默认值(例如${test:1})
if (propVal == null && this.valueSeparator != null) {
// 默认分隔符为“:”
int separatorIndex = placeholder.indexOf(this.valueSeparator);
if (separatorIndex != -1) {
// 截取获取属性key
String actualPlaceholder = placeholder.substring(0, separatorIndex);
// 截取获取默认值
String defaultValue = placeholder.substring(
separatorIndex + this.valueSeparator.length());
// 回调,下面会讲解
propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
if (propVal == null) {
// 如果解析失败使用默认值
propVal = defaultValue;
}
}
}
// 如果找到了属性对应的值
if (propVal != null) {
// 继续递归调用(因为值有可能也存在占位符)
propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
// 将占位符替换成解析出的值
result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
if (logger.isTraceEnabled()) {
logger.trace("Resolved placeholder '" + placeholder + "'");
}
startIndex = result.indexOf(
this.placeholderPrefix, startIndex + propVal.length());
} else if (this.ignoreUnresolvablePlaceholders) {
// 忽略目前的解析,继续处理未解析的值.
startIndex = result.indexOf(
this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
} else {
throw new IllegalArgumentException("Could not resolve placeholder '" +
placeholder + "'" + " in value \"" + value + "\"");
}
// 解析后移除待解析值
visitedPlaceholders.remove(originalPlaceholder);
} else {
startIndex = -1;
}
}
return result.toString();
}
}
以上的代码会递归调用,直到解析出的值不含占位符。这里面主要包含了默认值的设置,比如你设置 ${test :1},如果没有找到“test”对应的值,就会赋予默认值1。
回调获取
我们重新回到递归调用前的入口代码,如下:
public abstract class AbstractPropertyResolver implements ConfigurablePropertyResolver {
private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) {
return helper.replacePlaceholders(text, new PropertyPlaceholderHelper.PlaceholderResolver() {
@Override
public String resolvePlaceholder(String placeholderName) {
// 回调该方法
return getPropertyAsRawString(placeholderName);
}
});
}
}
其中匿名内部类的实现方法 resolvePlaceholder 会在递归调用中被回调。这个方法实现很简单,就是遍历所有的 PropertySource (按照加载的优先级),直到找出属性对应的值。
public class PropertySourcesPropertyResolver extends AbstractPropertyResolver {
@Override
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) {
// 遍历已加载的 PropertySource列表
for (PropertySource<?> propertySource : this.propertySources) {
if (logger.isTraceEnabled()) {
logger.trace("Searching for key '" + key + "' in PropertySource '" +
propertySource.getName() + "'");
}
// 获取属性值
Object value = propertySource.getProperty(key);
if (value != null) {
if (resolveNestedPlaceholders && value instanceof String) {
value = resolveNestedPlaceholders((String) value);
}
logKeyFound(key, propertySource, value);
// 必要时使用 DefaultConversionService对值进行转型
return convertValueIfNecessary(value, targetValueType);
}
}
}
if (logger.isDebugEnabled()) {
logger.debug("Could not find key '" + key + "' in any property source");
}
return null;
}
}
到此为之,已经将属性替换的源码分析完毕。接下来将对常用的 Profile 配置进行总结。
使用总结
1.Maven相关
<profiles>
<!--测试环境-->
<profile>
<id>dev</id>
<properties>
<!--此标签名称可自定义-->
<profileActive>dev</profileActive>
</properties>
<activation>
<!--默认此环境生效-->
<activeByDefault>true</activeByDefault>
</activation>
</profile>
</profile>
<!--生产环境-->
<profile>
<id>prod</id>
<properties>
<profileActive>prod</profileActive>
</properties>
</profile>
</profiles>
如果继承并使用了Spring-Boot的父POM默认配置,占位符默认为“@”,所以你可以在application.properties这么配:
spring.profiles.active = @profileActive@
当然也可以自定义占位符,配置插件:apache-resources-plugin。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<!--指定占位符-->
<delimiters>
<delimiter>${*}</delimiter>
</delimiters>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<!--指定true时,指定的文件中占位符会在打包时被替换-->
<filtering>true</filtering>
<includes>
<include>application.properties</include>
</includes>
</resource>
</resources>
</build>
指定之后,applicaiton.properties属性值修改为对应的占位符即可:
spring.profiles.active = ${profileActive}
最后使用打包命令打包,其中-P指定生效的环境(对应POM中配置的<profile>-<id>):
mvn package -Pprod
打包后,application.properties文件中的占位符会被替换为对应的值。
2.Servlet初始化参数
这里以SpringBoot实现为例,继承 SpringBootServletInitializer.onStartup 或实现 WebApplicationInitializer.onStartup,添加Servlet初始化参数即可。
public class WebStart extends SpringBootServletInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
//spring 环境配置
servletContext.setInitParameter("spring.profiles.active", "prod");
servletContext.setInitParameter("spring.profiles.default", "dev");
super.onStartup(servletContext);
}
}
3.JVM参数
-Dspring.profiles.active=prod
很eazy,指定即可。
4.环境变量
跟配置JDK环境变量相似,指定key为 spring.profiles.active,value为生效的环境即可。
5.SpringBoot
此种配置的优先级最低,是在上述的servlet初始化参数、jvm参数、环境变量都没找到的时候,才会生效。
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(Application.class);
application.setDefaultProperties(new Properties() {{
setProperty("spring.profiles.active", "pre");
}});
application.run(args);
}
}
总结
本篇文章对Spring Profile生效原理及使用做了总结,如有发下表述有误,请指正。