告别YAML,在SpringBoot中用数据库配置替代配置文件

传统的YAML配置文件虽然简洁,但在生产环境中却存在不少痛点:配置修改需要重启应用、多环境配置管理复杂、配置版本控制困难。

本文将带你探索一种新的配置方案------将所有应用配置(包括数据库配置本身)都存储到数据库中,实现完全动态的配置管理。

为什么要考虑数据库存储全部配置?

在传统的Spring Boot开发中,我们依赖application.yml来管理各种配置信息。这种静态配置文件的方式在项目初期确实带来了便利,但随着业务复杂度的增加,其局限性也逐渐暴露:

1. 变更成本高昂

每次配置修改都需要经历完整的发布流程:修改配置→重新构建→部署→重启服务。这在追求快速响应的生产环境中显得格外笨重。

想象一下,仅仅因为需要调整数据库连接池的大小,就要让整个服务经历一次完整的发布周期,这显然不够优雅。

2. 多环境管理复杂性

当项目涉及开发、测试、预生产、生产等多个环境时,配置管理变得异常复杂。

不同环境的配置差异往往只体现在少数几个参数上,但我们却需要维护多套几乎相同的配置文件。

更糟糕的是,配置的同步和一致性检查变成了手工活,容易出错且效率低下。

3. 安全性隐患

敏感信息如数据库密码、API密钥等直接明文存储在配置文件中,这在安全审计中是一个明显的风险点。

虽然可以通过环境变量等方式缓解,但这又增加了部署的复杂度。

4. 协作效率瓶颈

在团队协作中,配置文件的修改往往需要开发人员介入,运维团队无法独立完成配置调优工作。这种紧耦合关系降低了整体的运维效率。

为什么不选择配置中心?

在讨论数据库配置方案之前,我们必须先分析为什么不选择市面上现有的配置中心产品(如Nacos、Apollo、Spring Cloud Config等)。

配置中心的优势与局限

配置中心确实是一个成熟的解决方案,它们提供了动态配置、多环境管理、灰度发布等功能。但在实际使用中,配置中心也存在一些局限性:

1. 额外的基础设施复杂度

配置中心本身就是一个需要维护的基础设施。它需要独立的部署、监控、备份、升级等运维工作。对于中小团队来说,这增加了不必要的运维负担。

2. 学习成本和技术债务

每个配置中心都有自己的API、SDK、管理界面和最佳实践。团队需要投入时间学习这些工具,并且一旦选择了某个配置中心,后续的迁移成本较高。

3. 网络依赖和可用性风险

配置中心的不可用会直接影响应用的启动和运行。虽然大部分配置中心都提供了本地缓存机制,但这又带来了缓存一致性的问题。

4. 功能过载和定制困难

配置中心为了满足通用需求,往往功能庞大且复杂。

但在实际业务中,我们可能只需要其中的一部分功能,却要承担整体的复杂度。

同时,当需要特殊定制时,配置中心的扩展性往往有限。

5. 数据隔离和安全控制

配置中心通常是多应用共享的,在数据隔离和细粒度的安全控制方面可能无法满足某些企业的特殊需求。

全配置数据库化的优势

相比之下,基于数据库的配置方案有以下独特优势:

零额外基础设施:数据库是应用必备的基础设施,不需要额外引入新的组件。

完全可控:配置的存储、访问、安全策略完全由自己掌控,可以根据业务需求进行深度定制。

业务集成友好:配置与业务数据可以在同一个数据库中管理,便于实现复杂的业务逻辑。

简化架构:减少了系统的外部依赖,降低了整体架构的复杂度。

当然,这种方案也不是万能的。它更适合于对配置管理有特定需求、希望减少外部依赖且需要相对轻量解决方案的项目。

架构设计思路与核心实现

整体架构设计

基于数据库的全配置管理系统需要解决一个核心问题:冷启动问题。当所有配置都在数据库中时,应用启动时如何连接到配置数据库?

我们采用分层引导的策略:

1. 引导层配置:最小化的硬编码配置,仅包含连接配置数据库的信息

2. 核心配置层:存储在配置数据库中的应用核心配置,如数据源、缓存等

3. 业务配置层:存储在业务数据库中的业务相关配置参数

统一配置实体设计

typescript 复制代码
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApplicationConfig {
    
    private Long id;
    private String configKey;
    private String configValue;
    private String configType; // datasource, redis, kafka, business, framework
    private String environment;
    private String description;
    private Boolean encrypted = false;
    private Boolean requiredRestart = false; // 是否需要重启应用
    private Boolean active = true;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    private String createdBy;
    private String updatedBy;
}

这个通用的配置实体可以存储各种类型的配置信息,通过configType字段区分不同的配置类别。

配置加载与应用的核心实现

