
本文介绍在 Spring 应用中,如何安全、可靠地实现对 @Document 标注的配置类(如 ConfigData)的运行时动态刷新,避免单例 Bean 状态陈旧问题,并提供基于原型作用域、观察者模式与数据库监听的三种专业级解决方案。
本文介绍在 spring 应用中,如何安全、可靠地实现对 `@document` 标注的配置类(如 `configdata`)的运行时动态刷新,避免单例 bean 状态陈旧问题,并提供基于原型作用域、观察者模式与数据库监听的三种专业级解决方案。
在 Spring + Spring Data MongoDB 架构中,将 @Document 实体类(如 ConfigData)直接声明为 @Bean 是一种常见但存在根本性设计缺陷的做法:@Document 类本质是数据映射载体,而非有状态的服务组件;将其注入为单例 Bean 后,其字段值在容器启动后即固化,无法响应 MongoDB 中后续的数据变更——这正是你遇到“修改数据库后 @Autowired ConfigData 未更新”的核心原因。
✅ 推荐方案一:改用 @Scope("prototype") + 按需加载(最轻量、推荐首选)
将 ConfigData 从单例 Bean 改为原型 Bean,并通过服务层封装实时读取逻辑,彻底规避状态同步问题:
@Configuration
public class MongoConfig {
@Autowired private ConfigDataService configDataService;
@Autowired private Environment environment;
// 关键:声明为 prototype,每次获取都是新实例
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public ConfigData configData() {
String profile = environment.getActiveProfiles()[0].toUpperCase(Locale.ROOT);
return configDataService.getConfigDataByProfile(Profiles.valueOf(profile));
}
}
使用时不再直接注入 ConfigData,而是注入 ObjectProvider<configdata></configdata> 或 ApplicationContext,按需获取最新快照:
@Service
public class BusinessService {
@Autowired private ObjectProvider<configdata> configDataProvider;
@Autowired private ConfigDataService configDataService;
public void doSomething() {
// 每次调用都从数据库加载最新配置
ConfigData latest = configDataProvider.getObject();
System.out.println("Current schedule time: " + latest.getScheduledConfig().getTime());
}
}</configdata>
⚠️ 注意:此方式适用于配置变更不频繁、读取开销可接受的场景。若需高频访问,建议配合本地缓存(如
@Cacheable)+ 缓存失效策略。
Somark Document Parser下载使用 SoMark 将 PDF、图片(PNG/JPG/BMP/TIFF/WebP/HEIC)、Word、PPT 及其他文档解析为 Markdown 或 JSON,满足各类文档解析需求(如简历等)。
✅ 推荐方案二:引入观察者模式,实现事件驱动刷新
当必须保留单例 ConfigData 实例并实时响应变更时,应解耦“数据源”与“数据持有者”。推荐构建 ConfigDataRepository 作为单一可信源,并让 ConfigData 成为不可变值对象(或仅含 getter),再通过事件通知依赖方:
@Component
public class ConfigDataRepository {
@Autowired private ConfigDataService dataService;
@Autowired private Environment env;
private volatile ConfigData currentData;
@PostConstruct
public void init() {
refresh();
}
@Scheduled(fixedDelay = 5000) // 每5秒拉取一次
public void refresh() {
String profile = env.getActiveProfiles()[0].toUpperCase(Locale.ROOT);
ConfigData newData = dataService.getConfigDataByProfile(Profiles.valueOf(profile));
if (!Objects.equals(currentData, newData)) {
this.currentData = newData;
// 发布刷新事件
ApplicationEventPublisher.publishEvent(new ConfigDataRefreshedEvent(this, newData));
}
}
public ConfigData getCurrent() {
return currentData;
}
}
// 事件定义
public class ConfigDataRefreshedEvent extends ApplicationEvent {
private final ConfigData newData;
public ConfigDataRefreshedEvent(Object source, ConfigData newData) {
super(source);
this.newData = newData;
}
public ConfigData getNewData() { return newData; }
}
// 监听器(自动注入到所有关注配置的 Bean)
@Component
public class ConfigDataListener {
private ConfigData cachedConfig;
@EventListener
public void handleConfigRefresh(ConfigDataRefreshedEvent event) {
this.cachedConfig = event.getNewData();
System.out.println("ConfigData updated to: " + cachedConfig.getScheduledConfig());
}
public ConfigData getConfig() {
return cachedConfig;
}
}
✅ 推荐方案三:利用 MongoDB Change Stream(生产级高阶方案)
对于要求毫秒级响应、且 MongoDB 版本 ≥ 4.0 的环境,可启用 Change Stream 监听集合变更,替代轮询:
@Component
public class ConfigDataChangeListener {
@Autowired private MongoTemplate mongoTemplate;
@Autowired private ConfigDataRepository repository;
@PostConstruct
public void startListening() {
MongoCollection<document> collection = mongoTemplate.getCollection(CollectionNames.CONFIG_DATA);
MongoCursor<changestreamdocument>> cursor = collection.watch()
.fullDocument(FullDocument.UPDATE_LOOKUP)
.iterator();
// 在独立线程中消费流(注意资源释放与错误重连)
new Thread(() -> {
while (cursor.hasNext()) {
ChangeStreamDocument<document> change = cursor.next();
if ("update".equals(change.getOperationType().getValue())) {
repository.refresh(); // 触发重新加载
}
}
}).start();
}
}</document></changestreamdocument></document>
❌ 不推荐做法(已验证失败)
-
手动销毁/注册单例 Bean(如
DefaultSingletonBeanRegistry.destroySingleton()):破坏 Spring 容器生命周期管理,极易引发BeanCreationException或内存泄漏; -
在
@Scheduled中直接修改注入的ConfigData字段:因ConfigData是单例,多线程并发修改会导致竞态条件,且无法保证其他 Bean 能感知变更; -
将
@Document类同时标记为@Component和@Bean:语义冲突,Spring 无法正确管理其生命周期。
总结
| 方案 | 适用场景 | 实时性 | 复杂度 | 推荐指数 |
|---|---|---|---|---|
@Scope("prototype") + 按需加载 |
中低频配置读取,简单可靠 | 请求级延迟(无实时性) | ★☆☆ | ⭐⭐⭐⭐⭐ |
| 观察者模式 + 定时拉取 | 需统一配置中心、中等实时性(秒级) | ~5 秒延迟 | ★★☆ | ⭐⭐⭐⭐ |
| MongoDB Change Stream | 高实时性、高可用要求、MongoDB 4.0+ | 毫秒级 | ★★★ | ⭐⭐⭐ |
最终建议:优先采用方案一(原型 Bean),它符合 Spring 设计哲学,无状态、易测试、零副作用;若业务强依赖“单例配置引用”,则务必选用方案二(观察者),而非试图篡改单例 Bean 内部状态——那不是刷新,而是制造技术债务。











