org.springframework.core.env.Environment是當前應用運行環境的公開接口,主要包括應用程序運行環境的兩個關鍵方面:配置文件(profiles)和屬性。Environment繼承自接口PropertyResolver,而PropertyResolver提供了屬性訪問的相關方法。這篇文章從源碼的角度分析Environment的存儲容器和加載流程,而後基於源碼的理解給出一個生產級別的擴展。java
本文較長,請用一個舒服的姿式閱讀。react
本文已經轉移到我的博客中維護,由於維護多個地方的內容太麻煩:git
reactive相關的暫時不研究。github
通常狀況下,咱們在SpringMVC項目中啓用到的是StandardServletEnvironment,它的父接口問ConfigurableWebEnvironment,咱們能夠查看此接口提供的方法:web
Environment的靜態屬性和存儲容器都是在AbstractEnvironment中定義的,ConfigurableWebEnvironment接口提供的getPropertySources()
方法能夠獲取到返回的MutablePropertySources實例,而後添加額外的PropertySource。實際上,Environment的存儲容器就是org.springframework.core.env.PropertySource的子類集合,AbstractEnvironment中使用的實例是org.springframework.core.env.MutablePropertySources,下面看下PropertySource的源碼:spring
public abstract class PropertySource<T> { protected final Log logger = LogFactory.getLog(getClass()); protected final String name; protected final T source; public PropertySource(String name, T source) { Assert.hasText(name, "Property source name must contain at least one character"); Assert.notNull(source, "Property source must not be null"); this.name = name; this.source = source; } @SuppressWarnings("unchecked") public PropertySource(String name) { this(name, (T) new Object()); } public String getName() { return this.name; } public T getSource() { return this.source; } public boolean containsProperty(String name) { return (getProperty(name) != null); } @Nullable public abstract Object getProperty(String name); @Override public boolean equals(Object obj) { return (this == obj || (obj instanceof PropertySource && ObjectUtils.nullSafeEquals(this.name, ((PropertySource<?>) obj).name))); } @Override public int hashCode() { return ObjectUtils.nullSafeHashCode(this.name); } //省略其餘方法和內部類的源碼 }
源碼相對簡單,預留了一個getProperty
抽象方法給子類實現,重點須要關注的是覆寫了的equals
和hashCode
方法,實際上只和name
屬性相關,這一點很重要,說明一個PropertySource實例綁定到一個惟一的name,這個name有點像HashMap裏面的key,部分移除、判斷方法都是基於name屬性。PropertySource的最經常使用子類是MapPropertySource、PropertiesPropertySource、ResourcePropertySource、StubPropertySource、ComparisonPropertySource:json
AbstractEnvironment中的屬性定義:數組
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"; protected static final String RESERVED_DEFAULT_PROFILE_NAME = "default"; private final Set<String> activeProfiles = new LinkedHashSet<>(); private final Set<String> defaultProfiles = new LinkedHashSet<>(getReservedDefaultProfiles()); private final MutablePropertySources propertySources = new MutablePropertySources(this.logger); private final ConfigurablePropertyResolver propertyResolver = new PropertySourcesPropertyResolver(this.propertySources);
上面的propertySources(MutablePropertySources類型)屬性就是用來存放PropertySource列表的,PropertySourcesPropertyResolver是ConfigurablePropertyResolver的實現,默認的profile就是字符串default。MutablePropertySources的內部屬性以下:app
private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();
沒錯,這個就是最底層的存儲容器,也就是環境屬性都是存放在一個CopyOnWriteArrayList<PropertySource<?>>實例中。MutablePropertySources是PropertySources的子類,它提供了get(String name)
、addFirst
、addLast
、addBefore
、addAfter
、remove
、replace
等便捷方法,方便操做propertySourceList集合的元素,這裏挑選addBefore
的源碼分析:dom
public void addBefore(String relativePropertySourceName, PropertySource<?> propertySource) { if (logger.isDebugEnabled()) { logger.debug("Adding PropertySource '" + propertySource.getName() + "' with search precedence immediately higher than '" + relativePropertySourceName + "'"); } //前一個PropertySource的name指定爲relativePropertySourceName時候必須和添加的PropertySource的name屬性不相同 assertLegalRelativeAddition(relativePropertySourceName, propertySource); //嘗試移除同名的PropertySource removeIfPresent(propertySource); //獲取前一個PropertySource在CopyOnWriteArrayList中的索引 int index = assertPresentAndGetIndex(relativePropertySourceName); //添加當前傳入的PropertySource到指定前一個PropertySource的索引,至關於relativePropertySourceName對應的PropertySource後移到原來索引值+1的位置 addAtIndex(index, propertySource); } protected void assertLegalRelativeAddition(String relativePropertySourceName, PropertySource<?> propertySource) { String newPropertySourceName = propertySource.getName(); if (relativePropertySourceName.equals(newPropertySourceName)) { throw new IllegalArgumentException( "PropertySource named '" + newPropertySourceName + "' cannot be added relative to itself"); } } protected void removeIfPresent(PropertySource<?> propertySource) { this.propertySourceList.remove(propertySource); } private int assertPresentAndGetIndex(String name) { int index = this.propertySourceList.indexOf(PropertySource.named(name)); if (index == -1) { throw new IllegalArgumentException("PropertySource named '" + name + "' does not exist"); } return index; } private void addAtIndex(int index, PropertySource<?> propertySource) { //注意,這裏會再次嘗試移除同名的PropertySource removeIfPresent(propertySource); this.propertySourceList.add(index, propertySource); }
大多數PropertySource子類的修飾符都是public,能夠直接使用,這裏寫個小demo:
MutablePropertySources mutablePropertySources = new MutablePropertySources(); Map<String, Object> map = new HashMap<>(8); map.put("name", "throwable"); map.put("age", 25); MapPropertySource mapPropertySource = new MapPropertySource("map", map); mutablePropertySources.addLast(mapPropertySource); Properties properties = new Properties(); PropertiesPropertySource propertiesPropertySource = new PropertiesPropertySource("prop", properties); properties.put("name", "doge"); properties.put("gourp", "group-a"); mutablePropertySources.addBefore("map", propertiesPropertySource); System.out.println(mutablePropertySources);
Environment加載的源碼位於SpringApplication#prepareEnvironment
:
private ConfigurableEnvironment prepareEnvironment( SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) { // Create and configure the environment //建立ConfigurableEnvironment實例 ConfigurableEnvironment environment = getOrCreateEnvironment(); //啓動參數綁定到ConfigurableEnvironment中 configureEnvironment(environment, applicationArguments.getSourceArgs()); //發佈ConfigurableEnvironment準備完畢事件 listeners.environmentPrepared(environment); //綁定ConfigurableEnvironment到當前的SpringApplication實例中 bindToSpringApplication(environment); //這一步是非SpringMVC項目的處理,暫時忽略 if (this.webApplicationType == WebApplicationType.NONE) { environment = new EnvironmentConverter(getClassLoader()) .convertToStandardEnvironmentIfNecessary(environment); } //綁定ConfigurationPropertySourcesPropertySource到ConfigurableEnvironment中,name爲configurationProperties,實例是SpringConfigurationPropertySources,屬性實際是ConfigurableEnvironment中的MutablePropertySources ConfigurationPropertySources.attach(environment); return environment; }
這裏重點看下getOrCreateEnvironment
方法:
private ConfigurableEnvironment getOrCreateEnvironment() { if (this.environment != null) { return this.environment; } //在SpringMVC項目,ConfigurableEnvironment接口的實例就是新建的StandardServletEnvironment實例 if (this.webApplicationType == WebApplicationType.SERVLET) { return new StandardServletEnvironment(); } return new StandardEnvironment(); } //REACTIVE_WEB_ENVIRONMENT_CLASS=org.springframework.web.reactive.DispatcherHandler //MVC_WEB_ENVIRONMENT_CLASS=org.springframework.web.servlet.DispatcherServlet //MVC_WEB_ENVIRONMENT_CLASS={"javax.servlet.Servlet","org.springframework.web.context.ConfigurableWebApplicationContext"} //這裏,默認就是WebApplicationType.SERVLET private WebApplicationType deduceWebApplicationType() { if (ClassUtils.isPresent(REACTIVE_WEB_ENVIRONMENT_CLASS, null) && !ClassUtils.isPresent(MVC_WEB_ENVIRONMENT_CLASS, null)) { return WebApplicationType.REACTIVE; } for (String className : WEB_ENVIRONMENT_CLASSES) { if (!ClassUtils.isPresent(className, null)) { return WebApplicationType.NONE; } } return WebApplicationType.SERVLET; }
還有一個地方要重點關注:發佈ConfigurableEnvironment準備完畢事件listeners.environmentPrepared(environment)
,實際上這裏用到了同步的EventBus,事件的監聽者是ConfigFileApplicationListener,具體處理邏輯是onApplicationEnvironmentPreparedEvent
方法:
private void onApplicationEnvironmentPreparedEvent( ApplicationEnvironmentPreparedEvent event) { List<EnvironmentPostProcessor> postProcessors = loadPostProcessors(); postProcessors.add(this); AnnotationAwareOrderComparator.sort(postProcessors); //遍歷全部的EnvironmentPostProcessor對Environment實例進行處理 for (EnvironmentPostProcessor postProcessor : postProcessors) { postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication()); } } //從spring.factories文件中加載,一共有四個實例 //ConfigFileApplicationListener //CloudFoundryVcapEnvironmentPostProcessor //SpringApplicationJsonEnvironmentPostProcessor //SystemEnvironmentPropertySourceEnvironmentPostProcessor List<EnvironmentPostProcessor> loadPostProcessors() { return SpringFactoriesLoader.loadFactories(EnvironmentPostProcessor.class, getClass().getClassLoader()); }
實際上,處理工做大部分都在ConfigFileApplicationListener中,見它的postProcessEnvironment
方法:
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { addPropertySources(environment, application.getResourceLoader()); } protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) { RandomValuePropertySource.addToEnvironment(environment); new Loader(environment, resourceLoader).load(); }
主要的配置環境加載邏輯在內部類Loader,Loader會匹配多個路徑下的文件把屬性加載到ConfigurableEnvironment中,加載器主要是PropertySourceLoader的實例,例如咱們用到application-${profile}.yaml文件作應用主配置文件,使用的是YamlPropertySourceLoader,這個時候activeProfiles也會被設置到ConfigurableEnvironment中。加載完畢以後,ConfigurableEnvironment中基本包含了全部須要加載的屬性(activeProfiles是這個時候被寫入ConfigurableEnvironment)。值得注意的是,幾乎全部屬性都是key-value形式存儲,如xxx.yyyy.zzzzz=value、xxx.yyyy[0].zzzzz=value-一、xxx.yyyy[1].zzzzz=value-2。Loader
中的邏輯相對複雜,有比較多的遍歷和過濾條件,這裏不作展開。
上文提到過,都是委託到PropertySourcesPropertyResolver,先看它的構造函數:
@Nullable private final PropertySources propertySources; public PropertySourcesPropertyResolver(@Nullable PropertySources propertySources) { this.propertySources = propertySources; }
只依賴於一個PropertySources實例,在SpringBoot的SpringMVC項目中就是MutablePropertySources的實例。重點分析一下最複雜的一個方法:
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); //選用第一個不爲null的匹配key的屬性值 if (value != null) { if (resolveNestedPlaceholders && value instanceof String) { //處理屬性佔位符,如${server.port},底層委託到PropertyPlaceholderHelper完成 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; }
這裏的源碼告訴咱們,若是出現多個PropertySource中存在同名的key,返回的是第一個PropertySource對應key的屬性值的處理結果,所以咱們若是須要自定義一些環境屬性,須要十分清楚各個PropertySource的順序。
在不使用SpringCloud配置中心的狀況下,通常的SpringBoot項目的配置文件以下:
- src - main - resources - application-prod.yaml - application-dev.yaml - application-test.yaml
隨着項目發展,配置項愈來愈多,致使了application-${profile}.yaml迅速膨脹,大的配置文件甚至超過一千行,爲了簡化和劃分不一樣功能的配置,能夠考慮把配置文件拆分以下:
- src - main - resources - profiles - dev - business.yaml - mq.json - datasource.properties - prod - business.yaml - mq.json - datasource.properties - test - business.yaml - mq.json - datasource.properties - application-prod.yaml - application-dev.yaml - application-test.yaml
外層的application-${profile}.yaml只留下項目的核心配置如server.port等,其餘配置打散放在/profiles/${profile}/各自的配置文件中。實現方式是:依據當前配置的spring.profiles.active屬性,讀取類路徑中指定文件夾下的配置文件中,加載到Environment中,須要注意這一個加載步驟必須在Spring刷新上下文方法最後一步finishRefresh
以前完成(這一點緣由能夠參考以前在我的博客寫過的SpringBoot刷新上下文源碼的分析),不然有可能會影響到佔位符屬性的自動裝配(例如使用了@Value("${filed}"))。
先定義一個屬性探索者接口:
public interface PropertySourceDetector { /** * 獲取支持的文件後綴數組 * * @return String[] */ String[] getFileExtensions(); /** * 加載目標文件屬性到環境中 * * @param environment environment * @param name name * @param resource resource * @throws IOException IOException */ void load(ConfigurableEnvironment environment, String name, Resource resource) throws IOException; }
而後須要一個抽象屬性探索者把Resource轉換爲字符串,額外提供Map的縮進、添加PropertySource到Environment等方法:
public abstract class AbstractPropertySourceDetector implements PropertySourceDetector { private static final String SERVLET_ENVIRONMENT_CLASS = "org.springframework.web." + "context.support.StandardServletEnvironment"; public boolean support(String fileExtension) { String[] fileExtensions = getFileExtensions(); return null != fileExtensions && Arrays.stream(fileExtensions).anyMatch(extension -> extension.equals(fileExtension)); } private String findPropertySource(MutablePropertySources sources) { if (ClassUtils.isPresent(SERVLET_ENVIRONMENT_CLASS, null) && sources .contains(StandardServletEnvironment.JNDI_PROPERTY_SOURCE_NAME)) { return StandardServletEnvironment.JNDI_PROPERTY_SOURCE_NAME; } return StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME; } protected void addPropertySource(ConfigurableEnvironment environment, PropertySource<?> source) { MutablePropertySources sources = environment.getPropertySources(); String name = findPropertySource(sources); if (sources.contains(name)) { sources.addBefore(name, source); } else { sources.addFirst(source); } } protected Map<String, Object> flatten(Map<String, Object> map) { Map<String, Object> result = new LinkedHashMap<>(); flatten(null, result, map); return result; } private void flatten(String prefix, Map<String, Object> result, Map<String, Object> map) { String namePrefix = (prefix != null ? prefix + "." : ""); map.forEach((key, value) -> extract(namePrefix + key, result, value)); } @SuppressWarnings("unchecked") private void extract(String name, Map<String, Object> result, Object value) { if (value instanceof Map) { flatten(name, result, (Map<String, Object>) value); } else if (value instanceof Collection) { int index = 0; for (Object object : (Collection<Object>) value) { extract(name + "[" + index + "]", result, object); index++; } } else { result.put(name, value); } } protected String getContentStringFromResource(Resource resource) throws IOException { return StreamUtils.copyToString(resource.getInputStream(), Charset.forName("UTF-8")); } }
上面的方法參考SpringApplicationJsonEnvironmentPostProcessor,而後編寫各類類型配置屬性探索者的實現:
//Json @Slf4j public class JsonPropertySourceDetector extends AbstractPropertySourceDetector { private static final JsonParser JSON_PARSER = JsonParserFactory.getJsonParser(); @Override public String[] getFileExtensions() { return new String[]{"json"}; } @Override public void load(ConfigurableEnvironment environment, String name, Resource resource) throws IOException { try { Map<String, Object> map = JSON_PARSER.parseMap(getContentStringFromResource(resource)); Map<String, Object> target = flatten(map); addPropertySource(environment, new MapPropertySource(name, target)); } catch (Exception e) { log.warn("加載Json文件屬性到環境變量失敗,name = {},resource = {}", name, resource); } } } //Properties public class PropertiesPropertySourceDetector extends AbstractPropertySourceDetector { @Override public String[] getFileExtensions() { return new String[]{"properties", "conf"}; } @SuppressWarnings("unchecked") @Override public void load(ConfigurableEnvironment environment, String name, Resource resource) throws IOException { Map map = PropertiesLoaderUtils.loadProperties(resource); addPropertySource(environment, new MapPropertySource(name, map)); } } //Yaml @Slf4j public class YamlPropertySourceDetector extends AbstractPropertySourceDetector { private static final JsonParser YAML_PARSER = new YamlJsonParser(); @Override public String[] getFileExtensions() { return new String[]{"yaml", "yml"}; } @Override public void load(ConfigurableEnvironment environment, String name, Resource resource) throws IOException { try { Map<String, Object> map = YAML_PARSER.parseMap(getContentStringFromResource(resource)); Map<String, Object> target = flatten(map); addPropertySource(environment, new MapPropertySource(name, target)); } catch (Exception e) { log.warn("加載Yaml文件屬性到環境變量失敗,name = {},resource = {}", name, resource); } } }
子類的所有PropertySource都是MapPropertySource,name爲文件的名稱,全部PropertySource都用addBefore
方法插入到systemProperties
的前面,主要是爲了提升匹配屬性的優先級。接着須要定義一個屬性探索者的合成類用來裝載全部的子類:
public class PropertySourceDetectorComposite implements PropertySourceDetector { private static final String DEFAULT_SUFFIX = "properties"; private final List<AbstractPropertySourceDetector> propertySourceDetectors = new ArrayList<>(); public void addPropertySourceDetector(AbstractPropertySourceDetector sourceDetector) { propertySourceDetectors.add(sourceDetector); } public void addPropertySourceDetectors(List<AbstractPropertySourceDetector> sourceDetectors) { propertySourceDetectors.addAll(sourceDetectors); } public List<AbstractPropertySourceDetector> getPropertySourceDetectors() { return Collections.unmodifiableList(propertySourceDetectors); } @Override public String[] getFileExtensions() { List<String> fileExtensions = new ArrayList<>(8); for (AbstractPropertySourceDetector propertySourceDetector : propertySourceDetectors) { fileExtensions.addAll(Arrays.asList(propertySourceDetector.getFileExtensions())); } return fileExtensions.toArray(new String[0]); } @Override public void load(ConfigurableEnvironment environment, String name, Resource resource) throws IOException { if (resource.isFile()) { String fileName = resource.getFile().getName(); int index = fileName.lastIndexOf("."); String suffix; if (-1 == index) { //若是文件沒有後綴,看成properties處理 suffix = DEFAULT_SUFFIX; } else { suffix = fileName.substring(index + 1); } for (AbstractPropertySourceDetector propertySourceDetector : propertySourceDetectors) { if (propertySourceDetector.support(suffix)) { propertySourceDetector.load(environment, name, resource); return; } } } } }
最後添加一個配置類做爲入口:
public class PropertySourceDetectorConfiguration implements ImportBeanDefinitionRegistrar { private static final String PATH_PREFIX = "profiles"; @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) registry; ConfigurableEnvironment environment = beanFactory.getBean(ConfigurableEnvironment.class); List<AbstractPropertySourceDetector> propertySourceDetectors = new ArrayList<>(); configurePropertySourceDetectors(propertySourceDetectors, beanFactory); PropertySourceDetectorComposite propertySourceDetectorComposite = new PropertySourceDetectorComposite(); propertySourceDetectorComposite.addPropertySourceDetectors(propertySourceDetectors); String[] activeProfiles = environment.getActiveProfiles(); ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); try { for (String profile : activeProfiles) { String location = PATH_PREFIX + File.separator + profile + File.separator + "*"; Resource[] resources = resourcePatternResolver.getResources(location); for (Resource resource : resources) { propertySourceDetectorComposite.load(environment, resource.getFilename(), resource); } } } catch (IOException e) { throw new IllegalStateException(e); } } private void configurePropertySourceDetectors(List<AbstractPropertySourceDetector> propertySourceDetectors, DefaultListableBeanFactory beanFactory) { Map<String, AbstractPropertySourceDetector> beansOfType = beanFactory.getBeansOfType(AbstractPropertySourceDetector.class); for (Map.Entry<String, AbstractPropertySourceDetector> entry : beansOfType.entrySet()) { propertySourceDetectors.add(entry.getValue()); } propertySourceDetectors.add(new JsonPropertySourceDetector()); propertySourceDetectors.add(new YamlPropertySourceDetector()); propertySourceDetectors.add(new PropertiesPropertySourceDetector()); } }
準備就緒,在/resources/profiles/dev下面添加兩個文件app.json和conf:
//app.json { "app": { "name": "throwable", "age": 25 } } //conf name=doge
項目的application.yaml添加屬性spring.profiles.active: dev,最後添加一個CommandLineRunner的實現用來觀察數據:
@Slf4j @Component public class CustomCommandLineRunner implements CommandLineRunner { @Value("${app.name}") String name; @Value("${app.age}") Integer age; @Autowired ConfigurableEnvironment configurableEnvironment; @Override public void run(String... args) throws Exception { log.info("name = {},age = {}", name, age); } }
自動裝配的屬性值和Environment實例中的屬性和預期同樣,改造是成功的。
Spring中的環境屬性管理的源碼我的認爲是最清晰和簡單的:從文件中讀取數據轉化爲key-value結構,key-value結構存放在一個PropertySource實例中,而後獲得的多個PropertySource實例存放在一個CopyOnWriteArrayList中,屬性訪問的時候老是遍歷CopyOnWriteArrayList中的PropertySource進行匹配。可能相對複雜的就是佔位符的解析和參數類型的轉換,後者牽連到Converter體系,這些不在本文的討論範圍內。最後附上一張Environment存儲容器的示例圖:
參考資料:
示例項目:https://github.com/zjcscut/spring-boot-environment (本文完)