java 复制代码
@Slf4j
public class EarlyDatabaseConfigInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        ConfigurableEnvironment environment = applicationContext.getEnvironment();
        
        try {
            log.info("开始早期数据库配置加载...");
            
            // 从数据库加载配置
            Map<String, Object> dynamicProperties = loadDynamicProperties(environment);
            
            if (!dynamicProperties.isEmpty()) {
                // 创建一个高优先级的属性源
                MapPropertySource dynamicPropertySource = new MapPropertySource("earlyDatabaseConfiguration", dynamicProperties);
                
                // 添加到环境中,确保它具有最高优先级
                MutablePropertySources propertySources = environment.getPropertySources();
                propertySources.addFirst(dynamicPropertySource);

                /*// 遍历所有 logging.level.* 并实时生效
                LoggingSystem loggingSystem = LoggingSystem.get(LoggingSystem.class.getClassLoader());
                dynamicProperties.entrySet().stream()
                        .filter(e -> e.getKey().startsWith("logging.level."))
                        .forEach(e -> {
                            String loggerName = e.getKey().substring("logging.level.".length());
                            LogLevel level = LogLevel.valueOf(e.getValue().toString().toUpperCase());
                            loggingSystem.setLogLevel(loggerName, level);   // 毫秒级
                        });*/
                
                log.info("成功从数据库加载 {} 个早期配置项", dynamicProperties.size());
                
                // 记录重要的配置
                dynamicProperties.forEach((key, value) -> {
                    if (key.contains("port") || key.contains("server") || key.contains("datasource")) {
                        log.info("早期加载配置: {} = {}", key, isPasswordField(key) ? "******" : value);
                    }
                });
            } else {
                log.warn("数据库中没有找到早期配置数据");
            }
            
        } catch (Exception e) {
            log.error("早期数据库配置加载失败,将使用默认配置", e);
            // 不抛异常,允许应用使用默认配置启动
        }
    }
    
    private Map<String, Object> loadDynamicProperties(ConfigurableEnvironment environment) {
        Map<String, Object> properties = new HashMap<>();
        Map<String, String> loggingConfigs = new HashMap<>();
        
        try {
            // 从环境变量或默认值获取配置
            String username = environment.getProperty("spring.config-datasource.username", "root");
            String password = environment.getProperty("spring.config-datasource.password", "root");
            // 构建JDBC URL
            String jdbcUrl = environment.getProperty("spring.config-datasource.url");
            // 获取当前环境
            String activeEnvironment = System.getProperty("spring.profiles.active", 
                                      System.getenv().getOrDefault("SPRING_PROFILES_ACTIVE", "development"));
            
            // 连接数据库查询配置
            try (Connection conn = DriverManager.getConnection(jdbcUrl, username, password)) {
                log.debug("成功连接到配置数据库: {}", jdbcUrl);
                
                String sql = "SELECT config_key, config_value, config_type FROM application_config WHERE environment = ? AND active = true ORDER BY config_type, config_key";
                try (PreparedStatement stmt = conn.prepareStatement(sql)) {
                    stmt.setString(1, activeEnvironment);
                    
                    try (ResultSet rs = stmt.executeQuery()) {
                        while (rs.next()) {
                            String configKey = rs.getString("config_key");
                            String configValue = rs.getString("config_value");
                            String configType = rs.getString("config_type");
                            
                            // 构建完整的属性键
                            String propertyKey = buildPropertyKey(configKey, configType);
                            properties.put(propertyKey, configValue);
                            
                            // 收集日志配置以便后续应用
                            if (propertyKey.contains("logging.level")) {
                                // 提取日志器名称和级别
                                String loggerName = extractLoggerName(configKey);
                                if (loggerName != null && !loggerName.isEmpty()) {
                                    loggingConfigs.put(loggerName, configValue);
                                }
                            }
                            
                            log.debug("早期加载配置: {} = {} (类型: {})", propertyKey, 
                                isPasswordField(configKey) ? "******" : configValue, configType);
                        }
                    }
                }
            }
            
            // 应用日志级别配置
            if (!loggingConfigs.isEmpty()) {
                applyLoggingConfigurations(loggingConfigs);
                log.info("早期应用了 {} 个日志级别配置", loggingConfigs.size());
            }
            
        } catch (Exception e) {
            log.error("查询数据库配置时发生错误", e);
            throw new RuntimeException("数据库配置查询失败", e);
        }
        
        return properties;
    }
    
    /**
     * 提取日志器名称
     */
    private String extractLoggerName(String configKey) {
        // 处理类似 "level.com.example" 或 "level.ROOT" 的配置键
        if (configKey.startsWith("logging.level.")) {
            return configKey.substring("logging.level.".length()); // 移除 "level." 前缀
        }
        return null;
    }
    
    /**
     * 早期应用日志级别配置
     */
    private void applyLoggingConfigurations(Map<String, String> loggingConfigs) {
        try {
            // 在早期阶段直接使用LogBack API设置日志级别
            ch.qos.logback.classic.LoggerContext loggerContext = 
                (ch.qos.logback.classic.LoggerContext) org.slf4j.LoggerFactory.getILoggerFactory();
            
            for (Map.Entry<String, String> entry : loggingConfigs.entrySet()) {
                String loggerName = entry.getKey();
                String levelStr = entry.getValue();
                
                try {
                    // 获取或创建Logger
                    ch.qos.logback.classic.Logger logger = "ROOT".equals(loggerName) 
                        ? loggerContext.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME)
                        : loggerContext.getLogger(loggerName);
                    
                    // 解析和设置日志级别
                    ch.qos.logback.classic.Level level = ch.qos.logback.classic.Level.valueOf(levelStr.toUpperCase());
                    logger.setLevel(level);
                    
                    log.info("早期设置日志级别: {} = {}", loggerName, levelStr);
                    
                } catch (Exception e) {
                    log.warn("设置日志级别失败: {} = {}, 错误: {}", loggerName, levelStr, e.getMessage());
                }
            }
            
        } catch (Exception e) {
            log.error("早期应用日志级别配置失败", e);
        }
    }
    
    private String buildPropertyKey(String configKey, String configType) {
        if (configType == null) {
            return configKey;
        }
        
        // 根据配置类型构建完整的属性键
        switch (configType.toLowerCase()) {
            case "datasource":
                if (!configKey.startsWith("spring.datasource")) {
                    return "spring.datasource." + configKey;
                }
                break;
            case "redis":
                if (!configKey.startsWith("spring.redis")) {
                    return "spring.redis." + configKey;
                }
                break;
            case "server":
                if (!configKey.startsWith("server")) {
                    return "server." + configKey;
                }
                break;
            case "logging":
                if (!configKey.startsWith("logging")) {
                    return "logging." + configKey;
                }
                break;
            case "management":
                if (!configKey.startsWith("management")) {
                    return "management." + configKey;
                }
                break;
            case "framework":
            case "business":
            default:
                // 对于框架级和业务配置,直接使用原始键名
                break;
        }
        
        return configKey;
    }
    
    private boolean isPasswordField(String key) {
        String lowerKey = key.toLowerCase();
        return lowerKey.contains("password") || lowerKey.contains("passwd") || 
               lowerKey.contains("secret") || lowerKey.contains("key") ||
               lowerKey.contains("token");
    }
}

