Disconf

Distributed Configuration Management Platform(分布式配置管理平台)

专注于各种「分布式系统配置管理」的「通用组件」和「通用平台」, 提供统一的「配置管理服务」。

推荐

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

运行样式

主页

image0

登录页

可以使用 admin admin 进行登录。

image1

主界面

image2

左上角可以选择APP和环境,选择之后,就会在中间出现若干个版本,

选择版本后,就会显示 APP、环境、版本 三个条件下的配置列表:

image3

表格中 各个列的意义是:
  • APP:使用哪个APP,及它的ID
  • KEY:配置文件或配置项
  • 配置内容:配置文件或配置项在配置中心中的值
  • 实例列表:使用此配置文件或配置项的所有实例列表,及每个实例的配置值。如果实例的配置值与配置中心的值不一致,这里会标识出来。
  • 修改时间:修改此配置的最后一次时间
  • 操作:个性、删除、下载
右上角可以

新建配置项、新建配置文件、新建APP

表格右上方

可以批量下载所有配置文件至本地,还可以查看ZK上的部署情况。

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
上线前的初始化工作

初始化数据库:

可以参考 sql/readme.md 来进行数据库的初始化。注意顺序执行
0-init_table.sql
1-init_data.sql
201512/20151225.sql
20160701/20160701.sql

里面默认有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;
    }
}
关于host

这里的 host 设置成 disconf.com (可以自定义),但它 必须与 application.properties 里的domain一样。

然后浏览器的访问域名也是这个。

业务功能

  • 支持用户登录/登出
  • 浏览配置
    • 按 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;
    }
}

Tutorial1

配置更新回调

如果配置更新时,您需要的是 不仅注解类自动同步,并且其它类也需要做些变化,那么您需要一个回调来帮忙。

@Service
@Scope("singleton")
@DisconfUpdateService(classes = { JedisConfig.class }) // 这里或者写成 @DisconfUpdateService(confFileKeys = { "redis.properties" })
public class SimpleRedisServiceUpdateCallback implements IDisconfUpdate

Tutorial2

支持基于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>

Tutorial8

支持配置项

变量亦支持分布式配置哦

@DisconfItem(key = key)
public Double getMoneyInvest() {
    return moneyInvest;
}

Tutorial3

支持静态配置

除了支持@Service类以外,我们还支持 静态配置

@DisconfFile(filename = "static.properties")
public class StaticConfig {

    private static int staticVar;

    @DisconfFileItem(name = "staticVar", associateField = "staticVar")
    public static int getStaticVar() {
        return staticVar;
    }
}

Tutorial4

支持基于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>

Tutorial5

过滤要进行托管的配置

有时候你不想托管所有的配置文件,有1~2个配置文件你只想和本地的,可以:

# 忽略哪些分布式配置,用逗号分隔
ignore=jdbc-mysql.properties

Tutorial7

强大的WEB配置平台控制

在WEB配置平台上,您可以

  • 上传、更新 您的配置文件、配置项(有邮件通知),并且实现动态推送。
  • 批量下载配置文件,查看ZK上部署情况
  • 查看 此配置的影响范围: 哪些机器在使用,各机器上的配置内容各是什么,并且自动校验 一致性。
  • 支持 自动化校验配置一致性。
  • 简单权限控制

详见:Tutorial6

image0

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

具体步骤是:

  1. 此类实现了 InitializingBean, DisposableBean 两个接口,目的是在Bean初始化后进行Redis的连接。
  2. 为此类添加 @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>
其中这里,我们定义 属性“scanPackage” 的值是 com.example.disconf.demo。
这里需要填上你的项目的Package名。这与Spring的auto scan包名功能一样。

另外,从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"/>
支持 cglib aop

使你的项目支持 cglib的aop

<aop:aspectj-autoproxy proxy-target-class="true"/>
第三步:修改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;
    }
}

具体步骤是:

  1. 为这个类 JedisConfig 定义 @DisconfFile 注解,必须指定文件名为 redis.properties 。
  2. 定义域 host port,分别表示Host和Port。并使用Eclipse为其自动生成 get&set 方法。
  3. 为这两个域的get方法上添加注解 @DisconfFileItem 。添加标记 name, 表示配置文件中的KEY名,这是必填的。标记associateField是可选的,它表示此get方法相关连的域的名字,如果此标记未填,则系统会自动分析get方法,猜测其相对应于域名。强烈建议添加associateField标记,这样就可以避免Eclipse生成的Get/Set方法不符合Java规范的问题。
  4. 标记它为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上传配置文件哦。

点击主页面的新建配置文件按钮:

image0

进入页面后就可以上传 配置文件了

image1

第六步:在disconf-web上查看

