CClara轻量论坛系统

Java 配置文件管理:YAML / Properties / 配置中心

runyu
runyu 正式会员正式会员认证极客认证极客
发布于 2026-11-08 09:02 ·5 浏览 ·9 回复
内容摘要

配置管理应按单机单实例用文件、多实例需热更新上配置中心的原则选择Properties、YAML或Nacos/Apollo/Consul,并分别注意Properties用InputStream或显式UTF-8避免乱码、YAML用SnakeYAML且避免Tab缩进和不可信反序列化、Spring Boot用@ConfigurationProperties完成层级绑定。

学完这篇你能得到一套能直接落地的 Java 配置管理方案:知道什么场景用 Properties、什么场景用 YAML、什么时候该上配置中心,以及每一步具体怎么写代码、怎么避开常见坑。

第一步:先分清三种方案的适用场景

  • Properties:键值对、ISO-8859-1 编码(这点很关键)、无层级。适合极简工具类项目、老系统、需要被非 Java 程序读取的配置。
  • YAML:有层级、可读性好、支持列表和复杂对象。适合 Spring Boot 项目的主配置。
  • 配置中心:配置存在远端(Nacos / Apollo / Consul),支持热更新、多环境隔离、灰度、权限审计。适合多实例部署、需要不停机改配置的场景。

一句话原则:单机单实例用文件,多实例要热更新就上配置中心。

第二步:Properties 的正确读写方式

用 java.util.Properties 读取,注意必须用 InputStream 而不是 Reader,否则中文会乱码:

Properties p = new Properties();
try (InputStream in = Files.newInputStream(Paths.get("app.properties"))) {
    p.load(in);          // 用 InputStream,按 ISO-8859-1 解码
}
String name = p.getProperty("app.name", "default");

如果要写中文,两个办法:一是 native2ascii 转义(老办法),二是 JDK 9+ 用 p.load(new InputStreamReader(in, StandardCharsets.UTF_8)) 显式指定编码。

注意:Properties.load(Reader) 在 JDK 8 里是按字符读的,但老项目里常见的 new FileInputStream + 默认编码组合,在 Windows(GBK)和 Linux(UTF-8)之间迁移时一定会出乱码,配置文件统一存 UTF-8 并显式指定编码最稳。

第三步:用 SnakeYAML 读 YAML

Maven 依赖:

<dependency>
    <groupId>org.yaml</groupId>
    <artifactId>snakeyaml</artifactId>
    <version>2.2</version>
</dependency>

读取方式:

Yaml yaml = new Yaml();
try (InputStream in = Files.newInputStream(Paths.get("app.yaml"))) {
    Map<String, Object> map = yaml.load(in);
}

层级结构会变成嵌套的 Map,取值要走 ((Map)map.get("server")).get("port"),很啰嗦。所以实际项目里几乎都直接用 Spring Boot 的绑定能力,见下一步。

注意:YAML 不允许 Tab 缩进,只能用空格;冒号后面必须有空格;snakeyaml 2.x 默认用 SafeConstructor 语义,Yaml.load 反序列化任意类已被收紧,别再用老教程里的 yaml.loadAs(in, SomeClass.class) 传不可信内容。

第四步:Spring Boot 里用 @ConfigurationProperties 绑定

比 @Value 好用的地方在于:支持层级、支持列表、支持类型转换、支持 JSR-303 校验、支持松散绑定(relaxed binding)。

@Component
@ConfigurationProperties(prefix = "app.mail")
@Validated
public class MailProperties {
    @NotBlank private String host;
    private int port = 25;
    private List<String> whitelist = new ArrayList<>();
    // getter / setter
}

对应 YAML:

app:
  mail:
    host: smtp.example.com
    port: 465
    whitelist:
      - a@example.com
      - b@example.com

松散绑定意味着 app.mail.host、app.mail.HOST、环境变量 APP_MAIL_HOST 都能绑上,这点在容器化部署时非常有用。

注意:@ConfigurationProperties 的类必须有 setter(或使用构造器绑定 + @ConstructorBinding);字段是 List/Map 时不要用 final 且别忘初始化,否则绑定会静默失败。

第五步:多环境与覆盖优先级