动态配置管理服务

scss 复制代码
@Service
@Slf4j
public class DynamicConfigurationService {
    
    @Autowired
    private ApplicationConfigRepository configRepository;
    
    @Autowired
    private ConfigHistoryRepository historyRepository;
    
    @Autowired
    private ConfigurableApplicationContext applicationContext;
    
    @Autowired
    private ConfigEncryptionService encryptionService;
    
    @Autowired
    private EnvironmentUtil environmentUtil;
    
    private static final String DYNAMIC_PROPERTY_SOURCE_NAME = "dynamicConfigPropertySource";
    private static final String FRAMEWORK_PROPERTY_SOURCE_NAME = "frameworkConfigPropertySource";
    
    /**
     * 动态更新数据源配置
     */
    public void updateDataSourceConfig(String environment, Map<String, String> newConfig) {
        try {
            log.info("开始更新数据源配置 - 环境: {}", environment);
            
            // 1. 验证配置有效性
            validateDataSourceConfig(newConfig);
            
            // 2. 保存到数据库
            saveConfigToDatabase("datasource", environment, newConfig);
            
            // 3. 创建新的数据源
            DataSource newDataSource = createDataSource(newConfig);
            
            // 4. 优雅替换现有数据源
            replaceDataSource(newDataSource);
            
            // 5. 发布配置变更事件
            publishConfigChangeEvent("datasource", newConfig);
            
            log.info("数据源配置更新成功");
        } catch (Exception e) {
            log.error("更新数据源配置失败", e);
            throw new RuntimeException("Failed to update datasource configuration", e);
        }
    }
    
    /**
     * 动态更新Redis配置
     */
    public void updateRedisConfig(String environment, Map<String, String> newConfig) {
        try {
            log.info("开始更新Redis配置 - 环境: {}", environment);
            
            saveConfigToDatabase("redis", environment, newConfig);
            
            // 重新创建Redis连接工厂
            LettuceConnectionFactory newFactory = createRedisConnectionFactory(newConfig);
            replaceRedisConnectionFactory(newFactory);
            
            publishConfigChangeEvent("redis", newConfig);
            
            log.info("Redis配置更新成功");
        } catch (Exception e) {
            log.error("更新Redis配置失败", e);
            throw new RuntimeException("Failed to update redis configuration", e);
        }
    }
    
