
一、前言
在实际的项目开发中,配置管理是一个绕不开的话题。我们经常会遇到这样的场景:
- 运维人员修改了服务器上的配置文件,希望应用能实时感知并生效,而不是重启服务;
- 应用内部需要动态修改某些运行时参数(如限流阈值、功能开关),并且希望这些修改能持久化到配置文件中,即使重启也不会丢失。
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 等) |
| 线程安全 | 提供了线程安全的配置实现 |
Maven 依赖
xml
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-configuration2</artifactId>
<version>2.11.0</version>
</dependency>
<!-- Configuration2 依赖 commons-beanutils -->
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>
三、整体架构设计
┌─────────────────────────────────────────────────────┐
│ 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/
├── java/com/example/config/
│ ├── ConfigApplication.java
│ ├── config/
│ │ ├── DynamicConfigManager.java
│ │ ├── ConfigChangeEvent.java
│ │ └── ConfigChangeListener.java
│ ├── controller/
│ │ └── ConfigController.java
│ └── service/
│ └── BusinessService.java
└── resources/
├── application.yml
└── dynamic-config.properties
4.2 准备配置文件
src/main/resources/dynamic-config.properties:
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
这是整个方案的核心,负责配置的加载、监听、读取和写入。
java
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 javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.File;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.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<String, Object> 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<PropertiesConfiguration> builder =
new FileBasedConfigurationBuilder<>(PropertiesConfiguration.class)
.configure(builderParams);
// 设置为自动保存模式(关键:实现内 → 外的写入)
builder.setAutoSave(true);
// 3. 获取配置实例和 FileHandler
this.configuration = builder.getConfiguration();
this.fileHandler = builder.getFileHandler();
// 4. 初始化配置快照
initSnapshot();
// 5. 启动定时重加载任务(关键:实现外 → 内的感知)
startReloadingTask();
log.info("动态配置管理器初始化完成, 当前配置项数量: {}", getConfigKeys().size());
}
/**
* 初始化配置快照
*/
private void initSnapshot() {
Iterator<String> 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<String> keys = configuration.getKeys();
List<ConfigChangeEvent.ChangeItem> 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<String, Object> 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<String> 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<String> getList(String key) {
return configuration.getList(String.class, key);
}
/**
* 获取所有配置键
*/
public Set<String> getConfigKeys() {
Set<String> keys = new HashSet<>();
Iterator<String> iterator = configuration.getKeys();
while (iterator.hasNext()) {
keys.add(iterator.next());
}
return keys;
}
/**
* 获取指定前缀的所有配置
*/
public Map<String, Object> getConfigsByPrefix(String prefix) {
Map<String, Object> result = new LinkedHashMap<>();
Iterator<String> 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 设置了 autoSave=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<ConfigChangeEvent.ChangeItem> changes = Collections.singletonList(
new ConfigChangeEvent.ChangeItem(key, oldValue, value, changeType));
eventPublisher.publishEvent(new ConfigChangeEvent(this, changes));
// 更新快照
configSnapshot.put(key, value);
}
/**
* 批量设置配置值
*/
public void setProperties(Map<String, Object> 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<ConfigChangeEvent.ChangeItem> changes = Collections.singletonList(
new ConfigChangeEvent.ChangeItem(key, oldValue, null, ConfigChangeEvent.ChangeType.DELETED));
eventPublisher.publishEvent(new ConfigChangeEvent(this, changes));
configSnapshot.remove(key);
}
/**
* 手动触发持久化(autoSave=true 时通常不需要手动调用)
*/
public void save() throws ConfigurationException {
fileHandler.save();
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 配置变更事件
java
package com.example.config.config;
import org.springframework.context.ApplicationEvent;
import java.util.List;
/**
* 配置变更事件
* 当配置发生变更时(无论是外部文件修改还是应用内修改),都会发布此事件
*/
public class ConfigChangeEvent extends ApplicationEvent {
private final List<ChangeItem> changes;
public ConfigChangeEvent(Object source, List<ChangeItem> changes) {
super(source);
this.changes = changes;
}
public List<ChangeItem> 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,业务组件可以对配置变更做出响应:
java
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 查看和修改配置:
java
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 java.util.HashMap;
import java.util.Map;
import java.util.Set;
@RestController
@RequestMapping("/api/config")
public class ConfigController {
private final DynamicConfigManager configManager;
public ConfigController(DynamicConfigManager configManager) {
this.configManager = configManager;
}
/**
* 获取所有配置
*/
@GetMapping("/all")
public ResponseEntity<Map<String, Object>> getAllConfigs() {
Set<String> keys = configManager.getConfigKeys();
Map<String, Object> configs = new HashMap<>();
for (String key : keys) {
configs.put(key, configManager.getString(key));
}
return ResponseEntity.ok(configs);
}
/**
* 获取指定前缀的配置
*/
@GetMapping("/prefix/{prefix}")
public ResponseEntity<Map<String, Object>> getConfigsByPrefix(@PathVariable String prefix) {
return ResponseEntity.ok(configManager.getConfigsByPrefix(prefix));
}
/**
* 获取单个配置
*/
@GetMapping("/{key}")
public ResponseEntity<Map<String, Object>> getConfig(@PathVariable String key) {
Map<String, Object> result = new HashMap<>();
if (configManager.containsKey(key)) {
result.put("key", key);
result.put("value", configManager.getString(key));
result.put("exists", true);
} else {
result.put("key", key);
result.put("exists", false);
}
return ResponseEntity.ok(result);
}
/**
* 设置/更新配置(写入文件)
*/
@PutMapping("/{key}")
public ResponseEntity<Map<String, Object>> setConfig(
@PathVariable String key,
@RequestParam Object value) {
configManager.setProperty(key, value);
Map<String, Object> result = new HashMap<>();
result.put("key", key);
result.put("value", value);
result.put("message", "配置已更新并持久化到文件");
return ResponseEntity.ok(result);
}
/**
* 批量设置配置
*/
@PutMapping("/batch")
public ResponseEntity<Map<String, Object>> batchSetConfig(@RequestBody Map<String, Object> properties) {
configManager.setProperties(properties);
Map<String, Object> result = new HashMap<>();
result.put("updatedCount", properties.size());
result.put("message", "批量配置已更新并持久化到文件");
return ResponseEntity.ok(result);
}
/**
* 删除配置
*/
@DeleteMapping("/{key}")
public ResponseEntity<Map<String, Object>> deleteConfig(@PathVariable String key) {
configManager.clearProperty(key);
Map<String, Object> result = new HashMap<>();
result.put("key", key);
result.put("message", "配置已删除并持久化到文件");
return ResponseEntity.ok(result);
}
/**
* 手动触发重新加载
*/
@PostMapping("/reload")
public ResponseEntity<Map<String, Object>> reload() throws ConfigurationException {
configManager.reload();
Map<String, Object> result = new HashMap<>();
result.put("message", "配置已手动重新加载");
return ResponseEntity.ok(result);
}
/**
* 手动触发保存
*/
@PostMapping("/save")
public ResponseEntity<Map<String, Object>> save() throws ConfigurationException {
configManager.save();
Map<String, Object> result = new HashMap<>();
result.put("message", "配置已手动保存到文件");
return ResponseEntity.ok(result);
}
}
4.7 业务服务使用示例
java
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 配置
yaml
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 启动类
java
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 可以更加精准地实现自动重加载:
java
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 java.io.File;
import java.util.concurrent.TimeUnit;
/**
* 基于 ReloadingFileBasedConfigurationBuilder 的优化版配置管理器
*/
public class AdvancedDynamicConfigManager {
private static final Logger log = LoggerFactory.getLogger(AdvancedDynamicConfigManager.class);
private ReloadingFileBasedConfigurationBuilder<PropertiesConfiguration> 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.setAutoSave(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);
// autoSave=true 会自动持久化
} catch (ConfigurationException e) {
log.error("写入配置失败: {}={}", key, value, e);
}
}
public void destroy() {
if (reloadingTrigger != null) {
reloadingTrigger.stop();
}
}
}
六、测试验证
6.1 测试外 → 内(文件变更自动感知)
启动应用后,在终端手动修改配置文件:
bash
# 修改配置文件
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 修改配置:
bash
# 修改限流阈值
curl -X PUT "http://localhost:8080/api/config/app.rate-limit.max-requests?value=2000"
# 响应
{
"key": "app.rate-limit.max-requests",
"value": 2000,
"message": "配置已更新并持久化到文件"
}
查看配置文件,确认修改已持久化:
bash
cat src/main/resources/dynamic-config.properties
# app.rate-limit.max-requests=2000 ← 已更新
6.3 测试重启后配置保持
bash
# 修改配置
curl -X PUT "http://localhost:8080/api/config/app.cache.ttl-seconds?value=600"
# 重启应用后,配置仍然保持
curl http://localhost:8080/api/config/app.cache.ttl-seconds
# {"key":"app.cache.ttl-seconds","value":"600","exists":true}
七、线程安全与并发控制
在多线程环境下,配置的读写需要注意线程安全。以下是几个关键点:
7.1 Configuration2 的线程安全机制
java
// 方式1:使用 SynchronizedConfiguration(推荐)
// PropertiesConfiguration 本身已经继承自 AbstractConfiguration
// 其内部使用读写锁保证线程安全
// 方式2:如果需要更细粒度的控制,可以使用 Synchronized wrapper
Configuration synchronizedConfig = new SynchronizedConfiguration<>(configuration);
7.2 写入操作的原子性
java
/**
* 原子性地更新多个相关配置
*/
public void atomicUpdate(Map<String, Object> updates) {
synchronized (configuration) {
updates.forEach(configuration::setProperty);
try {
fileHandler.save();
} catch (ConfigurationException e) {
log.error("原子更新失败", e);
}
}
}
八、生产环境注意事项
8.1 配置文件路径
在生产环境中,建议使用绝对路径或外部化配置路径:
yaml
dynamic:
config:
# 生产环境使用绝对路径
file-path: /opt/app/config/dynamic-config.properties
8.2 文件锁与并发写入
当多个实例同时写入同一个配置文件时,需要引入分布式锁:
java
@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 配置变更审计
建议记录所有配置变更的审计日志:
java
@EventListener
public void auditConfigChange(ConfigChangeEvent event) {
for (ConfigChangeEvent.ChangeItem change : event.getChanges()) {
auditLogRepository.save(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 配置值校验
写入配置前应进行校验:
java
/**
* 带校验的配置写入
*/
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 的代码也能享受双向更新的能力,我们可以创建一个桥接层:
java
/**
* 桥接 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<String, Object> 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.setAutoSave(true)启用自动保存,调用configuration.setProperty()时自动将修改写入文件。 -
事件驱动 :通过 Spring 的
ApplicationEventPublisher发布配置变更事件,业务组件通过@EventListener监听并响应变更。 -
生产就绪:注意线程安全、分布式锁、配置校验和审计日志等生产环境的考量。
GitHub 示例项目地址:示例代码仅供学习参考,请根据实际项目需求调整
附录:完整 pom.xml
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
</parent>
<groupId>com.example</groupId>
<artifactId>config-demo</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
<commons-configuration2.version>2.11.0</commons-configuration2.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Apache Commons Configuration 2 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-configuration2</artifactId>
<version>${commons-configuration2.version}</version>
</dependency>
<!-- Commons BeanUtils (Configuration2 依赖) -->
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>
<!-- Lombok (可选) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
🎁 福利时间
如果你正在备战面试或者想要学习其他知识,给大家推荐一个宝藏知识库,作者整理了一些列 Java 程序员需要掌握的核心知识,有需要的自取不谢。
知识库地址:https://farerboy.com/