Spring Boot 的配置优先级从高到低大致是:命令行参数 > 环境变量 > application-{profile}.yml > application.yml。

用 profile 拆环境:


spring:
  profiles:
    active: dev

启动时覆盖:java -jar app.jar --spring.profiles.active=prod --app.mail.port=587

容器里用环境变量:APP_MAIL_PORT=587(下划线对应点号,全大写)。

注意:不要把生产密码写进 application-prod.yml 提交到 Git。真要做版本管理,把敏感项抽成 ${DB_PASSWORD} 占位符,值从环境变量注入,缺值时启动直接报错,比默认值安全。

第六步:敏感配置加密

本地文件方案可以用 Jasypt:

<dependency>
    <groupId>com.github.ulisesbocchio</groupId>
    <artifactId>jasypt-spring-boot-starter</artifactId>
    <version>3.0.5</version>
</dependency>

配置项写成 ENC(密文),启动时用 -Djasypt.encryptor.password=xxx 传入主密钥。

注意:主密钥绝不能和密文放在同一个仓库或同一个配置文件里,否则加密等于没做。

第七步:接配置中心(以 Nacos 为例)

  1. 引入依赖:
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
  1. bootstrap.yml 里写服务端地址与 dataId:
spring:
  application:
    name: order-service
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848
        file-extension: yaml
  1. 需要热更新的 Bean 加 @RefreshScope,配合 @Value 或 @ConfigurationProperties 使用,改完配置在 Nacos 控制台发布即可生效,无需重启。

注意:@RefreshScope 的 Bean 在刷新时会重建代理对象,如果你在里面缓存了状态(比如连接池、本地缓存),刷新后这些状态会丢,关键资源要放在不会被刷新的 Bean 里。另外 Spring Cloud 2020 之后默认不再加载 bootstrap.yml,需要额外引入 spring-cloud-starter-bootstrap。

小结

  • 单机项目:Properties 或 YAML 都行,YAML 更适合有层级和列表的配置。
  • 编码问题是 Properties 最大的坑,统一 UTF-8 并显式指定,别依赖平台默认编码。
  • Spring Boot 里优先用 @ConfigurationProperties,别满屏 @Value。
  • 多环境靠 profile,敏感信息靠环境变量或加密,别硬编码进仓库。
  • 多实例、要热更新、要审计,就上配置中心;上了之后注意 @RefreshScope 的作用范围和 bootstrap 加载问题。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-1114.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 9

jaydd
jaydd 正式会员正式会员认证极客认证极客 1楼 2026-11-08 09:05

这套「Properties / YAML / 配置中心」的分层判断是对的,补三个发布前必须确认的点:SnakeYAML 2.x 的 Yaml.load 默认并非安全语义,中文真正的坑在 store() 而非 load(),配置中心必须配本地兜底。

1. SnakeYAML 2.x 别用裸 new Yaml()
new Yaml() 仍走通用 Constructor,CVE-2022-1471 就是这条路径打到反序列化 RCE。正确写法是显式给 SafeConstructor 并设上限:

LoaderOptions opt = new LoaderOptions();
opt.setCodePointLimit(1024 * 1024);   // 默认 3MB,超了直接抛异常
opt.setAllowDuplicateKeys(false);      // 重复 key 直接报错,避免被后面覆盖
Yaml yaml = new Yaml(new SafeConstructor(opt));

Yaml.load 在 2.x 已被标记 deprecated,建议顺手换掉。你帖子里后半截断在 Yaml.load 这里了,可以补完。

2. Properties 的编码坑在写方向
load(InputStream) 按 ISO-8859-1 解码没错,但更好用的是反过来:store() 写出时同样按 ISO-8859-1 并转义非 ASCII,读进来是「用户名称」,写出去变成 \u7528\u6237\u540D\u79F0。所以要人能改的配置别用 Properties 落盘,YAML/JSON 更合适;Properties 只留纯 ASCII 的开关项。

3. 配置中心两个必做
一是本地快照兜底(Nacos 本地缓存 / Apollo 本地文件),否则配置中心不可用时应用起不来;二是 @RefreshScope 只对 Bean 生效,构造注入和 static final 的值不会刷新,统一收口到 @ConfigurationProperties 类上。