    /**
     * 动态更新业务配置
     */
    public void updateBusinessConfig(String configKey, String configValue) {
        String environment = environmentUtil.getCurrentEnvironment();
        try {
            log.info("开始更新业务配置 - Key: {}, 环境: {}", configKey, environment);
            
            Optional<ApplicationConfig> existingConfig = configRepository
                .findByConfigKeyAndEnvironmentAndConfigTypeAndActiveTrue(configKey, environment, "business");
            
            ApplicationConfig config = existingConfig.orElse(new ApplicationConfig());
            String oldValue = config.getConfigValue();
            
            config.setConfigKey(configKey);
            config.setConfigValue(configValue);
            config.setConfigType("business");
            config.setEnvironment(environment);
            config.setActive(true);
            
            // 如果是敏感配置,则加密存储
            if (encryptionService.isSensitiveConfig(configKey)) {
                config.setConfigValue(encryptionService.encryptSensitiveConfig(configValue));
                config.setEncrypted(true);
            }
            
            configRepository.save(config);
            
            // 记录变更历史
            recordConfigChange(configKey, "business", oldValue, configValue, environment, "系统自动更新");
            
            // 更新Environment中的属性
            updateEnvironmentProperty(configKey, configValue);
            
            publishConfigChangeEvent("business", Map.of(configKey, configValue));
            
            log.info("业务配置更新成功");
        } catch (Exception e) {
            log.error("更新业务配置失败", e);
            throw new RuntimeException("Failed to update business configuration", e);
        }
    }
    
    /**
     * 动态更新框架配置(如日志级别等)
     */
    public void updateFrameworkConfig(String configKey, String configValue) {
        String environment = environmentUtil.getCurrentEnvironment();
        try {
            log.info("开始更新框架配置 - Key: {}, Value: {}, 环境: {}", configKey, configValue, environment);
            
            Optional<ApplicationConfig> existingConfig = configRepository
                .findByConfigKeyAndEnvironmentAndConfigTypeAndActiveTrue(configKey, environment, "framework");
            
            ApplicationConfig config = existingConfig.orElse(new ApplicationConfig());
            String oldValue = config.getConfigValue();
            
            config.setConfigKey(configKey);
            config.setConfigValue(configValue);
            config.setConfigType("framework");
            config.setEnvironment(environment);
            config.setActive(true);
            
            configRepository.save(config);
            
            // 记录变更历史
            recordConfigChange(configKey, "framework", oldValue, configValue, environment, "框架配置更新");
            
            // 更新Environment中的框架属性
            updateFrameworkEnvironmentProperty(configKey, configValue);
            
            // 如果是日志配置,特殊处理
            if (configKey.startsWith("logging.level.")) {
                updateLoggingLevel(configKey, configValue);
            }
            
            publishConfigChangeEvent("framework", Map.of(configKey, configValue));
            
            log.info("框架配置更新成功");
        } catch (Exception e) {
            log.error("更新框架配置失败", e);
            throw new RuntimeException("Failed to update framework configuration", e);
        }
    }
    
    /**
     * 更新Environment中的普通属性
     */
    private void updateEnvironmentProperty(String key, String value) {
        ConfigurableEnvironment environment = applicationContext.getEnvironment();
        MutablePropertySources propertySources = environment.getPropertySources();
        
        // 获取或创建动态属性源
        MapPropertySource dynamicPropertySource = (MapPropertySource) propertySources.get(DYNAMIC_PROPERTY_SOURCE_NAME);
        if (dynamicPropertySource == null) {
            Map<String, Object> dynamicProperties = new HashMap<>();
            dynamicPropertySource = new MapPropertySource(DYNAMIC_PROPERTY_SOURCE_NAME, dynamicProperties);
            propertySources.addFirst(dynamicPropertySource);
        }
        
        // 更新属性值
        @SuppressWarnings("unchecked")
        Map<String, Object> source = (Map<String, Object>) dynamicPropertySource.getSource();
        source.put(key, value);
        
        log.info("已更新Environment属性: {} = {}", key, value);
    }
    
    /**
     * 更新Environment中的框架属性
     */
    private void updateFrameworkEnvironmentProperty(String key, String value) {
        ConfigurableEnvironment environment = applicationContext.getEnvironment();
        MutablePropertySources propertySources = environment.getPropertySources();
        
        // 获取或创建框架属性源,优先级更高
        MapPropertySource frameworkPropertySource = (MapPropertySource) propertySources.get(FRAMEWORK_PROPERTY_SOURCE_NAME);
        if (frameworkPropertySource == null) {
            Map<String, Object> frameworkProperties = new HashMap<>();
            frameworkPropertySource = new MapPropertySource(FRAMEWORK_PROPERTY_SOURCE_NAME, frameworkProperties);
            propertySources.addFirst(frameworkPropertySource);
        }
        
        // 更新框架属性值
        @SuppressWarnings("unchecked")
        Map<String, Object> source = (Map<String, Object>) frameworkPropertySource.getSource();
        source.put(key, value);
        
        log.info("已更新Framework Environment属性: {} = {}", key, value);
    }
    
