告别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. 业务配置层:存储在业务数据库中的业务相关配置参数
统一配置实体设计
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApplicationConfig {private Long id;private String configKey;private String configValue;private String configType; // datasource, redis, kafka, business, frameworkprivate 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
字段区分不同的配置类别。
配置加载与应用的核心实现
@Slf4j
public class EarlyDatabaseConfigInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {@Overridepublic 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 URLString 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 {// 获取或创建Loggerch.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");}
}
动态配置管理服务
@Service
@Slf4j
public class DynamicConfigurationService {@Autowiredprivate ApplicationConfigRepository configRepository;@Autowiredprivate ConfigHistoryRepository historyRepository;@Autowiredprivate ConfigurableApplicationContext applicationContext;@Autowiredprivate ConfigEncryptionService encryptionService;@Autowiredprivate 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.exampleString 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
@RestController
@RequestMapping("/api/config")
@Slf4j
public class UniversalConfigController {@Autowiredprivate DynamicConfigurationService configService;@Autowiredprivate 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);}
}
数据库自身配置的特殊处理
引导配置的最小化策略
由于所有配置都要存储在数据库中,我们面临一个"鸡生蛋、蛋生鸡"的问题:如何连接到存储配置的数据库?
解决方案是保留最小化的引导配置,仅包含连接配置数据库的基本信息或者通过环境变量的方式进行配置:
# bootstrap.yml - 仅保留引导配置
spring:application:name: dynamic-config-app# 配置数据库连接 - 这是唯一的硬编码配置config-datasource:url: jdbc:mysql://config-db:3306/app_configusername: ${CONFIG_DB_USER:config_user}password: ${CONFIG_DB_PASS:config_password}driver-class-name: com.mysql.cj.jdbc.Drivermanagement:endpoints:web:exposure:include: health,info,configprops
高级特性实现
配置加密与安全
@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");}
}
配置版本控制
@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;@CreationTimestampprivate LocalDateTime changeTime;// getter、setter省略
}@Service
public class ConfigVersionService {@EventListenerpublic 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);}
}
配置监控与告警
@Component
public class ConfigurationMonitorService {@EventListener@Asyncpublic 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();}
}
初始化和数据迁移
初始配置数据
-- 创建配置表
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
# 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.Driverhikari:maximum-pool-size: 5minimum-idle: 2# 应用配置数据库化开关
app:config:database-driven: trueself-management:enabled: truecache:enabled: truettl: 300000 # 5分钟logging:level:com.example.config: DEBUG
总结
这种配置管理方式虽然增加了系统的复杂度,但在合适的场景下能够显著提升系统的运维效率和业务敏捷性。
https://github.com/yuboon/java-examples/tree/master/springboot-db-cfg