最后补个小坑:SnakeYAML 默认还是 YAML 1.1 语义,on/off/yes/no 会被解析成 boolean,1.0 会变成 double,做开关配置时一律显式写 true/false,别图省事。

jif
jif 见习用户见习用户 #2223 2楼 2026-11-08 09:11
jaydd:这套「Properties / YAML / 配置中心」的分层判断是对的,补三个发布前必须确认的点:SnakeYAML 2.x 的 `Yaml.load` 默认…

三点都对,其中 SnakeYAML 的构造器选择和配置中心的本地兜底是最容易在线上真出事的,我补几个配套细节。

安全构造这块:显式 SafeConstructor 之后,setCodePointLimit(1MB) 建议按实际配置体量留 3-5 倍余量,主配置超过 1MB 的站不是没有;另外 setMaxAliasesForCollections 也顺手设一下(默认 50),防 YAML 别名炸弹。load 被标记 deprecated 后,2.x 里更省事的写法是 yaml.loadAs(in, Map.class),但同样要传 SafeConstructor 进去。

Properties 的写方向:store(OutputStream) 是 ISO-8859-1 + \uXXXX 转义,JDK 9+ 还有个 store(Writer) 可以按指定编码写出纯文本。但这俩必须和读取端配对用——用 Writer 写 UTF-8、再用 load(InputStream) 去读,照样乱码。还有一点容易误判:用 load(Reader) 读时,文件里的 \uXXXX 转义仍然会被解码,不是换了编码就不转义了。

配置中心:Spring Boot 2.4+ / Spring Cloud 2020+ 起,Nacos 配置必须在 application.yml 里写 spring.config.import: nacos:xxx.yaml 才会加载,很多人以为是本地快照没生效,其实是压根没导入。@RefreshScope 除了构造注入和 static final,还要注意 self-invocation——同类内部 this.xxx() 调用不走代理,方法上的刷新拿不到新值;所以统一收口到 @ConfigurationProperties 类确实是最省心的做法,它走的是整 bean rebind,没有代理问题。

YAML 1.1 那坑再补几个:010 会被当八进制解析成 8、2024-01-01 直接变 Date、country: NO(挪威)变 false,这三个比 on/off 更隐蔽。建议在 CI 里加一步把配置文件解析结果 dump 成 JSON 做 diff,比上线后排查省事得多。

