相信大家平时的配置都是直接写到代码里面的吧,并且根据运行环境拆分了多个配置文件。
如果我想要整个配置中心,又不想部署一个单独的配置中心服务,有什么好的办法吗?
于是,我就想到了,数据库不是现成的吗?何不好好利用呢?
实现原理
目标,可以将配置迁移到数据库中,且不影响项目的正常启动。
说起来简单,实际要求可不低。
首先,启动项目时,配置的加载太靠前了。
- 想要读取数据库配置 → 需要 DataSource 数据源
- 想要创建 DataSource → 需要完整加载所有配置
感觉陷入了鸡生蛋,蛋生鸡的循环里。
要打破整个死循环,只能手搓一个数据库连接了,先行一步读取配置。后面的事情就顺理成章了。
实现步骤
准备配置表和数据
-- PostgreSQL 适配版 系统配置表
CREATE TABLE sys_config (
id BIGSERIAL PRIMARY KEY,
config_key VARCHAR(200) NOT NULL UNIQUE,
config_value VARCHAR(1000) DEFAULT '',
status SMALLINT DEFAULT 1,
remark VARCHAR(500) DEFAULT ''
);
COMMENT ON TABLE sys_config IS '系统数据库配置表(替代yml业务配置)';
COMMENT ON COLUMN sys_config.id IS '主键';
COMMENT ON COLUMN sys_config.config_key IS '配置Key(与yml一致)';
COMMENT ON COLUMN sys_config.config_value IS '配置值';
COMMENT ON COLUMN sys_config.status IS '状态 0禁用 1启用';
COMMENT ON COLUMN sys_config.remark IS '备注';
写入配置数据,这里使用 server.port 有现成的日志方便观察。
INSERT INTO public.sys_config
(id, config_key, config_value, status, remark)
VALUES(4, 'server.port', '8998', 1, '启动端口号');
实现接口EnvironmentPostProcessor
接口注释写明了用途
Allows for customization of the application's Environment prior to the application context being refreshed.
翻译:在应用上下文刷新之前定制 Environment。
注意,此时数据库的连接配置仍然是加密的,也需要和上篇的DataSourceConfig类一样,先解密再创建数据库连接。实现类如下:
public class DbConfigEnvironmentProcessor implements EnvironmentPostProcessor {
private static final String ENC_PREFIX = "{enc}";
private static final String DB_PROPERTY_SOURCE_NAME = "db-custom-config";
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
// 从 environment 获取解密密钥(命令行参数 > 环境变量)
String encryptKey = environment.getProperty("db.encrypt.key", System.getenv("ENCRYPT_KEY"));
try {
// 1. 读取本地yml中的数据库基础配置
String url = environment.getProperty("spring.datasource.url");
String encryptedUsername = environment.getProperty("spring.datasource.username");
String encryptedPassword = environment.getProperty("spring.datasource.password");
// 默认适配PostgreSQL驱动
String driver = environment.getProperty("spring.datasource.driver-class-name", "org.postgresql.Driver");
// 验证必要配置
if (url == null || url.isEmpty()) {
throw new IllegalStateException("数据库 URL 不能为空");
}
if (encryptedUsername == null || encryptedUsername.isEmpty()) {
throw new IllegalStateException("数据库用户名不能为空");
}
if (encryptedPassword == null || encryptedPassword.isEmpty()) {
throw new IllegalStateException("数据库密码不能为空");
}
// 检查是否需要解密
String username;
String password;
if (encryptedUsername.startsWith(ENC_PREFIX)) {
// 需要解密用户名
logger.info("检测到加密的用户名,开始解密...");
validateEncryptKey(encryptKey);
String encryptedUser = encryptedUsername.substring(ENC_PREFIX.length());
username = decrypt(encryptedUser, encryptKey);
logger.info("用户名解密成功");
} else {
username = encryptedUsername;
logger.info("用户名未加密,直接使用");
}
if (encryptedPassword.startsWith(ENC_PREFIX)) {
// 需要解密密码
logger.info("检测到加密的密码,开始解密...");
validateEncryptKey(encryptKey);
String encryptedPass = encryptedPassword.substring(ENC_PREFIX.length());
password = decrypt(encryptedPass, encryptKey);
logger.info("密码解密成功");
} else {
password = encryptedPassword;
logger.info("密码未加密,直接使用");
}
// 2. 手动原生JDBC查询配置(此时无容器、无数据源Bean)
Class.forName(driver);
Map<String, Object> configMap = new HashMap<>();
try (Connection conn = DriverManager.getConnection(url, username, password);
PreparedStatement ps = conn.prepareStatement("SELECT config_key,config_value FROM sys_config WHERE status = 1");
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
String key = rs.getString("config_key");
String value = rs.getString("config_value");
configMap.put(key, value);
}
}
// 3. 封装为配置源,加入Spring环境(优先级高于本地yml)
if (!configMap.isEmpty()) {
PropertySource<?> propertySource = new MapPropertySource(DB_PROPERTY_SOURCE_NAME, configMap);
// addFirst:数据库配置优先覆盖本地配置,符合配置中心逻辑
environment.getPropertySources().addFirst(propertySource);
}
} catch (Exception e) {
throw new RuntimeException("【数据库配置中心】加载配置失败,请检查数据库配置", e);
}
}
/**
* 验证解密密钥是否存在
*/
private void validateEncryptKey(String encryptKey) {
if (encryptKey == null || encryptKey.isEmpty()) {
logger.error("❌ 未找到解密密钥!");
logger.error(" 请通过以下方式之一提供密钥:");
logger.error(" 1. 命令行参数: -Ddb.encrypt.key=YOUR_KEY");
logger.error(" 2. 环境变量: ENCRYPT_KEY=YOUR_KEY");
throw new IllegalStateException("解密密钥不能为空,请通过命令行参数或环境变量提供");
}
logger.info("已获取解密密钥(长度: {})", encryptKey.length());
}
}
代码中使用 addFirst(),优先级如下:
数据库配置 > 本地 application.yml 配置
注册扩展点
根据文档提示注册实现类
EnvironmentPostProcessor implementations have to be registered in META-INF/spring.factories, using the fully qualified name of this class as the key.
在 resources/META-INF/ 目录下创建文件:spring.factories。文件内写入当前加载器全类名。
org.springframework.boot.EnvironmentPostProcessor=com.xxx.xxx.config.DbConfigEnvironmentProcessor
验证效果
改造前
在配置文件中配置启动端口号为8999,输出日志如下:
o.s.boot.tomcat.TomcatWebServer : Tomcat started on port 8999 (http) with context path '/'
改造后
o.s.boot.tomcat.TomcatWebServer : Tomcat started on port 8998 (http) with context path '/'
既然启动端口都被覆盖了,其他配置也就能正常注入bean对象了。
对DbConfigEnvironmentProcessor类打断点,可以看到从数据库中读取到的配置:
到此已经实现了数据库作为配置中心的所有步骤了。
找找EnvironmentPostProcessor是在哪里执行的
从run方法找起
作为javaer,应该对run方法再熟悉不过了。这里只列出关键代码。
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 关键位置
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
context = createApplicationContext();
prepareEnvironment方法
listeners.environmentPrepared(bootstrapContext, environment);
发布ApplicationEnvironmentPreparedEvent事件
public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext,ConfigurableEnvironment environment) {
multicastInitialEvent(
new ApplicationEnvironmentPreparedEvent(bootstrapContext, this.application, this.args, environment));
}
处理ApplicationEnvironmentPreparedEvent事件
监听类是EnvironmentPostProcessorApplicationListener
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
ConfigurableEnvironment environment = event.getEnvironment();
SpringApplication application = event.getSpringApplication();
List<EnvironmentPostProcessor> postProcessors = getEnvironmentPostProcessors(application.getResourceLoader(),
event.getBootstrapContext());
addAotGeneratedEnvironmentPostProcessorIfNecessary(postProcessors, application);
for (EnvironmentPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessEnvironment(environment, application);
}
}
总结
虽说手动创建数据库连接,不那么优雅。
但是,既然都实现配置中心了,我觉得瑕不掩瑜了,可以接受。