统一配置中心 1 中记录了我之前项目中如何处理多系统中的配置问题,对于统一配置中心组件一般分为两种做法:
它的好外与缺点都非常明确。
开源的配置中心,我之前有提到过百度的 disconf,还有当当的 config-toolkit, 这些产品都有很多应用案例,功能也非常全。
当系统越来越多后,每个系统的配置如果没有统一的管理机制那么会非常难管理,需要有一种侵入性小的方案来将这些原本在单项目中维护的配置工作统一管理起来。
推荐使用 zookeeper 来存储项目中的配置项,也可以是其它的存储。下面设计的配置中心组件是可扩展,但实现暂时只实现了 zookeeper。
在系统加载时,想办法将存储在 zookeeper 中的配置内容加载到系统变量中,然后程序就可以像调用本地配置文件一样了,不需要改变现有系统引用变量的行为。至于本地配置文件直接使用 Spring 自带功能即可,当然也可以统一在组件中。
不需要支持本地文件的热更新
内容有点多,先看下配置加载的时序图,不包含事件通知以及热更新。
PropertyPlaceholderConfigurer, 可以利用它来启动组件的初始化进而从 zookeeper 中加载数据到系统中,即先加载 zookeeper 数据然后加载本地配置文件。
- @Bean
- public static PropertyPlaceholderConfigurerproperties() {SpringPropertyInjectSupportspringPropertyInjectSupport=new SpringPropertyInjectSupport();
- springPropertyInjectSupport.setConfigNameSpaces("configcenter/"+System.getProperty("env"));
- springPropertyInjectSupport.init();PropertyPlaceholderConfigurerppc= new PropertyPlaceholderConfigurer();Resource[] resources= new ClassPathResource[]
- {new ClassPathResource("application.properties") };
- ppc.setLocations( resources );
- ppc.setIgnoreUnresolvablePlaceholders(true);returnppc;
- }
SpringPropertyInjectSupport, 是自定义的组件启动类,它负责加载 zookeeper 中的数据到系统:核心方法是 init。
- public voidinit() {if(this.configNameSpaces!= null) {this.setSystemPropertiesFromConfigCenter();
- }
- }
configNameSpaces 是统一配置中心管理所有节点的父结点,比如 configcenter/test 是指测试环境的配置,系统中的配置节点名称不包含这些与框架相关的信息,只需要配置 dataSource=XXX 即代表 configcenter/test/dataSource 这个配置项。
ConfigCenterFactory 是个单例用来返回统一配置中心实例,ConfigCenterService 是配置接口,然后由配置接口获取所有的配置项(类型是一个 Map),最后将 Map 中的信息导入到系统变量中。
- private void setSystemPropertiesFromConfigCenter() {
- if (StringUtils.isBlank(this.configNameSpaces)) {
- return;
- }
- ConfigCenterFactory.getInstance().setSystemNameSpace(this.configNameSpaces);
- ConfigCenterService cc = ConfigCenterFactory.getInstance().getConfig(this.configNameSpaces);
- Map < String,
- Object > config = cc.getConfig();
- setSystemProperys(cc, config);
- }
- private void setSystemProperys(ConfigCenterService cc, Map < String, Object > config) {
- for (String key: config.keySet()) {
- String value = cc.get(key);
- if (key.contains(".")) {
- key = key.substring(1);
- }
- if (value == null) {
- value = "";
- }
- System.setProperty(key, value);
- }
- }
实例化配置组件,为了支持同时加载多个不同节点下的数据,所以以 nameSpace 做为 key 将实例放在 Map 中,比如想同时访问商品以及订单的配置,它们的 namespace 分别为
- private static final Object lockObj = new Object();
- private ConcurrentHashMap < String,
- ConfigCenterService > configCenterCache = null;
- public ConfigCenterService getConfig(final String hosts, final String nameSpace) {
- Preconditions.checkNotNull(hosts);
- Preconditions.checkNotNull(nameSpace);
- StringBuilder sb = new StringBuilder(hosts);
- sb.append(nameSpace);
- final String key = sb.toString().intern();
- ConfigCenterService config = this.configCenterCache.get(key);
- if (config == null) {
- synchronized(lockObj) {
- if (!this.configCenterCache.containsKey(key)) {
- ConfigOption co = new ConfigOption(nameSpace, hosts);
- ConfigCenterService cc = new ConfigCenterServiceImpl(co);
- this.configCenterCache.put(key, cc);
- }
- }
- } else {
- return config;
- }
- return this.configCenterCache.get(key);
- }
配置管理接口,主要包含三部分内容:
不是更新系统中的变量,是指系统中的变量发生改变之后需要做什么?比如数据库的配置发生变化,此时需要重新刷新数据库连接池的信息等。 一些获取配置的协助类,即强类型化返回配置,减少调用端的类型转换。
- public interface ConfigCenterService {
- public Map < String,
- Object > getConfig();
- public String get(String key);
- public Long getLongValue(String key);
- public Integer getIntegerValue(String key);
- public Double getDoubleValue(String key);
- public Boolean getBooleanValue(String key);
- public void notify(DataChangeEvent event);
- public void colse();
- }
一些通用的逻辑实现放在这里。
是具体的配置接口实现类,继承 AbstractConfigCenterService,实现 ConfigCenterService 接口。
- private voidstartClient() {if(this.client== null) {try{this.client= CuratorFrameworkFactory.builder()
- .connectString(configOption.getZkUrls())
- .namespace(configOption.getNameSpace())
- .retryPolicy(configOption.getRetryPolicy())
- .connectionTimeoutMs(20000)
- .build();this.client.start();
- logger.info("zkclient start" +configOption.getNameSpace());
- }catch(Throwablee) {if(null!=this.client){this.client.close();
- }throw new RuntimeException("CuratorFrameworkFactory start error",e);
- }
- }
- }
- private voidloadConfig(StringnodePath) {try{if(StringUtils.isNotEmpty(nodePath)) {
- loadData(nodePath);
- }GetChildrenBuilderchildrenBuilder=client.getChildren();try{List<String>children= null;if(StringUtils.isEmpty(nodePath)) {
- children=childrenBuilder.watched().forPath(null);
- }else{
- children=childrenBuilder.watched().forPath(nodePath);
- }
- loadChildsConfig(children, nodePath);
- }catch(Exceptione) {throw Throwables.propagate(e);
- }
- }catch(Exceptione) {
- logger.error("load zk config error namespace={}", configOption.getNameSpace());
- logger.error("load config error", e);this.client.close();throw new RuntimeException("load zk error");
- }
- }
- private WatchergetPathWatcher() {return new Watcher() {@Override
- public void process(WatchedEvent event) {if(event!= null) {try{booleanisDelete= false;if(event.getState()== Event.KeeperState.SyncConnected) {Stringpath=event.getPath();if(path== null ||path.equals("/"))return;switch(event.getType()) {case NodeDeleted:postRemovePath(event.getPath());
- isDelete= true;break;case NodeDataChanged:postDataChangeEvent(event);break;default:
- break;
- }if(!isDelete) {
- watchPathDataChange(event.getPath());
- }
- }
- }catch(Exceptione) {
- logger.info("zk data changed error:",e);
- }
- }
- }
- };
- }
节点数据变更后的事件通知,采用 guava 的 eventbus 来实现。
- private void postDataChangeEvent(WatchedEvent event) throws Exception {
- byte[] data = client.getData().forPath(event.getPath());
- String value = new String(data, Charsets.UTF_8);
- String key = event.getPath().replace("/", ".");
- postDataChangeKeyValue(key, value);
- }
- private void postDataChangeKeyValue(String key, String value) {
- this.config.put(key, value);
- Map < String,
- Object > map = new HashMap < String,
- Object > ();
- map.put(key, value);
- DataChangeEvent dataChangeEvent = new DataChangeEvent(map);
- configOption.getEnventBus().post(dataChangeEvent);
- }
统一配置源码
源码参考了网上开源的项目,由于时间久远原项目已经找到地址了。 我公布的源码是经过我重新整理后的结果,并非全部出自于自己。
来源: http://www.cnblogs.com/ASPNET2008/p/6752131.html