你在第五步上传了配置文件 redis.properties ,那么 ,当你的程序启动时,disconf就会帮忙你的程序去获取配置文件。

可以看到已经有一个实例在使用redis.properties了。

image2

点击查看它的详情,可以看到,确实是我的实例在使用它。

image3

完结

至此,分布式配置文件的撰写就已经写完了。

可以看到,基于注解的方式,不需要在xml定义 java bean(config类).

使用方便

大家可以看到,第一次使用时,需要

  • applicationContext.xml添加Disconf启动支持
  • 使用注解方式 修改配置类
  • 添加disconf.properties
  • disconf-web上上传配置文件

非第一次使用时,需要

  • 使用注解方式 修改配置类
  • disconf-web上上传配置文件

就可以支持分布式配置了。

强兼容性

并且,如果将 disconf.disconf.properties 中的 enable.remote.conf 设置为 false

那么,分布式配置就会失效,退化为 使用本地配置方式(即第一部分的功能)。(如果是这种情况,你必须确认你本地留有相应的配置文件。
一般来说,只要成功跑过一次基于disconf的程序,那么classpath目录下就会有此程序的所有相应配置文件。)

并且,如果 disconf-web无法正常服务(conf_server_host=127.0.0.1:8080),分布式配置也会失效,退化为 使用本地配置方式(即第一部分的功能)。(如果是这种情况,你必须确认你本地留有相应的配置文件)

也就是说,Disconf是具有兼容性的

  • 当开启Disconf时,
    • 如果Disconf正常运行,则正常使用分布式配置。
    • 如果Disconf非正常运行,则使用本地配置。(Disconf可以保证在Disconf失败时,原有程序能够按原有逻辑正确运行)
  • 当不开启Disconf时, 则使用本地配置。

注:

  1. 只要是运行一次分布式程序成功,则本地就含有最全的配置文件。此时,如果再运行一次分布式程序,如果出现失败,则上一次下载成功的配置文件就会当成本地配置生效,程序成功启动。

END

Tutorial 2 注解式分布式的配置文件高级篇: 配置更新的通知(最佳实践)

Tutorial 1 里,
我们实现了一个简单的Redis服务程序,它使用分布式配置进行管理,此Redis的配置文件存储在分布式服务器 disconf-web 上。

也许有一天,我们需要更新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);
    }
}

完结

至此,支持配置更新的 分布式配置文件 的撰写就已经写完了。

当用户在 disconf-web 上更新配置时,你的服务里的Redis就会指向新的地址。

Tutorial 3 注解式分布式的配置项(最佳实践)

Tutorial 1 里,
我们实现了一个简单的Redis服务程序,它使用注解式的分布式配置进行管理,此Redis的配置文件存储在分布式服务器 disconf-web 上。

那如果你的配置只是一个变量,不是配置文件,怎么办?还能实现分布式配置吗?

答案当然是肯定的!

Disconf支持配置项(配置项是指 一个类的某个域变量)的分布式化。

这里以 disconf-demo 某个程序片段为例,详细介绍了 分布式的配置项 的简单示例程序。

在这里,我们将分两种情况来进行演示:

  1. 配置项在某个配置类里:外部程序通过配置类的get***方法获取。
  2. 配置项在某个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;
    }
}

具体步骤是:

  1. 编写Coefficients类,添加域 discount
  2. 用Eclipse为域discount添加 get & set方法
  3. 为get方法添加注解 @DisconfItem(key = key) ,这里的key是分布式配置项的标识,这里是 discountRate
  4. 此类必须是JavaBean,Spring托管的,且 “scope” 都必须是singleton的。
  5. 可以使用@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 ,就实现了配置项更新的通知。怎么样?是不是很强大?

第四步:在disconf-web上上传配置

上传方式是先在首页点击 新建配置项

image0

然后新建就行啦

image1

完结

通过几行简单的配置,分布式配置项 就这样添加到你的应用程序里了。

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

}

具体实现步骤是:

  1. 添加域 moneyInvest ,并使用Eclipse自动生成 get&set 方法。
  2. 为get方法添加 @DisconfItem 注解,并添加 key 为 moneyInvest
  3. 在函数 calcMoney() 里 调用本身类的 getMoneyInvest() 方法。
  4. 此类必须是JavaBean,Spring托管的,且 “scope” 都必须是singleton的。
第二步:在disconf-web上上传配置

这里不再赘述。

完结

只需要上面一步,就完成了分布式配置项。

配置更新也是实时的,不需要写回调函数。

service类的配置项 其实和 配置项类的配置项 撰写方法 差不多。它的好处是不需要再新建一个配置项类。

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考虑到了此种情况。举个实例,数据库配置文件,每个人的数据库可能不大一样,那么,你可以修改 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

问题