    /**
     * 动态更新日志级别
     */
    private void updateLoggingLevel(String configKey, String configValue) {
        try {
            // 提取logger名称,例如 logging.level.com.example -> com.example
            String loggerName = configKey.substring("logging.level.".length());
            
            // 获取日志系统并更新级别
            ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) 
                org.slf4j.LoggerFactory.getLogger(loggerName);
            
            ch.qos.logback.classic.Level level = ch.qos.logback.classic.Level.valueOf(configValue.toUpperCase());
            logger.setLevel(level);
            
            log.info("已更新日志级别: {} -> {}", loggerName, configValue);
        } catch (Exception e) {
            log.error("更新日志级别失败: {} = {}", configKey, configValue, e);
        }
    }
    
    /**
     * 批量更新框架配置
     */
    public void updateFrameworkConfigs(String environment, Map<String, String> configMap) {
        try {
            log.info("开始批量更新框架配置 - 环境: {}, 配置数量: {}", environment, configMap.size());
            
            for (Map.Entry<String, String> entry : configMap.entrySet()) {
                updateFrameworkConfig(entry.getKey(), entry.getValue());
            }
            
            log.info("批量更新框架配置成功");
        } catch (Exception e) {
            log.error("批量更新框架配置失败", e);
            throw new RuntimeException("Failed to batch update framework configurations", e);
        }
    }
    
    private void validateDataSourceConfig(Map<String, String> config) {
        if (!config.containsKey("url")) {
            throw new IllegalArgumentException("数据源URL不能为空");
        }
        if (!config.containsKey("username")) {
            throw new IllegalArgumentException("数据源用户名不能为空");
        }
        if (!config.containsKey("password")) {
            throw new IllegalArgumentException("数据源密码不能为空");
        }
    }
    
    private void saveConfigToDatabase(String configType, String environment, Map<String, String> configMap) {
        for (Map.Entry<String, String> entry : configMap.entrySet()) {
            String configKey = entry.getKey();
            String configValue = entry.getValue();
            
            Optional<ApplicationConfig> existingConfig = configRepository
                .findByConfigKeyAndEnvironmentAndConfigTypeAndActiveTrue(configKey, environment, configType);
            
            ApplicationConfig config = existingConfig.orElse(new ApplicationConfig());
            String oldValue = config.getConfigValue();
            
            config.setConfigKey(configKey);
            config.setConfigType(configType);
            config.setEnvironment(environment);
            config.setActive(true);
            
            // 处理加密
            if (encryptionService.isSensitiveConfig(configKey)) {
                config.setConfigValue(encryptionService.encryptSensitiveConfig(configValue));
                config.setEncrypted(true);
            } else {
                config.setConfigValue(configValue);
                config.setEncrypted(false);
            }
            
            configRepository.save(config);
            
            // 记录变更历史
            recordConfigChange(configKey, configType, oldValue, configValue, environment, "配置更新");
        }
    }
    
    private DataSource createDataSource(Map<String, String> config) {
        HikariConfig hikariConfig = new HikariConfig();
        hikariConfig.setJdbcUrl(config.get("url"));
        hikariConfig.setUsername(config.get("username"));
        hikariConfig.setPassword(config.get("password"));
        hikariConfig.setDriverClassName(config.getOrDefault("driver-class-name", "com.mysql.cj.jdbc.Driver"));
        hikariConfig.setMaximumPoolSize(Integer.parseInt(config.getOrDefault("maximum-pool-size", "20")));
        hikariConfig.setMinimumIdle(Integer.parseInt(config.getOrDefault("minimum-idle", "5")));
        hikariConfig.setConnectionTimeout(Long.parseLong(config.getOrDefault("connection-timeout", "30000")));
        hikariConfig.setIdleTimeout(Long.parseLong(config.getOrDefault("idle-timeout", "600000")));
        hikariConfig.setMaxLifetime(Long.parseLong(config.getOrDefault("max-lifetime", "1800000")));
        
        return new HikariDataSource(hikariConfig);
    }
    
    private void replaceDataSource(@Qualifier("businessDataSource") DataSource newDataSource) {
        DefaultListableBeanFactory beanFactory = 
            (DefaultListableBeanFactory) applicationContext.getBeanFactory();
        
        try {
            // 优雅关闭旧数据源
            if (beanFactory.containsBean("businessDataSource")) {
                DataSource oldDataSource = beanFactory.getBean("businessDataSource", DataSource.class);
                if (oldDataSource instanceof HikariDataSource) {
                    ((HikariDataSource) oldDataSource).close();
                }
            }
            
            // 注册新数据源
            beanFactory.destroySingleton("businessDataSource");
            beanFactory.registerSingleton("businessDataSource", newDataSource);
            
            log.info("数据源替换成功");
        } catch (Exception e) {
            log.error("替换数据源失败", e);
            throw new RuntimeException("Failed to replace datasource", e);
        }
    }
    
    private LettuceConnectionFactory createRedisConnectionFactory(Map<String, String> config) {
        // 创建Redis连接工厂的实现
        log.info("创建Redis连接工厂: {}", config);
        return null; // 简化实现
    }
    
    private void replaceRedisConnectionFactory(LettuceConnectionFactory newFactory) {
        // Redis连接工厂替换的实现
        log.info("替换Redis连接工厂");
    }
    
    private void recordConfigChange(String configKey, String configType, String oldValue, String newValue, String environment, String reason) {
        try {
            ConfigHistory history = new ConfigHistory();
            history.setConfigKey(configKey);
            history.setConfigType(configType);
            history.setOldValue(oldValue);
            history.setNewValue(newValue);
            history.setEnvironment(environment);
            history.setOperatorId("system"); // 可以从SecurityContext获取
            history.setChangeReason(reason);
            
            historyRepository.save(history);
        } catch (Exception e) {
            log.error("记录配置变更历史失败", e);
        }
    }
    
    private void publishConfigChangeEvent(String configType, Map<String, String> config) {
        // 发布配置变更事件
        log.info("发布配置变更事件 - 类型: {}, 配置: {}", configType, config.keySet());
    }
    
    /**
     * 获取配置变更历史
     */
    public List<ConfigHistory> getConfigHistory(String configKey, String environment) {
        return historyRepository.findByConfigKeyAndEnvironmentOrderByChangeTimeDesc(configKey, environment);
    }
    
    /**
     * 配置回滚功能
     */
    public void rollbackConfig(String configKey, String environment, Long historyId) {
        ConfigHistory history = historyRepository.findById(historyId)
            .orElseThrow(() -> new RuntimeException("历史记录不存在"));
        
        // 恢复到历史版本的值
        if ("business".equals(history.getConfigType())) {
            updateBusinessConfig(configKey, history.getOldValue());
        } else if ("framework".equals(history.getConfigType())) {
            updateFrameworkConfig(configKey, history.getOldValue());
        }
        
        log.info("配置已回滚 - Key: {}, 环境: {}, 回滚到版本: {}", 
                configKey, environment, historyId);
    }
}

