包含两方面的含义:
一、变化的配置能够下发到客户端.
二、客户端能够方便地处理这些变化的配置。下面会讲普通监听器回调方式和spring 注解到field的方式。
配置下发
1)客户端间隔20秒(可配)去数据库拉取配置,sql传递的参数有配置名称、环境、上一次最大更新时间
2)取到配置后,合并更新本地备份。
3)通过监听器的方式通知配置变化。
#监听器接口public interface ConfigUpdateListener { void update(Properties properties);}#启动时添加监听器 public void addConfigUpdateListener(ConfigUpdateListener configUpdateListener) { configUpdateListeners.add(configUpdateListener); }#配置变化,调用监听器进行通知 for (ConfigUpdateListener configUpdateListener : configUpdateListeners) { configUpdateListener.update(changedProperties); }
spring 注解方式绑定成员变量
通过@ConfigValue注解来实现变量绑定,当配置变化的时候会同时更新这个变量。这种方式可以用于一起开关切换、降级处理的场景。
@ConfigValue("baseUrl")private String baseUrl;
实现原理:
1)从配置中心获取配置。
2)通过BeanPostProcessor 遍历每个spring bean,找出所有包含@ConfigValue注解的field,然后设置值为配置中心的属性值,同时把记录field到Map对象,以后更新配置时就不用想办法遍历了。这里有一个严格处理原则,如果配置中心找不到相应key或值赋值时候报异常,终止程序,保证安全性。
其实代码对非spring的、static 成员变量也支持,只是需要程序启动时候指定包含这些static变量的类名列表。
public class PropertyPlaceholderConfigurer extends org.springframework.beans.factory.config.PropertyPlaceholderConfigurer implements BeanPostProcessor{ private Map<String,List<BeanField>> configMap= new ConcurrentHashMap<String,List<BeanField>>(); @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { ConfigInjectSupport.inject(mergedProperties,bean.getClass(),bean,configMap); return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { return bean; }
public static void inject(Properties properties, Class clazz, Object bean, Map<String,List<BeanField>> configMap){ Field[] fields = clazz.getDeclaredFields(); if (null != fields) { for (Field f : fields) { if(bean==null && !Modifier.isStatic(f.getModifiers())) continue; ConfigValue configValue = f.getAnnotation(ConfigValue.class); if (configValue != null) { String key = configValue.value(); f.setAccessible(true); Object value = properties.get(key); if (value != null) { try { f.set(bean, converter.convertIfNecessary(value, f.getType(), f)); } catch (IllegalAccessException e) { throw new RuntimeException(e); } }else{ RuntimeException e= new RuntimeException("can not find config. key="+key+", class="+clazz+", field="+f.getName()); e.printStackTrace(); throw e; } List<BeanField> list = configMap.get(key); if (list == null) { list = new ArrayList<BeanField>(); configMap.put(key, list); } BeanField beanField=new BeanField(); beanField.setBean(bean); beanField.setField(f); list.add(beanField); } } } clazz = clazz.getSuperclass(); }
3)加入监听器,接收变化的配置,更新field值
public static void addConfigUpdateListener(final Map<String,List<BeanField>> configMap) { DefaultConfigClient.addConfigUpdateListener(new ConfigUpdateListener() { @Override public void update(Properties properties) { for (Map.Entry<Object, Object> e : properties.entrySet()) { List<BeanField> beanFields = configMap.get(e.getKey().toString()); if (beanFields != null) { Object value = properties.get(e.getKey()); for (BeanField beanField : beanFields) { try { ConfigValue configValue = beanField.getField().getAnnotation(ConfigValue.class); if(configValue.updatable()) { ConfigInjectSupport.setFieldValue(beanField, value); } } catch (IllegalAccessException ex) { ex.printStackTrace(); } } } } } }); } public static void setFieldValue(BeanField beanField,Object value) throws IllegalAccessException { Field f=beanField.getField(); f.set(beanField.getBean(), ConvertUtils.convert(value,f.getType())); }