欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2024-02-20 17:22:58
前提 org.springframework.core.env.environment是当前应用运行环境的公开接口,主要包括应用程序运行环境的两个关键方面:配置文件(...

前提

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

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

environment类体系

基于Spring Boot的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,我们可以查看此接口提供的方法:

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

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);
 }
}

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

自动装配的属性值和environment实例中的属性和预期一样,改造是成功的。

小结

spring中的环境属性管理的源码个人认为是最清晰和简单的:从文件中读取数据转化为key-value结构,key-value结构存放在一个propertysource实例中,然后得到的多个propertysource实例存放在一个copyonwritearraylist中,属性访问的时候总是遍历copyonwritearraylist中的propertysource进行匹配。可能相对复杂的就是占位符的解析和参数类型的转换,后者牵连到converter体系,这些不在本文的讨论范围内。最后附上一张environment存储容器的示例图:

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

参考资料:

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

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。