Spring Boot 配置文件读取的几种方式:用法、原理与常见误区

一、前言

写 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。

相关推荐
余槐i1 小时前
虚拟线程上线后吞吐反而掉了:Spring Boot 3 里 synchronized 锁住载体线程的定位与修复
java·spring boot·性能优化·虚拟线程·jdk 21
要开心吖ZSH1 小时前
DDD(领域驱动设计)篇三:实战对照——同一个订单业务,两种架构怎么写
java·架构·领域模型·ddd·领域驱动设计·设计思想
独泪了无痕9 小时前
Spring Data Redis 快速上手
spring boot·redis·后端
wangchunyu11411 小时前
JAVA CompletableFuture异步编程实战
java·开发语言·python
paopaokaka_luck12 小时前
流浪动物救助与收养系统(DeepSeek AI对话、 ECharts多维分析、救助流程衔接、领养与回访衔接、 捐赠确认与公示、志愿者协作)
java·前端·javascript·人工智能·mysql·echarts·推荐算法
Thomas.Sir12 小时前
第9课:SpringBoot4.x Web开发基础:WebMvc自动配置、静态资源、请求参数接收
java·spring boot
程序 代码狂人12 小时前
切片表与分区表的区别(数据开发面试版)
java·大数据·开发语言·数据仓库·算法
2501_9445215913 小时前
Deepmerge 跨端三方库仓颉适配全流程
java·python·eclipse·github
南归北隐14 小时前
Spring AI Alibaba Graph框架实现MCP数据库查询实战
java·后端·spring ai·mcp·spring ai mcp