前言
前段时间有个业务需求,需要区分服务部署环境,来执行不同的代码逻辑。虽然之前使用过 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> c, ServletContext ctx) throws ServletException;}
Spring 对它进行实现:
@HandlesTypes(WebApplicationInitializer.class)public class SpringServletContainerInitializer implements ServletContainerInitializer { @Override public void onStartup(Set> webAppInitializerClasses, ServletContext servletContext) throws ServletException { List initializers = new LinkedList (); // 初始化: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 servletpropertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME)); // 对应 web.xml 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 Setprofiles = new LinkedHashSet (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 SetactiveProfiles = new LinkedHashSet (); private final ConfigurablePropertyResolver propertyResolver = new PropertySourcesPropertyResolver(this.propertySources); @Override public String[] getActiveProfiles() { return StringUtils.toStringArray(doGetActiveProfiles()); } protected Set 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时,说明需要以解析嵌套占位符 protectedT getProperty(String key, Class 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()); } protected String parseStringValue( String value, PlaceholderResolver placeholderResolver, Set 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); } protectedT getProperty(String key, Class 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相关
dev dev true prod prod
如果继承并使用了Spring-Boot的父POM默认配置,占位符默认为“@”,所以你可以在application.properties这么配:
spring.profiles.active = @profileActive@
当然也可以自定义占位符,配置插件:apache-resources-plugin。
org.apache.maven.plugins maven-resources-plugin ${*} src/main/resources true application.properties
指定之后,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参数、环境变量都没找到的时候,才会生效。
@SpringBootApplicationpublic 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生效原理及使用做了总结,如有发下表述有误,请指正。