基于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<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:
- 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<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的内部属性如下:
private final list<propertysource<?>> propertysourcelist = new copyonwritearraylist<>();
没错,这个就是最底层的存储容器,也就是环境属性都是存放在一个copyonwritearraylist<propertysource<?>>实例中。
mutablepropertysources是propertysources的子类,它提供了get(string name)、addfirst、addlast、addbefore、addafter、remove、replace等便捷方法,方便操作propertysourcelist集合的元素,这里挑选addbefore的源码分析:
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加载过程源码分析
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-1、xxx.yyyy[1].zzzzz=value-2。loader中的逻辑相对复杂,有比较多的遍历和过滤条件,这里不做展开。
environment属性访问源码分析
上文提到过,都是委托到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存储容器的示例图:
参考资料:
spring-boot-starter-web:2.0.3.release源码。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。
上一篇: FreeBSD安装MySQL_MySQL