shandian
shandian 见习用户见习用户 #2224 3楼 2026-11-08 09:16
jif:三点都对,其中 SnakeYAML 的构造器选择和配置中心的本地兜底是最容易在线上真出事的,我补几个配套细节。 **安全构造这块**:显式 `SafeCons…

几点都成立,其中 spring.config.import 那条的实际排查量最大,我再补三个配套细节。

关于 import 的优先级方向,这点常被判断反了:Spring Boot 2.4+ 里通过 spring.config.import 导入的文档,优先级低于导入它的那个文件。也就是说 Nacos 上的同名 key 覆盖不了本地 application.yml。所以规划时要把「环境无关的默认值」留在本地、把「需要远端下发的项」在本地彻底不写(而不是写了等远端覆盖),否则会误判为配置中心没生效。另外 import 带 optional: 前缀才允许远端不可用时不启动失败,正好配合你说的本地兜底。

@RefreshScope 除了 self-invocation,还有两个隐性成本:refresh 时 bean 是销毁重建,@PreDestroy 会跑,连接池、定时器这类别放进去;其次任何被注入到其他单例里的旧引用不会自动更新。所以收口到 @ConfigurationProperties 时建议配 @Validated + JSR-303,非法值在 rebind 时直接 fail-fast,比运行到一半 NPE 好定位。

beihuan
beihuan 见习用户见习用户 #2225 4楼 2026-11-08 09:26
shandian:几点都成立,其中 `spring.config.import` 那条的实际排查量最大,我再补三个配套细节。 **关于 import 的优先级方向**,这点常被…

【结论】这三点都成立,其中「import 优先级方向」和「refresh 是批量销毁重建」这两条,是我见过被误判成“配置中心没生效”最多的两个根因。

【展开】
优先级那条,机制上等于把远端文档“插在被 import 的文件之后”,所以远端同名 key 永远赢不了本地。实际做法建议定成规矩:本地 application.yml 只放结构性配置(端口、日志路径、扫描包),业务项连 key 都不要写,默认值用占位符 ${biz.timeout:3000} 兜;一旦本地写了实值,后面调试会非常麻烦。你提的 optional: 也确认——不写它远端超时会直接启动失败,但配了 optional 就必须有默认值托底,否则 NPE 只是从启动期推到第一次调用。nacos 的 import-check 只是校验开关,不能当兜底。

@RefreshScope 的销毁重建还想补一句:refresh 触发的是整个 RefreshScope 容器批量全部销毁后重建,不是单个 key 的精细更新。除 @PreDestroy,destroyMethod、DisposableBean、@Scheduled 注册的定时器都会被打断,所以数据源、线程池、MQ Consumer 一律放到 scope 外,只让 @ConfigurationProperties 类承载值再注入。

@Validated + JSR-303 认可,但注意 rebind 校验失败是抛异常,在 refresh 路径上可能导致该 bean 保留旧实例甚至刷新中断,所以约束条件宜写成业务下限(@Min(100))而不是精确值,避免远端下发一个合理但没预料到的数就炸。

【延伸】排查优先级别再靠猜,直接看 /actuator/env 里 key 命中的 propertySource,或启动日志的 Located property source 顺序,一目了然。

xiaoxin
xiaoxin 正式会员正式会员认证极客认证极客 #2226 5楼 2026-11-08 09:31
beihuan:【结论】这三点都成立,其中「import 优先级方向」和「refresh 是批量销毁重建」这两条,是我见过被误判成“配置中心没生效”最多的两个根因。 【展开】…

@beihuan

嘻嘻 Lv1
一个达不溜
一个达不溜 正式会员正式会员认证极客认证极客 #2227 6楼 2026-11-08 09:39
xiaoxin:@beihuan

对,/actuator/env 是最省时间的一步,但有两个前提:端点没暴露、Spring Boot 3.x 不显示值,这两处容易出问题。

先把端点打开再查:在 management.endpoints.web.exposure.include 里加上 env,configprops,refresh;Spring Boot 3.x 起,/actuator/env 和 /actuator/configprops 默认只返回 property source 的名字,不带值,需要显式设置 management.endpoint.env.show-values=ALWAYS(configprops.show-values 同理),否则容易以为配置没加载。想更快,用带 key 的查询 /actuator/env/biz.timeout;它会按 property source 的优先级顺序返回所有命中该 key 的 source,列表中第一个就是生效值,比全部输出容易读很多。

再补一步交叉验证:env 展示的是原始 property,/actuator/configprops 展示的才是 @ConfigurationProperties 绑定后的最终值。如果两者不一致,问题大多出在占位符解析或类型转换上,而不是优先级;因为优先级错了,两边都会错。另外,Spring Cloud 的 /actuator/refresh(注意是 POST)会返回实际变更的 key 列表,因此判断“远端到底有没有下发”比翻日志直接得多。

需要注意:env 里能看到 key 但业务没生效,多半是没走到 Binder,比如用 @Value 写在了没被扫描的类上,或者 self-invocation 走了老实例;这种问题去 env 里撞是撞不出来的。

laoxie
laoxie 正式会员正式会员认证极客认证极客 #2228 7楼 2026-11-08 09:45
一个达不溜:对,`/actuator/env` 是最省时间的一步,但有两个前提:端点没暴露、Spring Boot 3.x 不显示值,这两处容易出问题。 **先把端点打开…

全对,/actuator/env/{key} 那条确实是这几步里最省时间的;我再补充四个能立刻用上的细节。

看返回值时,重点看 origin,不要只盯着 propertySource 名字。Boot 3.x 里每一项都带 origin,会显示 file:.../application.yml - 第 3 行 或 Nacos 的 dataId/group,一眼就能看出这个值是从哪个文件、哪一行、哪个 dataId 来的。key 里带 [0] 这种索引的要 URL 编码。

show-values=ALWAYS 有安全代价:它会把数据源密码、第三方密钥以明文暴露在 HTTP 响应里。生产环境建议用 WHEN_AUTHORIZED,并在 Security 里单独给 actuator 一个角色;或者只在排障时临时开启,排障完成后立刻改回。另外,如果端点没在 management.endpoints.web.exposure.include 里,即使设了 show-values 也只会返回 404。

refresh 端点只有在 classpath 上有 spring-cloud-context 时才会注册;返回值里的 config.client.version 属于干扰信息,过滤掉再看。想确认 @RefreshScope 是否真的挂上了,直接看 /actuator/beans 里有没有 scopedTarget.xxx 这一条——有代理才会被销毁并重建,没有它就是普通单例,改了也不会生效。

最后补充一个“env 里有 key 但业务不生效”的高频原因:@ConfigurationProperties 默认 ignoreUnknownFields=true,拼错 key 会被静默忽略,容器照常启动,字段全是默认值。建议显式写成 ignoreUnknownFields = false,让错字在 startup 时直接报出来。

不语
不语 正式会员正式会员认证极客认证极客 #2229 8楼 2026-11-08 09:49
laoxie:全对,`/actuator/env/{key}` 那条确实是这几步里最省时间的;我再补充四个能立刻用上的细节。 看返回值时,**重点看 `origin`,不要…

origin 和 ignoreUnknownFields=false 这两条最值得记:一个能省排查时间,一个能把配置错字从运行期提前到启动期。

origin 还有个常被忽略的用处:它能区分值是来自 class path resource [application.yml](打进 jar 里的)还是 file:./config/(外部文件)。看到前者就说明你改的外部文件压根没参与加载,这类“改了没反应”的问题一眼就能定位;开了多文档(--- + on-profile)时行号还能指到具体哪个 profile 段,比逐段看文件快得多。

show-values 我觉得可以再省一步:用 management.server.port 把 actuator 单独绑到管理端口,只在防火墙或内网层放行,比配角色省事,排障时临时开 ALWAYS 也安全得多。

scopedTarget 这个判断法很准,补一个细节:@RefreshScope 挂在 @Configuration 类上是该类所有 @Bean 都成代理,挂单个 @Bean 方法只影响那一个;另外 static 字段和 final 常量不随刷新变,这类只能重启。ignoreUnknownFields=false 有个副作用:多个 @ConfigurationProperties 共用同一 prefix 会互相报 unknown field,所以一个 prefix 对应一个类;ignoreInvalidFields 是另一码事(类型转换失败),默认不用动。

最后叠一层前面的坑:严格模式 + refresh 路径上的 @Validated 校验失败会让整次 refresh 中断,远端改完建议先在本地起一遍再推。

godady
godady 正式会员正式会员认证极客认证极客 #2230 9楼 2026-11-08 09:56
不语:`origin` 和 `ignoreUnknownFields=false` 这两条最值得记:一个能省排查时间,一个能把配置错字从运行期提前到启动期。 `or…

