在SpringBoot项目里用Apollo配置中心,不少开发者会遇到这样的情况:用@Value注解注入的属性,动态刷新功能妥妥的没问题,但换成@ConfigurationProperties注解的Bean时,配置变更后新值就是死活注入不进去。这篇文章就实打实跟大家分享三种解决办法,还会聊聊每种方法的坑和注意事项,代码示例都给大家摆好了,照着用就行。
先搞懂核心问题
Apollo对@Value注解的支持很直接,属性映射能实时响应配置变更。但@ConfigurationProperties是基于Bean的属性映射,默认情况下没法感知Apollo的配置变化。官方文档也明确说了,想让@ConfigurationProperties实现动态刷新,得配合EnvironmentChangeEvent或者RefreshScope来用,这也是咱们下面解决办法的核心思路。
先给大家贴一下常用的配置和实体类示例,后面三种方法都基于这个基础来搞:

程序员导航
优网导航旗下整合全网优质开发资源,一站式IT编程学习与工具大全网站
1. YML配置示例
my.config:
age: 12
name:
first-name: lao
last-name: liu
map:
eq: '09' # 这里必须加引号,不然会自动转成数字9,加引号才能保留字符串"09"
iq: 85
list:
- 吃喝睡
- 打豆豆
2. 对应的配置Bean
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.List;
import java.util.Map;
@Data // Lombok注解,省掉getter、setter这些模板代码
@Configuration
@ConfigurationProperties(prefix = "my.config") // 绑定前缀为my.config的配置
public class MyConfigProperties {
private Integer age; // 对应my.config.age
private Name name; // 嵌套对象,对应my.config.name下的子属性
private Map<String, Integer> map; // 对应my.config.map下的键值对
private List<String> list; // 对应my.config.list下的集合元素
// 嵌套静态内部类,封装name相关的子属性
@Data
public static class Name {
private String firstName; // 对应my.config.name.first-name(横线会自动转驼峰)
private String lastName;
}
}
三种动态刷新实现方案
方案一:EnvironmentChangeEvent + @ApolloConfigChangeListener
这种方式是通过Apollo的配置变更监听器,捕捉到配置变化后,发布EnvironmentChangeEvent事件,让Spring容器更新对应的Bean属性。
实现代码
import com.ctrip.framework.apollo.model.ConfigChange;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
@Slf4j
@Configuration
public class ApolloConfigRefreshListener {
// 注入Spring上下文,用于发布事件
@Autowired
private ApplicationContext applicationContext;
/**
* 监听application.yml命名空间下,前缀为my.config.的配置变更
* value:指定要监听的配置文件(命名空间)
* interestedKeyPrefixes:只关注指定前缀的配置,减少不必要的触发
*/
@ApolloConfigChangeListener(value = "application.yml", interestedKeyPrefixes = "my.config.")
public void handleConfigChange(ConfigChangeEvent changeEvent) {
// 打印变更的命名空间
log.info("配置变更 - 命名空间:{}", changeEvent.getNamespace());
// 遍历所有变更的key,打印详细变更信息
for (String key : changeEvent.changedKeys()) {
ConfigChange change = changeEvent.getChange(key);
log.info("属性变更详情 - key:{},旧值:{},新值:{},变更类型:{}",
change.getPropertyName(), change.getOldValue(), change.getNewValue(), change.getChangeType());
}
// 发布环境变更事件,Spring会自动更新@ConfigurationProperties注解的Bean属性
applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
// 这里可以加自己的业务处理逻辑,比如配置变更后需要做的额外操作
}
}
方案一的坑:不是所有场景都支持
实际用下来发现,这种方式对不同数据类型和操作的支持并不完整,整理了个表格给大家参考:
| 数据类型/操作场景 | 新增key-value | 更新value | 增改二维key-value | 删除二维key-value | 清空value | 删除key |
|---|---|---|---|---|---|---|
| Integer(比如age) | 支持 | 支持 | 无此场景 | 无此场景 | 不支持 | 不支持 |
| 嵌套对象(比如Name) | 支持 | 支持 | 支持 | 不支持 | 不支持 | 不支持 |
| Map集合(比如map) | 支持 | 支持 | 支持 | 不支持 | 不支持 | 不支持 |
| List集合(比如list) | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
简单说,除了List集合能完美支持所有操作,其他类型在“删除key”“清空value”这些场景下都会失效。如果你的业务场景里不需要这些操作,用这个方案没问题;如果需要全量支持,就得看后面的方案。
方案二:RefreshScope + @ApolloConfigChangeListener(推荐,完美解决)
这是最稳妥的方案,通过@RefreshScope注解标记配置Bean,配置变更时触发Bean刷新,重新从Apollo拉取最新配置并赋值,能解决方案一的所有坑。

