一. 背景
本文介绍了我在为Spring集成其它配置类(本文展示的是Dubbo)时,使用来自Disconf的动态配置的方法,以及我对项目配置在项目中结构的设计。Disconf相关的配置在上一篇文章的基础上进行了补充。ps: 目前只写了大纲,本文的细节仍需为完善。
二. 配置
Maven配置就不提了,首先谈谈我是如何设计项目配置类的。一个大型项目使用的配置会非常多,建议按照业务类别和配置来源对配置进行拆分,业务类别比如"公共配置"、"产品配置",配置来源比如"Disconf"、"Apollo",顺带一提,我们公司产品线最近准备用apollo替换disconf作为配置中心,但由于产品众多,历史包袱较重,导致一部分新产品使用了Apllo,还有一部分产品仍旧使用Disconf。幸运的是,我在设计初期就将项目配置类的出口统一化了,因此即便将Disconf升级升Apollo,产品中获取配置的代码也不会有任何感知。
以下是产品中项目配置的层次结构,ConfigCentral是项目配置的总出口。(ps: 在这个产品中,我没有把服务拆的太碎)
2.1 Dubbo服务声明
import com.alibaba.dubbo.config.annotation.Reference;
import lombok.Getter;
import xxx.xxx.xxx.TestService;
/**
* dubbo服务
*/
@Getter
@Component
public class DemoDubboReferences {
@Reference
private TestService testService;
}
2.2 Dubbo工厂
@Component
public class DubboFactory {
private static DemoDubboReferences demoDubboReferences;
public static DemoDubboReferences getDemoDubboReferences() {
if(null == demoDubboReferences){
SpringCoreUtils.getBean(DemoDubboReferences.class);
}
return demoDubboReferences;
}
@Autowired
private void setDemoDubboReferences(DemoDubboReferences demoDubboReferences) {
DubboFactory.demoDubboReferences = demoDubboReferences;
}
}
2.3 DubboConfiguration(Dubbo的配置类)
在项目启动时,扫描到DubboConfiguration类后,首先会根据@DubboComponentScan去指定包路径下扫描dubbo注解,发现有@com.alibaba.dubbo.config.annotation.Reference注解后,会想办法注册服务,而注册服务需要RegistryConfig,因此找到当前类的registryConfig( )方法,注意,我们就是要在这个方法中使用从Disconf而来的动态配置。如果不加任何特殊注解是无法实现的,因为此时项目配置类中所有的配置值都是null,当且仅当DisconfMgrBeanSecond进行二次扫描时,才会将新值绑定到项目配置类对应的属性(域)上。这里我借用了@DependsOn( ),在初始化RegistryConfig之前,一定要初始化名称为cat的对象。cat的功能和写法请继续往下看。
import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.ConsumerConfig;
import com.alibaba.dubbo.config.RegistryConfig;
import com.alibaba.dubbo.config.spring.context.annotation.DubboComponentScan;
import org.springframework.context.annotation.*;
import uyun.unipass.config.ConfigCentral;
@Configuration
@DubboComponentScan("uyun.unipass.config.dubbo")
public class DubboConfiguration {
@Bean
public ApplicationConfig applicationConfig() {
ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setName("unipass");
return applicationConfig;
}
@DependsOn("cat")
@Bean(name = "registryConfig")
public RegistryConfig registryConfig() {
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setCheck(false);
registryConfig.setProtocol("zookeeper");
//请求超时时间(单位: 毫秒)
registryConfig.setTimeout(60000);
registryConfig.setAddress(ConfigCentral.getDisConfConfig("zk.connects"));
registryConfig.setClient("curator");
//只订阅不注册,毕竟unipass目前也没有对外提供dubbo服务的打算
registryConfig.setRegister(false);
return registryConfig;
}
@Bean
public ConsumerConfig consumerConfig() {
ConsumerConfig consumerConfig = new ConsumerConfig();
consumerConfig.setTimeout(3000);
return consumerConfig;
}
}
2.4 CommonConfig
package uyun.unipass.config.disconf;
import com.baidu.disconf.client.common.annotations.DisconfFile;
import com.baidu.disconf.client.common.annotations.DisconfFileItem;
import com.baidu.disconf.client.common.annotations.DisconfUpdateService;
import com.baidu.disconf.client.common.update.IDisconfUpdate;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.context.annotation.Scope;
import uyun.unipass.config.ConfigCentral;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* disconfig common 配置文件
* @author mamr
*/
@Slf4j
@ToString
@Setter
@DisconfUpdateService(classes = CommonConfig.class)
@Scope("singleton")
@DisconfFile(filename = "common.properties")
@SuppressWarnings({"unused"})
public class CommonConfig implements IDisconfUpdate {
/**
* zookeeper服务地址
*/
private String zkUrl;
@DisconfFileItem(name = "zk.url")
public String getZkUrl() {
return zkUrl;
}
@Override
public void reload() throws Exception {
log.debug("disconf common.properties配置文件内容发生改变");
Method[] methods = CommonConfig.class.getMethods();
for(Method method : methods) {
DisconfFileItem disConfFileItemAnnotation = method.getAnnotation(DisconfFileItem.class);
if(disConfFileItemAnnotation != null && StringUtils.isNotEmpty(disConfFileItemAnnotation.name())) {
//格式化后的配置名称
String formatName = disConfFileItemAnnotation.name().replaceAll("\\.", "commaUnipass");
String newValue = (String)method.invoke(this);
String oldValue = ConfigCentral.repo.get(formatName);
if(oldValue != null && newValue != null && !StringUtils.equals(oldValue, newValue)) {
ConfigCentral.repo.put(formatName, newValue);
log.info("被更新的配置名称: {}, 旧值: {}, 新值: {}",
disConfFileItemAnnotation.name(), oldValue, newValue);
return;
}
}
}
log.debug("由于不满足条件,本轮没有任何配置被更新");
}
/**
* 初次加载CommonConfig后,初始化common本地配置至仓库
*/
public void init() {
log.info("初始化common本地配置至仓库");
Method[] methods = CommonConfig.class.getMethods();
for(Method method : methods) {
DisconfFileItem disConfFileItemAnnotation = method.getAnnotation(DisconfFileItem.class);
if (disConfFileItemAnnotation != null) {
//格式化后的配置名称
String formatName = disConfFileItemAnnotation.name().replaceAll("\\.", "commaUnipass");
try {
ConfigCentral.repo.put(formatName, (String)method.invoke(this));
}catch (IllegalAccessException | InvocationTargetException e) {
log.error("配置: {} 存入unipass本地配置至仓库时报错, 原因: {}", formatName, e.toString());
}
}
}
}
}
2.5 ConfigCentral
import freemarker.core.InvalidReferenceException;
import freemarker.template.Configuration;
import freemarker.template.Template;
import lombok.extern.slf4j.Slf4j;
/**
* 配置中心
* 用于管理可能用到的所有配置:
* 1. unipass.properties
* 2. common.properties
* 3. 系统配置
*
* @author mamr
*/
@Slf4j
public class ConfigCentral {
/**
* 配置仓库
*/
public static volatile Map<String, String> repo = new HashMap<>();
/**
* 获取来自disconf的配置信息
* @param configName 待获取的配置的名称 [必传]
* @param defaultValue 若没有获取到该配置,则返回的默认值(兜底) [可以为空]
* @return 配置值
*/
public static String getDisConfConfig(String configName, String defaultValue) {
//格式化configName,将.转换成commaUnipass
String formatName = configName.replaceAll("\\.", "commaUnipass");
//采集配置
String value = ConfigCentral.repo.get(formatName);
if(value == null) {
value = defaultValue;
}
if(value != null && value.contains("${")) {
StringWriter result = new StringWriter();
try {
Template t = new Template("template", new StringReader(
value.replaceAll("\\.", "commaUnipass")), new Configuration(Configuration.VERSION_2_3_23));
t.process(repo, result);
value = result.toString();
} catch (InvalidReferenceException e) {
log.error("不要紧张。翻译配置时,找不到配置项映射,因此报错: " + e.toString());
}
catch (Exception e) {
log.error("翻译配置时出现异常: " + e.toString());
}
}
return value;
}
public static String getDisConfConfig(String configName) {
return getDisConfConfig(configName, null);
}
}
2.6 DisconfConfiguration
DisconfMgrBean第一次扫描原本打算在Spring内部所有Bean定义完毕后才执行(间接的继承了BeanFactoryPostProcessor),但遗憾的是,等不到那个时候,DubboConfiguration中的registryConfig早就报错了,因为registryConfig( )方法中使用了来自disconf的配置作为zookeeper服务端的地址,所以我们一定要人为的将disconf扫描、填值得时机提前到registryConfig( )之前。
那么该怎么办呢?
首先,我借助PropertyPlaceholderConfigurer,让Spring扫描DisconfConfiguration配置类的顺序在DubboConfiguration之前,PropertyPlaceholderConfigurer并非是由Disconf提供的,它来自springframework。由于我在启动类中通过@ImportResource引入了spring dao相关的xml配置文件,这些配置文件中含有${ }占位符,Spring扫描到后,会去找申明了PropertyPlaceholderConfigurer的配置文件或配置类,从而间接的提高了DisconfConfiguration配置类的执行优先级。
接着,我借助InitalizingBean,无参构造函数执行后,就会执行afterPropertiesSet()方法通过new初始化CommonConfig等项目配置对象(此时对象中各个域的值是null)。Cat对象在初始化时,首先会强制进行disconf的二次扫描,这一步执行之后,项目配置对象各个域中才会有实际值。
import com.baidu.disconf.client.DisconfMgrBean;
import com.baidu.disconf.client.DisconfMgrBeanSecond;
import com.baidu.disconf.client.addons.properties.ReloadablePropertiesFactoryBean;
import com.google.common.collect.ImmutableList;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.core.Ordered;
import uyun.unipass.config.disconf.CommonConfig;
import uyun.unipass.config.disconf.UnipassConfig;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.Properties;
@Configuration(value = "disConfConfiguration")
public class DisConfConfiguration implements InitializingBean {
private UnipassConfig unipassConfig;
private CommonConfig commonConfig;
@Bean
public UnipassConfig unipassConfig() {
return unipassConfig;
}
@Bean
public CommonConfig commonConfig() {
return commonConfig;
}
@DependsOn({"unipassConfig", "commonConfig"})
@Bean
public Cat cat() {
this.getDisconfMgrBean2().init();
unipassConfig.init();
commonConfig.init();
return new Cat();
}
public class Cat{
}
/**
* DisconfMgrBean 下载远端disconf中的配置文件、扫描本地静态配置类以及配置文件信息,最终将所有的数据整合并入库
* 所谓的入库就是将配置信息存储到一个Map集合中
* DisconfMgrBean着重用处理文件
* @return DisconfMgrBean
*/
@Bean
public DisconfMgrBean getDisconfMgrBean(){
DisconfMgrBean bean = new DisconfMgrBean();
bean.setScanPackage("uyun.unipass.config.disconf");
return bean;
}
/**
* DisconfMgrBean2 扫描配置项或配置文件的回调函数,获取并处理指定的配置文件实例,接着处理配置文件中的所有文件项
* 比如:
* 1. 配置项在仓库中存在,则将此实例的配置文件项的域值设置成仓库里的值。
* 2. 配置项在仓库中不存在,则将此实例的配置文件项的域值设置成默认值。 (默认值可能来源于静态文件、也可能直接来源于数据类型(由内存擦除后获得))
* DisconfMgrBean2着重用处理文件中的每一条文件项。
* 显然,DisconfMgrBeanSecond一定要在DisconfMgrBean之后执行/生成
*/
@Bean(destroyMethod = "destroy", initMethod = "init")
public DisconfMgrBeanSecond getDisconfMgrBean2(){
return new DisconfMgrBeanSecond();
}
@Bean(name = "reloadablePropertiesFactoryBean")
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
public ReloadablePropertiesFactoryBean reloadablePropertiesFactoryBean() {
ReloadablePropertiesFactoryBean propertiesFactoryBean = new ReloadablePropertiesFactoryBean();
List<String> remoteFileNames = ImmutableList.of("classpath*:common.properties", "classpath*:platform-unipass.properties");
propertiesFactoryBean.setSingleton(true);
propertiesFactoryBean.setLocations(remoteFileNames);
return propertiesFactoryBean;
}
/**
* PropertyPlaceholderConfigurer是bean工厂后置处理器的实现,也是BeanFactoryPostProcessor接口的实现
* 它有一个非常重要的作用: 可以在xml中使用类似${spring.datasource.url}的注解,通过读取配置文件,将配置替换成真实值!
* Bootstrap启动类在@ImportResource后,首当其冲开始读取spring-dao.xml,由于该文件中含有${}这种注解,spring为了保证程序不报错,优先扫描
* 创建/提供PropertyPlaceholderConfigurer对象的配置类
* 之所以网上许多文章贴出的代码都喜欢在disconf的配置类中加上propertyPlaceholderConfigurer的初始化工作,其实就是借助了这个原理,让
* Disconf的配置信息先于其他配置类初始化。显然这是有好处的,因为其它配置类中极有可能使用了来自disconf的配置项。
*/
@Bean(name = "propertyPlaceholderConfigurer")
public PropertyPlaceholderConfigurer propertyPlaceholderConfigurer(ReloadablePropertiesFactoryBean reloadablePropertiesFactoryBean) {
PropertyPlaceholderConfigurer placeholderConfigurer = new PropertyPlaceholderConfigurer();
placeholderConfigurer.setIgnoreResourceNotFound(true);
placeholderConfigurer.setIgnoreUnresolvablePlaceholders(true);
try {
Properties properties = reloadablePropertiesFactoryBean.getObject();
placeholderConfigurer.setProperties(Objects.requireNonNull(properties));
} catch (IOException e) {
throw new RuntimeException("unable to find properties", e);
}
return placeholderConfigurer;
}
@Override
public void afterPropertiesSet() {
unipassConfig = new UnipassConfig();
commonConfig = new CommonConfig();
}
}