配置管理REST API

less 复制代码
@RestController
@RequestMapping("/api/config")
@Slf4j
public class UniversalConfigController {
    
    @Autowired
    private DynamicConfigurationService configService;
    
    @Autowired
    private ApplicationConfigRepository configRepository;
    
    /**
     * 获取指定类型的配置
     */
    @GetMapping("/{configType}/{environment}")
    public ResponseEntity<Map<String, String>> getConfig(
            @PathVariable String configType,
            @PathVariable String environment) {
        
        List<ApplicationConfig> configs = configRepository
            .findByConfigTypeAndEnvironment(configType, environment);
            
        Map<String, String> configMap = configs.stream()
            .collect(Collectors.toMap(
                ApplicationConfig::getConfigKey,
                ApplicationConfig::getConfigValue
            ));
            
        return ResponseEntity.ok(configMap);
    }
    
    /**
     * 批量更新数据源配置
     */
    @PostMapping("/datasource/{environment}")
    public ResponseEntity<?> updateDataSourceConfig(
            @PathVariable String environment,
            @RequestBody Map<String, String> configMap) {
        try {
            configService.updateDataSourceConfig(environment, configMap);
            return ResponseEntity.ok("数据源配置更新成功");
        } catch (Exception e) {
            log.error("更新数据源配置失败", e);
            return ResponseEntity.status(500).body("配置更新失败: " + e.getMessage());
        }
    }
    
    /**
     * 批量更新Redis配置
     */
    @PostMapping("/redis/{environment}")
    public ResponseEntity<?> updateRedisConfig(
            @PathVariable String environment,
            @RequestBody Map<String, String> configMap) {
        try {
            configService.updateRedisConfig(environment, configMap);
            return ResponseEntity.ok("Redis配置更新成功");
        } catch (Exception e) {
            log.error("更新Redis配置失败", e);
            return ResponseEntity.status(500).body("配置更新失败: " + e.getMessage());
        }
    }
    
    /**
     * 更新单个业务配置
     */
    @PostMapping("/business")
    public ResponseEntity<?> updateBusinessConfig(@RequestBody ConfigUpdateRequest request) {
        try {
            configService.updateBusinessConfig(request.getConfigKey(), request.getConfigValue());
            return ResponseEntity.ok("业务配置更新成功");
        } catch (Exception e) {
            log.error("更新业务配置失败", e);
            return ResponseEntity.status(500).body("配置更新失败: " + e.getMessage());
        }
    }
    
    /**
     * 获取配置变更历史
     */
    @GetMapping("/history/{configKey}")
    public ResponseEntity<List<ConfigHistory>> getConfigHistory(
            @PathVariable String configKey,
            @RequestParam String environment) {
        List<ConfigHistory> history = configService.getConfigHistory(configKey, environment);
        return ResponseEntity.ok(history);
    }
}