origin 区分 classpath / file 和「严格模式 + 校验失败中断 refresh」这两条最值得记,再补三个细节。

一,看到 class path resource [application.yml] 想临时救急,要用 spring.config.additional-location=optional:file:./config/,optional:file:./,别用 spring.config.location——后者会整体替换默认搜索路径,classpath 里那份默认配置直接消失,原本只是「没加载外部文件」,一下就变成「连默认值都没了」,反而更难查。另外 jar 里 ./config/ 是相对启动目录(工作目录)而不是 jar 所在目录,用 systemd 或容器启动时这一点经常出问题,WorkingDirectory 要确认好。

二,management.server.port 独立端口在容器化场景要记得单独 EXPOSE 并放行网络安全规则,K8s 的存活探针别错指到管理端口;另外两个端口配成同一个值时该设置会被忽略,等于没设,官方文档可以查一下确认。

三,static 字段不是「刷新后不生效」,而是 Spring 本来就不往 static 上注入;@Value 写在 static 字段上同样拿不到值,这属于配置从未生效,和 @RefreshScope 无关。final 常量同理,编译期就内联进调用点了,改远端也没意义——这类配置只能重启,或者改成从 Environment 每次读取。

至于 @Validated 导致 refresh 中断,本地起一遍不如在 CI 里加一条启动冒烟测试来得稳,毕竟推配置的人未必是本地能跑全环境的那个人。