目录
本次只基于@DisconfFile和@DisconfFileItem两个注解搭配使用的情况,@DisconfItem等注解的组成和数据结构将不会分析。
一、结构组成
1.模块组成
Disconf的大致组成如下图:
总的来说可以把Disconf分成三个大模块:
- 管理模块:这部分模块是与外界进行交互和Disconf启动的入口,其中大部分实现都是通过工厂模式和单例模式完成的,因此如果去看Disconf的源码就会发现,单例模式和工厂模式无处不在,简直泛滥成灾;
- 数据模块:这部分包括了Disconf系统默认的配置数据、用户自定义的配置数据、读取Disconf注解类的配置数据和保存已经解析过的数据仓库等;
- 执行模块:Disconf官方将其称为算子,实际上是差不多意思,这部分的功能便是使用数据模块的数据和Zookeeper实现具体的某种功能。如数据的拉取,Zookeeper节点的监听等。
2.UML类图
UML组成类图如下:
这个UML类图看起来很臃肿是因为Disconf在执行和分析数据这两个模块中大量的使用了工厂模式,导致本应该一个接口就能解决的扩展到了两个类,因此实际上而言重要的类并不是很多。接下来具体看下Disconf的模块包含了哪些类:
- 管理模块:UML类图中可以清晰的看到最下面的三个以DisconfMgr开头和DisconfCoreMgr接口,也就是图中的正下方和左下角的几个类和接口;
- 分析数据模块:UML类图中左上角ScanMgr接口和对应的工厂实现类,这部分的作用便是扫描包路径并且解析扫描到的类是否含有Disconf的注解,最后将解析过后的数据存放到存储数据模块的仓库中;
- 存储数据模块:在UML类图中由中间正上方以DisconfCenter开头为核心的类组成,存储了系统、用户和解析扫描包注解的数据;
- 拉取模块:以FetchMgr接口和其对应的工厂实现类组成,再搭配系列的Processor完成模块功能;
- 监听模块:以WatchMgr接口和其对应的工厂实现类组成,再搭配系列的Processor完成模块功能。
3.数据结构
相关数据结构图如下:
值得一提的是DisconfCenterStore这个Disconf的仓库类里面中的很多成员属性,在此我们只分析confFileMap对象。这个对象的作用便是把@DisconfFile注解中的fileName属性当成key,再将这个文件对应的各种属性使用DisconfCenterFile类封装保存,一一对应形成k-v键值对。
而在DisconfCenterFile类中,封装了注入实例对象,对象的class类型和keyMaps集合对象,keyMaps集合对象中存储的便是@DisconfFileItem注解的name配置名和对应的FileItemValue对象键值对,而在FileItemValue对象中又有@DisconfFileItem注解的对应字段和字段对应的setter方法。
二、原理流程
1.配置类
我们从Disconf的官方文档可以看到如果使用配置类,其推荐的配置方法如下:
@Configuration
public class DisconfConfiguration {
@Bean(destroyMethod = "destroy")
public DisconfMgrBean disconfMgrBean() {
DisconfMgrBean disconfMgrBean = new DisconfMgrBean();
disconfMgrBean.setScanPackage("com.XXXX.disconf");
return disconfMgrBean;
}
@Bean(initMethod = "init", destroyMethod = "destroy")
public DisconfMgrBeanSecond disconfMgrBeanSecond() {
return new DisconfMgrBeanSecond();
}
@Bean(name = "reloadable_disconf")
public ReloadablePropertiesFactoryBean reloadable() {
ReloadablePropertiesFactoryBean bean =
new ReloadablePropertiesFactoryBean();
bean.setLocations(
Collections.singletonList(
Constants.CLASSPATH_FILE
)
);
return bean;
}
@Bean(name = "propertyConfigurer")
public ReloadingPropertyPlaceholderConfigurer properties(
@Qualifier("reloadable_disconf")
ReloadablePropertiesFactoryBean
reloadablePropertiesFactoryBean) throws Exception {
ReloadingPropertyPlaceholderConfigurer ppc =
new ReloadingPropertyPlaceholderConfigurer();
ppc.setPropertiesArray(new Properties[]{
reloadablePropertiesFactoryBean.getObject()});
ppc.setIgnoreUnresolvablePlaceholders(true);
ppc.setIgnoreResourceNotFound(true);
return ppc;
}
}
接下来分别大致说一下各个对象的作用:
- DisconfMgrBean:这个类的大致作用有三点:
- 扫描加载静态的disconf注解类,包括@DisconfFile和@DisconfFileItem等这些注解属性;
- 获取数据,将数据注入到Disconf仓库中,在获取完数据后监听Zookeeper节点,当节点的数据发生变动时将同时Client端更新数据;
- 注册切面对象DisconfAspectJ,用来增强被@DisconfFile和@DisconfFileItem等注解过的类和方法。
- DisconfMgrBeanSecond:其作用有二:
- 获取DisconfMgrBean扫描到@DisconfUpdateService注解配置的回调类,并将其添加到对应的文件属性对象的disconfCommonCallbackModel中;
- 调用FileItemValue对象中的setMethod方法对象,并通过反射将对应的值设置到对象中。
- ReloadablePropertiesFactoryBean:对本机器文件系统中的某些文件进行监听,如果被监听的文件被改动过则修改对应属性的值,并发布文件被修改事件;
- ReloadingPropertyPlaceholderConfigurer:与ReloadablePropertiesFactoryBean搭配使用的刷新对象,当被监听的文件发生改变后则调用监听器的propertiesReloaded方法。该方法将会调用对应的属性对象setter方法进行赋值。
2.配置文件
配置好了上面四个关键bean,再配置一下disconf的用户配置文件:
# 是否使用远程配置文件,true会从远程获取配置,false会直接获取本地配置
disconf.enable.remote.conf=true
# disconf服务器,多个使用逗号分割
disconf.conf_server_host=XXX.XX.XX.XXX:XXXX
# disconf使用环境,优先使用注解的,注解没配置则使用配置文件的
disconf.env=DEV
# app版本号,优先使用注解的,注解没配置则使用配置文件的
disconf.version=v1.0.0
# app名称,优先使用注解的,注解没配置则使用配置文件的
disconf.app=XXXXXXXX
# 忽略哪些分布式配置文件,多个使用逗号分割文件名,如果配置了则不会从远程下载
disconf.ignore=
# 获取远程配置超时重试次数,默认3次
disconf.conf_server_url_retry_times=2
# 获取远程配置超时重试时休眠时间,单位秒
disconf.conf_server_url_retry_sleep_seconds=1
# 自定义远程配置文件下载路径,根路径为系统下
disconf.user_define_download_dir=disconf/remoteFile
# 是否下载到classpath目录下,默认true,推荐为true
# 因为如果配置的false则有可能导致临时文件无法删除
disconf.enable_local_download_dir_in_class_path=true
配置项以及对应相关的功能已经写在了配置中。
3.注解类
最后再配置注解类即可:
@Component
@DisconfFile(filename = Constants.DISCONF_FILE)
public class VirtualConfig {
private String defaultMessage;
@DisconfFileItem(name = "defaultMessage")
public String getDefaultMessage() {
return defaultMessage;
}
public void setDefaultMessage(String defaultMessage) {
this.defaultMessage = defaultMessage;
}
}
4.原理分析
我们知道Disconf一共对外的配置类官方的Demo只有四个(也不算少),其实那四个配置类可以分为两个原理实现:一个是实现Disconf配置和注解加载,并且拉取和监听ZK节点的功能;另一个是对已经下载下来的文件进行监听和回调bean赋值功能。接下来分析大致说明一下。
4.1 Disconf核心加载流程原理
原理流程图如下:
Disconf的核心加载流程我们可以看成就是两次加载,第一次加载为DisconfMgrBean,而第二次则是DisconfMgrBeanSecond,其各自实现了什么功能便不做过多介绍,前面已经分析过了,接下来大致说一下第一次加载和第二次加载的大致作用。
第一次加载:这一次加载是最重要的,这一次会初始化Disconf依赖的执行模块、数据模块和管理模块,并调用执行模块去分析和存储数据,以获得前面分析过的数据结构。在获得所需的数据结构后再调用和监听ZK用来完成拉取和更新文件的操作。当然注册DisconfAspectJ也是这部分功能的重中之重,因为后续调用被@DisconfFile和@DisconfFileItem注解过的类和方法时将直接使用这个增强切面从Disconf仓库中获取。
第二次加载:这一次加载可以说是锦上添花,添加注解回调类,并且调用第一次加载获得对应对象的setter方法为Disconf托管的类进行赋值,但是实际上因为有DisconfAspectJ的存在,因此调用被@DisconfFileItem注解的getter方法时,将直接从Disconf仓库中获取值,而不是获取的实际成员属性,因此这里的setter作用仅仅是让托管的配置类属性像是更新了。
4.2 监听下载文件流程原理
原理流程图如下:
这个监听流程功能可以看成是Disconf功能的扩展,较之于核心加载流程而言算是可有可无,因为这部分的作用对象是已经下载下来的配置文件,如果配置文件下载下来,则说明Disconf仓库中的数据也一定是最新的,要想获取最新的配置直接从Disconf仓库中获取即可,而无需使用这一套监听文件,监听到文件发生改变再调用bean的setter方法进行赋值。
三、使用思考
1.搭配注解的简易使用配置
如果要实现的仅仅只是Disconf-Web端实时的获取最新配置信息,那么完全可以不使用监听文件那一套,甚至Disconf的第二次加载流程也可以忽略。
在配置类中配置DisconfMgrBean类,配置如下:
@Configuration
public class DisconfConfiguration {
@Bean(destroyMethod = "destroy")
public DisconfMgrBean disconfMgrBean() {
DisconfMgrBean disconfMgrBean = new DisconfMgrBean();
disconfMgrBean.setScanPackage("com.iboxpay.disconf");
return disconfMgrBean;
}
}
disconf.properties文件照旧,配置类如下:
@Component
@DisconfFile(filename = Constants.DISCONF_FILE)
public class VirtualConfig {
private String defaultMessage;
@DisconfFileItem(name = "defaultMessage")
public String getDefaultMessage() {
return defaultMessage;
}
}
@DisconfFileItem的associateField属性如果没有配置会自动取get后面头一个字符小写的名字,因此正常情况下可以忽略这个属性,当需要使用defaultMessage属性时直接调用getter方法即可完成动态配置获取,因为DisconfAspectJ切面会直接从Disconf仓库中获取,字段defaultMessage实际有没有值无需关心,只要getter返回正确的值即可。
2.注解方式使用二次加载
既然使用Disconf的第一次加载就可以完成动态获取Disconf配置项的作用,那么第二次加载到底给我们用来干嘛的?实际上当我们需要自定义刷新回调逻辑,如自己实现和Spring的Environment对象对接的逻辑便可以实现IDisconfUpdate接口再使用@DisconfUpdateService注解来完成;再比如我们需要使用到配置类的具体字段属性,在配置类中使用这些字段数形再进行一些逻辑处理等(当然,这种实现方式是有问题的,如果需要对某些配置项进行额外特定的处理,使用设计模式的委派模式来创建一个缓存委派对象是最佳的)。
3.无侵入式另类的简易配置
使用@DisconfFile和@DisconfFileItem等注解来获取配置对代码的侵入性太大,要一直使用注解维护来维护配置类,比较麻烦。接下来要介绍的便是无代码侵入,直接调用方法即可完成获取Disconf配置项的功能。
Disconf经过了第一次加载后,所有的数据以及以后更新后的数据都会存储在Disconf仓库中,且这个仓库实例是一个静态单例,所以我们可以直接从这个仓库中获取数据。由此我们便可以使用另一种种简易的配置方法。
配置类如下:
@Configuration
public class DisconfConfiguration {
@Bean(name = "reloadable_disconf")
public ReloadablePropertiesFactoryBean reloadable() {
DisconfMgr.getInstance().start(new ArrayList<>());
ReloadablePropertiesFactoryBean bean =
new ReloadablePropertiesFactoryBean();
bean.setLocations(
Collections.singletonList(
Constants.DISCONF_FILE
)
);
return bean;
}
}
DisconfMgr实例的start方法里面会调用第一次加载和第二次加载,传个空的List进去将不会进行数据注入拉取等操作,只会进行Disconf核心模块的初始化。接着再使用setLocations方法,这个方法将会去进行数据拉取监听节点等动作。由此便使用了一个配置完成了Disconf数据的获取和监听。这样虽然会进行第二次加载浪费一些性能,但是由于没有什么数据需要操作,因此浪费的时间性能可以忽略不计。
接着定义一个直接从仓库获取数据的类:
public class DisconfVirtualHolder {
/**
* 从virtual文件中获取值
*
* @param key
* @return
*/
public static String getVirtualConfig(String key) {
return getString(DisconfDataGetter
.getByFileItem(Constants.DISCONF_FILE, key));
}
/**
* 转变成String对象
*
* @param obj
* @return
*/
private static String getString(Object obj) {
if (obj != null) {
if (obj instanceof String) {
return (String) obj;
} else if (obj instanceof Integer) {
return ((Integer) obj).toString();
} else if (obj instanceof Long) {
return ((Long) obj).toString();
}
}
return null;
}
}
DisconfDataGetter是Disconf官方给的从Disconf仓库中根据文件名和对应的配置项获取数据,写的例子这里不过是直接指定了文件,只传配置项而已。当我们要使用Disconf的配置时,直接调用getVirtualConfig方法即可,如果有其它的文件,增加一个方法固定文件名即可。