数据库自身配置的特殊处理

引导配置的最小化策略

由于所有配置都要存储在数据库中,我们面临一个"鸡生蛋、蛋生鸡"的问题:如何连接到存储配置的数据库?

解决方案是保留最小化的引导配置,仅包含连接配置数据库的基本信息或者通过环境变量的方式进行配置:

yaml 复制代码
# bootstrap.yml - 仅保留引导配置
spring:
  application:
    name: dynamic-config-app
  
  # 配置数据库连接 - 这是唯一的硬编码配置
  config-datasource:
    url: jdbc:mysql://config-db:3306/app_config
    username: ${CONFIG_DB_USER:config_user}
    password: ${CONFIG_DB_PASS:config_password}
    driver-class-name: com.mysql.cj.jdbc.Driver
    
management:
  endpoints:
    web:
      exposure:
        include: health,info,configprops

高级特性实现

配置加密与安全

typescript 复制代码
@Component
public class ConfigEncryptionService {
    
    private final AESUtil aesUtil;
    
    public ConfigEncryptionService() {
        // 加密密钥从环境变量或密钥管理服务获取
        this.aesUtil = new AESUtil(getEncryptionKey());
    }
    
    public String encryptSensitiveConfig(String plainText) {
        return aesUtil.encrypt(plainText);
    }
    
    public String decryptSensitiveConfig(String encryptedText) {
        return aesUtil.decrypt(encryptedText);
    }
    
    /**
     * 判断配置是否为敏感信息
     */
    public boolean isSensitiveConfig(String configKey) {
        return configKey.toLowerCase().contains("password") ||
               configKey.toLowerCase().contains("secret") ||
               configKey.toLowerCase().contains("key") ||
               configKey.toLowerCase().contains("token");
    }
}

配置版本控制

less 复制代码
@Entity
@Table(name = "config_history")
public class ConfigHistory {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String configKey;
    private String configType;
    private String oldValue;
    private String newValue;
    private String environment;
    private String operatorId;
    private String changeReason;
    
    @CreationTimestamp
    private LocalDateTime changeTime;
    
    // getter、setter省略
}

@Service
public class ConfigVersionService {
    
    @EventListener
    public void recordConfigChange(ConfigurationChangeEvent event) {
        ConfigHistory history = new ConfigHistory();
        history.setConfigKey(event.getConfigKey());
        history.setConfigType(event.getConfigType());
        history.setOldValue(event.getOldValue());
        history.setNewValue(event.getNewValue());
        history.setEnvironment(event.getEnvironment());
        history.setOperatorId(getCurrentUserId());
        history.setChangeReason(event.getChangeReason());
        
        historyRepository.save(history);
    }
    
    /**
     * 配置回滚功能
     */
    public void rollbackConfig(String configKey, String environment, Long historyId) {
        ConfigHistory history = historyRepository.findById(historyId)
            .orElseThrow(() -> new RuntimeException("历史记录不存在"));
            
        // 恢复到历史版本的值
        configService.updateConfig(configKey, environment, history.getOldValue());
        
        log.info("配置已回滚 - Key: {}, 环境: {}, 回滚到版本: {}", 
                configKey, environment, historyId);
    }
}

配置监控与告警

scss 复制代码
@Component
public class ConfigurationMonitorService {
    
    @EventListener
    @Async
    public void handleConfigChange(ConfigurationChangeEvent event) {
        // 记录监控指标
        recordConfigChangeMetrics(event);
        
        // 发送变更通知
        sendChangeNotification(event);
        
        // 检查配置合规性
        checkConfigCompliance(event);
    }
    
    private void recordConfigChangeMetrics(ConfigurationChangeEvent event) {
        // 使用Micrometer记录配置变更指标
        Metrics.counter("config.changes.total", 
                       "type", event.getConfigType(),
                       "environment", event.getEnvironment())
               .increment();
    }
    
    private void sendChangeNotification(ConfigurationChangeEvent event) {
        if (isCriticalConfig(event.getConfigKey())) {
            // 发送邮件/短信/钉钉通知
            notificationService.sendCriticalConfigChangeAlert(event);
        }
    }
    
    @Scheduled(fixedRate = 300000) // 每5分钟检查一次
    public void healthCheck() {
        // 检查配置数据库连接状态
        checkConfigDatabaseHealth();
        
        // 检查配置缓存状态
        checkConfigCacheHealth();
        
        // 检查配置同步状态
        checkConfigSyncStatus();
    }
}

初始化和数据迁移

初始配置数据

sql 复制代码
-- 创建配置表
CREATE TABLE application_config (
    config_key VARCHAR(255) NOT NULL,
    config_value TEXT NOT NULL,
    config_type VARCHAR(100) NOT NULL,
    environment VARCHAR(50) NOT NULL,
    description TEXT,
    encrypted BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (config_key, environment, config_type)
);

