基于Spring Boot的Environment源码理解实现分散配置详解

这篇文章主要给大家介绍了基于Spring Boot的Environment源码理解实现分散配置的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

前提

org.springframework.core.env.Environment是当前应用运行环境的公开接口,主要包括应用程序运行环境的两个关键方面:配置文件(profiles)和属性。Environment继承自接口PropertyResolver,而PropertyResolver提供了属性访问的相关方法。这篇文章从源码的角度分析Environment的存储容器和加载流程,然后基于源码的理解给出一个生产级别的扩展。

本文较长,请用一个舒服的姿势阅读。

Environment类体系

  • PropertyResolver:提供属性访问功能。
  • ConfigurablePropertyResolver:继承自PropertyResolver,主要提供属性类型转换(基于org.springframework.core.convert.ConversionService)功能。
  • Environment:继承自PropertyResolver,提供访问和判断profiles的功能。
  • ConfigurableEnvironment:继承自ConfigurablePropertyResolver和Environment,并且提供设置激活的profile和默认的profile的功能。
  • ConfigurableWebEnvironment:继承自ConfigurableEnvironment,并且提供配置Servlet上下文和Servlet参数的功能。
  • AbstractEnvironment:实现了ConfigurableEnvironment接口,默认属性和存储容器的定义,并且实现了ConfigurableEnvironment种的方法,并且为子类预留可覆盖了扩展方法。
  • StandardEnvironment:继承自AbstractEnvironment,非Servlet(Web)环境下的标准Environment实现。
  • StandardServletEnvironment:继承自StandardEnvironment,Servlet(Web)环境下的标准Environment实现。

reactive相关的暂时不研究。

Environment提供的方法

一般情况下,我们在SpringMVC项目中启用到的是StandardServletEnvironment,它的父接口问ConfigurableWebEnvironment,我们可以查看此接口提供的方法:

Environment的存储容器

Environment的静态属性和存储容器都是在AbstractEnvironment中定义的,ConfigurableWebEnvironment接口提供的getPropertySources()方法可以获取到返回的MutablePropertySources实例,然后添加额外的PropertySource。实际上,Environment的存储容器就是org.springframework.core.env.PropertySource的子类集合,AbstractEnvironment中使用的实例是org.springframework.core.env.MutablePropertySources,下面看下PropertySource的源码:

 public abstract class PropertySource { 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:

  • MapPropertySource:source指定为Map实例的PropertySource实现。
  • PropertiesPropertySource:source指定为Map实例的PropertySource实现,内部的Map实例由Properties实例转换而来。
  • ResourcePropertySource:继承自PropertiesPropertySource,source指定为通过Resource实例转化为Properties再转换为Map实例。
  • StubPropertySource:PropertySource的一个内部类,source设置为null,实际上就是空实现。
  • ComparisonPropertySource:继承自ComparisonPropertySource,所有属性访问方法强制抛出异常,作用就是一个不可访问属性的空实现。

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 activeProfiles = new LinkedHashSet<>(); private final Set 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的内部属性如下:

 private final List> propertySourceList = new CopyOnWriteArrayList<>();

没错,这个就是最底层的存储容器,也就是环境属性都是存放在一个CopyOnWriteArrayList

这里的源码告诉我们,如果出现多个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 flatten(Map map) { Map result = new LinkedHashMap<>(); flatten(null, result, map); return result; } private void flatten(String prefix, Map result, Map map) { String namePrefix = (prefix != null ? prefix + "." : ""); map.forEach((key, value) -> extract(namePrefix + key, result, value)); } @SuppressWarnings("unchecked") private void extract(String name, Map result, Object value) { if (value instanceof Map) { flatten(name, result, (Map) value); } else if (value instanceof Collection) { int index = 0; for (Object object : (Collection) 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 map = JSON_PARSER.parseMap(getContentStringFromResource(resource)); Map 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 map = YAML_PARSER.parseMap(getContentStringFromResource(resource)); Map 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 propertySourceDetectors = new ArrayList<>(); public void addPropertySourceDetector(AbstractPropertySourceDetector sourceDetector) { propertySourceDetectors.add(sourceDetector); } public void addPropertySourceDetectors(List sourceDetectors) { propertySourceDetectors.addAll(sourceDetectors); } public List getPropertySourceDetectors() { return Collections.unmodifiableList(propertySourceDetectors); } @Override public String[] getFileExtensions() { List 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 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 propertySourceDetectors, DefaultListableBeanFactory beanFactory) { Map beansOfType = beanFactory.getBeansOfType(AbstractPropertySourceDetector.class); for (Map.Entry 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存储容器的示例图:

参考资料:

spring-boot-starter-web:2.0.3.RELEASE源码。

总结

以上就是基于Spring Boot的Environment源码理解实现分散配置详解的详细内容,更多请关注0133技术站其它相关文章!

赞(0) 打赏
未经允许不得转载:0133技术站首页 » Java