需求的配置不知不觉中,Web开发已经进入了“微服务”和“分布式”时代。致力于提供Java通用开发解决方案的Spring自然也不甘示弱,提出SpringCloud来扩大Spring在微服务领域的影响力也得到了市场的认可,也被应用到我们的业务中。前几天在一个需求中也遇到了springcloud相关的问题。我们使用的是SpringCloud的config模块,用于支持分布式配置。使用SpringCloud后,原有的单机配置可以支持动态修改和重新加载第三方存储配置和配置。SpringCloud为了实现配置的重载,将整个过程抽取出一个框架,很好的集成到Spring原有的配置和Bean模块中。虽然在解决需求问题上走了一些弯路,但也借此机会了解了一部分SpringCloud,抽空总结了问题和自己在查询问题中学到的知识,分享给遇到这个问题的同学问题不会踩坑。酒吧。背景和问题我们的服务原来有一批单机配置。由于同一个key的配置太长,所以采用数组的形式配置,使用SpringBoot的@ConfigurationProperties和@Value注解解析成Bean属性。属性文件配置如:test.config.elements[0]=value1test.config.elements[1]=value2test.config.elements[2]=value3使用时:@ConfigurationProperties(prefix="test.config")ClassTest{@Value("${#elements}")privateString[]elements;}这样Spring会自动注入Test类,将数组[value1,value2,value3]注入到elements属性中。而我们使用SpringCloud自动加载配置的姿势是这样的:@RefreshScopeclassTest{@Value("${test.config.elements}")privateString[]elements;}@RefreshScope注解的类会随着环境变量的变化而变化自动重载将最新的属性注入到类属性中,但不支持自动注入数组。而我的目标是想办法支持数组类型属性的注入,利用SpringCloud的自动刷新配置特性。环境和属性无论SpringCloud的特性多么优秀,在Spring的地盘上,还是要入乡随俗,与Spring的基础组件进行集成。所以要想理解整个过程,首先要了解Spring的基础知识。Spring是一个大容器。它不仅存储了bean及其依赖,还存储了整个应用的配置。相对于存放各种bean的BeanFactory,Spring管理环境配置的容器是Environment。从Environment中,我们可以获取所有的配置,并且可以根据不同的场景(Profile,比如dev、test、prod)来切换配置。但是Spring管理配置的最小单位不是property,而是PropertySource(属性源)。我们可以理解为PropertySource是一个文件或者配置数据表。Spring在Environment中维护了一个PropertySourceList。当我们获取到配置后,Spring会从这些PropertySources中找到对应的值,并使用ConversionService将这些值转换为对应的类型并返回。SpringCloud配置刷新机制分布式配置SpringCloud提供了一个PropertySourceLocator接口来连接Spring的PropertySource系统。通过PropertySourceLocator,我们得到一个“自定义”的PropertySource。SpringCloud中也有ConfigServicePropertySourceLocator的实现。通过它,我们可以定义一个远程的ConfigService,通过共享这个ConfigService可以实现分布式配置服务。从ConfigClientProperties配置类可以看出,它还预置了远程配置的用户名和密码等安全控制选项,并使用标签来区分服务池等配置。作用域配置刷新远程配置,下一步就是监听变化,根据配置变化进行刷新。SpringCloud提供了ContextRefresher来帮助我们刷新环境。它的主要逻辑在refreshEnvironment方法和scope.refreshAll()方法中,我们分别来看。我们先看看springcloud支持的scope.refreshAll方法。publicvoidrefreshAll(){super.destroy();this.context.publishEvent(newRefreshScopeRefreshedEvent());}scope.refreshAll比较“野蛮”,直接销毁scope,发布一个RefreshScopeRefreshedEvent事件,销毁scope会导致scope里面的所有bean(用RefreshScope注解)都会被销毁。当这些被强制为lazyInit的bean再次被创建时,新配置的reload就完成了。ConfigurationProperties配置刷新再回头看refreshEnvironment方法。Mapbefore=extract(this.context.getEnvironment().getPropertySources());addConfigFilesToEnvironment();Setkeys=changes(before,extract(this.context.getEnvironment().getPropertySources())).keySet();this.context.publishEvent(newEnvironmentChangeEvent(context,keys));returnkeys;读取环境中所有PropertySources中的配置后,重新创建一个SpringApplication刷新配置,重新读取所有的配置item并与之前保存的配置项进行对比,最后针对前后的配置差异发布EnvironmentChangeEvent事件。EnvironmentChangeEvent的监听器由ConfigurationPropertiesRebinder实现,其主要逻辑在rebind方法中。Objectbean=this.applicationContext.getBean(name);if(AopUtils.isAopProxy(bean)){bean=ProxyUtils.getTargetObject(bean);}if(bean!=null){this.applicationContext.getAutowireCapableBeanFactory().destroyBean(bean);this.applicationContext.getAutowireCapableBeanFactory().initializeBean(bean,name);returntrue;可以看到它的处理逻辑,就是执行内部存储的ConfigurationPropertiesBeans的销毁逻辑,然后执行初始化逻辑,实现重新绑定特性。这里我们可以知道SpringCloud在进行配置刷新的时候考虑到了ConfigurationProperties。经测试,ContextRefresher刷新上下文后,ConfigurationProperties注解类的属性会被动态刷新。感觉一次考试就可以解决的事情去解决是浪费时间。.但既然我们已经在这里找到了它,让我们再深入一点。Bean的创建和环境接下来我们看一下创建Bean时环境中的属性是如何使用的。我们知道所有的Springbean都是在BeanFactory中创建的。创建逻辑的入口在AbstractBeanFactory.doGetBean(name,requiredType,args,false)方法中,具体实现在AbstractAutowireCapableBeanFactory.doCreateBean方法中。在这个方法中,Bean实例的创建、属性填充、初始化方法调用等逻辑。这里,一个很复杂的步骤就是调用全局的BeanPostProcessor。该接口是Spring为创建Bean准备的钩子接口。实现该接口的类可以在创建Bean时修改操作。它是一个非常重要的接口,也是我们干预SpringBean创建过程的一个重要切入点。我们说的是它的一个具体实现,ConfigurationPropertiesBindingPostProcessor,它通过调用链式ConfigurationPropertiesBinder.bind()-->Binder.bindObject()-->Binder.findProperty()方法在环境中查找属性。privateConfigurationPropertyfindProperty(ConfigurationPropertyNamename,Contextcontext){if(name.isEmpty()){returnnull;}returncontext.streamSources().map((source)->source.getConfigurationProperty(name)).filter(Objects::nonNull).findFirst().orElse(null);}找到对应的属性后,使用转换器将属性转换为对应的类型,注入到Bean骨骼中。privateObjectbindProperty(Bindabletarget,Contextcontext,ConfigurationPropertyproperty){context.setConfigurationProperty(属性);Objectresult=property.getValue();result=this.placeholdersResolver.resolvePlaceholders(result);result=context.getConverter().我们先梳理一下动态属性注入的关键点,然后从这些关键点中找出可修改的点。PropertySourceLocator从远程数据源引入PropertySource。如果此时我们能够修改数据源的结果,就可以达到目的。而SpringCloud的远程资源定位器ConfigServicePropertySourceLocator和远程调用工具RestTemplate都是实现类。修改,代码很不优雅。创建bean时,它将依次使用BeanPostProcessor对上下文进行操作。这时候添加一个BeanPostProcessor来手动修改Bean的属性。但是这个方法实现起来非常复杂,而且因为每个BeanPostProcessor都会在所有bean创建的时候被调用,所以可能存在安全问题。Spring在解析类属性注入时会使用PropertyResolver将配置项解析为类属性指定的类型。这时候添加一个属性解析器PropertyResolver或者类型转换器ConversionService就可以介入属性操作。但是他们都只负责处理一个属性,而由于我的目标是把“很多”属性做成一个属性,所以他们也无能为力。这里我能想到的办法是借用Spring自动注入的能力,将EnvironmentBean注入到某个类中,然后在该类的初始化方法中修改Environment中的PropertySource,也可以达到目的。这是一个伪代码。.@Component@RefreshScope//使用SpringCloud实现这个Bean的刷新publicclassListSupportPropertyResolver{@AutowiredConfigurableEnvironmentenv;//给Bean注入环境是修改环境的重要前提@PostConstructpublicvoidinit(){//获取属性键值对outoftheenvironmentMapproperties=extract(env.getPropertySources());//解析环境中的数组,提取数组配置Map>listProperties=collectListProperties(properties)MappropertiesMap=newHashMap<>(listProperties);MutablePropertySourcespropertySources=env.getPropertySources();//从数组配置生成一个PropertySource,放到环境propertySources的PropertySourceList中。addFirst(newMapPropertySource("modifiedProperties",propertiesMap));}}这样,在创建bean的时候,就可以把我们修改的PropertySource作为第一优先级。当然,有了更“正规”的方法,我们就不需要修改PropertySource了。毕竟全局修改就等于未知的风险或者埋坑。总结在寻找答案的过程中,我对Environment和BeanFactory是Spring的基石有了更深的理解。框架提供的各种花哨的功能都是基于它们。高级的功能很有用,以后找框架问题会更有针对性。