Disconf¶
Distributed Configuration Management Platform(分布式配置管理平台)
专注于各种「分布式系统配置管理」的「通用组件」和「通用平台」, 提供统一的「配置管理服务」。
- disconf: https://github.com/knightliao/disconf
- demos: https://github.com/knightliao/disconf-demos-java
- wiki: https://github.com/knightliao/disconf/wiki
推荐
- 有态度无广告的搜索引擎: https://www.sov5.com
- 高质量的微信公众号阅读: http://www.100weidu.com
- Python中国社区: http://www.python88.com
Install¶
目前项目包含了 客户端disconf-Client和 管理端disconf-Web两个模块。目前所有模块代码均是Java实现。
disconf-client Install¶
在您的 Maven POM 文件里加入:
<dependency>
<groupId>com.baidu.disconf</groupId>
<artifactId>disconf-client</artifactId>
<version>2.6.36</version>
</dependency>
disconf-web安装¶
分布式配置Web平台服务 模块
推荐使用最新的Chrome或Firefox浏览.
开放API¶
- 让开发者具有自定义定制web控制台界面的能力: Tutorial12 open api for web
How to deploy¶
安装依赖软件¶
- 安装Mysql(Ver 14.12 Distrib 5.0.45, for unknown-linux-gnu (x86_64) using EditLine wrapper)
- 安装Tomcat(apache-tomcat-7.0.50)
- 安装Nginx(nginx/1.5.3)
- 安装 zookeeeper (zookeeper-3.3.0)
- 安装 Redis (2.4.5)
准备配置¶
将你的配置文件放到此地址目录下(以下地址可自行设定):
/home/work/dsp/disconf-rd/online-resources
如果不确定如何配置,可以拷贝/disconf-web/profile/rd/目录下的文件,拷贝过去后修改即可。
配置文件包括:
- jdbc-mysql.properties (数据库配置)
- redis-config.properties (Redis配置,主要用于web登录使用)
- zoo.properties (Zookeeper配置)
- application.properties (应用配置)
注意,记得执行将application-demo.properties复制成application.properties:
cp application-demo.properties application.properties
*注意,即使只有一个redis,也应该配置两个redis client,否则将造成内部错误。*
设置War包将要被部署的地址(以下地址可自行设定):
/home/work/dsp/disconf-rd/war
构建¶
ONLINE_CONFIG_PATH=/home/work/dsp/disconf-rd/online-resources
WAR_ROOT_PATH=/home/work/dsp/disconf-rd/war
export ONLINE_CONFIG_PATH
export WAR_ROOT_PATH
cd disconf-web
sh deploy/deploy.sh
这样会在 /home/work/dsp/disconf-rd/war 生成以下结果:
-disconf-web.war
-html
-META-INF
-WEB-INF
上线前的初始化工作¶
初始化数据库:
里面默认有6个用户(请注意线上环境删除这些用户以避免潜在的安全问题)
name | pwd |
---|---|
admin | admin |
testUser1 | MhxzKhl9209 |
testUser2 | MhxzKhl167 |
testUser3 | MhxzKhl783 |
testUser4 | MhxzKhl8758 |
testUser5 | MhxzKhl112 |
如果想自己设置初始化的用户名信息,可以参考代码来自己生成用户:
src/main/java/com/baidu/disconf/web/tools/UserCreateTools.java
部署War¶
修改server.xml文件,在Host结点下设定Context:
<Context path="" docBase="/home/work/dsp/disconf-rd/war"></Context>
并设置端口为 8015
启动Tomcat,即可。
部署 前端¶
修改 nginx.conf
upstream disconf {
server 127.0.0.1:8015;
}
server {
listen 8081;
server_name disconf.com;
access_log /home/work/var/logs/disconf/access.log;
error_log /home/work/var/logs/disconf/error.log;
location / {
root /home/work/dsp/disconf-rd/war/html;
if ($query_string) {
expires max;
}
}
location ~ ^/(api|export) {
proxy_pass_header Server;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_pass http://disconf;
}
}
业务功能¶
- 支持用户登录/登出
- 浏览配置
- 按 APP/版本/环境 选择
- 修改配置
- 修改配置项
- 修改配置文件
- 新建配置
- 新建配置项
- 新建配置文件
- 新建APP
架构方案¶
Nginx(处理静态请求) + Tomcat(处理动态请求)
- 后端
- SpringMvc(Spring 4.1.7.RELEASE)
- Jdbc-Template
- Mysql
- RestFul API
- Redis for user login/logout
- H2内存数据库测试方案/Junit/SpringTest
- 前端
- HTML
- Jquery(1.10.4):JS工具集合
- Bootstrap(2.3.2):界面UI
- Node(ejs/fs/eventproxy): 用于前端的HTML的模板化管理
- 前后端接口(前后端分离)
- 完全Ajax接口
- JSON
- RestFul API
Quick Start¶
TutorialSummary 分布式配置系统功能概述¶
托管配置¶
通过简单的注解类方式 托管配置。托管后,本地不需要此配置文件,统一从配置中心服务获取。
当配置被更新后,注解类的数据自动同步。
@Service
@DisconfFile(filename = "redis.properties")
public class JedisConfig {
// 代表连接地址
private String host;
// 代表连接port
private int port;
/**
* 地址, 分布式文件配置
*
* @return
*/
@DisconfFileItem(name = "redis.host", associateField = "host")
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
/**
* 端口, 分布式文件配置
*
* @return
*/
@DisconfFileItem(name = "redis.port", associateField = "port")
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
}
配置更新回调¶
如果配置更新时,您需要的是 不仅注解类自动同步,并且其它类也需要做些变化,那么您需要一个回调来帮忙。
@Service
@Scope("singleton")
@DisconfUpdateService(classes = { JedisConfig.class }) // 这里或者写成 @DisconfUpdateService(confFileKeys = { "redis.properties" })
public class SimpleRedisServiceUpdateCallback implements IDisconfUpdate
支持基于XML的配置文件托管¶
除了支持基于注解式的配置文件,我们还支持 基于XML无代码侵入式的:
(properties文件更新时数据自动同步reload,非properties文件需要写回调来支持数据自动同步)
<bean id="configproperties_disconf"
class="com.baidu.disconf.client.addons.properties.ReloadablePropertiesFactoryBean">
<property name="locations">
<list>
<value>classpath:/autoconfig.properties</value>
<value>classpath:/autoconfig2.properties</value>
<value>classpath:/myserver_slave.properties</value>
<value>classpath:/testJson.json</value>
<value>classpath:/testXml2.xml</value>
<value>myserver.properties</value>
</list>
</property>
</bean>
<bean id="propertyConfigurer"
class="com.baidu.disconf.client.addons.properties.ReloadingPropertyPlaceholderConfigurer">
<property name="propertiesArray">
<list>
<ref bean="configproperties_disconf"/>
</list>
</property>
</bean>
<bean id="autoService" class="com.example.disconf.demo.service.AutoService">
<property name="auto" value="${auto=100}"/>
</bean>
支持静态配置¶
除了支持@Service类以外,我们还支持 静态配置
@DisconfFile(filename = "static.properties")
public class StaticConfig {
private static int staticVar;
@DisconfFileItem(name = "staticVar", associateField = "staticVar")
public static int getStaticVar() {
return staticVar;
}
}
支持基于XML的配置文件托管: 不会自动reload¶
与 支持基于XML的配置文件托管 相对应,只是在配置文件更改时,不会自动reload到java bean里。
值得说的是,此种方式支持 任意类型 格式配置文件。
<!-- 使用托管方式的disconf配置(无代码侵入, 配置更改不会自动reload)-->
<bean id="configproperties_no_reloadable_disconf"
class="com.baidu.disconf.client.addons.properties.ReloadablePropertiesFactoryBean">
<property name="locations">
<list>
<value>myserver.properties</value>
</list>
</property>
</bean>
<bean id="propertyConfigurerForProject1"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="ignoreResourceNotFound" value="true"/>
<property name="ignoreUnresolvablePlaceholders" value="true"/>
<property name="propertiesArray">
<list>
<ref bean="configproperties_no_reloadable_disconf"/>
</list>
</property>
</bean>
Tutorial-client¶
Tutorial 1 注解式分布式的配置文件(最佳实践)¶
这里以 disconf-demo/disconf-standalone-demo 某个程序片段为例,详细介绍了一个 分布式配置文件 的简单示例程序。
假设,我们的应用程序使用了Redis服务,我们将使用Jedis来进行编程。编程时,我们需要Redis的Host和Port,通常情况下,我们会把这两个参数放在配置文件里。
本教程将以两个部分来进行,
- 第一部分讲解正常情况下(不使用Disconf)的写法,这是我们以前常做的事情 。
- 第二部分,会在第一部分的基础上,添加Disconf的支持。从这一部分,大家就可以看到Disconf的简洁性和低侵入性。 并且,大家也可以看到关闭和开启Disconf,原有程序(第一部分)都可以正确Work。
第一部分:一个简单普通的Redis程序¶
第一步:准备一个配置文件 redis.properties¶
我们需要一个 redis.properties 文件,里面含有 Host 和 Port。文件内容是:
redis.host=127.0.0.1
redis.port=6379
我们需要把此文件放在项目的ClassPath路径下。
第二步:撰写配置文件相应的配置文件类¶
我们撰写一个类JedisConfig,它与 redis.properties 相对应。整个类的完整代码如下:
package com.example.disconf.demo.config;
import org.springframework.stereotype.Service;
/**
* Redis配置文件
*
* @author liaoqiqi
* @version 2014-6-17
*/
@Service
@Scope("singleton")
public class JedisConfig {
// 代表连接地址
private String host;
// 代表连接port
private int port;
/**
* 地址
*
* @return
*/
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
/**
* 端口
*
* @return
*/
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
}
注意,这里的Get&Set方法均是Eclipse自动生成的。
在applicationContext.xml 添加以下代码,目的是将配置值注入到此类中:
<bean id="propertyConfigurerForProject1"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="order" value="1" />
<property name="systemPropertiesModeName" value="SYSTEM_PROPERTIES_MODE_OVERRIDE" />
<property name="ignoreResourceNotFound" value="true" />
<property name="ignoreUnresolvablePlaceholders" value="true" />
<property name="locations">
<list>
<value>classpath*:/redis.properties</value>
</list>
</property>
</bean>
<bean id="jedisConfig" class="com.example.disconf.demo.config.JedisConfig">
<property name="host" value="${redis.host}" />
<property name="port" value="${redis.port}" />
</bean>
第三步:一个简单的Redis服务程序¶
我们的初衷是使用Redis服务。因此,我们需要撰写一个连接Redis的Service类,它使用的是第二步里的配置文件类。完整类的实现代码如下:
package com.example.disconf.demo.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import com.example.disconf.demo.config.JedisConfig;
import com.example.disconf.demo.utils.JedisUtil;
/**
* 一个简单的Redis服务
*
* @author liaoqiqi
* @version 2014-6-17
*/
@Service
@Scope("singleton")
public class SimpleRedisService implements InitializingBean, DisposableBean {
protected static final Logger LOGGER = LoggerFactory
.getLogger(SimpleRedisService.class);
// jedis 实例
private Jedis jedis = null;
/**
* 分布式配置
*/
@Autowired
private JedisConfig jedisConfig;
/**
* 关闭
*/
public void destroy() throws Exception {
if (jedis != null) {
jedis.disconnect();
}
}
/**
* 进行连接
*/
public void afterPropertiesSet() throws Exception {
jedis = JedisUtil.createJedis(jedisConfig.getHost(),
jedisConfig.getPort());
}
/**
* 获取一个值
*
* @param key
* @return
*/
public String getKey(String key) {
if (jedis != null) {
return jedis.get(key);
}
return null;
}
}
具体步骤是:
- 此类实现了 InitializingBean, DisposableBean 两个接口,目的是在Bean初始化后进行Redis的连接。
- 为此类添加 @Service ,代表它是一个Bean。Spring托管的,且 “scope” 都必须是singleton的。
第四步:使用SimpleRedisService¶
使用起来非常简单, 示例如下:
package com.example.disconf.demo.task;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.example.disconf.demo.config.JedisConfig;
import com.example.disconf.demo.service.SimpleRedisService;
/**
* 演示分布式配置文件、分布式配置的更新Demo
*
* @author liaoqiqi
* @version 2014-6-17
*/
@Service
public class DisconfDemoTask {
protected static final Logger LOGGER = LoggerFactory
.getLogger(DisconfDemoTask.class);
@Autowired
private SimpleRedisService simpleRedisService;
@Autowired
private JedisConfig jedisConfig;
private static final String REDIS_KEY = "disconf_key";
/**
*
*/
public int run() {
try {
while (true) {
Thread.sleep(5000);
LOGGER.info("redis( " + jedisConfig.getHost() + ","
+ jedisConfig.getPort() + ") get key: " + REDIS_KEY
}
} catch (Exception e) {
LOGGER.error(e.toString(), e);
}
return 0;
}
}
第二部分:支持分布式配置(disconf)的简单Redis程序¶
第一步:添加Disconf的支持¶
在applicationContext.xml里添加Bean定义:
<!-- 使用disconf必须添加以下配置 -->
<bean id="disconfMgrBean" class="com.baidu.disconf.client.DisconfMgrBean"
destroy-method="destroy">
<property name="scanPackage" value="com.example.disconf.demo"/>
</bean>
<bean id="disconfMgrBean2" class="com.baidu.disconf.client.DisconfMgrBeanSecond"
init-method="init" destroy-method="destroy">
</bean>
另外,从2.6.23起,这里的 scanPackage
属性支持扫描多包,逗号分隔,例如:
<bean id="disconfMgrBean" class="com.baidu.disconf.client.DisconfMgrBean"
destroy-method="destroy">
<property name="scanPackage" value="com.example.disconf.demo,com.example.disconf.demo2"/>
</bean>
第二步 项目准备¶
修改扫描类¶
你的项目的扫描类是com.example,为了支持disconf,因此,必须添加扫描类 com.baidu ,如:
<context:component-scan base-package="com.baidu,com.example"/>
注:从版本2.6.30
开始,不再需要扫描包com.baidu
了,扫描自己的包即可。即:
<context:component-scan base-package="com.example"/>
第三步:修改JedisConfig支持分布式配置¶
我们撰写一个类JedisConfig,它与 redis.properties 相对应。整个类的完整代码如下:
package com.example.disconf.demo.config;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import com.baidu.disconf.client.common.annotations.DisconfFile;
import com.baidu.disconf.client.common.annotations.DisconfFileItem;
/**
* Redis配置文件
*
* @author liaoqiqi
* @version 2014-6-17
*/
@Service
@Scope("singleton")
@DisconfFile(filename = "redis.properties")
public class JedisConfig {
// 代表连接地址
private String host;
// 代表连接port
private int port;
/**
* 地址, 分布式文件配置
*
* @return
*/
@DisconfFileItem(name = "redis.host", associateField = "host")
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
/**
* 端口, 分布式文件配置
*
* @return
*/
@DisconfFileItem(name = "redis.port", associateField = "port")
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
}
具体步骤是:
- 为这个类 JedisConfig 定义 @DisconfFile 注解,必须指定文件名为 redis.properties 。
- 定义域 host port,分别表示Host和Port。并使用Eclipse为其自动生成 get&set 方法。
- 为这两个域的get方法上添加注解 @DisconfFileItem 。添加标记 name, 表示配置文件中的KEY名,这是必填的。标记associateField是可选的,它表示此get方法相关连的域的名字,如果此标记未填,则系统会自动分析get方法,猜测其相对应于域名。强烈建议添加associateField标记,这样就可以避免Eclipse生成的Get/Set方法不符合Java规范的问题。
- 标记它为Spring托管的类 (使用@Service),且 “scope” 都必须是singleton的。
注意:
Eclipse自动生成的get方法,可能与Java的规范不同。这会导致很多问题。因此,建议加上 associateField 标记。
第四步:添加 disconf.properties¶
准备disconf.properties文件:
Disconf启动需要此文件,文件示例是:
# 是否使用远程配置文件
# true(默认)会从远程获取配置 false则直接获取本地配置
enable.remote.conf=true
#
# 配置服务器的 HOST,用逗号分隔 127.0.0.1:8000,127.0.0.1:8000
#
conf_server_host=127.0.0.1:8080
# 版本, 请采用 X_X_X_X 格式
version=1_0_0_0
# APP 请采用 产品线_服务名 格式
app=disconf_demo
# 环境
env=rd
# debug
debug=true
# 忽略哪些分布式配置,用逗号分隔
ignore=
# 获取远程配置 重试次数,默认是3次
conf_server_url_retry_times=1
# 获取远程配置 重试时休眠时间,默认是5秒
conf_server_url_retry_sleep_seconds=1
配置相关说明可参考:配置
注意:如果使用Disconf,则本地的配置文件redis.properties
可以删除掉(也可以不删除掉,建议删除掉)。如果不使用Disconf,则需要此配置文件。
第五步:在disconf-web
上上传配置文件(redis.properties
)¶
当你的程序启动时,disconf就会帮忙你的程序去获取配置文件。那如何让disconf知道你的配置呢?答案是需要在disconf-web上传配置文件哦。
点击主页面的新建配置文件按钮:
进入页面后就可以上传 配置文件了
第六步:在disconf-web
上查看¶
你在第五步上传了配置文件 redis.properties ,那么 ,当你的程序启动时,disconf就会帮忙你的程序去获取配置文件。
可以看到已经有一个实例在使用redis.properties了。
点击查看它的详情,可以看到,确实是我的实例在使用它。
完结¶
至此,分布式配置文件的撰写就已经写完了。
可以看到,基于注解的方式,不需要在xml定义 java bean(config类).
使用方便¶
大家可以看到,第一次使用时,需要
- 在
applicationContext.xml
添加Disconf启动支持 - 使用注解方式 修改配置类
- 添加
disconf.properties
- 在
disconf-web
上上传配置文件
非第一次使用时,需要
- 使用注解方式 修改配置类
- 在
disconf-web
上上传配置文件
就可以支持分布式配置了。
强兼容性¶
并且,如果将 disconf.disconf.properties
中的 enable.remote.conf
设置为 false
并且,如果
disconf-web无法正常服务(conf_server_host=127.0.0.1:8080
),分布式配置也会失效,退化为
使用本地配置方式(即第一部分的功能)。(如果是这种情况,你必须确认你本地留有相应的配置文件)
也就是说,Disconf是具有兼容性的
- 当开启Disconf时,
- 如果Disconf正常运行,则正常使用分布式配置。
- 如果Disconf非正常运行,则使用本地配置。(Disconf可以保证在Disconf失败时,原有程序能够按原有逻辑正确运行)
- 当不开启Disconf时, 则使用本地配置。
注:
- 只要是运行一次分布式程序成功,则本地就含有最全的配置文件。此时,如果再运行一次分布式程序,如果出现失败,则上一次下载成功的配置文件就会当成本地配置生效,程序成功启动。
END¶
Tutorial 2 注解式分布式的配置文件高级篇: 配置更新的通知(最佳实践)¶
也许有一天,我们需要更新Redis的Host和Port配置数据。由于 redis是根据配置生成的实例,因此,这种情况下,你有三种选择:
- 不使用Disconf(Tutorial 1 里第一部分的使用方法)。那么你需要 更改线上机器的配置文件 redis.properties,重启服务就可以了。
- 使用Disconf, 采用 Tutorial 1 里第二部分的使用的方案。那么你需要 更改 分布式服务器 disconf-web 上的 redis.properties 文件。 然后重启服务就可以了。和第一种方法的区别在于,不需要更改线上机器的配置文件。
- 使用Disconf,采用 Tutorial 1 里第二部分的使用的方案,并额外加上本Tutorial的方案,那么你 只需要 更改 分布式服务器 disconf-web 上的 redis .properties 文件。然后服务的配置自动生效。此过程无需要重新启动服务。
本教程就是阐述如何通过简单的配置和极简代码实现第三步的功能。
第一步:准备工作¶
完成 Tutorial 1 上第二部分方案里的步骤。
第二步:修改 SimpleRedisService,支持Redis重连接¶
在这里,我们这个类添加了一个方法,重新生成了一个Jedis对象,代码如下:
/**
* 更改Jedis
*/
public void changeJedis() {
LOGGER.info("start to change jedis hosts to: " + jedisConfig.getHost()
+ " : " + jedisConfig.getPort());
jedis = JedisUtil.createJedis(jedisConfig.getHost(),
jedisConfig.getPort());
LOGGER.info("change ok.");
}
之所以添加这个函数的原因是:在配置更新时,此函数要被调用,从而更改Jedis实例。
整个类的完整代码如下:
package com.example.disconf.demo.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import com.example.disconf.demo.config.JedisConfig;
import com.example.disconf.demo.utils.JedisUtil;
import redis.clients.jedis.Jedis;
/**
* 一个简单的Redis服务
*
* @author liaoqiqi
* @version 2014-6-17
*/
@Service
@Scope("singleton")
public class SimpleRedisService implements InitializingBean, DisposableBean {
protected static final Logger LOGGER = LoggerFactory.getLogger(SimpleRedisService.class);
// jedis 实例
private Jedis jedis = null;
/**
* 分布式配置
*/
@Autowired
private JedisConfig jedisConfig;
/**
* 关闭
*/
public void destroy() throws Exception {
if (jedis != null) {
jedis.disconnect();
}
}
/**
* 进行连接
*/
public void afterPropertiesSet() throws Exception {
jedis = JedisUtil.createJedis(jedisConfig.getHost(), jedisConfig.getPort());
}
/**
* 获取一个值
*
* @param key
*
* @return
*/
public String getKey(String key) {
if (jedis != null) {
return jedis.get(key);
}
return null;
}
/**
* 更改Jedis
*/
public void changeJedis() {
LOGGER.info("start to change jedis hosts to: " + jedisConfig.getHost() + " : " + jedisConfig.getPort());
jedis = JedisUtil.createJedis(jedisConfig.getHost(), jedisConfig.getPort());
LOGGER.info("change ok.");
}
}
第三步: 撰写配置更新回调类¶
当配置更新时,应用程序要得到通知。因此我们要写一个回调类来响应此“通知”。完整的类如下:
package com.example.disconf.demo.service.callbacks;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import com.baidu.disconf.client.common.annotations.DisconfUpdateService;
import com.baidu.disconf.client.common.update.IDisconfUpdate;
import com.example.disconf.demo.config.Coefficients;
import com.example.disconf.demo.config.JedisConfig;
import com.example.disconf.demo.service.SimpleRedisService;
/**
* 更新Redis配置时的回调函数
*
* @author liaoqiqi
* @version 2014-6-17
*/
@Service
@Scope("singleton")
@DisconfUpdateService(classes = {JedisConfig.class}, itemKeys = {Coefficients.key})
public class SimpleRedisServiceUpdateCallback implements IDisconfUpdate {
protected static final Logger LOGGER = LoggerFactory.getLogger(SimpleRedisServiceUpdateCallback.class);
@Autowired
private SimpleRedisService simpleRedisService;
/**
*
*/
public void reload() throws Exception {
simpleRedisService.changeJedis();
}
}
具体步骤是:
- 撰写此类,实现 IDisconfUpdate 接口。此类必须是JavaBean,Spring托管的,且 “scope” 都必须是singleton的。
- 添加 @DisconfUpdateService 注解,classes 值加上 JedisConfig.class ,表示当 JedisConfig.class 这个配置文件更新时,此回调类将会被调用。或者,使用 confFileKeys 也可以。
- 在函数 reload() 里调用 SimpleRedisService 的 changeJedis() 方法
回调类与配置类放在一起¶
如果你觉得写两个类太累,在某些场景下,则可以将回调与配置类放在一起的。
/**
* Redis配置文件
*
* @author liaoqiqi
* @version 2014-6-17
*/
@Service
@Scope("singleton")
@DisconfFile(filename = "redis.properties")
@DisconfUpdateService(classes = {JedisConfig.class})
public class JedisConfig implements IDisconfUpdate {
protected static final Logger LOGGER = LoggerFactory.getLogger(JedisConfig.class);
// 代表连接地址
private String host;
// 代表连接port
private int port;
/**
* 地址, 分布式文件配置
*
* @return
*/
@DisconfFileItem(name = "redis.host", associateField = "host")
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
/**
* 端口, 分布式文件配置
*
* @return
*/
@DisconfFileItem(name = "redis.port", associateField = "port")
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
@Override
public void reload() throws Exception {
LOGGER.info("host: " + host);
}
}
Tutorial 3 注解式分布式的配置项(最佳实践)¶
那如果你的配置只是一个变量,不是配置文件,怎么办?还能实现分布式配置吗?
答案当然是肯定的!
Disconf支持配置项(配置项是指 一个类的某个域变量)的分布式化。
这里以 disconf-demo 某个程序片段为例,详细介绍了 分布式的配置项 的简单示例程序。
在这里,我们将分两种情况来进行演示:
- 配置项在某个配置类里:外部程序通过配置类的get
***
方法获取。 - 配置项在某个Service类里。Service通过本身类的的get
***
方法获取。
对于这两种方式,Disconf的后台实现方式上有所不同。这里不会讲述具体原因。但是Disconf做到了兼容性,以上两种方式均支持。
配置类里的配置项¶
第一步:撰写 配置项类¶
这里假设有一个金融系数类,它有一个折扣率变量。
package com.example.disconf.demo.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import com.baidu.disconf.client.common.annotations.DisconfFile;
import com.baidu.disconf.client.common.annotations.DisconfFileItem;
import com.baidu.disconf.client.common.annotations.DisconfItem;
/**
* 金融系数文件
*
**/
@Service
@DisconfFile(filename = "coefficients.properties")
public class Coefficients {
public static final String key = "discountRate";
@Value(value = "2.0d")
private Double discount;
/**
* 折扣率,分布式配置
*
* @return
*/
@DisconfItem(key = key)
public Double getDiscount() {
return discount;
}
public void setDiscount(Double discount) {
this.discount = discount;
}
}
具体步骤是:
- 编写Coefficients类,添加域 discount
- 用Eclipse为域discount添加 get & set方法
- 为get方法添加注解 @DisconfItem(key = key) ,这里的key是分布式配置项的标识,这里是 discountRate
- 此类必须是JavaBean,Spring托管的,且 “scope” 都必须是singleton的。
- 可以使用@Value为它设置一个默认值。
第二步:使用此分布式配置项¶
撰写一个Service类,它使用 第二步里的 discountRate 变量,完整的类是:
package com.example.disconf.demo.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import com.baidu.disconf.client.common.annotations.DisconfItem;
import com.example.disconf.demo.config.Coefficients;
/**
* 金融宝服务,计算一天赚多少钱
*
* @author liaoqiqi
* @version 2014-5-16
*/
@Service
public class BaoBaoService {
protected static final Logger LOGGER = LoggerFactory
.getLogger(BaoBaoService.class);
@Autowired
private Coefficients coefficients;
/**
*
*
* @return
*/
public double calcMoney() {
return 10000 * coefficients.getDiscount();
}
}
calcMoney()会调用 coefficients.getDiscount() 获取折扣率 来计算 真正的money.
第三步:配置项更新回调类¶
当配置项更新时,你的服务程序自动就会获取最新的配置项数据(不需要写回调函数,因为这里不像Redis这种较“重”的服务,这里的配置是实时生效的)。
但是,如果当你的配置项更新时,配置项本身被更新后,可能还会其它类依赖此配置项的更新,那么,你需要撰写一个回调类来获取此通知。
为了简单,这里我们以 Tutorial 2 里的 SimpleRedisServiceUpdateCallback 类 为基础,进行扩展。
假设,当此配置项被更新时,Redis也需要重新被reload,那么,你可以这样来改写 SimpleRedisServiceUpdateCallback 类,完整的代码如下:
package com.example.disconf.demo.service.callbacks;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.baidu.disconf.client.common.annotations.DisconfUpdateService;
import com.baidu.disconf.client.common.update.IDisconfUpdate;
import com.example.disconf.demo.config.Coefficients;
import com.example.disconf.demo.config.JedisConfig;
/**
* 更新Redis配置时的回调函数
*
* @author liaoqiqi
* @version 2014-6-17
*/
@Service
@DisconfUpdateService(classes = { JedisConfig.class }, itemKeys = { Coefficients.key })
public class SimpleRedisServiceUpdateCallback implements IDisconfUpdate {
protected static final Logger LOGGER = LoggerFactory
.getLogger(SimpleRedisServiceUpdateCallback.class);
@Autowired
private SimpleRedisService simpleRedisService;
/**
*
*/
public void reload() throws Exception {
simpleRedisService.changeJedis();
}
}
这里通过为注解 @DisconfUpdateService 添加一个 itemKeys: Coefficients.key ,就实现了配置项更新的通知。怎么样?是不是很强大?
完结¶
通过几行简单的配置,分布式配置项 就这样添加到你的应用程序里了。
Service类的配置项¶
在上一节里,我们阐述了如何在 配置项类 里添加一个配置项的方法。
在本节里,我们将在上一部分的基础上,阐述如何实现 不创建配置项类 就可以 实现 分布式配置项 的方法。
准备¶
在 分布式配置服务器 disconf-web 上添加 moneyInvest 和 discountRate 配置项值。
第一步:撰写 含有 配置项 的Service类¶
在上一节里,我们撰写了一个 Coefficients.java 类,它含有 分布式配置项 discountRate,BaoBaoService.java 则是一个使用 discountRate 的服务。BaoBaoService.java 在计算(calcMoney)时,使用了固定值 10000.
在本节里,我们 将 10000 这个值动态化,标注为分布式配置项。
完整的类是:
package com.example.disconf.demo.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import com.baidu.disconf.client.common.annotations.DisconfItem;
import com.example.disconf.demo.config.Coefficients;
/**
*
* @author liaoqiqi
* @version 2014-5-16
*/
@Service
public class BaoBaoService {
protected static final Logger LOGGER = LoggerFactory
.getLogger(BaoBaoService.class);
public static final String key = "moneyInvest";
private Double moneyInvest = 1000d;
@Autowired
private Coefficients coefficients;
/**
*
* @return
*/
public double calcMoney() {
return coefficients.getDiscount()
* getMoneyInvest();
}
/**
* 投资的钱,分布式配置 <br/>
* <br/>
* 这里切面无法生效,因为SpringAOP不支持。<br/>
* 但是这里还是正确的,因为我们会将值注入到Bean的值里.
*
* @return
*/
@DisconfItem(key = key)
public Double getMoneyInvest() {
return moneyInvest;
}
public void setMoneyInvest(Double moneyInvest) {
this.moneyInvest = moneyInvest;
}
}
具体实现步骤是:
- 添加域 moneyInvest ,并使用Eclipse自动生成 get&set 方法。
- 为get方法添加 @DisconfItem 注解,并添加 key 为 moneyInvest
- 在函数 calcMoney() 里 调用本身类的 getMoneyInvest() 方法。
- 此类必须是JavaBean,Spring托管的,且 “scope” 都必须是singleton的。
第二步:在disconf-web
上上传配置¶
这里不再赘述。
Tutorial 4 注解式分布式静态配置文件和静态配置项(最佳实践)¶
配置类¶
定义¶
package com.example.disconf.demo.config;
import com.baidu.disconf.client.common.annotations.DisconfFile;
import com.baidu.disconf.client.common.annotations.DisconfFileItem;
/**
* 静态 配置文件 示例
*
* @author liaoqiqi
* @version 2014-6-17
*/
@DisconfFile(filename = "static.properties")
public class StaticConfig {
private static int staticVar;
@DisconfFileItem(name = "staticVar", associateField = "staticVar")
public static int getStaticVar() {
return staticVar;
}
public static void setStaticVar(int staticVar) {
StaticConfig.staticVar = staticVar;
}
}
使用¶
package com.example.disconf.demo.service;
import com.baidu.disconf.client.common.annotations.DisconfItem;
import com.example.disconf.demo.config.StaticConfig;
/**
* 使用静态配置文件的示例<br/>
* Plus <br/>
* 静态配置项 使用示例
*
* @author liaoqiqi
* @version 2014-8-14
*/
public class SimpleStaticService {
private static int staticItem = 56;
/**
*
* @return
*/
public static int getStaticFileData() {
return StaticConfig.getStaticVar();
}
}
和
LOGGER.info("static file data:"
+ SimpleStaticService.getStaticFileData());
配置项¶
定义¶
package com.example.disconf.demo.service;
import com.baidu.disconf.client.common.annotations.DisconfItem;
import com.example.disconf.demo.config.StaticConfig;
/**
* 使用静态配置文件的示例<br/>
* Plus <br/>
* 静态配置项 使用示例
*
* @author liaoqiqi
* @version 2014-8-14
*/
public class SimpleStaticService {
private static int staticItem = 56;
/**
*
* @return
*/
public static int getStaticFileData() {
return StaticConfig.getStaticVar();
}
@DisconfItem(key = "staticItem")
public static int getStaticItem() {
return staticItem;
}
public static void setStaticItem(int staticItem) {
SimpleStaticService.staticItem = staticItem;
}
}
使用¶
LOGGER.info("static item data:"
+ SimpleStaticService.getStaticItem());
Tutorial 5 基于XML的分布式配置文件管理,不会自动reload¶
在 Tutorial 1 里, 我们实现了一个简单的Redis服务程序,它使用分布式配置进行管理,此Redis的配置文件存储在分布式服务器 disconf-web 上。它使用的是注解式的配置管理。
Disconf亦支持非注解式的分布式配置管理,下面定义一下概念:
- 非注解式(托管式):配置文件没有相应的配置注解类,此配置文件不会被注入到配置类中。disconf只是简单的对其进行“托管”。 启动时下载配置文件;配置文件变化时,负责动态推送。注意,此种方式,程序不会自动reload配置,需要自己写回调函数。
由于此方法不会自动reload,因此,对于那些比较重的资源,比如jdbc等,是比较好的托管方式。
使用方法就是在你的applicationContext.xml里添加以下一段代码:
<!-- 使用托管方式的disconf配置(无代码侵入, 配置更改不会自动reload)-->
<bean id="configproperties_no_reloadable_disconf"
class="com.baidu.disconf.client.addons.properties.ReloadablePropertiesFactoryBean">
<property name="locations">
<list>
<value>myserver.properties</value>
</list>
</property>
</bean>
<bean id="propertyConfigurerForProject1"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="ignoreResourceNotFound" value="true"/>
<property name="ignoreUnresolvablePlaceholders" value="true"/>
<property name="propertiesArray">
<list>
<ref bean="configproperties_no_reloadable_disconf"/>
</list>
</property>
</bean>
该配置文件列表将被Disconf全部托管。实例启动时,这些配置文件将被下载,当配置更改时,实例亦能感知到,并重新下载这些配置,并且自动调用回调函数。
目前支持 任意类型的 配置文件的托管。
注意:这些配置文件没有相应的配置文件类。
Tutorial 7 可自定义的部分托管的分布式配置¶
假设我们已经将所有配置文件和配置项都使用Disconf进行托管了。
Disconf考虑到了此种情况。举个实例,数据库配置文件,每个人的数据库可能不大一样,那么,你可以修改 disconf.properties :
# 忽略哪些分布式配置,用逗号分隔
disconf.ignore=jdbc-mysql.properties
将此配置文件添加到ignore的列表里。这样,程序运行时,Disconf就会忽略托管此配置文件,而改为读取你本地的配置文件 jdbc-mysql.properties。
Tutorial 8 基于XML的分布式配置文件管理,自动reload¶
在 Tutorial1, Tutorial2, Tutorial3, Tutorial4 里讲到使用 注解式的分布式配置,它的特点是
- 优点:
- 支持.properties配置文件
- 支持配置项
- 通过撰写配置类,代码结构清晰
- 配置更新时,自动注入
- 支持并发时配置更新统一生效
- 无额外的XML配置,不需要在xml定义 java bean(config类)
- 代码风格 简单易懂
- 缺点:
- 需要撰写配置类,代码侵入
在 Tutorial5 里讲解了非注解式的分布式配置文件动态管理。它的特点是
- 优点:
- 支持任意类型的配置文件
- 无代码侵入
- 缺点:
- 需要在xml定义 java bean
- 配置更新时无法自动注入java bean里。你可以写回调函数来支持自动注入。
在本教程里,将讲解一种基于XML的分布式配置文件管理,它是Tutorial5方式的一种增强,它的特点是:
- 优点:
- 支持任意类型的配置文件
- 对于.properties配置文件,配置更新时,自动注入reload
- 无代码侵入
- 适合于旧项目的迁移
- 缺点:
- 需要在xml定义 java bean
- 非.properties配置更新时无法自动注入java bean里,你可以写回调函数来支持自动注入。
在这里将以 disconf-standalone-demo中的 demo为例,讲解如何实现 无代码侵入的分布式配置
第一步:上传配置文件¶
上传 autoconfig.properties 至 disconf-web里
第二步:修改配置文件¶
添加 基本的 disconf支持
<context:component-scan base-package="com.baidu,com.example"/>
<aop:aspectj-autoproxy proxy-target-class="true"/>
<!-- 使用disconf必须添加以下配置 -->
<bean id="disconfMgrBean" class="com.baidu.disconf.client.DisconfMgrBean"
destroy-method="destroy">
<property name="scanPackage" value="com.example.disconf.demo"/>
</bean>
<bean id="disconfMgrBean2" class="com.baidu.disconf.client.DisconfMgrBeanSecond"
init-method="init" destroy-method="destroy">
</bean>
注:从版本2.6.30
开始,不再需要扫描包com.baidu
了,扫描自己的包即可。即:
<context:component-scan base-package="com.example"/>
<aop:aspectj-autoproxy proxy-target-class="true"/>
<!-- 使用disconf必须添加以下配置 -->
<bean id="disconfMgrBean" class="com.baidu.disconf.client.DisconfMgrBean"
destroy-method="destroy">
<property name="scanPackage" value="com.example.disconf.demo"/>
</bean>
<bean id="disconfMgrBean2" class="com.baidu.disconf.client.DisconfMgrBeanSecond"
init-method="init" destroy-method="destroy">
</bean>
特别的,添加 需要进行托管的 配置文件:
<!-- 使用托管方式的disconf配置(无代码侵入, 配置更改会自动reload)-->
<bean id="configproperties_disconf"
class="com.baidu.disconf.client.addons.properties.ReloadablePropertiesFactoryBean">
<property name="locations">
<list>
<value>classpath:/autoconfig.properties</value>
<value>classpath:/autoconfig2.properties</value>
</list>
</property>
</bean>
<bean id="propertyConfigurer"
class="com.baidu.disconf.client.addons.properties.ReloadingPropertyPlaceholderConfigurer">
<property name="ignoreResourceNotFound" value="true" />
<property name="ignoreUnresolvablePlaceholders" value="true" />
<property name="propertiesArray">
<list>
<ref bean="configproperties_disconf"/>
</list>
</property>
</bean>
在这里,添加了 6个配置文件,其中有4个.properties, 2个非properties文件
添加需要配置的java bean¶
<bean id="autoService" class="com.example.disconf.demo.service.AutoService">
<property name="auto" value="${auto=100}"/>
</bean>
<bean id="autoService2" class="com.example.disconf.demo.service.AutoService2">
<property name="auto2" value="${auto2=100}"/>
</bean>
java bean 与 传统的 spring 写法没有任何区别
补充¶
如果想配置文件,但是不想自动reload,那么该怎么办?
可以使用以下与本方法非常相似的做法:
<!-- 使用托管方式的disconf配置(无代码侵入, 配置更改不会自动reload)-->
<bean id="configproperties_no_reloadable_disconf"
class="com.baidu.disconf.client.addons.properties.ReloadablePropertiesFactoryBean">
<property name="locations">
<list>
<value>myserver.properties</value>
</list>
</property>
</bean>
<bean id="propertyConfigurerForProject1"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="ignoreResourceNotFound" value="true"/>
<property name="ignoreUnresolvablePlaceholders" value="true"/>
<property name="propertiesArray">
<list>
<ref bean="configproperties_no_reloadable_disconf"/>
</list>
</property>
</bean>
在这里,myserver.properties被disconf托管,当在disconf-web上修改配置文件时,配置文件会被自动下载至本地,但是不会reload到系统里。
具体可参见:Tutorial5
完结¶
当在disconf-web中对 4个properties文件中的任何一个文件更新时,所有 使用这些配置文件的java bean都将自动重新注入。无需重启程序。
非properties文件,则需要重启程序才可以支持。当然你可以写回调函数来支持自动注入。
Tutorial 9 实现真正意义上的统一上线包¶
问题¶
一直以来,凡是使用 disconf的程序均需要 disconf.properties
,在这个文件里去控制 app/env/version。
因此,我们要部署到不同的环境中,还是需要 不同的
disconf.properties
。
有一种解决方法是,通过 jenkins 来进行打包,准备多份
disconf.properties
文件。
解决方法¶
真正的解决方法是,使用 java 命令行参数
目前 disconf 已经支持 disconf.properties
中所有配置项
通过参数传入方式 启动。
支持的配置项具体可参见: link
这样的话,未来大家只要通过 Java 参数 就可以 动态的改变启动的 app/env/version
standalone 启动示例¶
java -Ddisconf.env=rd \
-Ddisconf.enable.remote.conf=true \
-Ddisconf.conf_server_host=127.0.0.1:8000 \
-Dlogback.configurationFile=logback.xml \
-Dlog4j.configuration=file:log4j.properties \
-Djava.ext.dirs=lib \
-Xms1g -Xmx2g -cp ampq-logback-client-0.0.1-SNAPSHOT.jar \
com.github.knightliao.consumer.ConsumerMain >/dev/null 2>&1 &
这里表示使用 disconf.env=rd
tomcat 启动示例¶
Tutorial 10 实现一个配置更新下载器agent¶
解决方法¶
done.
Tutorial 11 配置文件下载地址讨论¶
问题一 从disconf下载的配置文件都放到哪里去了?¶
解决:按以下顺序进行判断¶
对于注解式配置文件:
- 一定会下载到 disconf.user_define_download_dir 目录下(使用此方法可以方便的构造一个下载器. Tutorial10 )
- 如果 disconf.enable_local_download_dir_in_class_path
为true(默认值), 则会执行以下判断:
- 如果 @DisconfFile 的 targetDirPath 值不为空, 则会下载到 targetDirPath 这个目录下, 配置数据从该路径读取。这对于那些不想放在classpath根目录的程序, 比较有用.
- 如果 @DisconfFile 的 targetDirPath 值为空, 则会下载到classpath路径下, 配置数据从该路径读取.
- 如果 disconf.enable_local_download_dir_in_class_path 为false, 则不会下载到classpath目录下. 配置数据从 disconf.user_define_download_dir 读取
对于XML式配置文件:
- 一定会下载到 disconf.user_define_download_dir 目录下(使用此方法可以方便的构造一个下载器).
- 如果 disconf.enable_local_download_dir_in_class_path
为true(默认值), 则会执行以下判断:
- 如果 @DisconfFile 的 targetDirPath 值不为空, 则会下载到 targetDirPath 这个目录下.
- 如果 @DisconfFile 的 targetDirPath 值为空, 则会下载到classpath路径下.
- 如果 disconf.enable_local_download_dir_in_class_path 为false, 则不会下载到classpath目录下.
注:
- 如果不作任何配置的改变,默认情况下,会下载到 disconf.user_define_download_dir 目录 和 classpath 两个目录下。
- targetDirPath 值说明:以”/”开头则是系统的全路径,否则则是相对于classpath的路径,默认是classpath根路径。注意:根路径要注意是否有权限,否则会出现找不到路径,推荐采用相对路径。
- 配置说明看这里 config
问题二 不想下载到classpath目录下¶
将 disconf.enable_local_download_dir_in_class_path 为false, 并 指定 下载目录 disconf.user_define_download_dir
Tutorial 13 增加统一的回调类 (unify-notify模式) 灵活处理更新配置通知¶
目的¶
当 任意的 配置文件 或 配置项 得到更新时,此类 就会被调用。
它与 Tutorial2 不一样,不需要注解,不需要必须指定变更地象。更加freely,方便大家在这里统一的、自由的控制更新逻辑.
demo¶
只要实现 IDisconfUpdatePipeline
接口即可。不要求必须是 java bean.
- 函数
reloadDisconfFile
是针对分布式配置文件的。key是文件名;filePath是文件路径。用户可以在这里(read file freely)按你喜欢的解析文件的方式进行处理。 - 函数
reloadDisconfItem
是针对分布式配置项的。key是配置项名;content是其值,并且含有类型信息。
示例代码:
/**
*/
@Service
public class UpdatePipelineCallback implements IDisconfUpdatePipeline {
public void reloadDisconfFile(String key, String filePath) throws Exception {
System.out.println(key + " : " + filePath);
}
public void reloadDisconfItem(String key, Object content) throws Exception {
System.out.println(key + " : " + content);
}
}
Tutorial 14 配置初始化 或 更新时,通知采用 “bean setter模式”¶
目的¶
当通过 java bean 进行控制配置时,当某个配置有初始化或更新时,可以在 bean property setter 方法里 做适合自己的业务逻辑。
demo1: java bean 注解式配置¶
/**
*/
@Service
@Scope("singleton")
@DisconfFile(filename = "redis.properties")
@DisconfUpdateService(classes = {JedisConfig.class})
public class JedisConfig implements IDisconfUpdate {
protected static final Logger LOGGER = LoggerFactory.getLogger(JedisConfig.class);
// 代表连接地址
private String host;
// 代表连接port
private int port;
/**
* 地址, 分布式文件配置
*
* @return
*/
@DisconfFileItem(name = "redis.host", associateField = "host")
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
/**
* 端口, 分布式文件配置
*
* @return
*/
@DisconfFileItem(name = "redis.port", associateField = "port")
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
LOGGER.info("i' m here: setting redis port");
}
public void reload() throws Exception {
LOGGER.info("host: " + host);
}
}
在这里的 setPort 方法 会在该 javabean 初始化
或者 配置更新
时
被调用 。
public void setPort(int port) {
this.port = port;
LOGGER.info("i' m here: setting redis port");
}
demo2: static class 注解式配置¶
/**
*/
@DisconfFile(filename = "static.properties")
public class StaticConfig {
protected static final Logger LOGGER = LoggerFactory.getLogger(StaticConfig.class);
private static int staticVar;
@DisconfFileItem(name = "staticVar", associateField = "staticVar")
public static int getStaticVar() {
return staticVar;
}
public static void setStaticVar(int staticVar) {
StaticConfig.staticVar = staticVar;
LOGGER.info("i' m here: setting static class variable");
}
}
在这里的 setStaticVar 方法 会在该 class 初始化
或者 配置更新
时
被调用 。
demo3: 基于XML配置文件的无侵入式 配置¶
class:
/**
*/
public class AutoService {
protected static final Logger LOGGER = LoggerFactory.getLogger(AutoService.class);
private String auto;
public String getAuto() {
return auto;
}
public void setAuto(String auto) {
this.auto = auto;
LOGGER.info("i' m here: setting auto");
}
}
配置:
<!-- 使用托管方式的disconf配置(无代码侵入, 配置更改会自动reload)-->
<bean id="configproperties_disconf"
class="com.baidu.disconf.client.addons.properties.ReloadablePropertiesFactoryBean">
<property name="locations">
<list>
<value>classpath:/autoconfig.properties</value>
</list>
</property>
</bean>
<bean id="propertyConfigurer"
class="com.baidu.disconf.client.addons.properties.ReloadingPropertyPlaceholderConfigurer">
<property name="ignoreResourceNotFound" value="true"/>
<property name="ignoreUnresolvablePlaceholders" value="true"/>
<property name="propertiesArray">
<list>
<ref bean="configproperties_disconf"/>
</list>
</property>
</bean>
demo4: 配置项 配置¶
@Service
public class BaoBaoService {
protected static final Logger LOGGER = LoggerFactory.getLogger(BaoBaoService.class);
public static final String key = "moneyInvest";
@Value(value = "2000d")
private Double moneyInvest;
@Autowired
private Coefficients coefficients;
/**
* 计算百发一天赚多少钱
*
* @return
*/
public double calcBaiFa() {
return coefficients.getBaiFaCoe() * coefficients.getDiscount() * getMoneyInvest();
}
/**
* k 计算余额宝一天赚多少钱
*
* @return
*/
public double calcYuErBao() {
return coefficients.getYuErBaoCoe() * coefficients.getDiscount() * getMoneyInvest();
}
/**
* 投资的钱,分布式配置 <br/>
* <br/>
* 这里切面无法生效,因为SpringAOP不支持。<br/>
* 但是这里还是正确的,因为我们会将值注入到Bean的值里.
*
* @return
*/
@DisconfItem(key = key)
public Double getMoneyInvest() {
return moneyInvest;
}
public void setMoneyInvest(Double moneyInvest) {
this.moneyInvest = moneyInvest;
LOGGER.info("i' m here: setting moneyInvest");
}
}
在这里的 setMoneyInvest 方法 会在该 class 初始化
或者 配置更新
时 被调用 。
配置更新时通知的所有方式 总结¶
- 指定key的注解式 Tutorial2
- 统一通知模式 unify-notify Tutorial13
- bean setter模式
-jar jar包启动支持¶
2.6.33 版本起开始支持¶
主要升级点¶
当使用以 -jar 方式启动的程序(非tomcat,web方式)时,例如 springboot 时,可以无缝对接(不会出现配置文件找不到的情况)
正确的使用方式¶
<!-- 使用托管方式的disconf配置(无代码侵入, 配置更改会自动reload)-->
<bean id="configproperties_disconf"
class="com.baidu.disconf.client.addons.properties.ReloadablePropertiesFactoryBean">
<property name="locations">
<list>
<value>classpath*:autoconfig.properties</value>
</list>
</property>
</bean>
<bean id="propertyConfigurer"
class="com.baidu.disconf.client.addons.properties.ReloadingPropertyPlaceholderConfigurer">
<property name="ignoreResourceNotFound" value="true"/>
<property name="ignoreUnresolvablePlaceholders" value="true"/>
<property name="propertiesArray">
<list>
<ref bean="configproperties_disconf"/>
</list>
</property>
</bean>
细节¶
在未升级前,要使用spring-boot,我们的配置可能是这样的
<!-- 使用托管方式的disconf配置(无代码侵入, 配置更改会自动reload)-->
<bean id="configproperties_disconf"
class="com.baidu.disconf.client.addons.properties.ReloadablePropertiesFactoryBean">
<property name="locations">
<list>
<value>file:autoconfig.properties</value>
</list>
</property>
</bean>
<bean id="propertyConfigurer"
class="com.baidu.disconf.client.addons.properties.ReloadingPropertyPlaceholderConfigurer">
<property name="ignoreResourceNotFound" value="true"/>
<property name="ignoreUnresolvablePlaceholders" value="true"/>
<property name="propertiesArray">
<list>
<ref bean="configproperties_disconf"/>
</list>
</property>
</bean>
注意,我们使用 file:autoconfig.properties
而不是
classpath*:autoconfig.properties
。这是为什么呢?
当以 -jar 的方式启动服务时,classpath可能会找不到,spring读取配置的原则是:
classpath*:autoconfig.properties
: 只能读取jar包内的配置文件file:autoconfig.properties
: 读取的当前路径下的配置文件
当以 java入口类 的方式启动服务时,classpath一定是WEB-INF/classes或者target/classes目录下,spring读取配置的原则是:
classpath*:autoconfig.properties
: 读取的WEB-INF/classes或者target/classes目录下的配置文件file:autoconfig.properties
: 读取的是当前路径下的配置文件
在以-jar方式启动时,原则上,我们希望将配置文件放在jar包外面,所以这里采用了
file:autoconfig.properties
,当以这种方式启动程序时,disconf将配置下载到当前路径下,程序可以找到配置,不会报错。
file:autoconfig.properties
,程序在当前路径下是无法找到的。这就产生了一个问题:我在IDE调试时,必须使用
classpath*:autoconfig.properties
,当以命令行启动时,必须使用
file:autoconfig.properties
本次升级就是为了避免这个问题。未来配置统一是classpath*:autoconfig.properties
。原理是,在以-jar启动时,通过将当前路径设置为classpath,程序启动后,disconf将配置下载到当前路径,通过读取classpath进行读取配置,就可以找到这个配置了。
统一类获取任何配置数据¶
2.6.34 版本起开始支持¶
主要升级点¶
增加统一的类 来个性化编程式的获取任何配置数据, 目前只支持 .properties 文件
接口¶
public class DisconfDataGetter {
private static IDisconfDataGetter iDisconfDataGetter = new DisconfDataGetterDefaultImpl();
/**
* 根据 分布式配置文件 获取该配置文件的所有数据,以 map形式呈现
*
* @param fileName
*
* @return
*/
public static Map<String, Object> getByFile(String fileName) {
return iDisconfDataGetter.getByFile(fileName);
}
/**
* 获取 分布式配置文件 获取该配置文件 中 某个配置项 的值
*
* @param fileName
* @param fileItem
*
* @return
*/
public static Object getByFileItem(String fileName, String fileItem) {
return iDisconfDataGetter.getByFileItem(fileName, fileItem);
}
/**
* 根据 分布式配置 获取其值
*
* @param itemName
*
* @return
*/
public static Object getByItem(String itemName) {
return iDisconfDataGetter.getByItem(itemName);
}
}
使用方式¶
获取 配置文件 redis.properties 的KV值:
DisconfDataGetter.getByFile("redis.properties");
获取 配置文件 autoconfig.properties 的KV值:
DisconfDataGetter.getByFile("autoconfig.properties")
获取 配置文件 autoconfig.properties 中 key为 auto 的值:
DisconfDataGetter.getByFile("autoconfig.properties").get("auto")
获取 配置文件 autoconfig.properties 中 key为 auto 的值:
DisconfDataGetter.getByFileItem("autoconfig.properties", "auto")
获取 配置项 moneyInvest 的值:
DisconfDataGetter.getByItem("moneyInvest");
Tutorial-web¶
Tutorial 6 disconf-web 功能详解¶
UI及架构¶
Tutorial 12 disconf-web 为界面 开放的 Http API¶
app接口¶
/api/app/list¶
描述:返回所有app的列表
请求类型: GET
参数示例:N/A
返回示例:
{“message”:{},”sessionId”:”3e560be1-9000-4a5c-8371-35312040d8ac”,”success”:”true”,”page”:{“result”:[{“id”:2,”name”:”disconf_demo”}],”order”:null,”orderBy”:null,”totalCount”:1,”pageNo”:null,”pageSize”:null,”footResult”:null}}
/api/app¶
描述:生成一个app
请求类型: POST
参数
name desc 是否必要 1 app app的名字 是 2 desc 描述 是 3 emails 关联的邮箱列表,逗号分隔 否 返回示例:
{“message”:{},”sessionId”:”31617346-2020-4016-bc0d-f7a62d91945b”,”success”:”true”,”result”:”创建成功”}
auth接口¶
/api/account/session¶
描述:获取当前会话信息
请求类型: GET
参数示例:N/A
返回示例:
{“message”:{},”sessionId”:”9d466ef4-1782-451a-8a4c-e2b99601dcba”,”success”:”true”,”result”:{“visitor”:{“id”:6,”name”:”admin”,”role”:null}}}
/api/account/signin¶
描述:登录
请求类型: POST
参数
# name desc 是否必要 1 name 用户名 是 2 password 密码 是 3 remember 是否记住自己 是 返回示例:
失败示例:
{“message”:{“field”:{“password”:”密码不正确”}},”sessionId”:”29efac2d-fec1-40c7-b0d2-8433fb8a8c2c”,”success”:”false”,”status”:2000}或成功示例
{“message”:{},”sessionId”:”53e68882-bf9a-47ab-a1f6-ba347906c2a5”,”success”:”true”,”result”:{“visitor”:{“id”:6,”name”:”admin”,”role”:null}}}
/api/account/signout¶
描述:退出
请求类型: GET
参数示例:N/A
返回示例:
{“message”:{},”sessionId”:”e6c3134d-ba55-4e34-837d-46d08604e2b1”,”success”:”true”,”result”:{“ok”:”ok”}}
env接口¶
/api/env/list¶
描述:返回所有环境的列表
请求类型: GET
参数示例:N/A
返回示例:
{“message”:{},”sessionId”:”826141a9-3255-4beb-88c8-018e40981f6c”,”success”:”true”,”page”:{“result”:[{“id”:1,”name”:”rd”},{“id”:2,”name”:”qa”},{“id”:3,”name”:”local”},{“id”:4,”name”:”online”}],”order”:null,”orderBy”:null,”totalCount”:4,”pageNo”:null,”pageSize”:null,”footResult”:null}}
config接口¶
/api/web/config/item¶
描述:创建配置项
请求类型: POST
参数
# name desc 是否必要 1 key 配置项key 是 2 value 配置项value 是 3 appId app 是 4 version 版本 是 5 envId 环境 是 返回示例:
{“message”:{},”sessionId”:”dc7b3355-2763-4122-bb69-96d2eb282027”,”success”:”true”,”result”:”创建成功”}
/api/web/config/file¶
描述:生成配置, 采用直接上传文件方式
请求类型: POST
参数
# name desc 是否必要 1 myfilerar 配置文件 是 2 appId app 是 3 version 版本 是 4 envId 环境 是 返回示例:
{“message”:{},”sessionId”:”b6a75894-a94b-4075-a4c7-05ed0be6b016”,”success”:”true”,”result”:”创建成功”}
/api/web/config/filetext¶
描述:生成配置, 采用直接上传文本方式
请求类型: POST
参数
# name desc 是否必要 1 fileName 文件名 是 2 fileContent 文件内容 是 3 appId app 是 4 version 版本 是 5 envId 环境 是 返回示例:
{“message”:{},”sessionId”:”b6a75894-a94b-4075-a4c7-05ed0be6b016”,”success”:”true”,”result”:”创建成功”}
/api/web/config/versionlist¶
描述:根据app, env 获取所有的 版本列表
请求类型: GET
参数
# name desc 是否必要 1 appId app 是 2 envId 环境 否 返回示例:
{“message”:{},”sessionId”:”cd908c6a-1dba-42b4-8a6f-3cb997ffb747”,”success”:”true”,”page”:{“result”:[“1_0_0_0”],”order”:null,”orderBy”:null,”totalCount”:1,”pageNo”:null,”pageSize”:null,”footResult”:null}}
/api/web/config/list¶
描述:根据app, env , version 获取所有的 配置列表,含有machine size, machine list,error num 等信息
请求类型: GET
参数
# name desc 是否必要 1 appId app 是 2 envId 环境 是 3 version 版本 是 返回示例:
{“message”:{},”sessionId”:”95839567-d098-4456-b44a-dd556454ec65”,”success”:”true”,”page”:{“result”:[{“configId”:148,”appName”:”disconf_demo”,”appId”:2,”version”:”1_0_0_0”,”envId”:1,”envName”:”rd”,”type”:”配置文件”,”typeId”:0,”key”:”autoconfig.properties”,”value”:”auto=bbdxxjdccdcccdxdcdc\nxx”,”createTime”:”20150320130619”,”modifyTime”:”201603271140”,”machineSize”:1,”machineList”:[{“machine”:”localhost_0_4b860678-290a-4bdf-9a79-2600598f419b”,”value”:”{“auto”:”bbdxxjdccdcccdxdcdc”,”xx”:”“}”,”errorList”:[]}],”errorNum”:0},{“configId”:149,”appName”:”disconf_demo”,”appId”:2,”version”:”1_0_0_0”,”envId”:1,”envName”:”rd”,”type”:”配置文件”,”typeId”:0,”key”:”autoconfig2.properties”,”value”:”auto2=cd你好 坑爹 22fd d”,”createTime”:”20150320130625”,”modifyTime”:”201602011810”,”machineSize”:1,”machineList”:[{“machine”:”localhost_0_4b860678-290a-4bdf-9a79-2600598f419b”,”value”:”{“auto2”:”cd你好 坑爹 22fd d”}”,”errorList”:[]}],”errorNum”:0}.....
/api/web/config/simple/list¶
描述:根据app, env , version 获取所有的 配置列表,无machine size, machine list,error num 等信息
请求类型: GET
参数
# name desc 是否必要 1 appId app 是 2 envId 环境 是 3 version 版本 是 返回示例:
{“message”:{},”sessionId”:”ee170075-0974-4b9a-b341-f8f33beda453”,”success”:”true”,”page”:{“result”:[{“configId”:155,”appName”:”测试”,”appId”:3,”version”:”1_0_0_0”,”envId”:1,”envName”:”rd”,”type”:”配置文件”,”typeId”:0,”key”:”a.properties”,”value”:””,”createTime”:”20160423115212”,”modifyTime”:”201604231152”,”machineSize”:0,”machineList”:[],”errorNum”:0},{“configId”:154,”appName”:”测试”,”appId”:3,”version”:”1_0_0_0”,”envId”:1,”envName”:”rd”,”type”:”配置项”,”typeId”:1,”key”:”dd”,”value”:””,”createTime”:”20160423114721”,”modifyTime”:”201604231147”,”machineSize”:0,”machineList”:[],”errorNum”:0}],”order”:”asc”,”orderBy”:”name”,”totalCount”:2,”pageNo”:null,”pageSize”:null,”footResult”:null}}
/api/web/config/{configId}¶
描述:获取某个config的内容
请求类型: GET
参数
# name desc 是否必要 1 configId configId 是 返回示例:
{“message”:{},”sessionId”:”2944fb48-3735-48a0-a1bf-ad1bf4980c71”,”success”:”true”,”result”:{“configId”:148,”appName”:”disconf_demo”,”appId”:2,”version”:”1_0_0_0”,”envId”:1,”envName”:”rd”,”type”:”配置文件”,”typeId”:0,”key”:”autoconfig.properties”,”value”:”auto=bbdxxjdccdcccdxdcdc\nxx”,”createTime”:”20150320130619”,”modifyTime”:”201603271140”,”machineSize”:0,”machineList”:null,”errorNum”:0}}
/api/web/config/zk/{configId}¶
描述:获取该配置相对应的机器列表以及数据
请求类型: GET
参数
# name desc 是否必要 1 configId configId 是 返回示例:
{“message”:{},”sessionId”:”6bf69e7e-a6f7-4c42-b04e-0336c132fef2”,”success”:”true”,”result”:{“datalist”:[{“machine”:”localhost_0_4b860678-290a-4bdf-9a79-2600598f419b”,”value”:”{“auto”:”bbdxxjdccdcccdxdcdc”,”xx”:”“}”,”errorList”:[]}],”errorNum”:0,”machineSize”:1}}
/api/web/config/download/{configId}¶
描述:以下载文件的形式下载配置
请求类型: GET
参数
# name desc 是否必要 1 configId configId 是 返回示例: N/A
/api/web/config/downloadfilebatch¶
描述:以下载文件的形式批量下载配置
请求类型: GET
参数
# name desc 是否必要 1 appId app 是 2 envId 环境 是 3 version 版本 是 返回示例: N/A
/api/web/config/item/{configId}¶
描述:修改配置项的值
请求类型: PUT
参数
# name desc 是否必要 1 configId configId 是 1 value value 是 返回示例:
{“message”:{},”sessionId”:”004cd21f-f2d4-4754-b5c1-f215266d63c4”,”success”:”true”,”result”:”修改成功,邮件发送失败,请检查邮箱配置”}
/api/web/config/file/{configId}¶
描述:以上传文件的形式 修改配置文件的值
请求类型: PUT
参数
# name desc 是否必要 1 configId configId 是 1 myfilerar 文件 是 - 返回示例: {“message”:{},”sessionId”:”6bacbb02-faf4-416b-bf12-b33d4df328ca”,”success”:”true”,”result”:”修改成功,邮件发送失败,请检查邮箱配置”}
/api/web/config/filetext/{configId}¶
描述:以上传文件内容的形式 修改配置文件的值
请求类型: PUT
参数
# name desc 是否必要 1 configId configId 是 1 fileContent 文件内容 是 - 返回示例: {“message”:{},”sessionId”:”6bacbb02-faf4-416b-bf12-b33d4df328ca”,”success”:”true”,”result”:”修改成功,邮件发送失败,请检查邮箱配置”}
/api/web/config/{configId}¶
描述:删除配置
请求类型: DELETE
参数
# name desc 是否必要 1 configId configId 是 返回示例: {“message”:{},”sessionId”:”b2e36172-1a60-479a-acc9-5854e3f93d98”,”success”:”true”,”result”:”删除成功”}
zk 接口¶
/api/zoo/zkdeploy¶
描述:获取ZK部署情况
请求类型: GET
参数
# name desc 是否必要 1 appId app 是 2 envId 环境 是 3 version 版本 是 返回示例: N/A
Tutorial 12 disconf-web 为客户端 开放的 Http API¶
前言¶
- 目标:让开发者具有自定义开发客户端的能力
- 目前已经支持 java
准备¶
- 获取配置时是从disconf-web获取
- 得到配置更新时是从ZK上获取,得到通知后,再从disconf-web上获取配置值
获取配置接口¶
以下接口均不需要权限控制,Http-Rest 风格
/api/config/item¶
描述:获取配置项
url示例: /api/config/item?app=disconf_demo&env=rd&version=1_0_0_0&key=discountRate
请求类型: GET
参数
# name desc 是否必要 1 app app值 是 2 version version值 是 3 env env值 是 4 key 配置项的key 是 返回示例:
{“status”:1,”message”:””,”value”:”0.5”}
curl 示例
➜ disconf git:(dev) curl 'http://disconf.com/api/config/item?app=disconf_demo&env=rd&version=1_0_0_0&key=discountRate' {"status":1,"message":"","value":"0.5"}
/api/config/file¶
描述:获取配置文件
url示例: /api/config/file?app=disconf_demo&env=rd&version=1_0_0_0&key=autoconfig.properties
请求类型: GET
参数
# name desc 是否必要 1 app app值 是 2 version version值 是 3 env env值 是 4 key 配置文件的key 是 返回示例: 文件
curl 示例
➜ disconf git:(dev) curl 'http://disconf.com/api/config/file?app=disconf_demo&env=rd&version=1_0_0_0&key=autoconfig.properties' auto=bbdxxjdccdcccdxdcdc xx%
得到更新通知的接口¶
客户端程序需要进行订阅ZK结点
在上面的两个示例中,需要分别订阅的结点是:
- /disconf/disconf_demo_1_0_0_0_rd/item/discountRate
- /disconf/disconf_demo_1_0_0_0_rd/file/autoconfig.properties
格式是
/disconf/{{app_name}}_{{version}}_{{env}}/item/keyname
/disconf/{{app_name}}_{{version}}_{{env}}/file/keyname
配置项¶
client配置¶
Disconf-Client¶
配置文件 disconf.properties 说明¶
所有配置均可以通过 命令行 -Dname=value
参数传入。
配置项 | 说明 | 是否必填 | 默认值 |
---|---|---|---|
disconf.conf_server_host | 配置服务器的 HOST,用逗号分隔 ,示例:127.0.0.1:8000,127.0.0.1:8000 | 是 | 必填 |
disconf.app | APP 请采用 产品线_服务名 格式 | 否 | 优先读取命令行参数,然后再读取此文件的值 |
disconf.version | 版本号, 请采用 X_X_X_X 格式 | 否 | 默认为 DEFAULT_VERSION。优先读取命令行参数,然后再读取此文件的值,最后才读取默认值。 |
disconf.enable.remote.conf | 是否使用远程配置文件,true(默认)会从远程获取配置, false则直接获取本地配置 | 否 | false |
disconf.env | 环境 | 否 | 默认为 DEFAULT_ENV。优先读取命令行参数,然后再读取此文件的值,最后才读取默认值 |
disconf.ignore | 忽略的分布式配置,用空格分隔 | 否 | 空 |
disconf.debug | 调试模式。调试模式下,ZK超时或断开连接后不会重新连接(常用于client单步debug)。非调试模式下,ZK超时或断开连接会自动重新连接。 | 否 | false |
disconf.conf_server_url_retry_times | 获取远程配置 重试次数,默认是3次 | 否 | 3 |
disconf.conf_server_url_retry_sleep_seconds | 获取远程配置 重试时休眠时间,默认是5秒 | 否 | 5 |
disconf.user_define_download_dir | 用户定义的下载文件夹, 远程文件下载后会放在这里。注意,此文件夹必须有有权限,否则无法下载到这里 | 否 | ./disconf/download |
disconf.enable_local_download_dir_in_class_path | 下载的文件会被迁移到classpath根路径下,强烈建议将此选项置为 true(默认是true) | 否 | true |
自定义 disconf.properties 文件的路径¶
一般情况下,disconf.properties 应该放在应用程序的根目录下,如果想自定义路径可以使用:
-Ddisconf.conf=/tmp/disconf.properties
常问问题¶
异常考虑¶
使用Disconf-client和Disconf-Web,如果进行一些非预期操作,可能会影响到配置服务的正常运行,下面归纳了 目前配置系统的异常处理策略:
disconf-web事件定义:
A1: 未启动disconf-web
A2: 启动Disconf-web后,未上传一个配置文件叫redis.properties
A3: 在Disconf-Web上更新分布式配置文件
disconf-client事件定义:
B1: 启动包含了Disconf-client的实例
B2: 启动包含了Disconf-client的实例,它需要一个redis.properties作为分布式配置
B3: 有一个包含了Disconf-client的实例使用此分布式配置文件redis.properties
以下为详细表格
操作前提 | 操作时间点 | 操作行为 | 配置系统的处理 | 影响 | 用户建议 |
---|---|---|---|---|---|
A1 | B1 | B1 | 此实例无法使用所有分布式配置 | 此实例使用本地(classpath目录下)的配置 | 启动disconf-web, 然后再重新实例 |
A2 | B1 | B2 | 此实例无法使用此分布式配置redis.properties | 此实例使用本地的配置redis.properties | 先上传redis.properties, 然后重新启动实例 |
A2 & B3 | A3 | 误操作,上传了一个空的配置文件redis.properties | 此实例无法正常更新配置文件redis.properties | 1. 此实例没有更新配置,仍沿用以前的配置 2. 实例日志打印ERROR错误 3. 监控系统报警(由于ZK上的配置数据与DB出现不同步,因此有报警) |
重新上传redis.properties |
A2 & B3 | A3 | 误操作,上传了一个别人的配置文件remote.properties | disconf-web拒绝了此次更新请求 | 上传失败 | 重新上传redis.properties |
Client常问问题¶
异常: ERROR c.b.d.c.c.processor.impl.DisconfCoreProcessUtils - Spring Context is null. Cannot autowire com.szzjcs.commons.thirdapi.push.config.JpushConfig¶
可能原因:
- 程序没有使用spring环境
<context:component-scan>
放在 disconfMgrBean 定义的后面- 对于版本2.6.28(包括此版本)之前的版本, component-scan 可能没有扫描
com.baidu.disconf
解决办法:
- 非静态配置 必须使用spring环境
<context:component-scan>
必须出现在disconfMgrBean之前- 对于版本2.6.28(包括此版本)之前的版本,必须使 component-scan
增加扫描项
com.baidu.disconf
社区贡献¶
其他disconf-web开源实现¶
其他disconf-client开源实现¶
网友贡献/使用教程¶
媒体报道¶
正在使用公司列表¶
- [百度](20+条产品线使用)
- 滴滴出行(上海/北京)
- [银联]
- 网易
- 苏宁易购 (搜索中心数据处理平台)
- 顺丰科技
- 润生活 (千万融资,全线产品使用)
- 拉勾网
- 人脉通 (目前已B轮融资,4条产品线使用)
- 普联(Tp-link)技术有限公司
- 杭州数梦工场科技有限公司
- 众钱网
- 快速递
- 杭州同盾科技
- 杭州趣维科技 (数千万RMB A轮投资)
- 百世物流科技 (在全国建立了400多个运作中心和250万平米的仓库及转运中心,拥有30000多员工和上万个认证加盟商及合作伙伴)
- 仙人掌股票(2015年度最火app,所有产品线已全面接入disconf)
- 多点APP
- 上海华禽网络科技有限公司 (整个公司所有产品线均在使用)
- 新东方在线
- 深圳斯凯荣科技
- 更多
正在关注的公司列表¶
- 洋码头
- 新意互动
联系和赞助¶
联系我¶
- weibo: http://weibo.com/knightliao
- wechat: knightliao
- 主页: http://liaoqiqi.com
版本更新¶
2.6.36¶
2.6.36 发布于 20160911
- disconf-web:
- 配置和配置项可支持自定义app https://github.com/knightliao/disconf/issues/147
- disconf-client:
- 支持https的web端 https://github.com/knightliao/disconf/issues/158
- path支持windows: https://github.com/knightliao/disconf/issues/166
- 删除类 DisconfMgrJustHostFileBean
- DisconfFile 的属性 copy2TargetDirPath 更改为 targetDirPath
2.6.35¶
2016年7月1号
- disconf-client:
- fix bug: support bean annotation @Value (但是不支持reload特性)
- fix bug: 当同时使用XML式和注解式时,当修改配置时,XML式的BEAN也可以重新被注入 https://github.com/knightliao/disconf/issues/70
- disconf-web:
- 支持自定义数据库名
- 需要修改 jdbc-mysql.properties :
- 以前是:jdbc.db_0.url=jdbc:mysql://127.0.0.1:3306?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&rewriteBatchedStatements=false
- 现在是 jdbc.db_0.url=jdbc:mysql://127.0.0.1:3306/disconf?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull &rewriteBatchedStatements=false
- 需要修改 jdbc-mysql.properties :
- 提供修改密码功能
- 需要执行 disconf-web/sql/20160701/20160701.sql
- 提供生成密码的工具
- /disconf-web/bin/sql/makeSql.py
- 支持client两个api
- /api/config/list 可以自由的 指定app env version 的配置项列表(包含值)
- /api/config/simple/list 可以自由的 指定app env version 的配置项列表(不包含值)
- 支持自定义数据库名
2.6.34¶
2016年5月31号
- disconf-client:
- 删除临时创建的lock文件
- 支持从自定义路径读取 disconf.properties
- 例如使用 -Ddisconf.conf=/tmp/disconf.properties 来读取
disconf.properties 文件。默认是从classpath根目录读取此文件。
- issue: https://github.com/knightliao/disconf/issues/93
- 说明: hhttp://disconf.readthedocs.io/zh_CN/stable/config/src/client-config.html#disconf-client
- 例如使用 -Ddisconf.conf=/tmp/disconf.properties 来读取
disconf.properties 文件。默认是从classpath根目录读取此文件。
- 增加统一的类 来个性化编程式的获取任何配置数据
- fix bug:
- 使用xml分布式配置时,当disconf.ignore掉某个配置文件时,启动后会抛异常说该配置文件找不到。修复后不会再报错
- 新增配置项,值可以为空格,修改该配置项,就算改为有效字符,都会提示“服务器内部错误”
- java.lang.IllegalArgumentException: wrong number of arguments
- upgrade common-lang -> common-lang3
- disconf-web:
- 删除web的jackson依赖
- upgrade common-lang -> common-lang3
2.6.33¶
2016月5月07日
- disconf-client:
- (随着以-jar方式启动的程序变得越来越流行)当使用以-jar方式启动的程序(非tomcat,web方式)时,例如springboot时,可以无缝对接(不会出现配置文件找不到的情况)。
2.6.32¶
2016月3月27日
- disconf-client:
- 增加统一的回调类,unify-notify模式:灵活处理更新配置通知(方便大家在这里自由控制更新逻辑)issue-67 Tutorial-13
- 配置初始化或更新时,通知采用 “bean setter模式”: 在注入配置到实例时,优先使用set方法(方便大家在这里写自己逻辑代码),其次才是反射注入。 Tutorial-14
2.6.31¶
发布于:2016月1月8日
- disconf-client:
- 精减库依赖,去掉使用jersey库,避免库冲突: 改采用简单的httpclient下载
- disconf-web:
- 支持历史操作记录第一期,支持数据库记录。
2.6.30¶
发布于:2015年12月1日
disconf-client:
不再需要将
com.baidu
加入扫描包了,只需要扫描自己的包即可。原来的方式
<context:component-scan base-package="com.baidu,com.example"/>
现在的
<context:component-scan base-package="com.example"/>
2.6.29¶
存在BUG,已废弃
2.6.28¶
发布于:2015年11月20日
- disconf-client:
- 每个配置文件的路径支持个性化指定,不一定非是classpath
- 支持spring-boot jar包识别方式 参见:https://github.com/knightliao/disconf-demos-java/tree/master/disconf-spring-boot-demo
- 修复bug:
- 在高于spring-bean3.1.2版本中出现基于XML配置的配置文件无法reload
- 使 disconf.enable_local_download_dir_in_class_path 该配置用户可以自行配置:
- 精减依赖项
2.6.27¶
发布于:2015年10月26日
- disconf-client:
- fix bean order 问题
- disconf-web:
- fix 上传文件(使用贴文本方式)的version无法指定的bug
2.6.26¶
发布于:2015年10月26日
- disconf-client:
- 实现真正意义上的统一上线包:disconf-client 配置文件 disconf.properties 的 所有配置项均支持环境变量方式(命令行)传入 均支持
- (重要)更新disconf.properties中所有配置项,均加上前缀
“disconf.”(此升级具有兼容性,原有配置亦可以运行,但推荐升级)
- 说明: 配置说明
- disconf-web:
- 新建配置时 app下拉页面被截断bug修复 https://github.com/knightliao/disconf/issues/22
2.6.25¶
发布于:2015年8月20日
- disconf-client:
- 实现真正意义上的统一上线包:disconf-client 配置文件 disconf.properties 的 app,env,version 均支持 环境变量方式(命令行)参数传入方式
- disconf.properties支持 user_define_download_dir 项目,用户可以指定将配置下载到你想要的目录
- fix bugs
- disconf-demos
- disconf-spring-boot-demo: 使用disconf的spring-boot demo程序,更少的配置
2.6.23¶
发布于:2015年7月2日
disconf-client:
增加功能:scanPackage 增加扫描多包功能,逗号分隔,例如:
<bean id="disconfMgrBean" class="com.baidu.disconf.client.DisconfMgrBean" destroy-method="destroy"> <property name="scanPackage" value="com.example.disconf.demo,com.example.disconf.demo2"/> </bean>
2.6.22¶
发布于:2015年6月3日
- disconf-client:
- fix bug: 当enable.remote.conf为false时,disconf-client可能无法读取本地配置的问题
2.6.19¶
发布于:2015年1月22日
- disconf-client:
- disconf-web:
- 支持角色系统【普通,管理员,只读管理员】
- 当配置文件里面含有unicode时,支持显示成UTF8
2.6.18¶
发布于:2014年12月19日
- disconf-client:
- FIX BUG: 同一台机器多个实例使用同一个classpath下的并发设置配置文件BUG(非常重要)
- FIX BUG: disconf store use ‘get’ (非常重要)
2.6.16¶
发布于:2014年12月3日
- disconf-client:
- fix zookeeper session expired error: 当ZK集群不可用时,disconf-client可以自动重连,并保证配置watch信息不丢失。
- disconf-web:
2.6.15¶
发布于:2014年11月7日
2.6.14¶
发布于:2014年9月18日
- disconf-client:
- 支持非注解方式(托管式)的配置文件统一化(只支持.propertes格式)
- fix bug: 静态配置文件无法动态更新的BUG
- ZK session expire time enlarge from 5 to 30 seconds
- 支持自定义过滤分布式配置
- disconf-web: 支持更便捷人性化的ZK查询
- 配置文件的输入支持 直接文本输入+上传配置文件方式
- 支持配置文件下载,批量下载
- 支持显示配置所影响的机器源,并提供配置数据校验工具
- 全新Web主页
2.6.13¶
发布于:2014年9月4日
- fix bug: 配置里解析Integer(或类似非String)数据时出错
- Zoo Preifix: client get this value from server, not from local config
- fix bug: disconf不是最高优先级启动,导致在本地没有配置文件时,PropertyPlaceholderConfigurer在Disconf启动前初始化, location为空,因此它认为没有配置文件存在,出现Spring启动失败。 修改方法是,使用BeanDefinitionRegistryPostProcessor使Disconf最高优先级启动,这样后面执行PropertyPlaceholderConfigurer初始化 时就可以发现所有的配置文件。
2.6.11 & 2.6.12¶
- 修复BUG: 当不使用Disconf时,close会有Null异常
- 打日志策略更新:原则上日志为Debug,出错为ERROR,需要注意为WARN
2.6.10¶
- change log:
- 注入静态配置域时不再打印错误字段
- 配置完成后打印配置仓库时打印方式pretty化
- 修复BUG: 支持空配置文件类,如EmptyConf.java,可以使用它来实现简单的同步
2.6.9¶
- FixBug:找不到 disconf_sys.properties
- 增加功能:
- 支持静态配置文件分布式
- 支持配置配置项分布式
2.6.8¶
- Init Version
Disconf设计¶
分布式配置管理平台Disconf¶
摘要¶
为了更好的解决分布式环境下多台服务实例的配置统一管理问题,本文提出了一套完整的分布式配置管理解决方案(简称为disconf[4],下同)。首先,实现了同构系统的配置发布统一化,提供了配置服务server,该服务可以对配置进行持久化管理并对外提供restful接口,在此基础上,基于zookeeper实现对配置更改的实时推送,并且,提供了稳定有效的容灾方案,以及用户体验良好的编程模型和WEB用户管理界面。其次,实现了异构系统的配置包管理,提出基于zookeeper的全局分布式一致性锁来实现主备统一部署、系统异常时的主备自主切换。通过在百度内部以及外部等多个产品线的实践结果表明,本解决方案是有效且稳定的。
技术背景¶
在一个分布式环境中,同类型的服务往往会部署很多实例。这些实例使用了一些配置,为了更好地维护这些配置就产生了配置管理服务。通过这个服务可以轻松地管理成千上百个服务实例的配置问题。
王阿晶提出了基于zooKeeper的配置信息存储方案的设计与实现[1], 它将所有配置存储在zookeeper上,这会导致配置的管理不那么方便,而且他们没有相关的源码实现。淘宝的diamond[2]是淘宝内部使用的一个管理持久配置的系统,它具有完整的开源源码实现,它的特点是简单、可靠、易用,淘宝内部绝大多数系统的配置都采用diamond来进行统一管理。他将所有配置文件里的配置打散化进行存储,只支持KV结构,并且配置更新的推送是非实时的。百度内部的BJF配置中心服务[3]采用了类似淘宝diamond的实现,也是配置打散化、只支持KV和非实时推送。
同构系统是市场的主流,特别地,在业界大量使用部署虚拟化(如JPAAS系统,SAE,BAE)的情况下,同一个系统使用同一个部署包的情景会越来越多。但是,异构系统也有一定的存在意义,譬如,对于“拉模式”的多个下游实例,同一时间点只能只有一个下游实例在运行。在这种情景下,就存在多台实例机器有“主备机”模式的问题。目前国内并没有很明显的解决方案来统一解决此问题。
功能特点与设计理念¶
disconf是一套完整的基于zookeeper的分布式配置统一解决方案。
它的功能特点是
- 支持配置(配置项+配置文件)的分布式化管理
- 配置发布统一化
- 配置发布、更新统一化(云端存储、发布):配置存储在云端系统,用户统一在平台上进行发布、更新配置。
- 配置更新自动化:用户在平台更新配置,使用该配置的系统会自动发现该情况,并应用新配置。特殊地,如果用户为此配置定义了回调函数类,则此函数类会被自动调用。
- 配置异构系统管理
- 异构包部署统一化:这里的异构系统是指一个系统部署多个实例时,由于配置不同,从而需要多个部署包(jar或war)的情况(下同)。使用Disconf后,异构系统的部署只需要一个部署包,不同实例的配置会自动分配。特别地,在业界大量使用部署虚拟化(如JPAAS系统,SAE,BAE)的情况下,同一个系统使用同一个部署包的情景会越来越多,Disconf可以很自然地与他天然契合。 异构主备自动切换:如果一个异构系统存在主备机,主机发生挂机时,备机可以自动获取主机配置从而变成主机。
- 异构主备机Context共享工具:异构系统下,主备机切换时可能需要共享Context。可以使用Context共享工具来共享主备的Context。
- 注解式编程,极简的使用方式:我们追求的是极简的、用户编程体验良好的编程方式。通过简单的标注+极简单的代码撰写,即可完成复杂的配置分布式化。
- 需要Spring编程环境
它的设计理念是:
- 简单,用户体验良好:
- 摒弃了打散化配置的管理方式[2,3],仍旧采用基于配置文件的编程方式,这和程序员以前的编程习惯(配置都是放在配置文件里)一致。特别的,为了支持较为小众的打散化配置功能,还特别支持了配置项。
- 采用了基于XML无代码侵入编程方式:只需要几行XML配置,即可实现配置文件发布更新统一化、自动化。
- 采用了基于注解式的弱代码侵入编程方式:通过编程规范,一个配置文件一个配置类,代码结构简单易懂。XML几乎没有任何更改,与原springXML配置一样。真正编程时,几乎感觉不到配置已经分布式化
- 可以托管任何类型的配置文件,这与[2,3]只能支持KV结构的功能有较大的改进。
- 配置更新实时推送
- 提供界面良好Web管理功能,可以非常方便的查看配置被哪些实例使用了。
详细设计¶
架构设计¶
disconf服务集群模式:
disconf的模块架构图:
每个模块的简单介绍如下:
- Disconf-core
- 分布式通知模块:支持配置更新的实时化通知
- 路径管理模块:统一管理内部配置路径URL
- Disconf-client
- 配置仓库容器模块:统一管理用户实例中本地配置文件和配置项的内存数据存储
- 配置reload模块:监控本地配置文件的变动,并自动reload到指定bean
- 扫描模块:支持扫描所有disconf注解的类和域
- 下载模块:restful风格的下载配置文件和配置项
- watch模块:监控远程配置文件和配置项的变化
- 主备分配模块:主备竞争结束后,统一管理主备分配与主备监控控制
- 主备竞争模块:支持分布式环境下的主备竞争
- Disconf-web
- 配置存储模块:管理所有配置的存储和读取
- 配置管理模块:支持配置的上传、下载、更新
- 通知模块:当配置更新后,实时通知使用这些配置的所有实例
- 配置自检监控模块:自动定时校验实例本地配置与中心配置是否一致
- 权限控制:web的简单权限控制
- Disconf-tools
- context共享模块:提供多实例间context的共享。
流程设计¶
运行流程详细介绍:
与2.0版本的主要区别是支持了:主备分配功能/主备切换事件。
- 启动事件A:以下按顺序发生。
- A3:扫描静态注解类数据,并注入到配置仓库里。
- A4+A2:根据仓库里的配置文件、配置项,去 disconf-web 平台里下载配置数据。这里会有主备竞争
- A5:将下载得到的配置数据值注入到仓库里。
- A6:根据仓库里的配置文件、配置项,去ZK上监控结点。
- A7+A2:根据XML配置定义,到 disconf-web 平台里下载配置文件,放在仓库里,并监控ZK结点。这里会有主备竞争。
- A8:A1-A6均是处理静态类数据。A7是处理动态类数据,包括:实例化配置的回调函数类;将配置的值注入到配置实体里。
- 更新配置事件B:以下按顺序发生。
- B1:管理员在 Disconf-web 平台上更新配置。
- B2:Disconf-web 平台发送配置更新消息给ZK指定的结点。
- B3:ZK通知 Disconf-cient 模块。
- B4:与A4一样。
- B5:与A5一样。
- B6:基本与A4一样,唯一的区别是,这里还会将配置的新值注入到配置实体里。
- 主备机切换事件C:以下按顺序发生。
- C1:发生主机挂机事件。
- C2:ZK通知所有被影响到的备机。
- C4:与A2一样。
- C5:与A4一样。
- C6:与A5一样。
- C7:与A6一样。
模块实现¶
本部分会重点介绍disconf-client的实现方式。
注解式disconf实现¶
本实现会涉及到 配置仓库容器模块、扫描模块、下载模块、watch模块,
使用AOP拦截的一个好处是可以比较轻松的实现配置控制,比如并发环境下的配置统一生效。关于这方面的讨论可以见这里。
特别地,本方式提供的编程模式非常简单,例如使用以下配置类的程序在使用它时,可以直接@Autowired进来进行调用,使用它时就和平常使用普通的JavaBean一样,但其实它已经分布式化了。配置更新时,配置类亦会自动更新。
@Service
@DisconfFile(filename = "redis.properties")
public class JedisConfig {
// 代表连接地址
private String host;
// 代表连接port
private int port;
/**
* 地址, 分布式文件配置
*
* @return
*/
@DisconfFileItem(name = "redis.host", associateField = "host")
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
/**
* 端口, 分布式文件配置
*
* @return
*/
@DisconfFileItem(name = "redis.port", associateField = "port")
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
}
基于XML配置disconf实现¶
本实现提供了无任何代码侵入方式的分布式配置。
ReloadablePropertiesFactoryBean继承了Spring Properties文件的PropertiesFactoryBean类,管理所有当配置更新时要进行reload的配置文件。对于被管理的每一个配置文件,都会通过 配置仓库容器模块、扫描模块、下载模块、watch模块 进行配置获取至配置仓库里。
ReloadingPropertyPlaceholderConfigurer继承了Spring Bean配置值控制类PropertyPlaceholderConfigurer。在第一次扫描spring bean 时,disconf会记录配置文件的配置与哪些bean有关联。
ReloadConfigurationMonitor是一个定时任务,定时check本地配置文件是否有更新。
当配置中心的配置被更新时,配置文件会被下载至实例本地,ReloadConfigurationMonitor即会监控到此行为,并且通知 ReloadingPropertyPlaceholderConfigurer 对相关的bean类进行值更新。
特别的,此种方式无法解决并发情况下配置统一生效的问题。
主备分配实现¶
在实现中,为每个配置提供主备选择的概念。用户实例在获取配置前需要先进行全局唯一性竞争才能得到配置值。在这里,我们采用基于zookeeper的全局唯一性锁来实现。
Comparisons¶
淘宝Diamond[2] | Disconf | 比较 | |
---|---|---|---|
数据持久性 | 存储在mysql上 | 存储在mysql上 | 都持久化到数据库里,都易于管理 |
推拉模型 | 拉模型,每隔15s拉一次全量数据 | 基于Zookeeper的推模型,实时推送 | disconf基于分布式的Zookeeper来实时推送,不断是在稳定性、实效性、易用性上均优于diamond |
配置读写 | 支持实例对配置读写。支持某台实例写配置数据,并广播到其它实例上 | 只支持实例对配置读。通过在disconf-web上更新配置到达到广播写到所有应用实例 | 从目前的应用场景来看,实例对配置的写需求不是那么明显。disconf支持的中心化广播方案可能会与人性思考更加相似。 |
容灾 | 多级容灾模式,配置数据会dump在本地,避免中心服务挂机时无法使用 | 多级容灾模式,优先读取本地配置文件。 | 双方均支持在中心服务挂机时配置实例仍然可以使用 |
配置数据模型 | 只支持KV结构的数据,非配置文件模式 | 支持传统的配置文件模式(配置文件),亦支持KV结构数据(配置项) | 使用配置文件的编程方式可能与程序员的编程习惯更为相似,更易于接受和使用。 |
编程模型 | 需要将配置文件拆成多个配置项,没有明显的编程模型 | 在使用配置文件的基础上,提供了注解式和基于XML的两种编程模型 | 无 |
并发性 | 多条配置要同时生效时,无法解决并发同时生效的问题 | 基于注解式的配置,可以解决并发性问题 | 无 |
Reference¶
- 王阿晶,邹仕洪: 基于ZooKeeper的配置信息存储方案的设计与实现
- 淘宝diamod实现:http://code.taobao.org/p/diamond/src/, 2012
- 百度BJF配置中心, 2014
- disconf github: https://github.com/knightliao/disconf, 2014
- 淘宝分布式配置管理服务Diamond
- zooKeeper和Diamond有什么不同
- diamond专题(一)– 简介和快速使用
Disconf-client详细设计文档¶
本文档主要阐述了版本 Disconf-Client 的设计。、
程序运行流程图¶
版本2.0的设计¶
运行流程详细介绍:
- 启动事件A:以下按顺序发生。
- A1:扫描静态注解类数据,并注入到配置仓库里。
- A2:根据仓库里的配置文件、配置项,到 disconf-web 平台里下载配置数据。
- A3:将下载得到的配置数据值注入到仓库里。
- A4:根据仓库里的配置文件、配置项,去ZK上监控结点。
- A5:根据XML配置定义,到 disconf-web 平台里下载配置文件,放在仓库里,并监控ZK结点。
- A6:A1-A5均是处理静态类数据。A6是处理动态类数据,包括:实例化配置的回调函数类;将配置的值注入到配置实体里。
- 更新配置事件B:以下按顺序发生。
- B1:管理员在 Disconf-web 平台上更新配置。
- B2:Disconf-web 平台发送配置更新消息给ZK指定的结点。
- B3:ZK通知 Disconf-cient 模块。
- B4:与A2一样。唯一不同的是它只处理一个配置文件或者一个配置项,而事件A2则是处理所有配置文件和配置项。下同。
- B5:与A3一样。
- B6:基本与A4一样,区别是,这里还会将配置的新值注入到配置实体里。
完全版的设计¶
[点击查看大图 ](http://ww3.sinaimg.cn/mw1024/60c9620fjw1eqj81no7shj20l50h2q65.jpg
运行流程详细介绍:
与2.0版本的主要区别是支持了:主备分配功能/主备切换事件。
- 启动事件A:以下按顺序发生。
- A3:扫描静态注解类数据,并注入到配置仓库里。
- A4+A2:根据仓库里的配置文件、配置项,去 disconf-web 平台里下载配置数据。这里会有主备竞争
- A5:将下载得到的配置数据值注入到仓库里。
- A6:根据仓库里的配置文件、配置项,去ZK上监控结点。
- A7+A2:根据XML配置定义,到 disconf-web 平台里下载配置文件,放在仓库里,并监控ZK结点。这里会有主备竞争。
- A8:A1-A6均是处理静态类数据。A7是处理动态类数据,包括:实例化配置的回调函数类;将配置的值注入到配置实体里。
- 更新配置事件B:以下按顺序发生。
- B1:管理员在 Disconf-web 平台上更新配置。
- B2:Disconf-web 平台发送配置更新消息给ZK指定的结点。
- B3:ZK通知 Disconf-cient 模块。
- B4:与A4一样。
- B5:与A5一样。
- B6:基本与A4一样,唯一的区别是,这里还会将配置的新值注入到配置实体里。
- 主备机切换事件C:以下按顺序发生。
- C1:发生主机挂机事件。
- C2:ZK通知所有被影响到的备机。
- C4:与A2一样。
- C5:与A4一样。
- C6:与A5一样。
- C7:与A6一样。
类设计图¶
Disconf-client包括的大模块有:
- scan 配置扫描模块
- core 配置核心处理模块
- fetch 配置抓取模块
- watch 配置监控模块
- store 配置仓库模块
- addons 配置reload模块
各个模块均采用以下设计模式来进设计:
- 各个模块均以接口的方式对外暴露,松耦合,强内聚
- 各个模块均提供工厂类由其它模块来进行获取实例,实例的操纵方式均采用接口方式。
- 对于配置文件和配置项,采用类扩展的方法来避免if else判断。
Disconf-client 的启动¶
启动分成两步,由两个Bean来实现
<bean id="disconfMgrBean" class="com.baidu.disconf.client.DisconfMgrBean"
destroy-method="destroy">
<property name="scanPackage" value="com.baidu.disconf.demo" />
</bean>
<bean id="disconfMgrBean2" class="com.baidu.disconf.client.DisconfMgrBeanSecond"
init-method="init" destroy-method="destroy">
</bean>
这里 com.baidu.disconf.dem 是要扫描的类。
第一步由Bean com.baidu.disconf.client.DisconfMgrBean 来控制。第二步由 com.baidu.disconf.client.DisconfMgrBeanSecond 控制。
第一步:com.baidu.disconf.client.DisconfMgrBean¶
此Bean实现了BeanFactoryPostProcessor和PriorityOrdered接口。它的Bean初始化Order是最高优先级的。
因此,当Spring扫描了所有的Bean信息后,在所有Bean初始化(init)之前,DisconfMgrBean的postProcessBeanFactory方法将被调用,在这里,Disconf-Client会进行第一次扫描。
扫描按顺序做了以下几个事情:
- 初始化Disconf-client自己的配置模块。
- 初始化Scan模块。
- 初始化Core模块,并极联初始化Watch,Fetcher,Restful模块。
- 扫描用户类,整合分布式配置注解相关的静态类信息至配置仓库里。
- 执行Core模块,从disconf-web平台上下载配置数据:配置文件下载到本地,配置项直接下载。
- 配置文件和配置项的数据会注入到配置仓库里。
- 使用watch模块为所有配置关联ZK上的结点。
其中对配置的处理详细为:
分布式配置的实现¶
下面将 分别详细阐述 分布式配置文件 和 分布式配置项 的实现方式。
由于目前版本只支持 Spring编程方式,因此,以下均只阐述Spring编程下的实现方式。
注解式实现¶
分布式配置文件的实现¶
定义分布式配置文件类
对于配置文件,我们必须实现一个Java类来表示此 分布式配置文件。如:
package com.example.disconf.demo.config;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import com.baidu.disconf.client.common.annotations.DisconfFile;
import com.baidu.disconf.client.common.annotations.DisconfFileItem;
/**
* Redis配置文件
*
* @author liaoqiqi
* @version 2014-6-17
*/
@Service
@Scope("singleton")
@DisconfFile(filename = "redis.properties")
public class JedisConfig {
// 代表连接地址
private String host;
// 代表连接port
private int port;
/**
* 地址, 分布式文件配置
*
* @return
*/
@DisconfFileItem(name = "redis.host", associateField = "host")
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
/**
* 端口, 分布式文件配置
*
* @return
*/
@DisconfFileItem(name = "redis.port", associateField = "port")
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
}
对于此Java类,它必须是Spring托管的。此配置文件是redis.properties。
此配置类必须标注为 @DisconfFile,标识它是一个分布式配置文件。且必须指定文件名。
此配置类含有两个配置项,分别是host和port。这两个变量必须有 get 方法。且get方法名必须是符合JavaBean规范的。
我们通过在这两个变量的 get 方法上添加 @DisconfFileItem 注解来标注它是分布式配置文件里的配置项。必须指定name参数,表示配置文件里的KEY值。associateField值是可选的,表示此get方法相对应的域的名字。
Disconf-client优先启动,并从平台上下载配置文件:
应用程序启动时,当Spring容器扫描了所有Java Bean却还未初始化这些Bean时,disconf-client 模块会优先开始初始化(最高优先级)。它会将 配置文件名、配置项名记录在配置仓库里,并去 disconf-web 平台下载配置文件至classpath目录下。并且,还会到ZK上生成相应的结点。
接着Spring开始初始化用户定义的SpringBean。由于配置文件已经被正确下载至Classpath路径下,因此,JavaBean的配置文件使用的是分布式配置文件,而非本地的配置文件。
待SpringBean初始化后,Disconf-client会获取配置更新回调类实例:
此时,Spring上的所有Bean均已被init。Disconf-client模块会再次运行,这时它会去获取用户撰写的配置更新回调函数类实例。
一个配置更新回调函数通常是这样撰写的:
package com.example.disconf.demo.service.callbacks;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import com.baidu.disconf.client.common.annotations.DisconfUpdateService;
import com.baidu.disconf.client.common.update.IDisconfUpdate;
import com.example.disconf.demo.config.Coefficients;
import com.example.disconf.demo.config.JedisConfig;
import com.example.disconf.demo.service.SimpleRedisService;
/**
* 更新Redis配置时的回调函数
*
* @author liaoqiqi
* @version 2014-6-17
*/
@Service
@Scope("singleton")
@DisconfUpdateService(classes = {JedisConfig.class}, itemKeys = {Coefficients.key})
public class SimpleRedisServiceUpdateCallback implements IDisconfUpdate {
protected static final Logger LOGGER = LoggerFactory.getLogger(SimpleRedisServiceUpdateCallback.class);
@Autowired
private SimpleRedisService simpleRedisService;
/**
*
*/
public void reload() throws Exception {
simpleRedisService.changeJedis();
}
}
此类必须实现接口IDisconfUpdate,它可以不必是Java托管的。如果是SpringBean,则disconf-client会从Spring容器里获取此Bean。如果它不是SpringBean,disconf-client就会new一个实例出来。
使用SpringBean来定义此类的好处是,我们可以在此类中使用@Autowired来使用其它SpringBean。比较方便些。
disconf-client根据注解@DisconfUpdateService 以配置文件为Key,将回调函数实例列表放在此Key的Map里。当配置文件更新时,这些回调函数实例就会被按顺序执行。
配置文件更新时,分布式配置文件会重新被下载:
当配置文件更新时,disconf-client便会重新从 disconf-web 平台下载配置文件,并重新将值放在配置仓库里。并按顺序进行调用回调函数类的 reload() 方法。
如何使用分布式配置文件类:
在上面我们说到,配置文件类中的配置项必须有 get 方法,并且必须有 @DisconfFileItem 注解。
在 get 上面添加注解的原因就是为了做切面。
disconf-cient使用Spring AOP拦截 系统里所有含有@DisconfFileItem注解的 get 方法,把所有此类请求都定向到用户程序的配置仓库中去获取。
通过这种方式,我们可以实现统一的、集中式的在配置仓库里去获取配置文件数据。这是一种简洁的实现方式。
分布式配置项的实现¶
配置项相对于配置文件,比较灵活。我们可以在任何SpringBean里添加配置项。
如以下是在一个配置文件类里添加配置项:
package com.example.disconf.demo.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import com.baidu.disconf.client.common.annotations.DisconfFile;
import com.baidu.disconf.client.common.annotations.DisconfFileItem;
import com.baidu.disconf.client.common.annotations.DisconfItem;
/**
* 金融系数文件
*/
@Service
@DisconfFile(filename = "coefficients.properties")
public class Coefficients {
public static final String key = "discountRate";
@Value(value = "2.0d")
private Double discount;
private double baiFaCoe;
private double yuErBaoCoe;
/**
* 阿里余额宝的系数, 分布式文件配置
*
* @return
*/
@DisconfFileItem(name = "coe.baiFaCoe")
public double getBaiFaCoe() {
return baiFaCoe;
}
public void setBaiFaCoe(double baiFaCoe) {
this.baiFaCoe = baiFaCoe;
}
/**
* 百发的系数, 分布式文件配置
*
* @return
*/
@DisconfFileItem(name = "coe.yuErBaoCoe")
public double getYuErBaoCoe() {
return yuErBaoCoe;
}
public void setYuErBaoCoe(double yuErBaoCoe) {
this.yuErBaoCoe = yuErBaoCoe;
}
/**
* 折扣率,分布式配置
*
* @return
*/
@DisconfItem(key = key)
public Double getDiscount() {
return discount;
}
public void setDiscount(Double discount) {
this.discount = discount;
}
}
或者,我们也可以在一个Service类里添加配置项:
package com.example.disconf.demo.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import com.baidu.disconf.client.common.annotations.DisconfItem;
import com.example.disconf.demo.config.Coefficients;
/**
* 金融宝服务,计算一天赚多少钱
*
* @author liaoqiqi
* @version 2014-5-16
*/
@Service
public class BaoBaoService {
protected static final Logger LOGGER = LoggerFactory.getLogger(BaoBaoService.class);
public static final String key = "moneyInvest";
@Value(value = "2000d")
private Double moneyInvest;
@Autowired
private Coefficients coefficients;
/**
* 计算百发一天赚多少钱
*
* @return
*/
public double calcBaiFa() {
return coefficients.getBaiFaCoe() * coefficients.getDiscount() * getMoneyInvest();
}
/**
* k 计算余额宝一天赚多少钱
*
* @return
*/
public double calcYuErBao() {
return coefficients.getYuErBaoCoe() * coefficients.getDiscount() * getMoneyInvest();
}
/**
* 投资的钱,分布式配置 <br/>
* <br/>
* 这里切面无法生效,因为SpringAOP不支持。<br/>
* 但是这里还是正确的,因为我们会将值注入到Bean的值里.
*
* @return
*/
@DisconfItem(key = key)
public Double getMoneyInvest() {
return moneyInvest;
}
public void setMoneyInvest(Double moneyInvest) {
this.moneyInvest = moneyInvest;
}
}
采用哪种方式,由用户选择。
值得注意的是,在第二种实现中,它的方法calcBaiFa() 时调用了 getMoneyInvest() 方法。 getMoneyInvest() 是配置项的get方法,它添加了@DisconfItem注解,表明它是一个配置项,并且会被切面拦截,moneyInvest的值会在配置仓库里获取。但是,可惜的是,SpringAOP是无法拦截”Call myself”方法的。也就是说getMoneyInvest()是无法被切面拦截到的。
为了解决此问题,在实现中,我们不仅将它的值 注入到配置仓库中,而且还注入到配置项所在类的实例里。因此,在上面第二种实现中,虽然 getMoneyInvest() 方法无法被拦截,但是它返回的还是正确的分布式值的。
配置文件也一样,配置值亦会注入到配置文件类实体中。
非Spring编程的实现¶
在非Spring方式下,无法使用AOP切面编程,因此无法统一的拦截配置数据请求。
在这种情况下,用户配置类的实现有两种方式:
- 配置类的域是static。用户直接访问这些域便可以获取得到配置类数据。
- 配置类使用单例。用户通过单例访问配置获取配置类数据。
注意:此两种方式均无法自动避免“配置读取不一致问题”。
当事件发生时,用户程序处理配置的方式是:
- 配置文件更新时,系统会自动去下载配置文件存储到本地,并存储到配置仓库。对于static变量,系统会自动注入到配置类中。对于使用单例实现方式,用户必须在回调函数中进行用户配置类的更新。
- 配置项更新时,与配置文件更新一样。
Zookeeper的目录存储结构¶
|----disconf
|----app1_version1_env1
|----file
|----confA.properties
|----item
|----keyA
|----app2_version2_env2
|----file
|----conf2.properties
|----item
|----key2
基于XML的实现¶
ReloadablePropertiesFactoryBean实现了配置文件的disconf托管¶
ReloadablePropertiesFactoryBean继承了PropertiesFactoryBean类,它主要做到:
- 托管配置文件至disconf仓库,并下载至本地。
- 解析配置数据传递到 ReloadingPropertyPlaceholderConfigurer
ReloadingPropertyPlaceholderConfigurer实现了配置数据至Bean的映射¶
ReloadingPropertyPlaceholderConfigurer继承自Spring的配置类PropertyPlaceholderConfigurer,它会在Spring启动时将配置数据与Bean做映射,以便在检查到配置文件更改时,可以实现Bean相关域值的自动注入。
ReloadConfigurationMonitor 定时校验配置是否更新¶
它是一个Timer类,定时校验配置是否有更改,进而促发 ReloadingPropertyPlaceholderConfigurer 类来分析要对哪些 Bean实例进行重新注入。
系统配置¶
配置项 | 说明 | 是否必填 | 默认值 |
---|---|---|---|
conf_server_store_action | 仓库 URL | 是 | /api/config |
conf_server_zoo_action | zoo URL | 是 | /api/zoo |
conf_server_master_num_action | 获取远程主机个数的URL | 是 | /api/getmasterinfo |
zookeeper_url_prefix | zookeeper的前缀路径名 | 是 | /disconfserver2 |
local_dowload_dir | 下载文件夹, 远程文件下载后会放在这里 | 是 | ./disconf/download |
异构系统主备控制实现¶
disconf将会为所有配置提供主备功能的开关,对于一个配置,多台实例机器可以进行竞争成为主机(使用主配置),竞争失败的实例将会成为备机(使用备配置)。基于zookeeper提供的分布式一致性锁,可以非常容易的达到此目的。
Disconf-web详细设计文档¶
本文档主要阐述了版本 Disconf-Web 的设计。
表结构设计¶
配置数据是存储在Mysql里的。
config 配置(配置文件或配置项)
config_id 唯一的ID(没有啥意义,主键,自增长而已)
type 配置文件/配置项
name 配置文件名/配置项KeY名
value 配置文件:文件的内容,配置项:配置值
app_id appid
version 版本
env_id envid
create_time 生成时间
update_time 修改时间
app
app_id APPID(主键,自增长)
name APP名(一般是产品线+服务名)
description 介绍
create_time 生成时间
update_time 修改时间
emails 邮箱列表逗号分隔
env (rd/qa/local可以自定义,默认为 DEFAULT_ENV)
env_id 环境ID(主键,自增长)
name 环境名字
user
user_id 用户ID(主键,自增长)
name 姓名
password 密码
token token
ownapps 能操作的APPID,逗号分隔
role_id 角色ID
role
role_id ID(主键,自增长)
role_name 角色名
create_time 生成时间
create_by 创建人
update_time 修改时间
update_by 更新人
role_resource
role_res_id role-resource id(主键,自增长)
role_id 用户角色id
url_pattern controller_requestMapping_value + method_requestMapping_value
url_description url功能描述
method_mask GET, PUT, POST, DELETE, 1: accessible
update_time 更新时间
局限性和注意事项¶
disconf的Zookeeper异常考虑¶
disconf-web的ZK异常处理¶
disconf-web可以完全保证在任何情况下,与ZK集群的自动连接。
下面按情况进行分析:
服务启动前,zk连接不上:¶
- 开始连接不上:
- apache ZK client自身会自动(永久)去连接ZK server. 但是一直连接不上。
- 因此,web上所有操作均会失败,抛大异常,请求失败,只会重试一次,不会重试多次
- 后面突然连接上了:
- apache ZK client 收到server SyncConnected消息。
- 这时所有操作均成功
- 后面又突然连接不上了:
- apache ZK client 收到server Disconnected 消息。
- 这时,apache ZK client自身会自动(永久)去连接ZK server. 但是一直连接不上。
- 这时 web 上 所有操作均会失败,抛大异常,请求失败,只会重试一次,不会重试多次
- 后面突然连接上了:
- apache ZK client 收到server Expired 消息。
- 这时表示会话丢失啦,apache ZK client 自动断开与Server的连接,表示此时让你来处理,因为它不知道应该如何处理。
- 这时,disconf-core会reconnect zkserver,重新建立会话。
- 成功后,apache ZK client 收到server SyncConnected 消息。表示连接成功
- 后面又突然连接不上了:
- apache ZK client 收到server Disconnected 消息。
- 这时,apache ZK client自身会自动(永久)去连接ZK server. 但是一直连接不上。
- 这时 web 上 所有操作均会失败,抛大异常,请求失败,只会重试一次,不会重试多次
服务启动前,zk连接上了:¶
- 开始连接:
- apache ZK client 收到server SyncConnected消息。
- 这时所有操作均成功功
- 后面又突然连接不上了…… (与上面分析一样,此不再赘述)
注意¶
ZK一般需要以集群的形式提供出来。假设有N台ZK,
- 只要至少有一台ZK存活,disconf-web就可以正常工作。而且永远不会收到 server Expired 的消息。
- 只要有一台ZK死亡,disconf-web就会收到 Disconnected 消息。但是系统仍可以继续工作。
- 如果所有zk都死亡,那么disconf-web会收到 Disconnected 消息。只要有一台存活,disconf-web就会收到
disconf-client的ZK异常处理¶
disconf-client可以完全保证: 如果在启动程序时保证ZK集群是可用的,那么,就可以保证在任何情况下,与ZK集群的自动连接。
下面按情况进行分析:
程序启动前,zk连接不上:¶
这时disconf-client无法在ZK上注册信息。这是必须禁止发生的情况。也是disconf-client无法支持的情况。
一旦发生这种情况,请先恢复ZK集群,再启动你的程序。
程序启动前,zk连接上了:¶
如果在程序启动过程中,ZK是正常的,那么,disconf-client可以保证与ZK连接的自动性。
- 只要集群有一台还存活着,你的程序配置还是受disconf托管。
- 如果集群所有机器均死亡,这时你的程序将游离于disconf之外。只要集群中有任何一台ZK机器重新开启,那么 你的程序将重新 由disconf进行托管。
注意¶
disconf-client必须保证在程序在启动时,ZK集群的可用性。
细节讨论¶
解决 配置“不一致性读“ 问题¶
问题描述:¶
应用系统的配置更新过程,它会涉及到多个配置项的更新,它不是一个原子过程。如果在配置更新的过程中,应用程序去读取配置,这里可能存在些“时间窗口”,从而导致不一致性读问题。
解决方法:¶
前提:无论何种实现,要实现统一读取,避免“非一致性”问题,就必须要对所有读取操作“统一化”。
Disconf支持Web或非Web系统,对于这个问题,Web系统或非Web系统需要区分来看:
对于Web系统:
要实现统一读取,可以使用ThreadContext+AOP来实现。
AOP的使用:通过对配置的get方法做切面,统一将用户的配置请求转发至 “我们自己的配置仓库” 里获取,从而实现统一读取。
ThreadContext的使用方式有以下几种:
- 解决方法一:提供ThreadContext包,在每次请求一开始时都复制系统里的所有配置缓存(复制过程要与配置更新Sync互斥),从而保证每次会话的数据的一致性。
- 解决方法二:提供ThreadContext包,每次请求都绑定一个版本号,如果读取时版本号不一致则报错,需要重新请求。
- 解决方法三:方法二的加强版,添加一个注解定义,标注它是需要强一制性的,每次会话读取时只复制这些强一制性配置(复制过程要与配置更新Sync互斥)。
- 解决方法四:提供ThreadContext包,系统内保存有多个配置缓存层,读取时统一读取某个版本的缓存。每当配置更新时,缓存层增加。
第一种方法,代价太大。第二种方法,严重增加用户负担,第三种还是需要用户关心这个事情。我们将采用第四种方法。
对于非Web项目:
比较难解决非一致性读取的问题。因为它没有了会话这样一个概念。Apache的FileChangedReloadingStrategy Reload配置文件的方案也没有解决此问题。所以,我们打算放弃这方面的解决。但是,我们还是会提供一个简单却Ugly的解决方案:提供函数来标识用户读取配置的边界。用户可以放弃使用这个方案,但是我们不保证不会发生“不一致读’问题。
配置放在哪里¶
关于配置应该放在哪里?有许多讨论,可以放在Web平台上,也可以放在Zookeeper上。从Disconf的实现中,可以看到Disconf是将配置数据放在Web平台的,而不是放在Zookeeper上的。那为什么这么设计呢?下面有个表格比较一下优劣。
配置放在ZK上 | 配置放在Disconf-web平台上 | 比较 | |
---|---|---|---|
配置管理 | 不易管理。ZK相比Mysql不易管理。 | 用Mysql统一存储所有配置数据,非常方便管理,可扩展性强。 | 配置放在Disconf-web显然容易管理和扩展。 |
职责分配 | ZK负责通知与配置数据存储,并提供给client获取数据。disconf-web负责新建、更新配置,并存储一些数据(非配置数据,用作管理),它不为client提供下载配置服务。 | ZK只负责通知。disconf-web负责新建、更新、存储 配置,并为client提供下载配置服务。 | 如果采用第一种方案,数据分散存储了。第二种方案职责明确,数据统一存储在Disconf-web上,ZK只负责通知。 |
client的配置获取 | client启动时,需要从ZK上下载配置。因此必须使用disconf-web先写到ZK上,否则client启动时就无法使用最新配置。配置更新时,client直接从ZK获取最新配置。disconf-web必须统一的在ZK上新建、更新结点。 | client不管是启动还是更新时,均是从disconf-web上获取配置。启动时,client会在ZK监控结点(如果不存在,则新建);更新时,disconf-web更新ZK结点。disconf-web不会在ZK上新建结点。 | 这两种方案其实都差不多 |
配置的一致性问题 | 把数据存储在ZK上,无法像Mysql一样可持久化存储。ZK集群关闭后,数据全部丢失。client直接从ZK上获取配置数据,系统运行久之后,client的配置是否正确无从验证。 | 数据存储在Mysql上,可持久化存储。不管未来是迁移或者扩展之类都非常方便。client是从Web上获取数据,然后再写到ZK上。理论上来说,ZK上的数据应该是与Web平台数据一样的,这可以作为验证平台正确性的一个方法。 | 从可持久化性、可验证性方面来讲,第二个方案好。 |
配置获取 | 配置存储在ZK上,非client想要获取配置,很不容易。 | 配置存储在Web平台上,通过提供RestHttp接口,不管是谁想要获取配置都非常方便。 | 第二种方案获取简单便捷。 |