ConfigX¶
ConfigX是一套完整的配置管理解决方案,分为配置管理平台和配置管理客户端。
入门指南¶
项目介绍¶
ConfigX项目提供了一个解决分布式系统的配置管理方案,为分布式系统中的外部配置提供服务器和客户端支持。 使用ConfigX Server,您可以在所有环境中管理应用程序的外部属性。 客户端和服务器上的概念映射与Spring Environment和PropertySource抽象相同,因此它们与Spring应用程序非常契合,但可以与任何以任何语言运行的应用程序一起使用。
ConfigX包含了Client和Server两个部分: configx-web 配置服务端,管理配置信息 configx-client 配置客户端,客户端调用服务端暴露接口获取配置信息,并集成到Spring Environment和PropertySource中。
ConfigX Client根据Spring框架的Environment和PropertySource从ConfigX Sever获取配置,因此需要先了解Spring Environment、PropertySource 、Profile等技术。
动机¶
随着微服务的流行,配置管理是实现微服务必不可少的一个组件。
现在市面上有很多配置管理系统:
- 淘宝的diamond
- 百度的disconf
- 奇虎360的QConf
- 微博的vintage
其中disconf对spring集成比较好,但是我觉得disconf有以下几点不足:
- 与Spring集成并不是特别简单。
- 没有提供配置文件到Spring Bean简单映射实现。
- 没有提供Spring国际化消息文件的集成。
- 无法实现配置继承(覆盖)。
- 无法实现配置发布的原子性。
- 没有提供配置修改历史记录。
Spring Cloud是一个实现微服务的框架,它提供了Spring Cloud Config来管理配置,Spring Cloud Config客户端和服务器上的概念映射与Spring Environment和PropertySource抽象相同,因此它们与Spring应用程序非常契合,但可以与任何以任何语言运行的应用程序一起使用。
但是个人觉得Spring Cloud Config有以下几点不足:
- 没有提供配置文件到Spring Bean简单映射实现。
- 没有提供Spring国际化消息文件的集成。
- 无法实现配置发布的原子性。
为了解决这些问题,我决定自己实现一套配置管理系统,它必须做到以下几点:
与Spring集成简单
ConfigX-Client借鉴了Spring Boot和Spring Cloud Config的设计,全部使用标准的Spring扩展点与Spring进行集成,所以与Spirng应用的集成非常简单。
在Spring应用中使用ConfigX只需要4个注解:
- @EnableConfigService
- 启用ConfigX支持
- @ConfigBean
- 定义配置文件转换成Spring Bean
- @VersionRefreshScope
- 指定Spring Bean是可刷新的bean,当配置修改时,bean会重新刷新
- @EnableMessageSource
- 启用ConfigX对Spring消息国际化的支持
支持配置文件映射成Spring Bean
ConfigX只需要在Spring Bean上加上注解@ConfigBean,ConfigBean的value直接配置文件名称,就可以轻松将配置文件映射成Spring的bean。
支持管理Spring国际化消息文件
ConfigX只需要通过注解@EnableMessageSource来启用ConfigX对Spring国际化的支持,并不用修改获取国际化消息的代码,使用Spring标准的API来获取国际化消息。
支持配置继承(覆盖)
ConfigX的配置文件定义了不同的profile,客户端通过激活多个profile来激活不同的配置,如果不同的profile下有相同的配置,则按客户端激活profile的顺序优先获取较前面的profile的配置。
支持配置发布的原子性
ConfigX通过对配置进行多版本并发控制,使得修改的多个配置可以原子性的发布,应用程序中不用担心访问到部分生效的配置。
特性¶
- 配置管理
- 属性配置管理,配置管理平台提供友好的配置编辑器,对于日期时间可以非常方便的进行设置。
- 文件配置管理,配置管理平台提供强大的文件编辑器,可以非常方便的对文件进行操作。
- 配置修改并发布后,应用可以实时获取到修改的配置并实时生效。
- 维度设计
App
具体的应用
Env
应用的环境,比如开发环境,测试环境,线上环境,不同环境之间的配置完全隔离。
Profile
Maven和Spring中都提供了Profile,然后可以激活不同的profiles,Spring Boot和Spring Cloud Config中的配置管理也充分使用了Spring的Profile技术来进行配置管理。
Profile可以让我们定义一系列的配置,然后客户端指定激活哪些profile。这样我们就可以定义多个profile,然后不同的客户端激活不同的profiles,从而达到不同环境使用不同配置信息的效果,如果多个profile下有相同的配置,则按profile的指定顺序来获取较前面的profile下的配置。
由于Env之间的配置是完全隔离的,所以增加Profile维度来弥补Env维度的不足。
配置管理平台会创建一个default profile,如果未定义profile,配置将会放在default profile下,default profile自动激活,不用客户端显示激活。
通过Profile维度,可以轻松实现以下功能:
- 配置继承,一个应用部署的多套系统使用的配置几乎相同,只有少部分不同,这样这些系统就可以使用同一个环境,然后定义多个Profile,这些系统通过激活不同的profiles来达到配置继承(覆盖)的目的。
- 配置分支,当项目同时多个分支开发时,为了保证配置不相互影响,可以给每个分支定义新的profile,每个分支的配置放在分支自己的profile下,然后激活分支自己的profile即可。
- 灰度发布,为新版本的代码定义一个新的profile,然后灰度发布的系统激活这个新的profile下的配置。
配置版本控制
跟代码版本控制类似,我们对配置也就行了版本控制,可以查看配置的历史修改记录。
发布模式
我们提供两种发布模式
自动发布
配置修改完立刻自动发布,配置实时生效
审核发布
配置修改完后,并不立刻发布,而是需要建立一个发布单,发布单审核通过后,才能发布,审核发布模式可以实现更安全可控的发布,减少配置错误导致的严重后果的风险。
配置发布的原子性
在客户端中实现了多版本的配置管理控制,当发布多个配置时,每个线程看到的这几个配置要么都是修改前的值,要么都是修改后的值,不会出现某些配置是修改后值而另外一些配置是修改前的值,保证配置的原子性。
与Spring集成简单,对应用代码无侵入。
- 支持Spring属性文件
原来Spring应用中的属性文件迁移到配置管理平台后,仍然可以使用@Value注解来注入属性文件中的属性,应用程序并不需要修改任何代码,原因是ConfigX使用了Spring框架的Environment和PropertySource扩展。
- 支持XML/JSON等文件映射到Spring Bean
当需要将一个文件映射成Spring Bean时,只需要将这个文件配置在配置管理平台中,然后在Spring Bean上增加注解@ConfigBean(value=”配置文件名”, converter=文件转换成Bean的转换器类)。
- 支持Spring国际化消息文件
原来Spring应用中的i18n消息文件迁移到配置管理平台后,仍然可以使用MessageSource.getMessage方法来获取国际化消息,应用程序并不需要修改任何代码,原因是CongigX使用了Spring的MessageSource扩展。
维度设计¶
ConfigX配置包括App、Env和Profile三个维度。
App 应用¶
App用来指定一个具体的应用。
Env 环境¶
Env环境用来指定应用的环境,比如开发环境,测试环境,线上环境,不同环境之间的配置完全隔离。
Profile 剖面¶
Profile是ConfigX中所定义的一系列配置的逻辑组名称,只有当这些Profile被激活的时候,才会将Profile中所对应的配置注册到应用中。
Profile可以让我们定义一系列的配置,然后客户端指定激活哪些profile。这样我们就可以定义多个profile,然后不同的客户端激活不同的profiles,从而达到不同环境使用不同配置信息的效果,如果多个profile下有相同的配置,则按profile的指定顺序来获取较前面的profile下的配置。
由于Env之间的配置是完全隔离的,所以增加Profile维度来弥补Env维度的不足。
配置管理平台会创建一个default profile,如果未定义profile,配置将会放在default profile下,default profile自动激活,不用客户端显示激活。
通过Profile维度,可以轻松实现以下功能:
- 配置继承,一个应用部署的多套系统使用的配置几乎相同,只有少部分不同,这样这些系统就可以使用同一个环境,然后定义多个Profile,这些系统通过激活不同的profiles来达到配置继承(覆盖)的目的。
- 配置分支,当项目同时多个分支开发时,为了保证配置不相互影响,可以给每个分支定义新的profile,每个分支的配置放在分支自己的profile下,然后激活分支自己的profile即可。
- 灰度发布,为新版本的代码定义一个新的profile,然后灰度发布的系统激活这个新的profile下的配置。
更多Profile设计参考¶
Maven Profile
mvn命令可以通过-p选项来激活profiles。
-P,–activate-profiles <arg> Comma-delimited list of profiles
了解更多 Maven Profiles
Spring Profile
Spring Profile是Spring 3引入的概念,包括默认Profile和明确激活的Profiles。
默认Profile是指在没有任何profile被激活的情况下也能自动激活的profile,通过spring.profiles.default指定默认的profile。
明确激活的Profile,通过spring.profiles.active指定,也可以在程序中使用ConfigurableEnvironment#setActiveProfiles来激活profiles。
了解更多 Spring Environment abstraction
Spring Boot
Spring Boot中也支持profiles来获取配置。
相关的属性:
- spring.profiles.active
- spring.profiles.include
- spring.profiles
了解更多 Spring Boot Profiles
Spring Cloud Config
Spring Cloud Config也支持profile来获取配置。
了解更多 Spring Cloud Config
安装¶
configx-web安装¶
下载configx-web
https://github.com/zouzhirong/configx/releases/latest
解压
tar -zxvf configx-web-1.0.1.tar.gz
安装Mysql
https://dev.mysql.com/downloads/mysql/
执行sql文件
tables_mysql.sql
修改配置文件configx.properties
# 端口
http.port=3964
# 数据库信息
datasource.host=localhost
datasource.port=3306
datasource.database=configx
datasource.username=root
datasource.password=
启动configx-web
java -jar configx-web-1.0.1.jar
configx-client安装¶
添加configx-client的Maven依赖
<dependency>
<groupId>com.configx</groupId>
<artifactId>configx-client</artifactId>
<version>1.0.1</version>
</dependency>
先决条件¶
- Java 8
- Spring 3.1及以上版本
Configx Client¶
ConfigX与Spring无缝集成,支持Spring里面Environment和PropertySource的接口,对于已有的Spring应用程序的迁移成本非常低,在配置获取的接口上完全一致。
configx-client使用¶
添加configx-client Maven依赖
<dependency>
<groupId>com.configx</groupId>
<artifactId>configx-client</artifactId>
<version>1.0.1</version>
</dependency>
在src/main/resources下增加configx.properties配置文件
configx.client.config.app=应用ID,必填
configx.client.config.env=应用环境名称,必填
configx.client.config.profiles=Profile列表,多个之间用逗号分隔,如果多个Profile存在相同的配置,则越靠前的Profile优先级越高,选填,如果为空,则只获取默认Profile下的配置
configx.client.config.uri=配置管理系统提供的API URL:http://配置管理系统host/v1/config/
使用@EnableConfigService注解开启配置管理服务
Configx Client 教程¶
通过一些例子来讲解Configx Client的使用。
使用Configx管理Spring属性配置¶
configx与Spring无缝集成,支持Spring里面Environment和PropertySource的接口,对于已有的Spring应用程序的迁移成本非常低,在配置获取的接口上完全一致。
假设我有一个TestBean,它有一个timeout属性需要注入:
public class TestBean {
@Value("${timeout}")
private long timeout;
public void setTimeout(long timeout) {
this.timeout = timeout;
}
public long getTimeout() {
return timeout;
}
}
在configx配置管理平台中添加timeout配置项
configx既支持直接将属性作为一个配置项,也支持将属性放在属性文件中并将属性文件作为配置项。
直接将timeout属性作为配置项
如果直接将属性作为一个配置项,只需要在configx管理平台中添加一个配置项,配置名称为属性名timeout,配置值为属性值比如2000.
属性配置文件作为配置项
将timeout属性放在属性配置管理中,并将属性配置文件作为一个配置项。
跟Spring属性配置文件类似,当使用属性配置文件来配置属性时,需要指定属性配置文件名称。
在Spring中通过context:property-placeholder来指定属性配置文件。
<context:property-placeholder location="classpath:foo.properties" />
在configx管理平台中通过spring.property.sources配置项来指定属性配置文件的配置项名称
spring.property.sources=application.properties,database.properties,redis.properties
如果没有spring.property.sources配置项,则默认使用application.properties属性文件来配置属性。
在程序中使用timeout属性
在configx管理平台添加完timeout配置项后,就可以在Spring应用中跟使用普通Spring属性一样来使用timeout属性了。
基于XML的配置 在使用属性之前,首先需要通过configx扩展标签:<configx:config/>来开启configx。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:configx="http://www.configx.com/schema/configx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.configx.com/schema/configx
http://www.configx.com/schema/configx/configx.xsd">
<!-- 开启配置管理服务 -->
<configx:config/>
<bean name="testBean" class="com.configx.demo.TestBean">
<property name="timeout" value="${timeout}"/>
</bean>
</beans>
基于Java的配置(推荐) 在使用属性之前,首先需要通过@EnableConfigService注解来开启configx。
@Configuration
@EnableConfigService // 启动配置管理,并注册XmlConfigConverter
public class Application {
}
@Component
public class TestBean {
@Value("${timeout}")
private long timeout;
public void setTimeout(long timeout) {
this.timeout = timeout;
}
public long getTimeout() {
return timeout;
}
}
到这里,属性注入的工作已经完成。
Spring并没有提供属性刷新的功能,为了解决这个问题,configx通过自定义的scope来解决bean的属性热修改问题。
这个自定义的scope叫version-refresh,即基于配置版本刷新,类似于spring cloud config的refresh scope,但是区别在于configx会基于配置版本自动刷新,并且做了多版本并发控制。
如果将一个bean的scope设置为version-refresh,那么当configx有新版本的配置发布时,并且bean依赖的属性在新版本有修改时,那么这个bean会重新创建并在创建时重新注入新的属性值,一个bean可能同时存在多个版本,当有新版本的bean创建时,旧版本的bean在没有任何引用的情况下configx会将其销毁。
为了将TestBean中的timeout能够热修改,只需要将bean的scope设置为version-refresh,同时设置bean依赖的属性为timeout即可。
基于XML的配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:configx="http://www.configx.com/schema/configx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.configx.com/schema/configx
http://www.configx.com/schema/configx/configx.xsd">
<!-- 开启配置管理服务 -->
<configx:config/>
<bean name="testBean" class="com.configx.demo.TestBean">
<property name="timeout" value="${timeout}"/>
<configx:version-refresh dependsOn="timeout"/>
<aop:scoped-proxy proxy-target-class="true"/>
</bean>
</beans>
注意:基于XML的配置中,属性的热修改无法正常工作,这是因为在Bean定义解析阶段,spring就将${timeout}属性占位符解析成最终的值并添加到bean定义的propertyValues中,当bean创建时,直接使用的是timeout实际的值,而非$timeout}占位符。 所以尽管将testBean的scope设置为version-refresh,且设置依赖的属性为timeout,但是在timeout属性修改时,testBean会重新创建,但是使用的仍然是bean定义中的最初的timeout的值。
具体可查阅Spring源码:org.springframework.beans.factory.config.PlaceholderConfigurerSupport.doProcessProperties
所以要想bean属性支持热修改,请使用基于Java的配置。
基于Java的配置(推荐)
@Configuration
@EnableConfigService // 启动配置管理,并注册XmlConfigConverter
public class Application {
}
@Component
@VersionRefreshScope(dependsOn = {"timeout"})
public class TestBean {
@Value("${timeout}")
private long timeout;
public void setTimeout(long timeout) {
this.timeout = timeout;
}
public long getTimeout() {
return timeout;
}
}
在TestBean类上添加@VersionRefreshScope(dependsOn = {“timeout”}),就将bean的scope设置成了version-refresh,并且设置了依赖的属性为timeout,这样当有新版本的配置发布时,如果timeout属性修改了,那么TestBean将会重新创建。
注意:TestBean的scope虽然为version-refresh,但是依然可以正常注入到其他单例的bean中,其原理是Spring为自定义scope的bean创建了一个代理,并将代理注入到其他单例bean中。
具体可参考Spring文档。
使用Configx来配置自定义文件¶
在项目开发中,不仅有简单的key-value属性,还会有自定义的复杂的配置文件,比如有一个学生的配置文件: students.xml:
<students>
<student id="1" name="张三"></student>
<student id="2" name="李四"></student>
<student id="3" name="王五"></student>
<student id="4" name="赵六"></student>
</students>
然后我们需要将这个配置文件映射到Spring的bean。
首先,我们需要在configx配置管理平台中创建一个students.xml的配置文件。

