《Spring Boot 入门》6.1 application.yml 与 Profile

本节围绕「图书管理服务」的 dev/test/prod 三套配置展开:先对比 properties 与 yml 的取舍,再拆解 YAML 的缩进、冒号、隐式类型、多文档等六个常见坑;然后讲清 application-{profile}.yml 的命名、激活方式、@Profile 与组合表达式,最后用 spring.config.import 读取 .env 并介绍 4.1 的编码指定能力。

本节目标:把「随环境变化的东西」从代码里搬进配置文件,为图书管理服务建立 dev / test / prod 三套配置,并搞清每一行最终落在哪里。
适用版本:Spring Boot 4.1.x(Java 21)

6.1 application.yml 与 Profile

上一章我们让 BookApplication 顺利启动,也把图书相关的 Bean 交给了容器管理。但参数还是写死在代码里的——数据库地址、端口、借阅上限。只要换一个环境,就得改代码重新打包。本节要解决的正是这件事:把配置从代码里抽出来,再用 Profile 按环境分堆。

我们继续用贯穿本章的「图书管理服务」(book service)作为例子:dev 用内存数据库、test 用独立的测试库、prod 连生产库。

为什么配置要外部化

把配置写死在代码里有三个后果:同一份代码要发布到三个环境就得打三个包,构建产物和源码版本对不上,线上出问题无法回溯;改一个端口要重新编译、重新走发布流程;密钥和连接串一旦进了 Git 历史,清理成本极高。

外部化配置的核心思路只有一句话:代码只描述「怎么算」,配置描述「对谁算」。Spring Boot 把配置抽象成一组有序的 PropertySource,绑定阶段按优先级取第一个命中的值——这正是 6.3 节要展开的机制。

properties 还是 yml

Spring Boot 同时支持 application.properties 和 application.yml(以及 .yaml)。两者能力等价,差别在表达方式。

维度application.propertiesapplication.yml
层级表达扁平,靠 . 串起来缩进表达嵌套
重复前缀每行都要写全父键只写一次
多文档不支持--- 分隔
列表book.categories[0]=小说缩进 - 小说
解析器java.util.PropertiesSnakeYAML
出错方式拼错键名不报错,静默失效缩进或语法错直接启动失败
适合零散几个键、极简场景结构化、成块的配置

同样的配置,两种写法:

book.name=图书管理服务
book.contact.email=ops@example.com
book.contact.phone=010-00000000
book:
  name: 图书管理服务
  contact:
    email: ops@example.com
    phone: 010-00000000

属性少时 properties 更直白;一旦出现 book.contact.*、spring.datasource.hikari.* 这类深层前缀,yml 的层级优势就很明显。建议团队统一选一种,混用会让「哪个文件覆盖了哪个」变得难以推理。本书示例统一用 YAML。

YAML 的六个常见坑

YAML 好看,但对格式极其敏感。下面六个坑出现频率最高。

坑一:用 Tab 缩进。 YAML 规范禁止用 Tab 缩进,只允许空格。编辑器里看着对齐,实际存的是 \t,启动时报 found character '\t' that cannot start any token。开启「显示空白字符」并让 Tab 自动转空格即可。

坑二:冒号后面忘了空格。 key: value 里的空格是语法的一部分。

book:
  name:图书管理服务    # 错误:整行被当成一个字符串标量,key 根本不存在

正确写法是 name: 图书管理服务。

坑三:隐式类型转换。 YAML 1.1 会自动推断类型,很多「字符串」会被吃掉:

book:
  code: no                   # 布尔 false,不是字符串 "no"
  version: 1.0               # 浮点数
  zip: 0755                  # 前导 0 → 八进制整数 493
  open-time: 12:30           # 六十进制 → 整数 750
  publish-date: 2026-09-15   # 日期对象

凡是「像数字、布尔或日期、但语义上是字符串」的值,加引号即可:code: "no"、zip: "0755"、open-time: "12:30"。12:30 变 750 这条尤其阴——绑定到 String openTime 时你会看到 "750",排查半天想不到是 YAML 干的。

坑四:特殊字符与 #。 # 是注释起点,出现在值中间会截断:token: abc#123 的实际值只有 abc,必须写成 token: "abc#123"。另外 @ 和反引号不能作为纯量的开头,owner: @admin 会解析失败,要写成 owner: "@admin"。中文、空格,以及后面不跟空格的冒号(如 url: http://x)都不需要引号。

