SpringBoot集成Configuration2实现配置实时双向更新方案
时间:2026-08-20 | 作者:风起客 | 阅读:0一、前言
说起配置管理,这几乎是每个项目都躲不开的话题。在实际开发中,总会遇到这样的场景:
- 运维人员改动了服务器上的配置文件,希望应用能实时感知到变化,然后立刻生效,而不是重启服务——这年头谁还愿意没事重启呢?
- 应用内部需要动态调整某些运行时参数(比如限流阈值、功能开关),而且这些修改最好能持久化到配置文件中,哪怕重启也不会丢失。
Spring Boot 自带的 @ConfigurationProperties 和 @Value,配合 @RefreshScope 再加上 Spring Cloud Config,确实能实现配置的热更新。但它的能力是单向的——只能从外部读取配置,无法将应用内的修改写回配置文件。这就有点尴尬了。
那么,有没有一种方案,既能监听到外部的文件变更,又能把应用内部的修改自动写回文件呢?
本文要介绍的,就是使用 Apache Commons Configuration 2(以下简称 Configuration2)与 Spring Boot 深度集成,实现配置的实时双向更新:
- 外 → 内:配置文件被外部修改时,应用自动感知并更新内存中的配置值
- 内 → 外:应用在运行时修改配置值,自动持久化回配置文件
二、Apache Commons Configuration 2 简介
Apache Commons Configuration 2 是 Apache 基金会提供的一套强大的配置管理框架,是 Commons Configuration 1.x 的升级版本。它提供了以下核心能力:
| 特性 | 说明 |
|---|---|
| 多格式支持 | Properties、XML、YAML、JSON、INI、数据库等 |
| 文件变更监听 | 内置 FileHandlerListener,支持监听文件变更并自动重新加载 |
| 配置写入 | 支持将配置值写回文件 |
| 组合配置 | 支持多个配置源的合并、覆盖 |
| 类型转换 | 自动类型转换(String → int/boolean/List 等) |
| 线程安全 | 提供了线程安全的配置实现 |
Ma ven 依赖
org.apache.commons commons-configuration2 2.11.0 commons-beanutils commons-beanutils 1.9.4
三、整体架构设计
┌─────────────────────────────────────────────────────┐
│ Spring Boot App │
│ │
│ ┌──────────────┐ ┌────────────────────────┐ │
│ │ ConfigService │────│ DynamicConfigManager │ │
│ │ (业务调用) │ │ (配置管理核心组件) │ │
│ └──────────────┘ └────────┬───────────────┘ │
│ │ │
│ ┌─────────────┼─────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────┐ ┌────────┐ │
│ │ FileBased │ │ Reloading│ │ Event │ │
│ │ Config │ │ Strategy │ │ Listener│ │
│ └──────┬───────┘ └──────────┘ └────────┘ │
│ │ │
└──────────────────┼────────────────────────────────────┘
│
▼
┌────────────────┐
│ application.yml │ (外部配置文件)
│ / config.properties │
└────────────────┘
▲ ▲
│ │
外部修改 应用写入
(运维/脚本) (API调用)
核心思路其实很清晰:
- 用
FileBasedConfiguration加载配置文件,这是基础。 - 用
ReloadingStrategy实现文件变更的自动检测与重新加载,解决“外→内”的问题。 - 通过
FileHandler实现配置值写回文件,解决“内→外”的问题。 - 最后封装成 Spring Bean,对外提供统一的读写 API。
四、实战:从零搭建双向配置更新
4.1 项目结构
config-demo/
├── pom.xml
└── src/main/
├── ja va/com/example/config/
│ ├── ConfigApplication.ja va
│ ├── config/
│ │ ├── DynamicConfigManager.ja va
│ │ ├── ConfigChangeEvent.ja va
│ │ └── ConfigChangeListener.ja va
│ ├── controller/
│ │ └── ConfigController.ja va
│ └── service/
│ └── BusinessService.ja va
└── resources/
├── application.yml
└── dynamic-config.properties
4.2 准备配置文件
src/main/resources/dynamic-config.properties:
# 动态配置文件 - 支持运行时双向更新 app.feature.toggle.dark-mode=true app.feature.toggle.maintenance=false app.rate-limit.max-requests=1000 app.rate-limit.window-seconds=60 app.greeting.message=Hello, World! app.cache.ttl-seconds=300
4.3 核心组件:DynamicConfigManager
这是整个方案的心脏,负责配置的加载、监听、读取和写入。代码量虽然不小,但逻辑很清晰,注释也写得很详细,可以直接复制到项目中使用。
package com.example.config.config;
import org.apache.commons.configuration2.*;
import org.apache.commons.configuration2.builder.*;
import org.apache.commons.configuration2.builder.fluent.Parameters;
import org.apache.commons.configuration2.event.ConfigurationEvent;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.configuration2.io.FileHandler;
import org.apache.commons.configuration2.reloading.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
import ja vax.annotation.PostConstruct;
import ja vax.annotation.PreDestroy;
import ja va.io.File;
import ja va.util.*;
import ja va.util.concurrent.ConcurrentHashMap;
import ja va.util.concurrent.Executors;
import ja va.util.concurrent.ScheduledExecutorService;
import ja va.util.concurrent.TimeUnit;
@Component
public class DynamicConfigManager {
private static final Logger log = LoggerFactory.getLogger(DynamicConfigManager.class);
private final ApplicationEventPublisher eventPublisher;
@Value("${dynamic.config.file-path:src/main/resources/dynamic-config.properties}")
private String configFilePath;
@Value("${dynamic.config.reload-interval-seconds:5}")
private int reloadIntervalSeconds;
private PropertiesConfiguration configuration;
private FileHandler fileHandler;
private ScheduledExecutorService scheduler;
/**
* 本地缓存,用于存储上一次的配置快照,便于检测变更
*/
private final Map configSnapshot = new ConcurrentHashMap<>();
public DynamicConfigManager(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
@PostConstruct
public void init() throws ConfigurationException {
log.info("初始化动态配置管理器, 配置文件路径: {}", configFilePath);
// 1. 构建配置参数
Parameters params = new Parameters();
FileBasedBuilderParameters builderParams = params.fileBased()
.setFile(new File(configFilePath))
.setThrowExceptionOnMissing(false);
// 2. 创建 FileBasedConfigurationBuilder
FileBasedConfigurationBuilder builder =
new FileBasedConfigurationBuilder<>(PropertiesConfiguration.class)
.configure(builderParams);
// 设置为自动保存模式(关键:实现内 → 外的写入)
builder.setAutoSa ve(true);
// 3. 获取配置实例和 FileHandler
this.configuration = builder.getConfiguration();
this.fileHandler = builder.getFileHandler();
// 4. 初始化配置快照
initSnapshot();
// 5. 启动定时重加载任务(关键:实现外 → 内的感知)
startReloadingTask();
log.info("动态配置管理器初始化完成, 当前配置项数量: {}", getConfigKeys().size());
}
/**
* 初始化配置快照
*/
private void initSnapshot() {
Iterator keys = configuration.getKeys();
while (keys.hasNext()) {
String key = keys.next();
configSnapshot.put(key, configuration.getProperty(key));
}
}
/**
* 启动定时重加载任务
* 定期检测文件是否被外部修改,如果有变化则重新加载并触发变更事件
*/
private void startReloadingTask() {
scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "config-reload-thread");
t.setDaemon(true);
return t;
});
scheduler.scheduleWithFixedDelay(() -> {
try {
reloadIfChanged();
} catch (Exception e) {
log.error("配置重加载失败", e);
}
}, reloadIntervalSeconds, reloadIntervalSeconds, TimeUnit.SECONDS);
log.info("配置重加载任务已启动, 检测间隔: {}秒", reloadIntervalSeconds);
}
/**
* 检测文件变更并重新加载
*/
private void reloadIfChanged() throws ConfigurationException {
// 使用 ReloadingFileBasedConfigurationBuilder 的方式检测文件变更
ReloadingController reloadingController = createReloadingController();
if (reloadingController.checkForReloading(null)) {
log.info("检测到配置文件变更,正在重新加载...");
// 重新加载文件
fileHandler.load(configuration);
// 检测具体变更了哪些配置项
detectAndPublishChanges();
// 更新快照
updateSnapshot();
log.info("配置文件重新加载完成");
}
}
/**
* 创建文件变更检测控制器
*/
private ReloadingController createReloadingController() {
FileChangesMonitor monitor = new FileChangesMonitor();
ReloadingDetectorFactory factory = new ReloadingDetectorFactory() {
@Override
public ReloadingDetector createReloadingDetector(FileHandler handler) throws Exception {
return new FileHandlerReloadingDetector(handler);
}
};
ReloadingController controller = new ReloadingController(
new CombinedReloadingDetector(
new FileHandlerReloadingDetector(fileHandler)
)
);
return controller;
}
/**
* 检测变更并发布事件
*/
private void detectAndPublishChanges() {
Iterator keys = configuration.getKeys();
List changes = new ArrayList<>();
while (keys.hasNext()) {
String key = keys.next();
Object newValue = configuration.getProperty(key);
Object oldValue = configSnapshot.get(key);
if (oldValue == null) {
// 新增的配置项
changes.add(new ConfigChangeEvent.ChangeItem(key, null, newValue, ConfigChangeEvent.ChangeType.ADDED));
} else if (!Objects.equals(oldValue, newValue)) {
// 修改的配置项
changes.add(new ConfigChangeEvent.ChangeItem(key, oldValue, newValue, ConfigChangeEvent.ChangeType.MODIFIED));
}
}
// 检测被删除的配置项
for (Map.Entry entry : configSnapshot.entrySet()) {
if (!configuration.containsKey(entry.getKey())) {
changes.add(new ConfigChangeEvent.ChangeItem(
entry.getKey(), entry.getValue(), null, ConfigChangeEvent.ChangeType.DELETED));
}
}
if (!changes.isEmpty()) {
ConfigChangeEvent event = new ConfigChangeEvent(this, changes);
eventPublisher.publishEvent(event);
log.info("发布配置变更事件, 变更项数量: {}", changes.size());
}
}
/**
* 更新配置快照
*/
private void updateSnapshot() {
configSnapshot.clear();
Iterator keys = configuration.getKeys();
while (keys.hasNext()) {
String key = keys.next();
configSnapshot.put(key, configuration.getProperty(key));
}
}
// ==================== 读取 API ====================
/**
* 获取 String 类型配置
*/
public String getString(String key) {
return configuration.getString(key);
}
/**
* 获取 String 类型配置,带默认值
*/
public String getString(String key, String defaultValue) {
return configuration.getString(key, defaultValue);
}
/**
* 获取 Integer 类型配置
*/
public int getInt(String key, int defaultValue) {
return configuration.getInt(key, defaultValue);
}
/**
* 获取 Boolean 类型配置
*/
public boolean getBoolean(String key, boolean defaultValue) {
return configuration.getBoolean(key, defaultValue);
}
/**
* 获取 Long 类型配置
*/
public long getLong(String key, long defaultValue) {
return configuration.getLong(key, defaultValue);
}
/**
* 获取 Double 类型配置
*/
public double getDouble(String key, double defaultValue) {
return configuration.getDouble(key, defaultValue);
}
/**
* 获取 List 类型配置(逗号分隔的值)
*/
public List getList(String key) {
return configuration.getList(String.class, key);
}
/**
* 获取所有配置键
*/
public Set getConfigKeys() {
Set keys = new HashSet<>();
Iterator iterator = configuration.getKeys();
while (iterator.hasNext()) {
keys.add(iterator.next());
}
return keys;
}
/**
* 获取指定前缀的所有配置
*/
public Map getConfigsByPrefix(String prefix) {
Map result = new LinkedHashMap<>();
Iterator keys = configuration.getKeys(prefix);
while (keys.hasNext()) {
String key = keys.next();
result.put(key, configuration.getProperty(key));
}
return result;
}
/**
* 判断配置项是否存在
*/
public boolean containsKey(String key) {
return configuration.containsKey(key);
}
// ==================== 写入 API ====================
/**
* 设置配置值(自动持久化到文件)
* 由于 builder 设置了 autoSa ve=true,修改会自动写入文件
*/
public void setProperty(String key, Object value) {
Object oldValue = configuration.getProperty(key);
configuration.setProperty(key, value);
log.info("配置已更新并持久化: {} = {} (旧值: {})", key, value, oldValue);
// 发布变更事件
ConfigChangeEvent.ChangeType changeType = oldValue == null
ConfigChangeEvent.ChangeType.ADDED
: ConfigChangeEvent.ChangeType.MODIFIED;
List changes = Collections.singletonList(
new ConfigChangeEvent.ChangeItem(key, oldValue, value, changeType));
eventPublisher.publishEvent(new ConfigChangeEvent(this, changes));
// 更新快照
configSnapshot.put(key, value);
}
/**
* 批量设置配置值
*/
public void setProperties(Map properties) {
properties.forEach((key, value) -> {
configuration.setProperty(key, value);
configSnapshot.put(key, value);
});
log.info("批量更新配置完成, 更新项数量: {}", properties.size());
}
/**
* 删除配置项
*/
public void clearProperty(String key) {
Object oldValue = configuration.getProperty(key);
configuration.clearProperty(key);
log.info("配置已删除并持久化: {} (旧值: {})", key, oldValue);
List changes = Collections.singletonList(
new ConfigChangeEvent.ChangeItem(key, oldValue, null, ConfigChangeEvent.ChangeType.DELETED));
eventPublisher.publishEvent(new ConfigChangeEvent(this, changes));
configSnapshot.remove(key);
}
/**
* 手动触发持久化(autoSa ve=true 时通常不需要手动调用)
*/
public void sa ve() throws ConfigurationException {
fileHandler.sa ve();
log.info("配置已手动持久化到文件: {}", configFilePath);
}
/**
* 手动触发重新加载
*/
public void reload() throws ConfigurationException {
fileHandler.load(configuration);
detectAndPublishChanges();
updateSnapshot();
log.info("配置已手动重新加载");
}
@PreDestroy
public void destroy() {
if (scheduler != null && !scheduler.isShutdown()) {
scheduler.shutdown();
log.info("配置重加载任务已停止");
}
}
}
4.4 配置变更事件
package com.example.config.config;
import org.springframework.context.ApplicationEvent;
import ja va.util.List;
/**
* 配置变更事件
* 当配置发生变更时(无论是外部文件修改还是应用内修改),都会发布此事件
*/
public class ConfigChangeEvent extends ApplicationEvent {
private final List changes;
public ConfigChangeEvent(Object source, List changes) {
super(source);
this.changes = changes;
}
public List getChanges() {
return changes;
}
/**
* 变更类型枚举
*/
public enum ChangeType {
ADDED, // 新增
MODIFIED, // 修改
DELETED // 删除
}
/**
* 单个配置变更项
*/
public static class ChangeItem {
private final String key;
private final Object oldValue;
private final Object newValue;
private final ChangeType changeType;
public ChangeItem(String key, Object oldValue, Object newValue, ChangeType changeType) {
this.key = key;
this.oldValue = oldValue;
this.newValue = newValue;
this.changeType = changeType;
}
public String getKey() { return key; }
public Object getOldValue() { return oldValue; }
public Object getNewValue() { return newValue; }
public ChangeType getChangeType() { return changeType; }
@Override
public String toString() {
return String.format("[%s] %s: %s → %s", changeType, key, oldValue, newValue);
}
}
}
4.5 配置变更监听器
通过监听 ConfigChangeEvent,业务组件可以对配置变更做出响应。下面是一个示例监听器,展示了如何根据不同配置项执行不同的处理逻辑:
package com.example.config.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
/**
* 配置变更监听器示例
* 业务组件可以类似地监听配置变更事件,做出相应的处理
*/
@Component
public class ConfigChangeListener {
private static final Logger log = LoggerFactory.getLogger(ConfigChangeListener.class);
@EventListener
public void onConfigChange(ConfigChangeEvent event) {
log.info("========== 配置变更通知 ==========");
for (ConfigChangeEvent.ChangeItem change : event.getChanges()) {
log.info("变更详情: {}", change);
// 根据不同的配置项做不同的处理
handleSpecificConfig(change);
}
log.info("==================================");
}
private void handleSpecificConfig(ConfigChangeEvent.ChangeItem change) {
String key = change.getKey();
switch (key) {
case "app.feature.toggle.maintenance":
boolean maintenance = Boolean.parseBoolean(String.valueOf(change.getNewValue()));
if (maintenance) {
log.warn(" 系统进入维护模式!");
// 可以在这里执行维护模式相关逻辑
} else {
log.info(" 系统退出维护模式");
}
break;
case "app.rate-limit.max-requests":
log.info("限流阈值已更新为: {}", change.getNewValue());
// 通知限流组件更新配置
break;
case "app.cache.ttl-seconds":
log.info("缓存TTL已更新为: {}秒", change.getNewValue());
// 通知缓存组件更新TTL
break;
default:
break;
}
}
}
4.6 REST API 控制器
提供 REST 接口,方便通过 HTTP 查看和修改配置。这在运维和调试场景下非常实用:
package com.example.config.controller;
import com.example.config.config.DynamicConfigManager;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import ja va.util.HashMap;
import ja va.util.Map;
import ja va.util.Set;
@RestController
@RequestMapping("/api/config")
public class ConfigController {
private final DynamicConfigManager configManager;
public ConfigController(DynamicConfigManager configManager) {
this.configManager = configManager;
}
/**
* 获取所有配置
*/
@GetMapping("/all")
public ResponseEntity
4.7 业务服务使用示例
来看一个具体的业务服务如何使用这些动态配置。这里展示了一个基于配置进行限流和功能开关控制的例子:
package com.example.config.service;
import com.example.config.config.DynamicConfigManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
/**
* 业务服务示例 - 展示如何在业务代码中使用动态配置
*/
@Service
public class BusinessService {
private static final Logger log = LoggerFactory.getLogger(BusinessService.class);
private final DynamicConfigManager configManager;
public BusinessService(DynamicConfigManager configManager) {
this.configManager = configManager;
}
/**
* 处理请求 - 示例:根据动态配置进行限流和功能开关控制
*/
public String processRequest(String request) {
// 1. 检查维护模式开关
boolean maintenance = configManager.getBoolean(
"app.feature.toggle.maintenance", false);
if (maintenance) {
return "系统维护中,请稍后再试";
}
// 2. 获取限流配置
int maxRequests = configManager.getInt("app.rate-limit.max-requests", 1000);
int windowSeconds = configManager.getInt("app.rate-limit.window-seconds", 60);
log.info("当前限流配置: 最大请求数={}, 时间窗口={}秒", maxRequests, windowSeconds);
// 3. 获取功能开关
boolean darkMode = configManager.getBoolean(
"app.feature.toggle.dark-mode", false);
// 4. 获取缓存TTL
long cacheTtl = configManager.getLong("app.cache.ttl-seconds", 300);
// 5. 获取问候语
String greeting = configManager.getString("app.greeting.message", "Hello!");
return String.format("处理完成 | 问候: %s | 暗色模式: %s | 缓存TTL: %d秒",
greeting, darkMode, cacheTtl);
}
/**
* 动态修改限流阈值
*/
public void updateRateLimit(int maxRequests, int windowSeconds) {
configManager.setProperty("app.rate-limit.max-requests", maxRequests);
configManager.setProperty("app.rate-limit.window-seconds", windowSeconds);
log.info("限流配置已更新: maxRequests={}, windowSeconds={}", maxRequests, windowSeconds);
}
/**
* 切换维护模式
*/
public void toggleMaintenanceMode(boolean enabled) {
configManager.setProperty("app.feature.toggle.maintenance", enabled);
log.info("维护模式已{}", enabled "开启" : "关闭");
}
}
4.8 application.yml 配置
server:
port: 8080
# 动态配置管理器参数
dynamic:
config:
# 配置文件路径(支持绝对路径和相对路径)
file-path: src/main/resources/dynamic-config.properties
# 文件变更检测间隔(秒)
reload-interval-seconds: 5
spring:
application:
name: config-demo
logging:
level:
com.example.config: DEBUG
4.9 启动类
package com.example.config;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ConfigApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigApplication.class, args);
}
}
五、进阶优化:使用 ReloadingFileBasedConfigurationBuilder
上面的方案使用的是定时轮询来检测文件变更。不过,Configuration2 还提供了一个更优雅的 ReloadingFileBasedConfigurationBuilder,配合 PeriodicReloadingTrigger 可以更加精准地实现自动重加载。来看看这个进阶版本:
package com.example.config.config;
import org.apache.commons.configuration2.PropertiesConfiguration;
import org.apache.commons.configuration2.builder.ReloadingFileBasedConfigurationBuilder;
import org.apache.commons.configuration2.builder.fluent.Parameters;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.configuration2.io.FileHandler;
import org.apache.commons.configuration2.reloading.PeriodicReloadingTrigger;
import org.apache.commons.configuration2.reloading.ReloadingController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ja va.io.File;
import ja va.util.concurrent.TimeUnit;
/**
* 基于 ReloadingFileBasedConfigurationBuilder 的优化版配置管理器
*/
public class AdvancedDynamicConfigManager {
private static final Logger log = LoggerFactory.getLogger(AdvancedDynamicConfigManager.class);
private ReloadingFileBasedConfigurationBuilder builder;
private PeriodicReloadingTrigger reloadingTrigger;
private PropertiesConfiguration configuration;
public void init(String filePath, long reloadIntervalSeconds) throws ConfigurationException {
Parameters params = new Parameters();
// 创建 ReloadingFileBasedConfigurationBuilder
builder = new ReloadingFileBasedConfigurationBuilder<>(PropertiesConfiguration.class)
.configure(params.fileBased()
.setFile(new File(filePath))
.setThrowExceptionOnMissing(false));
// 设置自动保存
builder.setAutoSa ve(true);
// 创建定时重加载触发器
reloadingTrigger = new PeriodicReloadingTrigger(
builder.getReloadingController(),
null,
reloadIntervalSeconds,
TimeUnit.SECONDS
);
// 注册重加载事件监听器
ReloadingController reloadingController = builder.getReloadingController();
reloadingController.addEventListener(
org.apache.commons.configuration2.event.ConfigurationEvent.ANY,
event -> {
if (event.isBeforeOperation()) {
log.info("配置即将重新加载...");
} else {
log.info("配置重新加载完成");
// 这里可以触发变更通知
}
}
);
// 获取配置实例
configuration = builder.getConfiguration();
// 启动定时触发器
reloadingTrigger.start();
log.info("高级动态配置管理器已启动, 重加载间隔: {}秒", reloadIntervalSeconds);
}
/**
* 读取配置(每次读取时自动检测文件变更)
*/
public String getString(String key) {
// ReloadingFileBasedConfigurationBuilder 在获取配置时
// 会自动检查是否需要重新加载
try {
return builder.getConfiguration().getString(key);
} catch (ConfigurationException e) {
log.error("读取配置失败: {}", key, e);
return null;
}
}
/**
* 写入配置
*/
public void setProperty(String key, Object value) {
try {
builder.getConfiguration().setProperty(key, value);
// autoSa ve=true 会自动持久化
} catch (ConfigurationException e) {
log.error("写入配置失败: {}={}", key, value, e);
}
}
public void destroy() {
if (reloadingTrigger != null) {
reloadingTrigger.stop();
}
}
}
六、测试验证
6.1 测试外 → 内(文件变更自动感知)
启动应用后,在终端手动修改配置文件,比如改个问候语:
# 修改配置文件 echo 'app.greeting.message=你好,世界!' >> src/main/resources/dynamic-config.properties
观察应用日志,你会发现系统自动检测到了变更:
[config-reload-thread] 检测到配置文件变更,正在重新加载...
[config-reload-thread] 配置重新加载完成
[config-reload-thread] 发布配置变更事件, 变更项数量: 1
[main] ========== 配置变更通知 ==========
[main] 变更详情: [MODIFIED] app.greeting.message: Hello, World! → 你好,世界!
[main] ==================================
6.2 测试内 → 外(应用修改持久化到文件)
通过 REST API 修改配置,看看效果如何:
# 修改限流阈值
curl -X PUT "http://localhost:8080/api/config/app.rate-limit.max-requestsvalue=2000"
# 响应
{
"key": "app.rate-limit.max-requests",
"value": 2000,
"message": "配置已更新并持久化到文件"
}
然后查看配置文件,确认修改已经持久化:
cat src/main/resources/dynamic-config.properties # app.rate-limit.max-requests=2000 ← 已更新
6.3 测试重启后配置保持
这点很关键——修改过的配置,重启应用后还能保持住吗?
# 修改配置
curl -X PUT "http://localhost:8080/api/config/app.cache.ttl-secondsvalue=600"
# 重启应用后,配置仍然保持
curl http://localhost:8080/api/config/app.cache.ttl-seconds
# {"key":"app.cache.ttl-seconds","value":"600","exists":true}
七、线程安全与并发控制
多线程环境下,配置的读写需要注意线程安全。有几个关键点值得关注:
7.1 Configuration2 的线程安全机制
// 方式1:使用 SynchronizedConfiguration(推荐) // PropertiesConfiguration 本身已经继承自 AbstractConfiguration // 其内部使用读写锁保证线程安全 // 方式2:如果需要更细粒度的控制,可以使用 Synchronized wrapper Configuration synchronizedConfig = new SynchronizedConfiguration<>(configuration);
7.2 写入操作的原子性
/** * 原子性地更新多个相关配置 */ public void atomicUpdate(Mapupdates) { synchronized (configuration) { updates.forEach(configuration::setProperty); try { fileHandler.sa ve(); } catch (ConfigurationException e) { log.error("原子更新失败", e); } } }
八、生产环境注意事项
8.1 配置文件路径
生产环境中,建议使用绝对路径或外部化配置路径,避免路径问题带来的麻烦:
dynamic:
config:
# 生产环境使用绝对路径
file-path: /opt/app/config/dynamic-config.properties
8.2 文件锁与并发写入
当多个实例同时写入同一个配置文件时,就需要引入分布式锁来保证一致性。这里以一个基于 Redisson 的实现为例:
@Component
public class DistributedConfigManager extends DynamicConfigManager {
private final RedissonClient redissonClient;
public void setPropertyWithLock(String key, Object value) {
RLock lock = redissonClient.getLock("config:write:lock");
try {
if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
super.setProperty(key, value);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
8.3 配置变更审计
建议记录所有配置变更的审计日志,方便追溯问题:
@EventListener
public void auditConfigChange(ConfigChangeEvent event) {
for (ConfigChangeEvent.ChangeItem change : event.getChanges()) {
auditLogRepository.sa ve(AuditLog.builder()
.action("CONFIG_CHANGE")
.configKey(change.getKey())
.oldValue(String.valueOf(change.getOldValue()))
.newValue(String.valueOf(change.getNewValue()))
.changeType(change.getChangeType().name())
.operator(getCurrentUser())
.timestamp(LocalDateTime.now())
.build());
}
}
8.4 配置值校验
写入配置前最好进行校验,防止误操作导致系统异常:
/**
* 带校验的配置写入
*/
public void setPropertyWithValidation(String key, Object value) {
// 校验 key
if (key == null || key.trim().isEmpty()) {
throw new IllegalArgumentException("配置键不能为空");
}
// 校验特定配置的值范围
switch (key) {
case "app.rate-limit.max-requests":
int maxReq = Integer.parseInt(String.valueOf(value));
if (maxReq < 1 || maxReq > 100000) {
throw new IllegalArgumentException("限流阈值必须在 1-100000 之间");
}
break;
case "app.cache.ttl-seconds":
long ttl = Long.parseLong(String.valueOf(value));
if (ttl < 0) {
throw new IllegalArgumentException("缓存TTL不能为负数");
}
break;
}
setProperty(key, value);
}
九、与 Spring @ConfigurationProperties 的桥接
如果现有代码大量使用了 @ConfigurationProperties,又想让它们也能享受到双向更新的能力,可以创建一个桥接层:
/**
* 桥接 Configuration2 与 Spring 的 @ConfigurationProperties
* 使得 Spring 管理的配置 Bean 也能感知到动态配置的变化
*/
@Component
public class SpringConfigBridge {
private final DynamicConfigManager configManager;
private final ConfigurableApplicationContext context;
public SpringConfigBridge(DynamicConfigManager configManager,
ConfigurableApplicationContext context) {
this.configManager = configManager;
this.context = context;
}
/**
* 将动态配置同步到 Spring Environment
*/
@EventListener
public void syncToSpringEnvironment(ConfigChangeEvent event) {
ConfigurableEnvironment env = context.getEnvironment();
Map dynamicProps = new HashMap<>();
for (ConfigChangeEvent.ChangeItem change : event.getChanges()) {
if (change.getChangeType() == ConfigChangeEvent.ChangeType.DELETED) {
// Spring Environment 不支持直接删除,设为空
dynamicProps.put(change.getKey(), "");
} else {
dynamicProps.put(change.getKey(), change.getNewValue());
}
}
// 将动态配置添加到 Spring Environment 的最高优先级
MapPropertySource propertySource = new MapPropertySource(
"dynamicConfig", dynamicProps);
if (!env.getPropertySources().contains("dynamicConfig")) {
env.getPropertySources().addFirst(propertySource);
} else {
MapPropertySource existing = (MapPropertySource)
env.getPropertySources().get("dynamicConfig");
dynamicProps.forEach(existing.getSource()::put);
}
}
}
十、总结
方案对比
| 特性 | Spring @Value/@RefreshScope | Configuration2 方案 |
|---|---|---|
| 外 → 内(文件变更感知) | 需要 Spring Cloud Config + Bus | 内置支持 |
| 内 → 外(写回文件) | 不支持 | 内置支持 |
| 配置持久化 | 仅内存 | 文件持久化 |
| 变更事件通知 | 有限 | 完整的事件模型 |
| 多格式支持 | 有限 | Properties/XML/YAML/JSON/INI |
| 依赖复杂度 | 需要 Spring Cloud 全家桶 | 仅需 commons-configuration2 |
核心要点回顾
- 外 → 内:通过
ScheduledExecutorService定时检测文件变更(或使用PeriodicReloadingTrigger),检测到变化后自动重新加载配置并触发变更事件。 - 内 → 外:通过
builder.setAutoSa ve(true)启用自动保存,调用configuration.setProperty()时自动将修改写入文件。 - 事件驱动:通过 Spring 的
ApplicationEventPublisher发布配置变更事件,业务组件通过@EventListener监听并响应变更。 - 生产就绪:注意线程安全、分布式锁、配置校验和审计日志等生产环境的考量。
GitHub 示例项目地址:[示例代码仅供学习参考,请根据实际项目需求调整]
附录:完整 pom.xml
4.0.0 org.springframework.boot spring-boot-starter-parent 3.2.5 com.example config-demo 1.0.0 17 2.11.0 org.springframework.boot spring-boot-starter-web org.apache.commons commons-configuration2 ${commons-configuration2.version} commons-beanutils commons-beanutils 1.9.4 org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-ma ven-plugin
免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。
相关文章
更多-
- SpringBoot跨域配置不生效的3种解决方法详解
- 时间:2026-08-20
精选合集
更多大家都在玩
大家都在看
更多-
- 糖尿病完全不能吃糖吗
- 时间:2026-09-15
-
- 蚂蚁庄园小课堂2026年9月16日最新题目答案
- 时间:2026-09-15
-
- 小鸡答题今天的答案是什么2026年9月16日
- 时间:2026-09-15
-
- 蚂蚁庄园每日答题答案2026年9月16日
- 时间:2026-09-15
-
- 以下哪种粮食是酿造绍兴黄酒的主要原料 蚂蚁庄园今日答案9月16日
- 时间:2026-09-15
-
- 劝学名句“及时当勉励,岁月不待人”出自哪位诗人 蚂蚁庄园今日答案9.16
- 时间:2026-09-15
-
- 蚂蚁庄园今天答题答案2026年9月16日
- 时间:2026-09-15
-
- 蚂蚁庄园答题今日答案2026年9月16日
- 时间:2026-09-15
