关于SpringBoot的自定义配置源、配置刷新之前也介绍过几篇博文;最近正好在使用apollo时,排查配置未动态刷新的问题时,看了下它的具体实现发现挺有意思的;
接下来我们致敬经典,看一下如果让我们来实现配置的动态刷新,应该怎么搞?
I. 配置使用姿势 既然要支持配置的动态刷新,那么我们就得先看一下,在SpringBoot中,常见的配置使用姿势有哪些
1. @Value注解绑定 直接通过@Value
注解,将一个对象得成员变量与Environment中的配置进行绑定,如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Slf 4j@RestController public class IndexController @Value("${config.type:-1}") private Integer type; @Value ("${config.wechat:默认}" ) private String wechat; private String email; @Value ("${config.email:default@email}" ) public IndexController setEmail (String email) { this .email = email; return this ; } }
注意:@Value
支持SpEL
2. @ConfigurationProperties绑定 通过@ConfigurationProperties
注解声明一个配置类,这个类中的成员变量都是从Environment
中进行初始化
如:
1 2 3 4 5 6 7 8 9 @ConfigurationProperties (prefix = "config" )public class MyConfig { private String user; private String pwd; private Integer type; }
3. Environment.getProperty()直接获取配置 直接从上下文中获取配置,也常见于各种使用场景中,如
1 environment.getProperty("config.user" );
II. 配置刷新 接下来我们看一下,如何实现配置刷新后,上面的三种使用姿势都能获取到刷新后的值
1. 自定义一个属性配置源 自定义一个配置源,我们直接基于内存的ConcurrentHashMap
来进行模拟,内部提供了一个配置更新的方法,当配置刷新之后,还会对外广播一个配置变更事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 public class SelfConfigContext { private static volatile SelfConfigContext instance = new SelfConfigContext(); public static SelfConfigContext getInstance () { return instance; } private Map<String, Object> cache = new ConcurrentHashMap<>(); public Map<String, Object> getCache () { return cache; } private SelfConfigContext () { cache.put("config.type" , 33 ); cache.put("config.wechat" , "一灰灰blog" ); cache.put("config.github" , "liuyueyi" ); } public void updateConfig (String key, Object val) { cache.put(key, val); ConfigChangeListener.publishConfigChangeEvent(key); } } @Component public class ConfigChangeListener implements ApplicationListener <ConfigChangeListener .ConfigChangeEvent > { @Override public void onApplicationEvent (ConfigChangeEvent configChangeEvent) { SpringValueRegistry.updateValue(configChangeEvent.getKey()); } public static void publishConfigChangeEvent (String key) { SpringUtil.getApplicationContext().publishEvent(new ConfigChangeEvent(Thread.currentThread().getStackTrace()[0 ], key)); } @Getter public static class ConfigChangeEvent extends ApplicationEvent { private String key; public ConfigChangeEvent (Object source, String key) { super (source); this .key = key; } } }
接下来就需要将这个自定义的配置元,注册到 environment
上下文,在这里我们可以借助ApplicationContextInitializer
来实现,在上下文初始化前,完成自定义配置注册
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class SelfConfigContextInitializer implements ApplicationContextInitializer <ConfigurableApplicationContext > { @Override public void initialize (ConfigurableApplicationContext configurableApplicationContext) { System.out.println("postProcessEnvironment#initialize" ); ConfigurableEnvironment env = configurableApplicationContext.getEnvironment(); initialize(env); } protected void initialize (ConfigurableEnvironment environment) { if (environment.getPropertySources().contains("selfSource" )) { return ; } MapPropertySource propertySource = new MapPropertySource("selfSource" , SelfConfigContext.getInstance().getCache()); environment.getPropertySources().addFirst(propertySource); } }
接下来注册这个扩展点,直接选择在项目启动时,进行注册
1 2 3 4 5 6 7 8 @SpringBootApplication public class Application { public static void main (String[] args) { SpringApplication springApplication = new SpringApplication(Application.class ) ; springApplication.addInitializers(new SelfConfigContextInitializer()); springApplication.run(args); } }
2. Environment配置刷新 envionment实时获取配置的方式,支持配置刷新应该相对简单,如直接吐出一个接口,支持更新我们自定义配置源的配置,不做任何变更,这个配置应该时同时更新的
首先提供一个Spring的工具类,用于更简单的获取Spring上下文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @Component public class SpringUtil implements ApplicationContextAware , EnvironmentAware { private static ApplicationContext applicationContext; private static Environment environment; private static Binder binder; @Override public void setApplicationContext (ApplicationContext applicationContext) throws BeansException { SpringUtil.applicationContext = applicationContext; } @Override public void setEnvironment (Environment environment) { SpringUtil.environment = environment; binder = Binder.get(environment); } public static ApplicationContext getApplicationContext () { return applicationContext; } public static Environment getEnvironment () { return environment; } public static Binder getBinder () { return binder; } }
配置更新的示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Slf 4j@RestController public class IndexController { @GetMapping (path = "update" ) public String updateCache (String key, String val) { SelfConfigContext.getInstance().updateConfig(key, val); return "ok" ; } @GetMapping (path = "get" ) public String getProperty (String key) { return SpringUtil.getEnvironment().getProperty(key); } }
执行验证一下:
3. @ConfigurationProperties
配置刷新
【基础系列】自定义属性配置绑定极简实现姿势介绍 | 一灰灰Blog
之前在介绍自定义属性配置绑定时介绍过,通过Binder
来实现绑定配置的Config对象动态刷新,我们这里同样可以实现配置变更时,主动刷新@ConfigurationProperties
注解绑定的属性
具体实现如下,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 @Slf 4j@Component public class ConfigAutoRefresher implements ApplicationRunner { private Binder binder; @EventListener () public void refreshConfig (ConfigChangeListener.ConfigChangeEvent event) { log.info("配置发生变更,开始动态刷新: {}" , event); SpringUtil.getApplicationContext().getBeansWithAnnotation(ConfigurationProperties.class ).values ().forEach (bean -> { Bindable<?> target = Bindable.ofInstance(bean).withAnnotations(AnnotationUtils.findAnnotation(bean.getClass(), ConfigurationProperties.class )) ; bind(target); }); } public <T> void bind (Bindable<T> bindable) { ConfigurationProperties propertiesAno = bindable.getAnnotation(ConfigurationProperties.class ) ; if (propertiesAno != null ) { BindHandler bindHandler = getBindHandler(propertiesAno); this .binder.bind(propertiesAno.prefix(), bindable, bindHandler); } } private BindHandler getBindHandler (ConfigurationProperties annotation) { BindHandler handler = new IgnoreTopLevelConverterNotFoundBindHandler(); if (annotation.ignoreInvalidFields()) { handler = new IgnoreErrorsBindHandler(handler); } if (!annotation.ignoreUnknownFields()) { UnboundElementsSourceFilter filter = new UnboundElementsSourceFilter(); handler = new NoUnboundElementsBindHandler(handler, filter); } return handler; } @Override public void run (ApplicationArguments args) throws Exception { log.info("初始化!" ); ConfigurableEnvironment environment = (ConfigurableEnvironment) SpringUtil.getEnvironment(); this .binder = new Binder(ConfigurationPropertySources.from(environment.getPropertySources()), new PropertySourcesPlaceholdersResolver(environment), new DefaultConversionService(), ((ConfigurableApplicationContext) SpringUtil.getApplicationContext()) .getBeanFactory()::copyRegisteredEditorsTo); } }
注意上面的实现,分三类:
public <T> void bind(Bindable<T> bindable)
: 具体实现绑定配置刷新的逻辑
核心思想就是将当前对象与environment配置进行重新绑定
public void run
: binder初始化
在应用启动之后进行回调,确保是在environment准备完毕之后回调,获取用于属性配置绑定的binder,避免出现envionment
还没有准备好
也可以借助实现EnvironmentPostProcessor
来实现
public void refreshConfig(ConfigChangeListener.ConfigChangeEvent event)
: 配置刷新
通过@EventListener
监听配置变更事件,找到所有的ConfigurationProperties
修饰对象,执行重新绑定逻辑
接下来我们验证一下配置变更是否会生效
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Data @Component @ConfigurationProperties (prefix = "config" )public class UserConfig { private String user; private String pwd; private Integer type; private String wechat; } @Slf 4j@RestController public class IndexController { @Autowired private UserConfig userConfig; @GetMapping (path = "/user" ) public UserConfig user () { return userConfig; } @GetMapping (path = "update" ) public String updateCache (String key, String val) { selfConfigContainer.refreshConfig(key, val); SelfConfigContext.getInstance().updateConfig(key, val); return JSON.toJSONString(userConfig); } }
定义一个UserConfig来接收config
前缀开始的配置,通过update接口来更新相关配置,更新完毕之后返回UserConfig的结果
4. @Value 配置刷新 最后我们再来看一下@Value注解绑定的配置的刷新策略,很久很久之前我也介绍一篇博文,如何实现动态刷新,欢迎查看
【基础系列】SpringBoot应用篇@Value注解支持配置自动刷新能力扩展 | 一灰灰Blog
其核心思想就是找出所有@Value
绑定的成员变量,当监听到配置变更之后,通过反射的方式进行刷新
关键的实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 @Slf 4j@Component public class SpringValueProcessor implements BeanPostProcessor { private final PlaceholderHelper placeholderHelper; public SpringValueProcessor () { this .placeholderHelper = new PlaceholderHelper(); } @Override public Object postProcessBeforeInitialization (Object bean, String beanName) throws BeansException { Class clazz = bean.getClass(); for (Field field : findAllField(clazz)) { processField(bean, beanName, field); } for (Method method : findAllMethod(clazz)) { processMethod(bean, beanName, method); } return bean; } private List<Field> findAllField (Class clazz) { final List<Field> res = new LinkedList<>(); ReflectionUtils.doWithFields(clazz, res::add); return res; } private List<Method> findAllMethod (Class clazz) { final List<Method> res = new LinkedList<>(); ReflectionUtils.doWithMethods(clazz, res::add); return res; } protected void processField (Object bean, String beanName, Field field) { Value value = field.getAnnotation(Value.class ) ; if (value == null ) { return ; } Set<String> keys = placeholderHelper.extractPlaceholderKeys(value.value()); if (keys.isEmpty()) { return ; } for (String key : keys) { SpringValueRegistry.SpringValue springValue = new SpringValueRegistry.SpringValue(key, value.value(), bean, beanName, field); SpringValueRegistry.register(key, springValue); log.debug("Monitoring {}" , springValue); } } protected void processMethod (Object bean, String beanName, Method method) { Value value = method.getAnnotation(Value.class ) ; if (value == null ) { return ; } if (method.getAnnotation(Bean.class ) ! = null ) { return ; } if (method.getParameterTypes().length != 1 ) { log.error("Ignore @Value setter {}.{}, expecting 1 parameter, actual {} parameters" , bean.getClass().getName(), method.getName(), method.getParameterTypes().length); return ; } Set<String> keys = placeholderHelper.extractPlaceholderKeys(value.value()); if (keys.isEmpty()) { return ; } for (String key : keys) { SpringValueRegistry.SpringValue springValue = new SpringValueRegistry.SpringValue(key, value.value(), bean, beanName, method); SpringValueRegistry.register(key, springValue); log.info("Monitoring {}" , springValue); } } }
上面的实现,主要利用到BeanPostProcessor
,在bean初始化之后,扫描当前bean中是否有@Value
绑定的属性,若有,则注册到自定义的SpringValueRegistry
中
注意事项:
@Value
有两种绑定姿势,直接放在成员变量上,以及通过方法进行注入
所以上面的实现策略中,有Field
和Method
两种不同的处理策略;
@Value
支持SpEL表达式,我们需要对配置key进行解析
相关的源码,推荐直接在下面的项目中进行获取,demo中的实现也是来自apollo-client
接下来再看一下注册配置绑定的实现,核心方法比较简单,两个,一个注册,一个刷新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 @Slf 4jpublic class SpringValueRegistry { public static Map<String, Set<SpringValue>> registry = new ConcurrentHashMap<>(); public static void register (String key, SpringValue val) { if (!registry.containsKey(key)) { synchronized (SpringValueRegistry.class ) { if (!registry.containsKey(key)) { registry.put(key, new HashSet<>()); } } } Set<SpringValue> set = registry.getOrDefault(key, new HashSet<>()); set.add(val); } public static void updateValue (String key) { Set<SpringValue> set = registry.getOrDefault(key, new HashSet<>()); set.forEach(s -> { try { s.update((s1, aClass) -> SpringUtil.getBinder().bindOrCreate(s1, aClass)); } catch (Exception e) { throw new RuntimeException(e); } }); } @Data public static class SpringValue { private MethodParameter methodParameter; private Field field; private WeakReference<Object> beanRef; private String beanName; private String key; private String placeholder; private Class<?> targetType; public SpringValue (String key, String placeholder, Object bean, String beanName, Field field) { this .beanRef = new WeakReference<>(bean); this .beanName = beanName; this .field = field; this .key = key; this .placeholder = placeholder; this .targetType = field.getType(); } public SpringValue (String key, String placeholder, Object bean, String beanName, Method method) { this .beanRef = new WeakReference<>(bean); this .beanName = beanName; this .methodParameter = new MethodParameter(method, 0 ); this .key = key; this .placeholder = placeholder; Class<?>[] paramTps = method.getParameterTypes(); this .targetType = paramTps[0 ]; } public void update (BiFunction<String, Class, Object> newVal) throws Exception { if (isField()) { injectField(newVal); } else { injectMethod(newVal); } } private void injectField (BiFunction<String, Class, Object> newVal) throws Exception { Object bean = beanRef.get(); if (bean == null ) { return ; } boolean accessible = field.isAccessible(); field.setAccessible(true ); field.set(bean, newVal.apply(key, field.getType())); field.setAccessible(accessible); log.info("更新value: {}#{} = {}" , beanName, field.getName(), field.get(bean)); } private void injectMethod (BiFunction<String, Class, Object> newVal) throws Exception { Object bean = beanRef.get(); if (bean == null ) { return ; } Object va = newVal.apply(key, methodParameter.getParameterType()); methodParameter.getMethod().invoke(bean, va); log.info("更新method: {}#{} = {}" , beanName, methodParameter.getMethod().getName(), va); } public boolean isField () { return this .field != null ; } } }
SpringValue的构建,主要就是基于反射需要使用到的一些关键信息的组成上;可以按需进行设计补充
到此,关于@Value注解的配置动态刷新就已经实现了,接下来写几个demo验证一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Slf 4j@RestController public class IndexController { @Value ("${config.type:-1}" ) private Integer type; @Value ("${config.wechat:默认}" ) private String wechat; private String email; @Value ("${config.email:default@email}" ) public IndexController setEmail (String email) { this .email = email; return this ; } @GetMapping (path = "update" ) public String updateCache (String key, String val) { SelfConfigContext.getInstance().updateConfig(key, val); return wechat + "_" + type + "_" + email; } }
5. 小结 本文主要介绍了项目中配置的动态刷新的实现方案,也可以看作是apollo配置中心的简易实现原理,其中涉及到的知识点较多,下面做一个简单的小结
配置的三种使用姿势
@Value
绑定
@ConfigurationProperties
绑定对象
environment.getProperty()
自定义配置源加载
environment.getPropertySources().addFirst(MapPropertySource)
配置刷新
Binder实现ConfigurationProperties刷新
反射实现@Value注解刷新
III. 不能错过的源码和相关知识点 0. 项目
1. 微信公众号: 一灰灰Blog 尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
一灰灰blog
Related Issues not found
Please contact @liuyueyi to initialize the comment