然后,定义一个解析students.xml的解析器类。
/**
* 通过实现ConfigBeanConverter接口自定义配置转换器,将XML文件转换成Bean
* 我这里使用的是[simple-xml](http://simple.sourceforge.net/)框架将xml映射成Bean。
*/
public class XmlConfigConverter implements ConfigBeanConverter {
private static final Logger LOGGER = LoggerFactory.getLogger(XmlConfigConverter.class);
@Override
public boolean matches(String propertyName, String propertyValue, TypeDescriptor targetType) {
return propertyName != null && propertyName.endsWith(".xml");
}
@Override
public Object convert(String propertyName, String propertyValue, TypeDescriptor targetType) {
Class<?> targetClass = targetType.getType();
Serializer serializer = new Persister(new AnnotationStrategy());
try {
return serializer.read(targetClass, propertyValue, false);
} catch (Exception e) {
LOGGER.error("Convert xml to " + targetClass + " error", e);
}
return null;
}
}
最后,在Students类上使用注解@ConfigBean和VersionRefreshScope。
public class Student {
@Attribute
private String id;
@Attribute
private String name;
}
@ConfigBean(value="students.xml", converter=XmlConfigConverter.class)
@VersionRefreshScope
public class Students {
@ElementList(inline = true, entry = "student")
private List<Student> students;
}
@ConfigBean首先是一个@Component注解,另外通过value指定了bean由哪个配置名转换而来,通过converter指定具体的转换器。
@VersionRefreshScope用于当student.xml修改时,刷新Students bean,这里并不需要设置dependsOn={“students.xml”},因为对于@ConfigBean的bean,会自动把@ConfigBean的value的值当成dependsOn。
当项目中有大量的@ConfigBean时,并且使用相同的转换器来解析配置,可以在@EnableConfigService注解中通过converters来统一注册多个转换器,并不需要在每个ConfigBean上注册转换器。
@EnableConfigService(converters = {XmlConfigConverter.class}) // 启动配置管理,并注册XmlConfigConverter
Configx支持Spring国际化消息¶
开启Configx对Spring国际化消息的支持
- 基于XML的配置
需要把configx相关的xml namespace加到配置文件头上。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:configx="http://www.configx.com/schema/configx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.configx.com/schema/configx
http://www.configx.com/schema/configx/configx.xsd">
<!-- 开启配置管理对message source的支持 -->
<configx:message-source fallbackToSystemLocale="false" basenames="messages"/>
</beans>
- 基于Java的配置(推荐)
相对于基于XML的配置,基于Java的配置是目前比较流行的方式,也是Spring Boot的默认配置方式。
@Configuration
@EnableMessageSource(fallbackToSystemLocale = false, basenames = {}) // 开启配置管理对消息国际化的支持
public class Application {
}
指定basenames
有3种方式可以指定basenames:
- 通过@EnableMessageSource注解的basenames方法指定,或configx:message标签的basenames属性指定。
- 在configx配置管理平台中通过spring.messages.basename配置项来指定。
- 在Spring的environment中指定spring.messages.basename属性。
首先,在配置管理系统中,创建一个配置spring.messages.basename,内容为messages。 然后,创建一个messages.xml文件,将项目本地中的messages.xml内容复制到配置管理系统的messages.xml文件中。


