spring.profiles.active 和 @Profile 這兩個我相信各位都熟悉吧,主要功能是可以實現不同環境下(開發、測試、生產)引數配置的切換。其實關於環境的切換,小編在部落格 【死磕Spring】—– IOC 之 PropertyPlaceholderConfigurer 的應用 已經介紹了利用 PropertyPlaceholderConfigurer 來實現動態切換配置環境,當然這種方法需要我們自己實現,有點兒麻煩。但是對於這種非常實際的需求,Spring 怎麼可能沒有提供呢?下麵小編就問題來對 Spring 的環境 & 屬性來做一個分析說明。
概括
Spring 環境 & 屬性由四個部分組成:PropertySource、PropertyResolver、Profile 和 Environment。
- PropertySource:屬性源,key-value 屬性對抽象,用於配置資料。
- PropertyResolver:屬性解析器,用於解析屬性配置
- Profile:剖面,只有啟用的剖面的元件/配置才會註冊到 Spring 容器,類似於 Spring Boot 中的 profile
- Environment:環境,Profile 和 PropertyResolver 的組合。
下麵是整個體系的結構圖:

下麵就針對上面結構圖對 Spring 的 Properties & Environment 做一個詳細的分析。
Properties
PropertyResolver
屬性解析器,用於解析任何基礎源的屬性的介面
public interface PropertyResolver {// 是否包含某個屬性boolean containsProperty(String key);// 獲取屬性值 如果找不到傳回null@NullableString getProperty(String key);// 獲取屬性值,如果找不到傳回預設值String getProperty(String key, String defaultValue);// 獲取指定型別的屬性值,找不到傳回null@Nullable<T> T getProperty(String key, Class<T> targetType);// 獲取指定型別的屬性值,找不到傳回預設值<T> T getProperty(String key, Class<T> targetType, T defaultValue);// 獲取屬性值,找不到丟擲異常IllegalStateExceptionString getRequiredProperty(String key) throws IllegalStateException;// 獲取指定型別的屬性值,找不到丟擲異常IllegalStateException<T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException;// 替換文字中的佔位符(${key})到屬性值,找不到不解析String resolvePlaceholders(String text);// 替換文字中的佔位符(${key})到屬性值,找不到丟擲異常IllegalArgumentExceptionString resolveRequiredPlaceholders(String text) throws IllegalArgumentException;}
從 API 上面我們就知道屬性解析器 PropertyResolver 的作用了。下麵是一個簡單的運用。
PropertyResolver propertyResolver = new PropertySourcesPropertyResolver(propertySources);System.out.println(propertyResolver.getProperty("name"));System.out.println(propertyResolver.getProperty("name", "chenssy"));System.out.println(propertyResolver.resolvePlaceholders("my name is ${name}"));
下圖是 PropertyResolver 體系結構圖:

- ConfigurablePropertyResolver:供屬性型別轉換的功能
- AbstractPropertyResolver:解析屬性檔案的抽象基類
- PropertySourcesPropertyResolver:PropertyResolver 的實現者,他對一組 PropertySources 提供屬性解析服務
ConfigurablePropertyResolver
提供屬性型別轉換的功能
通俗點說就是 ConfigurablePropertyResolver 提供屬性值型別轉換所需要的 ConversionService。
public interface ConfigurablePropertyResolver extends PropertyResolver {// 傳回執行型別轉換時使用的 ConfigurableConversionServiceConfigurableConversionService getConversionService();// 設定 ConfigurableConversionServicevoid setConversionService(ConfigurableConversionService conversionService);// 設定佔位符字首void setPlaceholderPrefix(String placeholderPrefix);// 設定佔位符字尾void setPlaceholderSuffix(String placeholderSuffix);// 設定佔位符與預設值之間的分隔符void setValueSeparator(@Nullable String valueSeparator);// 設定當遇到巢狀在給定屬性值內的不可解析的佔位符時是否丟擲異常// 當屬性值包含不可解析的佔位符時,getProperty(String)及其變體的實現必須檢查此處設定的值以確定正確的行為。void setIgnoreUnresolvableNestedPlaceholders(boolean ignoreUnresolvableNestedPlaceholders);// 指定必須存在哪些屬性,以便由validateRequiredProperties()驗證void setRequiredProperties(String... requiredProperties);// 驗證setRequiredProperties指定的每個屬性是否存在並解析為非null值void validateRequiredProperties() throws MissingRequiredPropertiesException;}
從 ConfigurablePropertyResolver 所提供的方法來看,除了訪問和設定 ConversionService 外,主要還提供了一些解析規則之類的方法。
就 Properties 體系而言,PropertyResolver 定義了訪問 Properties 屬性值的方法,而 ConfigurablePropertyResolver 則定義瞭解析 Properties 一些相關的規則和值進行型別轉換所需要的 Service。該體繫有兩個實現者:AbstractPropertyResolver 和 PropertySourcesPropertyResolver,其中 AbstractPropertyResolver 為實現的抽象基類,PropertySourcesPropertyResolver 為真正的實現者。
AbstractPropertyResolver
解析屬性檔案的抽象基類
AbstractPropertyResolver 作為基類它僅僅只是設定了一些解析屬性檔案所需要配置或者轉換器,如 setConversionService()、 setPlaceholderPrefix()、 setValueSeparator(),其實這些方法的實現都比較簡單都是設定或者獲取 AbstractPropertyResolver 所提供的屬性,如下:
// 型別轉換去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;// 與預設值的分割private String valueSeparator = SystemPropertyUtils.VALUE_SEPARATOR;// 必須要有的欄位值private final Set<String> requiredProperties = new LinkedHashSet<>();
這些屬性都是 ConfigurablePropertyResolver 介面所提供方法需要的屬性,他所提供的方法都是設定和讀取這些值,如下幾個方法:
public ConfigurableConversionService getConversionService() {// 需要提供獨立的DefaultConversionService,而不是PropertySourcesPropertyResolver 使用的共享DefaultConversionService。ConfigurableConversionService cs = this.conversionService;if (cs == null) {synchronized (this) {cs = this.conversionService;if (cs == null) {cs = new DefaultConversionService();this.conversionService = cs;}}}return cs;}@Overridepublic void setConversionService(ConfigurableConversionService conversionService) {Assert.notNull(conversionService, "ConversionService must not be null");this.conversionService = conversionService;}public void setPlaceholderPrefix(String placeholderPrefix) {Assert.notNull(placeholderPrefix, "'placeholderPrefix' must not be null");this.placeholderPrefix = placeholderPrefix;}public void setPlaceholderSuffix(String placeholderSuffix) {Assert.notNull(placeholderSuffix, "'placeholderSuffix' must not be null");this.placeholderSuffix = placeholderSuffix;}
而對屬性的訪問則委託給子類 PropertySourcesPropertyResolver 實現。
public String getProperty(String key) {return getProperty(key, String.class);}public String getProperty(String key, String defaultValue) {String value = getProperty(key);return (value != null ? value : defaultValue);}public <T> T getProperty(String key, Class<T> targetType, T defaultValue) {T value = getProperty(key, targetType);return (value != null ? value : defaultValue);}public String getRequiredProperty(String key) throws IllegalStateException {String value = getProperty(key);if (value == null) {throw new IllegalStateException("Required key '" + key + "' not found");}return value;}public <T> T getRequiredProperty(String key, Class<T> valueType) throws IllegalStateException {T value = getProperty(key, valueType);if (value == null) {throw new IllegalStateException("Required key '" + key + "' not found");}return value;
PropertySourcesPropertyResolver
PropertyResolver 的實現者,他對一組 PropertySources 提供屬性解析服務
它僅有一個成員變數:PropertySources。該成員變數內部儲存著一組 PropertySource,表示 key-value 鍵值對的源的抽象基類,即一個 PropertySource 物件則是一個 key-value 鍵值對。如下:
public abstract class PropertySource<T> {protected final Log logger = LogFactory.getLog(getClass());protected final String name;protected final T source;//......}
對外公開的 getProperty() 都是委託給 getProperty(Stringkey,Class<T>targetValueType,booleanresolveNestedPlaceholders) 實現,他有三個引數,分別表示為:
- key:獲取的 key
- targetValueType: 標的 value 的型別
- resolveNestedPlaceholders:是否解決巢狀佔位符
原始碼如下:
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;}
首先從 propertySource 中獲取指定 key 的 value 值,然後判斷是否需要進行巢狀佔位符解析,如果需要則呼叫 resolveNestedPlaceholders() 進行巢狀佔位符解析,然後呼叫 convertValueIfNecessary() 進行型別轉換。
resolveNestedPlaceholders()
該方法用於解析給定字串中的佔位符,同時根據 ignoreUnresolvableNestedPlaceholders 的值,來確定是否對不可解析的佔位符的處理方法:是忽略還是丟擲異常(該值由 setIgnoreUnresolvableNestedPlaceholders() 設定)。
protected String resolveNestedPlaceholders(String value) {return (this.ignoreUnresolvableNestedPlaceholders ?resolvePlaceholders(value) : resolveRequiredPlaceholders(value));}
如果 this.ignoreUnresolvableNestedPlaceholders 為 true,則呼叫 resolvePlaceholders() ,否則呼叫 resolveRequiredPlaceholders() 但是無論是哪個方法,最終都會到 doResolvePlaceholders(),該方法接收兩個引數:
- String 型別的 text:待解析的字串
- PropertyPlaceholderHelper 型別的 helper:用於解析佔位符的工具類。
private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) {return helper.replacePlaceholders(text, this::getPropertyAsRawString);}
PropertyPlaceholderHelper 是用於處理包含佔位符值的字串,構造該實體需要四個引數:
- placeholderPrefix:佔位符字首
- placeholderSuffix:佔位符字尾
- valueSeparator:佔位符變數與關聯的預設值之間的分隔符
- ignoreUnresolvablePlaceholders:指示是否忽略不可解析的佔位符(true)或丟擲異常(false)
建構式如下:
public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix,@Nullable String valueSeparator, boolean ignoreUnresolvablePlaceholders) {Assert.notNull(placeholderPrefix, "'placeholderPrefix' must not be null");Assert.notNull(placeholderSuffix, "'placeholderSuffix' must not be null");this.placeholderPrefix = placeholderPrefix;this.placeholderSuffix = placeholderSuffix;String simplePrefixForSuffix = wellKnownSimplePrefixes.get(this.placeholderSuffix);if (simplePrefixForSuffix != null && this.placeholderPrefix.endsWith(simplePrefixForSuffix)) {this.simplePrefix = simplePrefixForSuffix;}else {this.simplePrefix = this.placeholderPrefix;}this.valueSeparator = valueSeparator;this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders;}
就 PropertySourcesPropertyResolver 而言,其父類 AbstractPropertyResolver 已經對上述四個值做了定義:placeholderPrefix 為 ${,placeholderSuffix 為 },valueSeparator 為 :,ignoreUnresolvablePlaceholders 預設為 false,當然我們也可以使用相應的 setter 方法自定義。
呼叫 PropertyPlaceholderHelper 的 replacePlaceholders() 對佔位符進行處理,該方法接收兩個引數,一個是待解析的字串 value ,一個是 PlaceholderResolver 型別的 placeholderResolver,他是定義佔位符解析的策略類。如下:
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {Assert.notNull(value, "'value' must not be null");return parseStringValue(value, placeholderResolver, new HashSet<>());}
內部委託給 parseStringValue() 實現:
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) {// 字首和字尾之間的字串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);// propval 為空,則提取預設值if (propVal == null && this.valueSeparator != null) {int separatorIndex = placeholder.indexOf(this.valueSeparator);if (separatorIndex != -1) {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) {// Proceed with unprocessed value.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();}
其實就是獲取佔位符 ${} 中間的值,這裡面會涉及到一個遞迴的過程,因為可能會存在這種情況 ${${name}}。
convertValueIfNecessary()
該方法是不是感覺到非常的熟悉,該方法就是完成型別轉換的。如下:
protected <T> T convertValueIfNecessary(Object value, @Nullable Class<T> targetType) {if (targetType == null) {return (T) value;}ConversionService conversionServiceToUse = this.conversionService;if (conversionServiceToUse == null) {// Avoid initialization of shared DefaultConversionService if// no standard type conversion is needed in the first place...if (ClassUtils.isAssignableValue(targetType, value)) {return (T) value;}conversionServiceToUse = DefaultConversionService.getSharedInstance();}return conversionServiceToUse.convert(value, targetType);}
首先獲取型別轉換服務 conversionService ,若為空,則判斷是否可以透過反射來設定,如果可以則直接強轉傳回,否則構造一個 DefaultConversionService 實體,最後呼叫其 convert() 完成型別轉換,後續就是 Spring 型別轉換體系的事情了,如果對其不瞭解,可以參考小編這篇部落格:【死磕 Spring】—– IOC 之深入分析 Bean 的型別轉換體系
Environment
表示當前應用程式正在執行的環境
應用程式的環境有兩個關鍵方面:profile 和 properties。
- properties 的方法由 PropertyResolver 定義。
- profile 則表示當前的執行環境,對於應用程式中的 properties 而言,並不是所有的都會載入到系統中,只有其屬性與 profile 一直才會被啟用載入,
所以 Environment 物件的作用是確定哪些配置檔案(如果有)當前處於活動狀態,以及預設情況下哪些配置檔案(如果有)應處於活動狀態。properties 在幾乎所有應用程式中都發揮著重要作用,並且有多種來源:屬性檔案,JVM 系統屬性,系統環境變數,JNDI,servlet 背景關係引數,ad-hoc 屬性物件,對映等。同時它繼承 PropertyResolver 介面,所以與屬性相關的 Environment 物件其主要是為使用者提供方便的服務介面,用於配置屬性源和從中屬性源中解析屬性。
public interface Environment extends PropertyResolver {// 傳回此環境下啟用的配置檔案集String[] getActiveProfiles();// 如果未設定啟用配置檔案,則傳回預設的啟用的配置檔案集String[] getDefaultProfiles();boolean acceptsProfiles(String... profiles);}
Environment 體系結構圖如下:

- PropertyResolver:提供屬性訪問功能
- Environment:提供訪問和判斷 profiles 的功能
- ConfigurableEnvironment:提供設定啟用的 profile 和預設的 profile 的功能以及操作 Properties 的工具
- ConfigurableWebEnvironment:提供配置 Servlet 背景關係和 Servlet 引數的功能
- AbstractEnvironment:實現了 ConfigurableEnvironment 介面,預設屬性和儲存容器的定義,並且實現了 ConfigurableEnvironment 的方法,並且為子類預留可改寫了擴充套件方法
- StandardEnvironment:繼承自 AbstractEnvironment ,非 Servlet(Web) 環境下的標準 Environment 實現
- StandardServletEnvironment:繼承自 StandardEnvironment ,Servlet(Web) 環境下的標準 Environment 實現
ConfigurableEnvironment
提供設定啟用的 profile 和預設的 profile 的功能以及操作 Properties 的工具
該類除了繼承 Environment 介面外還繼承了 ConfigurablePropertyResolver 介面,所以它即具備了設定 profile 的功能也具備了操作 Properties 的功能。同時還允許客戶端透過它設定和驗證所需要的屬性,自定義轉換服務等功能。如下:
public interface ConfigurableEnvironment extends Environment, ConfigurablePropertyResolver {// 指定該環境下的 profile 集void setActiveProfiles(String... profiles);// 增加此環境的 profilevoid addActiveProfile(String profile);// 設定預設的 profilevoid setDefaultProfiles(String... profiles);// 傳回此環境的 PropertySourcesMutablePropertySources getPropertySources();// 嘗試傳回 System.getenv() 的值,若失敗則傳回透過 System.getenv(string) 的來訪問各個鍵的對映Map<String, Object> getSystemEnvironment();// 嘗試傳回 System.getProperties() 的值,若失敗則傳回透過 System.getProperties(string) 的來訪問各個鍵的對映Map<String, Object> getSystemProperties();void merge(ConfigurableEnvironment parent);}
AbstractEnvironment
Environment 的基礎實現
允許透過設定 ACTIVEPROFILESPROPERTYNAME 和DEFAULTPROFILESPROPERTYNAME 屬性指定活動和預設配置檔案。子類的主要區別在於它們預設新增的 PropertySource 物件。而 AbstractEnvironment 則沒有新增任何內容。子類應該透過受保護的 customizePropertySources(MutablePropertySources) 鉤子提供屬性源,而客戶端應該使用 ConfigurableEnvironment.getPropertySources()進行自定義並對MutablePropertySources API進行操作。
在 AbstractEnvironment 有兩對變數,這兩對變數維護著啟用和預設配置 profile。如下:
public static final String ACTIVE_PROFILES_PROPERTY_NAME = "spring.profiles.active";private final Set<String> activeProfiles = new LinkedHashSet<>();public static final String DEFAULT_PROFILES_PROPERTY_NAME = "spring.profiles.default";private final Set<String> defaultProfiles = new LinkedHashSet<>(getReservedDefaultProfiles());
由於實現方法較多,這裡只關註兩個方法: setActiveProfiles() 和 getActiveProfiles()。
setActiveProfiles()
public void setActiveProfiles(String... profiles) {Assert.notNull(profiles, "Profile array must not be null");if (logger.isDebugEnabled()) {logger.debug("Activating profiles " + Arrays.asList(profiles));}synchronized (this.activeProfiles) {this.activeProfiles.clear();for (String profile : profiles) {validateProfile(profile);this.activeProfiles.add(profile);}}}
該方法其實就是操作 activeProfiles 集合,在每次設定之前都會將該集合清空重新新增,新增之前呼叫 validateProfile() 對新增的 profile 進行校驗,如下:
protected void validateProfile(String profile) {if (!StringUtils.hasText(profile)) {throw new IllegalArgumentException("Invalid profile [" + profile + "]: must contain text");}if (profile.charAt(0) == '!') {throw new IllegalArgumentException("Invalid profile [" + profile + "]: must not begin with ! operator");}}
這個校驗過程比較弱,子類可以提供更加嚴格的校驗規則。
getActiveProfiles()
從 getActiveProfiles() 中我們可以猜出這個方法實現的邏輯:獲取 activeProfiles 集合即可。
public String[] getActiveProfiles() {return StringUtils.toStringArray(doGetActiveProfiles());}
委託給 doGetActiveProfiles() 實現:
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 為空,則從 Properties 中獲取 spring.profiles.active 配置,如果不為空,則呼叫 setActiveProfiles() 設定 profile,最後傳回。
到這裡整個環境&屬性已經分析完畢了,至於在後面他是如何與應用背景關係結合的,我們後面分析。
知識星球