配置中心里的占位符只替换了一半:本地起服务连不上库
远程配置看着全对,本地启动报 Access denied。三个占位符只覆盖了一个,剩下两个的默认值是密文;查清楚得靠 actuator 把每个属性的来源列出来。
同事在群里问:"配置中心里明明是好的,为什么我本地起服务连不上库?"
他贴了日志,第一行是 Access denied for user 'app'@'192.0.2.77',第二行是 Druid 连接池初始化失败。配置中心里那段连接串我看了,主机、库名、账号都对。唯一的区别是:那段配置在本地被替换了一半。
这种"远程配置没问题、本地就是不行"的排查,重点不在看配置写了什么,而在看应用最终拿到了什么。Spring 的属性来源有五六个,谁覆盖谁不是靠猜的。
一、先把"最终值"打出来
启动参数里加一个 actuator 端点,直接看每个属性的来源:
curl -s http://127.0.0.1:8080/actuator/env | python -m json.tool | head -60
返回里每个属性都带 propertySources,按优先级从高到低排列,第一个就是生效的那个。当时 MASTER_DB_URL 的来源是 systemEnvironment,MASTER_DB_USER 的来源却是 nacos —— 一半被本地覆盖了,一半没有。
只想知道值、不想翻 JSON 的话,用 env 的单键查询:
curl -s 'http://127.0.0.1:8080/actuator/env/MASTER_DB_USER' | python -m json.tool
这一步做完,问题基本就定位了一半:不是"连不上",是"用了半套远程配置、半套本地配置"。
二、占位符里有几个变量,就得分几次覆盖
配置中心里那段是这样的:
master:
db:
url: ${MASTER_DB_URL:jdbc:mysql://mysql.example.com:3306/appdb?useSSL=false}
username: ${MASTER_DB_USER:ENC(3f9a...)}
password: ${MASTER_DB_PASSWORD:ENC(71c2...)}
三个占位符,三个独立的键。同事只在本地设了 MASTER_DB_URL,因为他要连的是另一套库;用户名和口令他就没管,以为"默认值会兜住"。而默认值里那两个是 ENC(...) 开头的密文 —— 那是配置中心加密后的形态,本地没有解密密钥,Spring 会把它当普通字符串传给驱动。
于是 MySQL 收到的用户名是字面量 ENC(3f9a...),报 Access denied。这个报错很有欺骗性:它看起来是权限问题,实际是"你把密文当明文用了"。
判断方法很简单,把最终值打出来看前缀:
mysql -h 127.0.0.1 -P 3306 -u 'ENC(3f9a...)' -p -e 'select 1' 2>&1 | head -3
# ERROR 1045 (28000): Access denied for user 'ENC(3f9a...)'@'localhost'
能复现出同样的报错,就不用再查账号权限了。
三、环境变量到底能不能覆盖自定义前缀
能,但要知道规则。Spring 的 SystemEnvironmentPropertySource 会把属性名做一次宽松匹配:点换成下划线、忽略大小写、也接受全大写带下划线的形式。所以下面这几种写法都能命中 ${master.db.url}:
# 全大写带下划线(最常用)
export MASTER_DB_URL='jdbc:mysql://192.0.2.77:3306/appdb'
# 点号形式,某些 shell 不支持,但 JVM 参数里可以
java -jar app.jar --master.db.url='jdbc:mysql://192.0.2.77:3306/appdb'
不能命中是因为名字对不上,比如设成 DB_URL、MYSQL_URL、MASTER_DATASOURCE_URL —— 这些都不会被匹配到,占位符继续用默认值。查这个问题只需要一条:
env | grep -i -E 'master|db_|datasource'
顺带一个容易搞混的点:${MASTER_DB_URL} 和 ${master.db.url} 在环境变量这一层是等价的,因为宽松匹配发生在属性源里,不在占位符解析时。我们专门写了个最小工程验证过,结论是:名字对不上才是唯一原因。
| 设置方式 | 示例 | 能否覆盖配置中心的值 |
|---|---|---|
| 同名环境变量 | MASTER_DB_URL=... |
能,优先级最高 |
| 命令行参数 | --master.db.url=... |
能,优先级最高 |
SPRING_APPLICATION_JSON |
{"master":{"db":{"url":"..."}}} |
能,按层级展开 |
本地 application-local.yml |
同名键 | 不能,配置中心优先级更高 |
| 名字写错的环境变量 | DB_URL=... |
不能,静默失效 |
四、三套地址口径不能混用
这套系统里同一份配置要在三种地方生效,地址写法完全不同:
- 跑在集群里的服务,连的是 Service 名加端口,比如
mysql.app.svc.cluster.local:3306; - 开发机本地起服务,连的是对外暴露的 VIP 或一个固定地址;
- 前端只需要一个网关地址,其余中间件地址对它没有意义。
混用的表现是 UnknownHostException: mysql.app.svc.cluster.local,或者连接被拒绝。这个错在本地很好认,但在容器里反过来 —— 有人把地址改成了 VIP,Pod 出集群再回来,绕一圈还多了一跳 NAT。
我的做法是准备一份本地环境变量模板,只改地址,不碰账号:
# dev.env(不要提交到仓库)
export MASTER_DB_URL='jdbc:mysql://192.0.2.243:3306/appdb?useSSL=false&connectTimeout=3000'
export REDIS_HOST='192.0.2.247'
export REDIS_PORT='6379'
export NACOS_SERVER='192.0.2.246:8848'
# 账号口令仍走配置中心的密文,本地不复制
账号不落到本地文件里,这条要坚持。一旦为了"跑起来方便"把明文口令写进 dev.env 并提交,后面就很难收回去了。
五、刷新之后字段还是旧值
配置能覆盖了,又被另一个问题绊了一下:改完配置中心的值,应用日志说"配置已刷新",但用到的还是旧值。
原因是那个类里字段是 static 的:
@Component
@RefreshScope
public class DbProps {
// 刷新只重建 bean 实例,静态字段不会重新赋值
private static String url;
@Value("${master.db.url}")
public void setUrl(String url) { DbProps.url = url; }
}
@RefreshScope 的工作方式是销毁旧实例、下次访问时重建,静态字段挂在类上,重建实例不会碰它。同理,在构造函数里读到的值也只在那一次有效。
改成实例字段加 @ConfigurationProperties 就正常了:
@Component
@ConfigurationProperties(prefix = "master.db")
public class DbProps {
private String url;
private String username;
private String password;
// getter / setter 省略
}
@ConfigurationProperties 走的是绑定而不是逐个 @Value 解析,刷新时会重新绑定整个对象,静态字段和构造时机的问题都不会出现。
六、启动自检比看日志快
现在每个服务启动后都会跑一遍自检,把"能连上"变成一条命令:
set -e
curl -sf http://127.0.0.1:8080/actuator/health | grep -q '"status":"UP"'
curl -s 'http://127.0.0.1:8080/actuator/env/MASTER_DB_URL' \
| python -c 'import json,sys; d=json.load(sys.stdin); print(d["propertySources"][0]["name"])'
第一行判断服务是否健康(数据源连不上时 db 组件会是 DOWN,整体状态也跟着变),第二行打印这个属性最终来自哪个源。输出是 systemEnvironment 说明本地覆盖生效了,是 nacos 说明还在用远程值 —— 很多时候一眼就能看出"我以为我覆盖了"。
小结
- 排查配置问题先看
actuator/env,它告诉你每个属性最终来自哪个源,比翻配置文件快得多。 - 一个占位符一个键,覆盖了 URL 不等于覆盖了账号口令;默认值是密文时,报错会伪装成权限问题。
- 环境变量覆盖靠名字匹配,名字写错就静默失效,用
env | grep对一遍。 - 本地
application-local.yml压不过配置中心,别指望它;要覆盖就用环境变量或命令行参数。 @RefreshScope不会重新赋值static字段,配置类一律用实例字段加@ConfigurationProperties。- 集群内用 Service 名、开发机用固定地址,两套口径别混;账号口令不要落到本地文件。
正在加载评论…