使用configx热修改数据源¶
在configx配置管理平台中增加配置
在configx配置管理平台中,创建一个文件类型的配置database.properties,内容为:
datasource.driverClassName=com.mysql.jdbc.Driver
datasource.url=jdbc:mysql://192.168.1.199:3306/configx
datasource.username=root
datasource.password=test


将database.properties添加到spring.property.sources配置项中,如果没有spring.property.sources配置项,则创建一个。

程序代码如下:
/**
* 数据源属性
* <p>
* Created by zouzhirong on 2017/9/26.
*/
public class DataSourceProperties {
@Value("${datasource.driverClassName}")
private String driverClassName;
@Value("${datasource.url}")
private String url;
@Value("${datasource.username}")
private String username;
@Value("${datasource.password}")
private String password;
public String getDriverClassName() {
return driverClassName;
}
public String getUrl() {
return url;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
}
/**
* 数据源Bean Configuration
* Created by zouzhirong on 2017/9/25.
*/
@Configuration
public class DataSourceConfiguration {
@Bean
@VersionRefreshScope(dependsOn = {"datasource.url"})
public DataSourceProperties dataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@VersionRefreshScope(proxyMode = ScopedProxyMode.TARGET_CLASS, dependsOn = {"datasource.url"})
public BasicDataSource dataSource(DataSourceProperties dataSourceProperties) {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName(dataSourceProperties.getDriverClassName());
dataSource.setUrl(dataSourceProperties.getUrl());
dataSource.setUsername(dataSourceProperties.getUsername());
dataSource.setPassword(dataSourceProperties.getPassword());
return dataSource;
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
}
/**
* 数据库样例
* 支持热修改数据源地址
*/
@Service
public class DatabaseExample implements InitializingBean {
// inject the actual template
@Autowired
private JdbcTemplate jdbcTemplate;
}
将dataSourceProperties和dataSource两个bean的scope都设置为version-refresh,并且设置依赖属性为datasource.url,这样当有新版本的配置发布且datasource.url属性有更改,那么dataSourceProperties和dataSource两个bean会重新创建。
新的请求会使用新创建的dataSource,但是这时候旧的dataSource的bean并没有destory,当没有任何线程使用旧的dataSource时,configx-client会将其destory并从scope中移除,然后被gc掉,所有可能同时存在多个dataSource bean实例。 这个有点像nginx重启一样,nginx先启动新的进程用于服务新的请求,但是这时候旧的nginx进程并没有关闭,继续在服务旧的请求,直接没有任何旧的请求了,再关闭旧的nginx进程。 通过这种方式,可以实现线上热修改redis到新的地址,而并不会影响正在使用旧redis地址的请求。
使用configx热修改redis¶
在configx配置管理平台中增加配置
在configx配置管理平台中,创建一个文件类型的配置redis.properties,内容为:
redis.host=localhost
redis.port=6379


将redis.properties添加到spring.property.sources配置项中,如果没有spring.property.sources配置项,则创建一个。

程序代码如下:
public class RedisProperties {
@Value("${redis.host}")
private String host;
@Value("${redis.port}")
private int port;
public String getHost() {
return host;
}
public int getPort() {
return port;
}
}
@Configuration
public class RedisConfiguration {
@Bean
@VersionRefreshScope(dependsOn = {"redis.host", "redis.port"})
public RedisProperties redisProperties() {
return new RedisProperties();
}
@Bean
@VersionRefreshScope(dependsOn = {"redis.host", "redis.port"})
public JedisConnectionFactory jedisConnFactory(RedisProperties redisProperties) {
JedisConnectionFactory jedisConnFactory = new JedisConnectionFactory();
jedisConnFactory.setHostName(redisProperties.getHost());
jedisConnFactory.setPort(redisProperties.getPort());
jedisConnFactory.setUsePool(true);
return jedisConnFactory;
}
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}
@Service
public class RedisExample implements InitializingBean {
// inject the actual template
@Autowired
private RedisTemplate<String, String> template;
}
将redisProperties和redisTemplate两个bean的scope都设置为version-refresh,并且设置依赖属性为redis.host和redis.port,这样当有新版本的配置发布且redis.host和redis.port任一属性有更改,那么redisProperties和redisTemplate两个bean会重新创建。
新的请求会使用新创建的RedisTemplate,但是这时候旧的RedisTemplate的bean并没有destory,当没有任何线程使用旧的RedisTemplate时,configx-client会将其destory并从scope中移除,然后被gc掉,所有可能同时存在多个RedisTemplate bean实例。 这个有点像nginx重启一样,nginx先启动新的进程用于服务新的请求,但是这时候旧的nginx进程并没有关闭,继续在服务旧的请求,直接没有任何旧的请求了,再关闭旧的nginx进程。 通过这种方式,可以实现线上热修改redis到新的地址,而并不会影响正在使用旧redis地址的请求。
使用configx热修改线程池¶
在configx配置管理平台中增加配置
在configx配置管理平台中,创建一个文件类型的配置threadpool.properties,内容为:
threadpool.corePoolSize=10


将threadpool.properties添加到spring.property.sources配置项中,如果没有spring.property.sources配置项,则创建一个。

程序代码如下:
/**
* 线程池属性
* Created by zouzhirong on 2017/9/26.
*/
public class ThreadPoolProperties {
@Value("${threadpool.corePoolSize}")
private int corePoolSize;
public void setCorePoolSize(int corePoolSize) {
this.corePoolSize = corePoolSize;
}
public int getCorePoolSize() {
return corePoolSize;
}
}
/**
* 线程池Bean Configuration
* Created by zouzhirong on 2017/9/25.
*/
@Configuration
public class ThreadPoolConfiguration {
@Bean
@VersionRefreshScope(dependsOn = {"threadpool.corePoolSize"})
public ThreadPoolProperties threadPoolProperties() {
return new ThreadPoolProperties();
}
@Bean
@VersionRefreshScope(dependsOn = {"threadpool.corePoolSize"})
public ThreadPoolExecutor threadPoolExecutor(ThreadPoolProperties threadPoolProperties) {
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor)
Executors.newFixedThreadPool(threadPoolProperties.getCorePoolSize());
return threadPoolExecutor;
}
}
/**
* 线程池样例
* 支持热修改线程池参数
*/
@Service
public class ThreadPoolExample implements ConfigItemListener, InitializingBean {
@Autowired
private ThreadPoolExecutor threadPoolExecutor;
@Override
public void onApplicationEvent(ConfigItemChangeEvent event) {
if (event.getItemList() != null) {
for (ConfigItem configItem : event.getItemList()) {
if ("threadpool.corePoolSize".equals(configItem.getName())) {
// threadpool.corePoolSize属性修改,需要更新ThreadPoolExecutor的corePoolSize
int corePoolSize = Integer.valueOf(configItem.getValue());
threadPoolExecutor.setCorePoolSize(corePoolSize);
}
}
}
}
}
多版本并发控制¶
在自动发布模式下,更改任何一个配置,都会立刻自动发布。 在审核发布模式下,有可能修改了多个配置,然后一起发布。
客户端默认情况下,每次访问配置相关的属性或者配置Bean时,都会获取最新的配置。 例如:
List<Student> studentList1 = students.getStudents();
List<Student> studentList2 = students.getStudents();
假如执行完第一行代码之后,得到了studentList1的值,然后更新到了students配置文件的内容,那么studentList2将会得到最新的值。
这在大部分情况下是正确的,但是假如配置是一个跟金钱相关的,比如买道具需要的钱的数量,如果同一次请求中,两次获取到的钱的数量不一致,就可能导致问题。
为了保证在一个“事务”中,获取到的配置都是一致的,我们在configx-client中增加了多版本控制(mvcc)支持,即在configx-client中会保存配置的多个版本,而“事务”的整个生命周期中看到的只是配置的一个版本。 还是以上面的例子为例:
List<Student> studentList1 = students.getStudents();
List<Student> studentList2 = students.getStudents();
假如执行第一行代码时,students的配置版本为1,接着更新到了students配置文件的内容,配置版本为2,configx-client中同时有两个版本的students,由于在第一行代码执行时,获取到的配置版本为1,所以执行第二行代码时,获取到的配置版本还是1,配置版本为2的更新需要等待下一次“事务”才会生效。
多版本控制为了保证同一个“事务”中,不管配置是否被修改,这个事务中看到的配置是一致的。 开启多版本控制,需要在configx.properties中添加属性:
configx.client.mvcc.enabled=true
开启了mvcc之后,需要在程序中手动清除线程的版本号信息,否则线程将一直使用第一次的版本号,不会随着配置的更新而自动更新。 清除线程中的版本号,调用以下方法:
VersionContextHolder.clearCurrentVersion();
通常在一个“事务”结束以后,需要清理线程中的版本号。 常见的“事务”比如: 1、一个Http请求,可以在Filter中清除,比如:
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException
{
try {
...
} catch (Exception e) {
...
} finally {
VersionContextHolder.clearCurrentVersion();
}
}
2、在线程池中执行的任务,可以在ThreadPoolExecutor的afterExecute方法中清除,比如:
@Override
protected void afterExecute(Runnable r, Throwable t) {
VersionContextHolder.clearCurrentVersion();
super.afterExecute(r, t);
}
如果是Spring TaskExecutor,可以使用ConcurrentTaskExecutor来自定义ThreadPoolExecutor,覆盖afterExecute方法。
如果是Spring TaskScheduler,可以覆盖ConcurrentTaskScheduler来自定义ThreadPoolExecutor,覆盖afterExecute方法。
详细请参考:
Spring Task Execution and Scheduling
3、自定义“事务”,可以使用try...finally来清除,比如:
try {
...
} finally {
VersionContextHolder.clearCurrentVersion();
}
Configx Server¶
configx-web使用¶
打开configx配置管理平台
安装完configx-web并启动后,访问http://host:port/,host是运行configx-web的主机ip,port是configx-web的端口,默认是3964,进入configx配置管理平台。
登录
configx-web默认的管理员账号:
账号 admin
密码 admin123

创建应用
进入配置管理平台后,出现应用列表界面。

现在还没有应用,点击创建应用。

- 名称:应用的名称,唯一标识一个应用,可以包括中文。
- 描述:应用的描述信息。
- 管理员邮箱:设置对这个应用有管理员权限的用户的邮箱列表,可以使用(,)逗号、(;)分号、( )空格、(t)Tab、(n)换行来分隔,默认会把当前创建应用的用户邮箱添加到管理员列<表中。
- 开发者邮箱:设置对这个应用有开发者权限的用户的邮箱列表,可以使用(,)逗号、(;)分号、( )空格、(t)Tab、(n)换行来分隔,默认会把当前创建应用的用户邮箱添- 加到开发者列表中。
应用创建后,在应用列表中可以看到刚才新创建的应用。

创建环境
在应用列表页面,点击应用列表右侧的“环境”按钮,进入到应用环境管理页面。

现在应用下没有任何环境,点击创建环境。

- 名称:环境的名称,唯一标识应用的一个环境,只能包含英文。
- 别名:环境的别名,为这个环境设置别名,客户端在指定环境时,既可以使用环境名称,也可以使用环境别名。
- 描述:环境的描述信息。
- 顺序:设置环境的显示顺序,与功能无关,在管理系统中,在涉及到环境选择列表时,都会根据这个顺序来排序显示,值越小越靠前,值越大越靠后。
创建完环境后,就可以添加配置了。点击顶部导航条的“配置管理”,进入到配置管理页面。

创建文件配置

创建文本配置

创建数值配置

创建日期配置

创建时间配置

创建日期时间配置

查看配置列表