-- 数据源配置
INSERT INTO application_config VALUES 
('url', 'jdbc:mysql://localhost:3306/business_db', 'datasource', 'production', '生产数据库URL', false, NOW(), NOW()),
('username', 'prod_user', 'datasource', 'production', '生产数据库用户', false, NOW(), NOW()),
('password', 'encrypted_password_here', 'datasource', 'production', '生产数据库密码', true, NOW(), NOW()),
('driver-class-name', 'com.mysql.cj.jdbc.Driver', 'datasource', 'production', 'JDBC驱动', false, NOW(), NOW()),
('maximum-pool-size', '20', 'datasource', 'production', '最大连接池大小', false, NOW(), NOW()),
('minimum-idle', '5', 'datasource', 'production', '最小空闲连接数', false, NOW(), NOW());

-- Redis配置
INSERT INTO application_config VALUES 
('host', 'redis-cluster.example.com', 'redis', 'production', 'Redis主机', false, NOW(), NOW()),
('port', '6379', 'redis', 'production', 'Redis端口', false, NOW(), NOW()),
('password', 'encrypted_redis_password', 'redis', 'production', 'Redis密码', true, NOW(), NOW()),
('database', '0', 'redis', 'production', 'Redis数据库', false, NOW(), NOW()),
('timeout', '2000', 'redis', 'production', '连接超时', false, NOW(), NOW());

-- Kafka配置
INSERT INTO application_config VALUES 
('bootstrap-servers', 'kafka1:9092,kafka2:9092,kafka3:9092', 'kafka', 'production', 'Kafka集群地址', false, NOW(), NOW()),
('acks', 'all', 'kafka', 'production', '确认机制', false, NOW(), NOW()),
('retries', '3', 'kafka', 'production', '重试次数', false, NOW(), NOW()),
('batch-size', '16384', 'kafka', 'production', '批量大小', false, NOW(), NOW());

-- 业务配置
INSERT INTO application_config VALUES 
('app.max-file-size', '10MB', 'business', 'production', '最大文件上传大小', false, NOW(), NOW()),
('app.session-timeout', '1800', 'business', 'production', '会话超时时间(秒)', false, NOW(), NOW()),
('app.enable-debug', 'false', 'business', 'production', '调试模式开关', false, NOW(), NOW()),
('app.api-rate-limit', '1000', 'business', 'production', 'API限流阈值', false, NOW(), NOW());

最小化的bootstrap.yml

yaml 复制代码
# bootstrap.yml - 仅保留引导配置
spring:
  application:
    name: dynamic-config-app
  
  # 配置数据库连接 - 这是唯一的硬编码配置
  config-datasource:
    url: jdbc:mysql://${CONFIG_DB_HOST:localhost}:${CONFIG_DB_PORT:3306}/${CONFIG_DB_NAME:app_config}
    username: ${CONFIG_DB_USER:config_user}
    password: ${CONFIG_DB_PASS:config_password}
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      maximum-pool-size: 5
      minimum-idle: 2
      
# 应用配置数据库化开关
app:
  config:
    database-driven: true
    self-management:
      enabled: true
    cache:
      enabled: true
      ttl: 300000 # 5分钟
      
logging:
  level:
    com.example.config: DEBUG

总结

这种配置管理方式虽然增加了系统的复杂度,但在合适的场景下能够显著提升系统的运维效率和业务敏捷性。

github.com/yuboon/java...

相关推荐
豌豆花下猫14 小时前
Python 潮流周刊#118:Python 异步为何不够流行?(摘要)
后端·python·ai
尚学教辅学习资料14 小时前
Ruoyi-vue-plus-5.x第五篇Spring框架核心技术:5.1 Spring Boot自动配置
vue.js·spring boot·spring
晚安里15 小时前
Spring 框架(IoC、AOP、Spring Boot) 的必会知识点汇总
java·spring boot·spring
秋难降15 小时前
SQL 索引突然 “罢工”?快来看看为什么
数据库·后端·sql
上官浩仁15 小时前
springboot ioc 控制反转入门与实战
java·spring boot·spring
Access开发易登软件16 小时前
Access开发导出PDF的N种姿势,你get了吗?
后端·低代码·pdf·excel·vba·access·access开发
叫我阿柒啊16 小时前
从Java全栈到前端框架:一位程序员的实战之路
java·spring boot·微服务·消息队列·vue3·前端开发·后端开发
中国胖子风清扬16 小时前
Rust 序列化技术全解析:从基础到实战
开发语言·c++·spring boot·vscode·后端·中间件·rust
bobz96517 小时前
分析 docker.service 和 docker.socket 这两个服务各自的作用
后端
野犬寒鸦17 小时前
力扣hot100:旋转图像(48)(详细图解以及核心思路剖析)
java·数据结构·后端·算法·leetcode