一、前言
写 Spring Boot 项目时,读取配置往往是最先接触的功能之一。给字段加一个 @Value,就能拿到端口、地址或业务开关。等到配置越来越多,问题也随之出现:一组配置该怎么管理?YAML 中的列表怎么读?同一个配置写在多个地方,最终以哪个为准?
要回答这些问题,先分清两个环节:配置如何进入 Spring,以及业务代码如何使用配置。理解这条路径,再选择读取方式,就容易得多。本文采用的是Java17、Spring Boot 3.x作为示例版本。
二、代码读取的配置,不一定来自文件
先在 src/main/resources/application.yml 中准备一组配置:
app:
name: order-service
upload:
enabled: true
max-size: 10MB
timeout: 5s
allowed-types:
- jpg
- png
labels:
owner: order-team
region: cn-east
对应用而言,这组配置包含一个服务名称,以及上传功能的开关、大小限制、超时时间、文件类型和标签。
Spring Boot 启动时,会加载配置数据,将其转换为属性源,加入 Environment。YAML 的层级结构可以用属性路径表达,例如 app.upload.enabled;列表元素则可以通过 app.upload.allowed-types[0] 这样的索引路径访问。负责加载并应用配置数据的入口之一是 ConfigDataEnvironmentPostProcessor。它属于启动过程中的环境准备环节。读取过程可以简单概括为:
application.yml、外部配置文件、环境变量、启动参数等
↓
按优先级组织的属性源
↓
Environment
↓
单个属性读取 / 占位符注入 / 配置对象绑定
↓
业务代码
因此,@Value("${app.name}") 并不意味着"打开 application.yml,找到这一行"。它获取的是当前配置环境中生效的值。文件只是配置来源之一。举个例子,在默认开启命令行属性支持的情况下:
java -jar app.jar --app.name=order-service-gray
即使 YAML 中仍然写着 order-service,代码拿到的也会是 order-service-gray,因为优先级不一样。具体可以了解:Externalized Configuration :: Spring Boot | Spring 中文网
三、配置读取方式
1、使用 @Value:读取少量独立配置
(1)使用方式:只有一两个配置项时,@Value 最直接。
java
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Service
public class AppInfoService {
@Value("${app.name}")
private String appName;
@Value("${app.upload.enabled:false}")
private boolean uploadEnabled;
}
${app.name} 表示读取属性;${app.upload.enabled:false} 表示属性不存在时使用默认值 false。默认值用于缺失场景,并不会替你修复错误的配置内容。例如,把布尔值写成 abc,依然可能导致类型转换失败。
(2)原理:解析占位符,再完成类型转换
Spring 在创建 Bean、解析构造器参数或注入字段时,处理 @Value 中的表达式。对于 ${...},先通过容器的占位符解析机制取得属性值,再转换成目标类型。
Spring Boot 默认配置了 PropertySourcesPlaceholderConfigurer,使占位符能够使用环境中的属性。在默认严格解析行为下,没有默认值、又无法解析的占位符,会导致相关 Bean 创建失败。@Value 也支持 #{...} 形式的 SpEL 表达式,两种语法不要混淆。
(3)使用场景:@Value 适合服务名称、简单开关等零散配置。一旦十几个字段都属于同一个模块,逐个注入就容易出现键名重复、默认值分散的问题。
2、使用 @ConfigurationProperties:把一组配置绑定成对象
(1)使用方式:
java
import java.time.Duration;
import java.util.List;
import java.util.Map;
import jakarta.validation.constraints.NotNull;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.util.unit.DataSize;
import org.springframework.validation.annotation.Validated;
@Validated
@ConfigurationProperties(prefix = "app.upload")
public record UploadProperties(
@DefaultValue("false") boolean enabled,
@NotNull DataSize maxSize,
@NotNull Duration timeout,
List<String> allowedTypes,
Map<String, String> labels) {
}
注意⚠️:record 首次出现在 Java 14 ,当时是预览特性;在 Java 16 正式定稿。主要用来定义以承载数据为主的类 ,例如接口返回值、查询结果、配置对象。它能省掉构造器、访问方法、equals()、hashCode() 和 toString() 等重复代码。
然后在启动类注册配置类型:
java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@SpringBootApplication
@EnableConfigurationProperties(UploadProperties.class)
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
业务类直接注入配置对象:
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UploadPolicy {
@Autowired
private UploadProperties properties;
public boolean accepts(long fileSize) {
return properties.enabled()
&& fileSize >= 0
&& fileSize <= properties.maxSize().toBytes();
}
}
(2)原理:按前缀绑定,按类型转换
@ConfigurationProperties(prefix = "app.upload") 声明了绑定范围。Spring Boot 的绑定基础设施使用 Binder,把对应属性转换为目标对象所需的值。
在这个例子中,max-size 对应 Java 的 maxSize,allowed-types 对应 allowedTypes;列表和 Map 也会被绑定为相应集合。这样的名称匹配属于宽松绑定。它让配置文件可以沿用常见的短横线命名,而 Java 代码继续使用驼峰命名。
只写 @ConfigurationProperties,并不代表配置类一定会成为可注入的 Bean。本文使用显式注册;配置类较多时,也可以改用 @ConfigurationPropertiesScan 扫描。
3、使用 Environment:在代码中按键查询
如果配置键需要在运行时确定,或者你正在编写通用基础组件,可以直接注入 Environment。
java
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
@Component
public class FeatureSettings {
private final Environment environment;
public FeatureSettings(Environment environment) {
this.environment = environment;
}
public String appName() {
return environment.getRequiredProperty("app.name");
}
public boolean isEnabled(String feature) {
return environment.getProperty(
"features." + feature + ".enabled",
Boolean.class,
false);
}
}
这里的 features.* 是独立的可选配置。例如调用 isEnabled("export"),就会查找 features.export.enabled;没有配置时返回 false。几种查询方法的差别如下:
| 调用方式 | 属性缺失时的行为 |
|---|---|
getProperty(key) |
返回 null |
getProperty(key, defaultValue) |
返回指定默认值 |
getProperty(key, Type.class, defaultValue) |
缺失时返回默认值,存在时尝试转换类型 |
getRequiredProperty(key) |
抛出异常 |
Environment 很灵活,但业务代码大量使用它,会让字符串键散落在各处。对于结构固定的一组业务配置,配置对象通常更便于维护。
4、自定义配置文件:先加载,再读取
项目中可能还会有 business.properties、upload.yml。这些文件不能仅靠"放进 resources 目录"就保证自动加载。需要使用 @PropertySource 加载 properties 文件。
创建 src/main/resources/business.properties:
business.order-prefix=ORD
business.customer-service=订单客服
再声明一个配置类:
java
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@Configuration(proxyBeanMethods = false)
@PropertySource(value = "classpath:business.properties", encoding = "UTF-8")
public class BusinessConfig {
}
之后就能用 @Value、Environment 或配置对象绑定读取 business.*。 @PropertySource 的职责是把指定资源加入环境中的属性源。 它本身不会把属性注入业务对象,因此不宜把它当作与 @Value 完全并列的"取值 API"。默认工厂支持常规 properties 和 XML properties,读取 YAML 需要额外的工厂实现。
YAML 文件优先考虑 spring.config.import
在 Spring Boot 2.4 及以上版本,可以在 application.yml 中导入文件:
spring:
config:
import:
"classpath:upload.yml"
"optional:file:./config/business.properties"
这里要求类路径中的 upload.yml 必须存在,而外部 business.properties 可以缺失。optional: 表达的是"资源可不存在",不表示忽略文件格式等其他错误。
5、使用 Properties 手动读取:适合脱离 Spring 的工具代码
如果只是编写一个独立工具,想读取某个 properties 文件,可以使用 JDK 自带的 Properties。
java
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.Properties;
public final class LocalPropertiesReader {
private LocalPropertiesReader() {
}
public static Properties load() throws IOException {
InputStream input = LocalPropertiesReader.class
.getResourceAsStream("/business.properties");
if (input == null) {
throw new IOException("找不到 business.properties");
}
Properties properties = new Properties();
try (Reader reader = new InputStreamReader(
input, StandardCharsets.UTF_8)) {
properties.load(reader);
}
return properties;
}
}
注意⚠️:如果应用本来就在 Spring Boot 容器中运行,手动读取同一份应用配置,容易得到与其他 Bean 不一致的值。除非明确需要读取原始文件,否则优先使用环境或配置绑定。
四、配置没生效时,如何排查?
遇到读取结果不符合预期,先检查数据从哪里来,再检查数据如何进入对象。
- 文件有没有加载? 核对文件名、位置、当前激活的 Profile;自定义文件是否通过 import 或其他机制加载。
- 值有没有被覆盖? 检查启动命令、环境变量和外部配置。对于常见来源,可先按"命令行参数 → Java 系统属性 → 操作系统环境变量 → 配置数据"由高到低检查;这不是包含测试、Servlet 环境等来源的完整优先级清单。
- 对象有没有交给 Spring 管理? 普通组件要被扫描到;配置属性类要被扫描或显式启用。
- 前缀、类型和约束是否匹配? 对照配置键与绑定目标,查看启动异常中的属性路径、转换错误或校验信息。
五、实际如何选择?
| 场景 | 建议方式 | 原因 |
|---|---|---|
| 一两个独立配置 | @Value |
写法直接,构造器依赖清晰 |
| 某个模块的一组配置 | @ConfigurationProperties |
集中管理,便于类型转换与校验 |
| 配置键需要动态组合 | Environment |
可以按键查询并指定缺失时的行为 |
| 额外的应用配置文件 | 先用 spring.config.import 等机制加载 |
加载后继续使用上述取值方式 |
| 脱离 Spring 的独立 properties 工具 | Properties |
直接解析指定文件即可 |
六、小结
对于普通业务项目,可以从一个简单约定开始:零散属性使用 @Value,同一业务模块的配置集中到 @ConfigurationProperties,只有确实需要动态查询或主动绑定时,再引入 Environment。