Apollo支持@ConfigurationProperties动态刷新
问题
apollo动态刷新,应用在@value
这种注入方式的属性没有问题,但是如果使用@ConfigurationProperties
注解的bean,动态刷新就不好使了,会注入不到的。
Apollo动态刷新官网介绍
@Value
,动态刷新
@ConfigurationProperties
, 需要添加apollo配置监听器@ApolloConfigChangeListener
实现动态刷新
官方文档原话:
需要注意的是,
@ConfigurationProperties
如果需要在Apollo配置变化时自动更新注入的值,需要配合使用EnvironmentChangeEvent
或RefreshScope
。相关代码实现,可以参考apollo-use-cases项目中的ZuulPropertiesRefresher.java和apollo-demo项目中的SampleRedisConfig.java以及SpringBootApolloRefreshConfig.java
@Value:直接属性映射
@ConfigurationProperties:bean属性映射的解决办法
@ConfigurationProperties及yml
my.config:
age: 12
name: #{first-name: lao, last-name: liu}
first-name: lao
last-name: liu
map: #{eq: 90, iq: 85}
eq: '09' #不加引号会转化成9,加上则转成09
iq: 85
list: #[吃喝睡, 打豆豆]
- 吃喝睡
- 打豆豆
@Data
@Configuration
@ConfigurationProperties(prefix = "my.config")
public class MyConfigProperties {
private Integer age;
private Name name;
private Map<String, Integer> map;
private List<String> list;
@Data
public static class Name {
private String firstName;
private String lastName;
}
}
方式一: EnvironmentChangeEvent + @ApolloConfigChangeListener
- 新建一个监听类:
@Slf4j
@Configuration
public class ApolloChangeListener {
@Autowired
private ApplicationContext applicationContext;
@ApolloConfigChangeListener(value = "application.yml", interestedKeyPrefixes = "my.config.")
public void refresh(ConfigChangeEvent changeEvent) {
log.info("Changes for namespace " + changeEvent.getNamespace());
for(String key : changeEvent.changedKeys()) {
ConfigChange change = changeEvent.getChange(key);
log.info(String.format("Found change - key: %s, oldValue: %s, newValue: %s, changeType: %s", change.getPropertyName(), change.getOldValue(), change.getNewValue(), change.getChangeType()));
}
// 更新相应的bean的属性值,主要是存在@ConfigurationProperties注解的bean
applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
// 相关业务处理
// ...
}
}
方式一的坑
Apollo动态刷新后, MyConfigProperties
不同数据类型实时刷新的情况如下:
数据类型/Apollo操作 | 新增key,val | 更新val | 增改2维key,val | 删除2维key,val | 清空val | 删除key |
---|---|---|---|---|---|---|
Integer age | √ | √ | - | - | × | × |
Name name | √ | √ | √ | × | × | × |
Map<String, Integer> map | √ | √ | √ | × | × | × |
List list | √ | √ | √ | √ | √ | √ |
说明: √: 可以实现; ×: 不可实现; -: 无此场景;
结论: 使用EnvironmentChangeEvent
来实现Apollo动态刷新@ConfigurationProperties
注解的bean,存在未全量覆盖的问题,需考虑使用场景酌情使用.
方式二(完美方案): RefreshScope + @ApolloConfigChangeListener
- 被@ConfigurationProperties修饰的类需要添加如下注解:
@Component
@RefreshScope
- 新建一个监听类:
@Slf4j
@Configuration
public class ApolloChangeListener {
@Autowired
private RefreshScope refreshScope;
@ApolloConfigChangeListener(value = "application.yml", interestedKeyPrefixes = "my.config.")
public void refresh(ConfigChangeEvent changeEvent) {
log.info("Changes for namespace " + changeEvent.getNamespace());
for(String key : changeEvent.changedKeys()) {
ConfigChange change = changeEvent.getChange(key);
log.info(String.format("Found change - key: %s, oldValue: %s, newValue: %s, changeType: %s", change.getPropertyName(), change.getOldValue(), change.getNewValue(), change.getChangeType()));
}
// 刷新的bean
refreshScope.refreshAll();
// 刷新指定的bean
// refreshScope.refresh("myConfigProperties");
// ...
}
}
监听触发后会刷新
@ConfigurationProperties
注解的bean, 重新赋值, 完美解决方式一的坑.
方式三: 自定义 + @ApolloConfigChangeListener
- 新建一个监听类:
@Slf4j
@Configuration
public class ApolloChangeConfig {
@ApolloConfigChangeListener(value = "application.yml", interestedKeyPrefixes = "my.config.")
public void refresh(ConfigChangeEvent changeEvent) {
log.info("Changes for namespace " + changeEvent.getNamespace());
for(String key : changeEvent.changedKeys()) {
ConfigChange change = changeEvent.getChange(key);
log.info(String.format("Found change - key: %s, oldValue: %s, newValue: %s, changeType: %s", change.getPropertyName(), change.getOldValue(), change.getNewValue(), change.getChangeType()));
//如果改的是配置类的参数,则手动修改配置
if (key.startsWith("age")) {
//根据变更类型进行添加或删除操作
switch (change.getChangeType()) {
case ADDED:
this.add(change.getPropertyName(), change.getNewValue());
break;
case MODIFIED:
this.put(change.getPropertyName(), change.getNewValue());
break;
case DELETED:
this.remove(change.getPropertyName());
break;
default:
break;
}
}
}
}
}
redis配置动态刷新成功了,但起作用的仍是旧配置,需手动重新建立链接才能生效(Ribbon也类似).
额外说明:
有时候,动态刷新成功后,发现有些调用相关接口执行还是原配置的效果,没成功(比如某个花钱的配置属性的环境切换,pro改为test,调用相关接口发现执行时属性还是pro时的效果)。
那有可能是因为涉及到配置类启动时初始化的问题,比如说,有个注入到spring容器的配置类(@Configuration@bean)刚好用到你这个带有@ConfigurationProperties类在项目启动进行初始化。项目启动后,已经注入到了spring容器中,你动态刷新的配置属性值,但是没有去把容器中的这个配置类再次刷新,那么配置类还是原来的配置类,里面的属性也还是原来的。
解决方法:在上面相关业务处理下面,也就是配置动态刷新后,再次初始化一下spring容器中的那个配置类,就能生效成功了。
(可能表达的不是很好。。。,说白了可能就是调用到相关配置属性另一个bean没有刷新)
@ApolloConfigChangeListener介绍
@ApolloConfigChangeListener()
注解中可填写参数
value
:指定监听的配置文件。
interestedKeyPrefixes
:指定监听的配置属性前缀。当在Apollo配置中心修改配置,配置发生变化的前缀是所填值,那么就执行带有@ApolloConfigChangeListener()
注解方法。
注:如果不填写属性值,默认是只要发生变化都执行方法
举例:
@ApolloConfigChangeListener()
:默认监听的是application命名空间。
@ApolloConfigChangeListener("mysql-config")
:指定mysql-config命名空间。
@ApolloConfigChangeListener("${apollo.bootstrap.namespaces}")
:指定参数所配置的命名空间(多个用逗号分隔)。
@ApolloConfigChangeListener(value = "application.yml", interestedKeyPrefixes = "my.config.")
:当在Apollo配置中心,修改application.yml
下的,前缀为my.config
的属性时,触发刷新方法执行。
@ApolloConfigChangeListener(value = {"application.yml","MySQL"}, interestedKeyPrefixes = {"spring.","my.config"})
:当在Apollo配置中心,修改application.yml
或MySQL.properties
下的,前缀为spring.
或my.config
的属性时,触发刷新方法执行。
参考文档
-
[https://blog.csdn.net/weixin_43453386/article/details/112787765](CSDN - Apollo客户端监听配置变化、动态刷新)