我想在我的机器上做一个配置下载器agent, 可以实现以下功能:

  • 启动时下载配置
  • 配置被更新时,可以感知并下载下来

解决方法

可以修改一下 disconf-demos/disconf-standalone-demo 这个项目,让其变成一个 长驻进程,并指定
disconf.user_define_download_dir 这个配置到你想指定的路径。

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目录下.

注:

  1. 如果不作任何配置的改变,默认情况下,会下载到 disconf.user_define_download_dir 目录 和 classpath 两个目录下。
  2. targetDirPath 值说明:以”/”开头则是系统的全路径,否则则是相对于classpath的路径,默认是classpath根路径。注意:根路径要注意是否有权限,否则会出现找不到路径,推荐采用相对路径。
  3. 配置说明看这里 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 初始化 或者 配置更新 时 被调用 。

配置更新时通知的所有方式 总结

-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将配置下载到当前路径下,程序可以找到配置,不会报错。

但当你使用IDE(eclipse or intellij)启动时,它就报错了,说配置文件找不到了。这是因为IDE启动时,不是以-jar的方式启动的,它是以Java入口类的方式进行启动的,disconf将配置下载到target/classes
目录下,如果还是使用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及架构

disconf-web

主要功能

新建功能
  • 新建APP
  • 新建配置文件、新建配置项
修改功能
  • 修改配置文件
  • 修改配置项
删除功能
  • 删除配置文件
  • 删除配置项
下载功能
  • 下载单个配置文件
  • 批量下载指定 APP、ENV、VERSION 下的所有配置文件
查询功能
  • 查询配置功能
    • 查询指定 APP、ENV、VERSION 下的所有配置文件列表
    • 查询配置文件内容
    • 查询配置项内容
    • 查询使用此配置的所有实例列表,并且 当实例使用的配置值与中心不一致时,会有飘红提示
  • 查询ZK功能
    • 查询 指定 APP、ENV、VERSION 下的ZK信息。
权限功能
  • 用户访问哪些APP可以控制(USER表里的 ownapps 字段)
手动触发的提醒功能
  • 当新建或修改配置时,会有邮件提醒(app表里的 email 字段)

image0

自动提醒功能
  • 系统可以自动监控 所有实例与 配置中心的值 是否一致,如果不一致,就会报警(app表里的 email 字段)

image1

  • 手动触发的提醒功能自动提醒功能 两个功能的 DIFF 功能 采用的是 java-diff

Tutorial 12 disconf-web 为界面 开放的 Http API

前言

  • 目标:让开发者具有自定义定制web控制台界面的能力

以下接口均需要权限控制

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

准备

    1. 获取配置时是从disconf-web获取
    1. 得到配置更新时是从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常问问题

找不到 get/set方法

请使用 Lombok https://github.com/knightliao/disconf/issues/38

社区贡献

其他disconf-web开源实现

其他disconf-client开源实现

正在使用公司列表

正在关注的公司列表

  • 洋码头
  • 新意互动

联系和赞助

赞助

如果您觉得disconf不错,可以资助作者, 资助公司或个人会留下名字。

image0

联系我

版本更新

2.6.36

2.6.36 发布于 20160911

2.6.35

2016年7月1号

  • disconf-client:
  • 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
    • 提供修改密码功能
      • 需要执行 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号

2.6.33

2016月5月07日

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日

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日

2.6.27

发布于:2015年10月26日

  • disconf-client:
    • fix bean order 问题
  • disconf-web:
    • fix 上传文件(使用贴文本方式)的version无法指定的bug

2.6.26

发布于:2015年10月26日

2.6.25

发布于:2015年8月20日

2.6.24

发布于:2015年7月3日

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.21

发布于:2015年4月14日

  • disconf-client:
    • 其它小修改
    • 优化 pom.xml

2.6.20

发布于:2015年3月27日

2.6.19

发布于:2015年1月22日

2.6.18

发布于:2014年12月19日

  • disconf-client:
    • FIX BUG: 同一台机器多个实例使用同一个classpath下的并发设置配置文件BUG(非常重要)
    • FIX BUG: disconf store use ‘get’ (非常重要)

2.6.16

发布于:2014年12月3日

2.6.14

发布于:2014年9月18日

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服务集群模式

image0

disconf的模块架构图

image1