坑五:--- 多文档。 一个文件里可以用 --- 分隔多个文档,每个文档各自成块:

book:
  name: 图书管理服务

---
spring:
  config:
    activate:
      on-profile: prod
book:
  name: 图书管理服务(生产)

--- 必须顶格写;文档级 profile 要用 spring.config.activate.on-profile,老的 spring.profiles 已被移除。profile 专属文档里不允许再出现 spring.profiles.active 或 spring.profiles.include,否则启动抛 InvalidConfigDataPropertyException。

坑六:列表与空值。

book:
  categories:
    - 小说
    - 技术
  tags: [文学, 编程]         # 行内写法等价
  aliases: []                # 空列表
  description:               # 值为 null

列表项前面的 - 后必须有一个空格;写成 -小说 会被当成字符串 -小说。

为图书服务建立三套配置

约定优于配置:application.yml 是所有环境共享的基线,application-{profile}.yml 只放差异项。

src/main/resources/application.yml:

book:
  name: 图书管理服务
  max-borrow-days: 30
  page-size: 20
spring:
  application:
    name: book-service

src/main/resources/application-dev.yml:

book:
  max-borrow-days: 90
spring:
  datasource:
    url: jdbc:h2:mem:books
    username: sa
    password: ""
  h2:
    console:
      enabled: true

src/main/resources/application-test.yml:

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/books_test
    username: books
    password: books

src/main/resources/application-prod.yml:

spring:
  datasource:
    url: ${DB_URL}
    username: ${DB_USER}
    password: ${DB_PASSWORD}
book:
  page-size: 50

book.name 只在基线里出现一次,三个环境都不用重复;dev 把借阅上限放宽到 90 天,prod 把分页调大。没有出现在 profile 文件里的键,自动继承基线值。

激活 Profile 的五种方式

方式写法典型场景
配置文件spring.profiles.active: dev本机默认开发
命令行参数--spring.profiles.active=prod部署脚本、java -jar
环境变量SPRING_PROFILES_ACTIVE=prod容器 / Kubernetes
JVM 系统属性-Dspring.profiles.active=prod启动脚本
代码SpringApplication.setAdditionalProfiles("dev")追加而非替换
java -jar book-service.jar --spring.profiles.active=prod

容器里则用环境变量 SPRING_PROFILES_ACTIVE=prod,因为镜像不该为环境而变。

几个容易踩的点:

  • spring.profiles.active 只能写在非 profile 专属的文档里,写进 application-prod.yml 会直接报错。
  • 可以同时激活多个:--spring.profiles.active=dev,local。冲突的键后写的赢,所以 dev,prod 里 prod 覆盖 dev。
  • 没有激活任何 profile 时会落到名为 default 的默认 profile,启动日志可见 No active profile set, falling back to 1 default profile: "default"。想改名用 spring.profiles.default: dev。
  • 想「打包」一组 profile,用 spring.profiles.group:写成 prod: prod,monitoring 后,--spring.profiles.active=prod 会同时激活 prod 与 monitoring。

用 @Profile 标注 Bean

profile 不只影响配置值,还能决定「哪些 Bean 存在」。这对「开发用假实现、生产用真实现」特别有用。

import java.util.concurrent.atomic.AtomicLong;

import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;

public interface IsbnService {
    String nextIsbn();
}

@Repository
@Profile("prod")
class DatabaseIsbnService implements IsbnService {
    public String nextIsbn() {
        return "978-7-" + System.currentTimeMillis();   // 从数据库序列取号
    }
}

@Component
@Profile({"dev", "test"})
class InMemoryIsbnService implements IsbnService {
    private final AtomicLong seq = new AtomicLong(1000);

    public String nextIsbn() {
        return "TEST-" + seq.incrementAndGet();
    }
}

@Profile 的值是一个数组,数组内是「或」的关系:{"dev", "test"} 表示 dev 或 test 时注册。! 表示取反,例如 @Profile("!prod") 让 MockNotificationSender 只在非生产环境生效。

profile 组合表达式

条件不止「某个 profile 在不在」时,可以用表达式。Spring 的 Profiles 支持 !(非)、&(与)、|(或)和括号。下面两个类需要 @Component 与 @Profile 的 import,与上一段相同。

