java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot配置双向更新

SpringBoot集成Configuration2实现配置的实时双向更新

作者:(farerboy)

在实际的项目开发中,配置管理是一个绕不开的话题,本文将介绍如何使用 Apache Commons Configuration 2与 Spring Boot 深度集成,实现配置的实时双向更新,感兴趣的小伙伴可以了解下

一、前言

在实际的项目开发中,配置管理是一个绕不开的话题。我们经常会遇到这样的场景:

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 依赖

<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调用)

核心思路:

  1. 使用 FileBasedConfiguration 加载配置文件
  2. 使用 ReloadingStrategy 实现文件变更的自动检测与重新加载
  3. 通过 FileHandler 实现配置值写回文件
  4. 封装为 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

# 动态配置文件 - 支持运行时双向更新
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 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 配置变更事件

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,业务组件可以对配置变更做出响应:

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 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 业务服务使用示例

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 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 测试外 → 内(文件变更自动感知)

启动应用后,在终端手动修改配置文件:

# 修改配置文件
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-requests?value=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-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 的线程安全机制

// 方式1:使用 SynchronizedConfiguration(推荐)
// PropertiesConfiguration 本身已经继承自 AbstractConfiguration
// 其内部使用读写锁保证线程安全

// 方式2:如果需要更细粒度的控制,可以使用 Synchronized wrapper
Configuration synchronizedConfig = new SynchronizedConfiguration<>(configuration);

7.2 写入操作的原子性

/**
 * 原子性地更新多个相关配置
 */
public void atomicUpdate(Map<String, Object> updates) {
    synchronized (configuration) {
        updates.forEach(configuration::setProperty);
        try {
            fileHandler.save();
        } catch (ConfigurationException e) {
            log.error("原子更新失败", e);
        }
    }
}

八、生产环境注意事项

8.1 配置文件路径

在生产环境中,建议使用绝对路径或外部化配置路径:

dynamic:
  config:
    # 生产环境使用绝对路径
    file-path: /opt/app/config/dynamic-config.properties

8.2 文件锁与并发写入

当多个实例同时写入同一个配置文件时,需要引入分布式锁:

@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.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 配置值校验

写入配置前应进行校验:

/**
 * 带校验的配置写入
 */
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<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/@RefreshScopeConfiguration2 方案
外 → 内(文件变更感知)需要 Spring Cloud Config + Bus✅ 内置支持
内 → 外(写回文件)❌ 不支持✅ 内置支持
配置持久化❌ 仅内存✅ 文件持久化
变更事件通知有限✅ 完整的事件模型
多格式支持有限✅ Properties/XML/YAML/JSON/INI
依赖复杂度需要 Spring Cloud 全家桶仅需 commons-configuration2

核心要点回顾

GitHub 示例项目地址:[示例代码仅供学习参考,请根据实际项目需求调整]

附录:完整 pom.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>

到此这篇关于SpringBoot集成Configuration2实现配置的实时双向更新的文章就介绍到这了,更多相关SpringBoot配置双向更新内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文