每个模块的简单介绍如下:

  • Disconf-core
    • 分布式通知模块:支持配置更新的实时化通知
    • 路径管理模块:统一管理内部配置路径URL
  • Disconf-client
    • 配置仓库容器模块:统一管理用户实例中本地配置文件和配置项的内存数据存储
    • 配置reload模块:监控本地配置文件的变动,并自动reload到指定bean
    • 扫描模块:支持扫描所有disconf注解的类和域
    • 下载模块:restful风格的下载配置文件和配置项
    • watch模块:监控远程配置文件和配置项的变化
    • 主备分配模块:主备竞争结束后,统一管理主备分配与主备监控控制
    • 主备竞争模块:支持分布式环境下的主备竞争
  • Disconf-web
    • 配置存储模块:管理所有配置的存储和读取
    • 配置管理模块:支持配置的上传、下载、更新
    • 通知模块:当配置更新后,实时通知使用这些配置的所有实例
    • 配置自检监控模块:自动定时校验实例本地配置与中心配置是否一致
    • 权限控制:web的简单权限控制
  • Disconf-tools
    • context共享模块:提供多实例间context的共享。
流程设计

image2

运行流程详细介绍:

与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-web提供了前后端分离的web架构,具体可见:

本部分会重点介绍disconf-client的实现方式。

注解式disconf实现

本实现会涉及到 配置仓库容器模块、扫描模块、下载模块、watch模块,

http://ww1.sinaimg.cn/bmiddle/60c9620fjw1eqj9zzgc7yj20b20pn41v.jpg

使用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的两种编程模型
并发性 多条配置要同时生效时,无法解决并发同时生效的问题 基于注解式的配置,可以解决并发性问题

Disconf-client详细设计文档

本文档主要阐述了版本 Disconf-Client 的设计。、

程序运行流程图

版本2.0的设计

image0

点击查看大图

运行流程详细介绍:

  • 启动事件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一样,区别是,这里还会将配置的新值注入到配置实体里。
完全版的设计

image1

[点击查看大图 ](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一样。

类设计图

image2

查看大图

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会进行第一次扫描。

扫描按顺序做了以下几个事情:

  1. 初始化Disconf-client自己的配置模块。
  2. 初始化Scan模块。
  3. 初始化Core模块,并极联初始化Watch,Fetcher,Restful模块。
  4. 扫描用户类,整合分布式配置注解相关的静态类信息至配置仓库里。
  5. 执行Core模块,从disconf-web平台上下载配置数据:配置文件下载到本地,配置项直接下载。
  6. 配置文件和配置项的数据会注入到配置仓库里。
  7. 使用watch模块为所有配置关联ZK上的结点。

其中对配置的处理详细为:

image3

查看大图

第二步:com.baidu.disconf.client.DisconfMgrBeanSecond

DisconfMgrBean的扫描主要是静态数据的初始化,并未涉及到动态数据。DisconfMgrBeanSecond Bean则是将一些动态的数据写到仓库里。

本次扫描按顺序做了以下几个事情:

  1. 将配置更新回调实例放到配置仓库里
  2. 为配置实例注入值。

image4

查看大图

分布式配置的实现

下面将 分别详细阐述 分布式配置文件 和 分布式配置项 的实现方式。

由于目前版本只支持 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切面编程,因此无法统一的拦截配置数据请求。

在这种情况下,用户配置类的实现有两种方式:

  1. 配置类的域是static。用户直接访问这些域便可以获取得到配置类数据。
  2. 配置类使用单例。用户通过单例访问配置获取配置类数据。

注意:此两种方式均无法自动避免“配置读取不一致问题”。

当事件发生时,用户程序处理配置的方式是:

  1. 配置文件更新时,系统会自动去下载配置文件存储到本地,并存储到配置仓库。对于static变量,系统会自动注入到配置类中。对于使用单例实现方式,用户必须在回调函数中进行用户配置类的更新。
  2. 配置项更新时,与配置文件更新一样。
Zookeeper的目录存储结构
|----disconf
        |----app1_version1_env1
                |----file
                        |----confA.properties
                |----item
                        |----keyA
        |----app2_version2_env2
                |----file
                        |----conf2.properties
                |----item
                        |----key2
基于XML的实现
虽然注解式编程简单、直观,易维护,但是,它是具有一定的代码侵入性的。
disconf考虑到有些用户不想写代码,只想通过XML配置(可能是在旧项目中使用disconf)来实现分布式配置的需求。因此,disconf亦实现了基于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     更新时间

局限性和注意事项

局限性和注意事项

  • 配置文件类、配置项所在的类、回调函数类 都必须是JavaBean,并且它们的”scope” 都必须是singleton的。
  • 本系统实现的注解方案具有些局限性,具体如下:
    • 用户标注配置时略有些不习惯。目前注解是放在get方法之上的,而不是放在域上。
    • 注解放在get方法上,一般情况下是没有问题的。但是对于”call self”的方法调用,AOP无法拦截得到,这样就无法统一处理这些配置。一旦出现这种情况,“非一致性读问题”就会产生。

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接口,不管是谁想要获取配置都非常方便。 第二种方案获取简单便捷。