前段時間有個業務需求,須要區分服務部署環境,來執行不一樣的代碼邏輯。雖然以前使用過 Spring profile 提供的環境切換功能,但沒有深刻了解,因此也踩了許多坑,這篇主要是對 Spring profile 機制的分析總結。java
分爲兩部分,源碼分析及使用總結。本文基於 SpringBoot 源碼(版本1.5.9.RELEASE)進行的分析。web
分析以前,先來回憶下 Spring-Boot 以前的 java web 工程結構,應該都會有一個 web.xml 這樣的文件。這是舊版本 Servlet 規範的部署文件,管理着初始化參數、Servlet、Filter、Listener等主要組件的配置。spring
Servlet3 問世以後,支持註解方式配置以上組件。SpringBoot 便在本身的標準工程結構中移除了 web.xml 文件,取而代之的是使用代碼、註解的配置組件。利用的就是 Servlet3 提供的擴展接口:apache
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 對該接口的實現:app
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 是如何建立的。jvm
該類做用是管理已生效的 Profile 以及全局加載的 PropertySource 集合。父類(AbstractEnvironment)的構造器會調用 customizePropertySources 方法。maven
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 邏輯。ide
該類承載了SpringBoot啓動及相關的邏輯。spring-boot
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 配置進行總結。
<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文件中的佔位符會被替換爲對應的值。
這裏以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); } }
-Dspring.profiles.active=prod
很eazy,指定便可。
跟配置JDK環境變量類似,指定key爲 spring.profiles.active,value爲生效的環境便可。
此種配置的優先級最低,是在上述的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生效原理及使用作了總結,若有發下表述有誤,請指正。