@Component
@Profile("prod & !test")
class ProdOnlyMetricsExporter {
}

@Component
@Profile("(dev | test) & !cloud")
class LocalOnlyCacheManager {
}

表达式同样能用在配置文件的 on-profile 上:

---
spring:
  config:
    activate:
      on-profile: "prod & !test"
book:
  audit-enabled: true

优先级规则是 ! 最高、& 次之、| 最低;拿不准就加括号,别依赖记忆。

默认配置与覆盖关系

把「谁覆盖谁」画成一张从下往上的表(越靠下越晚加载、优先级越高):

层级文件 / 来源
基线classpath:/application.yml
基线(配置目录)classpath:/config/application.yml
外部基线./application.yml
外部基线(配置目录)./config/application.yml
profile 专属classpath:/application-{profile}.yml
外部 profile./application-{profile}.yml
外部 profile(配置目录)./config/application-{profile}.yml

规则可以归纳成三句:

  1. 基线永远先加载,profile 文件在其上做增量覆盖,没写的键继承基线。
  2. profile 文件之间后加载的赢,顺序由 spring.profiles.active 列表决定。
  3. jar 包外的文件赢过 jar 包内的同名文件,所以运维改配置不必重新打包。

.env 与 spring.config.import

很多团队习惯把本地密钥放在 .env 里并写进 .gitignore。Spring Boot 默认不读 .env,但可以用 spring.config.import 把它当 properties 文件导入:

spring:
  config:
    import: optional:file:./.env[.properties]
  • optional: 前缀表示文件不存在时不报错,这对「本地有、CI 没有」的场景很关键。
  • [.properties] 是位置提示:.env 没有扩展名,不提示的话 Spring 猜不出格式。
  • 导入的键值优先级高于导入它的那个文档,即 .env 里的 book.page-size 会盖掉 application.yml 里的同名项。

.env 与 .properties 语法大部分重合(都是 KEY=VALUE),但不完全兼容:.env 允许 export KEY=VALUE,也允许不加引号的值里有空格。最稳妥的用法是只放简单的 KEY=VALUE。

Spring Boot 4.1 给 spring.config.import 增加了指定编码的能力:当被导入的文件不是 UTF-8(例如 Windows 导出的 GBK 文件)时,可以在位置提示里追加 encoding 参数(语法见官方 4.1 Release Notes),避免中文乱码:

spring:
  config:
    import: "optional:file:./config/legacy.properties[encoding=GBK]"

以前只能靠 JVM 的 -Dfile.encoding 全局兜底,现在可以只对某个文件生效,对从老系统迁移配置的团队很实用。

本节常见坑速查

现象原因处理
启动报 cannot start any token缩进用了 Tab换成空格
某个键「写了没生效」冒号后缺空格,整行成标量补空格
值变成 false / 750 / 493YAML 隐式类型转换加引号
值被截断# 被当成注释加引号
InvalidConfigDataPropertyExceptionprofile 文档里写了 spring.profiles.active移到基线文档
中文乱码导入文件不是 UTF-84.1 用 encoding 指定

小结

  • 配置外部化的目标是同一份产物跑多个环境;Spring Boot 用有序的 PropertySource 实现「按优先级取第一个命中」。
  • properties 与 yml 能力等价,深层结构选 yml;一个项目只选一种。
  • YAML 的六个坑集中在缩进、冒号空格、隐式类型、特殊字符、多文档和列表空值上,凡「像数字、布尔或日期」的字符串一律加引号。
  • application.yml 是基线,application-{profile}.yml 做增量覆盖;激活方式有配置文件、命令行、环境变量、系统属性、代码五种,冲突时后激活的赢。
  • @Profile 决定 Bean 是否注册,支持 !、&、| 与括号的组合表达式。
  • spring.config.import 可以读 .env,4.1 起还能为被导入文件单独指定编码。

到这里,配置文件已经能按环境切换了。但用 @Value("${book.max-borrow-days}") 一个个取值既啰嗦又不安全——下一节我们用 @ConfigurationProperties 把一整块配置绑成类型安全的对象。

阅读导航:上一节:5.3 生命周期回调 · 下一节:6.2 @ConfigurationProperties 类型安全配置 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Spring Boot 入门》18.3 打包与运行
  2. 《Spring Boot 入门》18.2 实现
  3. 《Spring Boot 入门》18.1 需求与设计