AI 工具导航
优网导航旗下AI工具导航,精选全球千款优质 AI 工具集
实现步骤
- 先给配置Bean加
@RefreshScope和@Component注解:
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
@Data
@Component // 注册为Spring组件,让RefreshScope能扫描到
@RefreshScope // 关键注解,支持Bean刷新
@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;
}
}
- 编写监听类,触发刷新:
import com.ctrip.framework.apollo.model.ConfigChange;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
public class ApolloRefreshScopeListener {
// 注入RefreshScope,用于触发Bean刷新
@Autowired
private RefreshScope refreshScope;
@ApolloConfigChangeListener(value = "application.yml", interestedKeyPrefixes = "my.config.")
public void handleConfigChange(ConfigChangeEvent changeEvent) {
log.info("配置变更 - 命名空间:{}", changeEvent.getNamespace());
for (String key : changeEvent.changedKeys()) {
ConfigChange change = changeEvent.getChange(key);
log.info("属性变更详情 - key:{},旧值:{},新值:{},变更类型:{}",
change.getPropertyName(), change.getOldValue(), change.getNewValue(), change.getChangeType());
}
// 两种刷新方式,选一种就行
refreshScope.refreshAll(); // 刷新所有加了@RefreshScope的Bean
// refreshScope.refresh("myConfigProperties"); // 只刷新指定名称的Bean(Bean名称默认是类名首字母小写)
}
}
方案二的优势
不管是哪种数据类型(Integer、嵌套对象、Map、List),还是哪种操作(新增、更新、删除、清空),都能完美支持。配置变更后,Bean会被重新初始化,所有属性都会更新为最新值,不会有方案一的局限性。
方案三:自定义逻辑 + @ApolloConfigChangeListener
如果你的配置场景比较特殊,比如只需要监听部分属性,或者需要在配置变更时做复杂的业务处理,就可以用这种自定义的方式,手动处理属性的新增、修改、删除。
实现代码
import com.ctrip.framework.apollo.model.ConfigChange;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
public class ApolloCustomRefreshListener {
// 注入配置Bean,手动更新属性
@Autowired
private MyConfigProperties myConfigProperties;
@ApolloConfigChangeListener(value = "application.yml", interestedKeyPrefixes = "my.config.")
public void handleConfigChange(ConfigChangeEvent changeEvent) {
log.info("配置变更 - 命名空间:{}", changeEvent.getNamespace());
for (String key : changeEvent.changedKeys()) {
ConfigChange change = changeEvent.getChange(key);
log.info("属性变更详情 - key:{},旧值:{},新值:{},变更类型:{}",
change.getPropertyName(), change.getOldValue(), change.getNewValue(), change.getChangeType());
// 这里根据key判断要处理的属性,手动更新Bean的属性值
if (key.equals("my.config.age")) {
// 处理age属性的变更
switch (change.getChangeType()) {
case ADDED:
case MODIFIED:
myConfigProperties.setAge(Integer.parseInt(change.getNewValue()));
break;
case DELETED:
myConfigProperties.setAge(null); // 删除时设为默认值
break;
default:
break;
}
} else if (key.startsWith("my.config.name.")) {
// 处理name嵌套对象的属性变更,比如my.config.name.first-name
String subKey = key.substring("my.config.name.".length());
MyConfigProperties.Name name = myConfigProperties.getName();
if (subKey.equals("first-name")) {
name.setFirstName(change.getNewValue());
} else if (subKey.equals("last-name")) {
name.setLastName(change.getNewValue());
}
}
// 其他属性可以继续加else if判断,按需处理
}
}
}
方案三的注意事项
这种方式灵活性最高,但需要自己写大量重复代码,每个属性的变更都要手动处理。而且如果是Redis、Ribbon这类需要初始化连接的配置,即使手动更新了属性,旧的连接可能还在生效,需要手动重新建立连接才能用新配置。
关键注解@ApolloConfigChangeListener详解
这个注解是实现配置监听的核心,给大家详细说下它的参数用法,避免用错:
1. 核心参数
value:指定要监听的配置命名空间(比如配置文件名称),可以填单个值,也可以填数组(多个命名空间)。interestedKeyPrefixes:指定要监听的属性前缀,只当这些前缀的属性变更时才触发方法,减少无效触发。
2. 常用用法示例
// 1. 不填参数:默认监听application命名空间,所有属性变更都触发
@ApolloConfigChangeListener
// 2. 监听单个命名空间(比如mysql-config.properties)
@ApolloConfigChangeListener("mysql-config")
// 3. 监听多个命名空间,用数组形式
@ApolloConfigChangeListener(value = {"application.yml", "redis-config"})
// 4. 监听指定前缀的属性,多个前缀用数组
@ApolloConfigChangeListener(interestedKeyPrefixes = {"spring.datasource.", "my.config."})
// 5. 组合使用:监听application.yml和MySQL命名空间,且属性前缀是spring.或my.config.
@ApolloConfigChangeListener(
value = {"application.yml", "MySQL"},
interestedKeyPrefixes = {"spring.", "my.config."}
)
// 6. 用配置参数指定命名空间(支持多个,用逗号分隔)
@ApolloConfigChangeListener("${apollo.bootstrap.namespaces}")
避坑指南:动态刷新后配置不生效?
有时候明明看到日志打印了配置变更,但调用接口时还是用的旧配置,大概率是下面这两种情况:
1. 配置Bean被其他Bean依赖,且依赖Bean没刷新
比如有个XXXService在项目启动时注入了MyConfigProperties,并初始化了一些数据。即使MyConfigProperties的属性更新了,但XXXService里缓存的旧数据没更新,导致接口还是用旧配置。

免费在线工具导航
优网导航旗下整合全网优质免费、免注册的在线工具导航大全
解决办法:给依赖的Bean也加上@RefreshScope注解,或者在配置刷新后,手动重新初始化依赖Bean。
2. 配置涉及连接池、客户端初始化(比如Redis、Ribbon)
这类配置在项目启动时会建立连接或初始化客户端,配置变更后,旧的连接/客户端还在运行,新配置不会自动生效。
解决办法:在配置刷新后,手动关闭旧连接,重新初始化客户端。比如Redis可以重新创建RedisTemplate,Ribbon可以刷新负载均衡规则。
总结
- 简单场景,不需要删除、清空操作:用方案一(EnvironmentChangeEvent),代码简单,不用额外依赖
RefreshScope。 - 大多数业务场景,需要全量支持各种操作:用方案二(RefreshScope),完美解决所有坑,推荐优先选。
- 特殊场景,需要自定义业务逻辑:用方案三(自定义处理),灵活性高,但需要自己写更多代码。
其实核心就是利用Apollo的@ApolloConfigChangeListener捕捉配置变更,再通过合适的方式更新@ConfigurationPropertiesBean的属性。根据自己的业务场景选对方案,就能轻松实现配置动态刷新,不用重启项目啦。



