在 Spring Boot 中处理配置属性时,可以类比我们编写业务代码的顺序:程序员通常先通过 @Controller@Service 等注解定义 Bean,再借助 @ComponentScan 让 Spring 扫描发现,最后用 @Autowired 注入使用。配置属性的处理流程也类似,同样需要经历扫描配置类、绑定属性、注册 Bean 以及最终注入使用这几个环节。下面将按照这一思路,详细介绍 Spring Boot 中属性配置的最佳实践。

一、扫描配置类的两个注解

@EnableConfigurationProperties@ConfigurationPropertiesScan 用于指定配置类。

1、注解特性对比

特性 @EnableConfigurationProperties @ConfigurationPropertiesScan
注册方式 手动逐个指定配置类 自动扫描指定包及其子包下的配置类
使用场景 配置类数量少、位置明确 配置类数量多、分散在不同包
灵活性 精确控制哪些类被注册 批量注册,并可排除不需要的类

2、使用方式示例

@EnableConfigurationProperties(精确注册)

@ConfigurationProperties(prefix = "app")
public class AppProperties { }

@ConfigurationProperties(prefix = "db")
public class DatabaseProperties { }

@SpringBootApplication
@EnableConfigurationProperties({AppProperties.class, DatabaseProperties.class})
public class Application { }

@ConfigurationPropertiesScan(自动扫描)

@ConfigurationProperties(prefix = "app")
public class AppProperties { }

@ConfigurationProperties(prefix = "db")
public class DatabaseProperties { }

@SpringBootApplication
@ConfigurationPropertiesScan("com.example.config")  // 扫描该包及子包
public class Application { }

二、属性绑定的两个注解

1、两种注解的适用场景

  • @Value:适合少量、零散的配置项。每个字段单独声明,写法灵活,但缺乏类型安全与松散绑定。
  • @ConfigurationProperties:适合将一组相关的配置属性(如数据库、Redis 等)批量绑定到一个 Java 对象上。它支持松散绑定、复杂类型(List、Map、Duration 等)以及元数据提示,结构更清晰,便于批量管理。

2、具体使用方案

方案一:@Value 方式(少量配置)

@Service
public class MyService {
    @Value("${api.url}")
    private String apiUrl;
    
    @Value("${api.key}")
    private String apiKey;
    
    @Value("${api.timeout:5000}")
    private int timeout;
}

方案二:@ConfigurationProperties + @EnableConfigurationProperties

// ApiProperties.java
@ConfigurationProperties(prefix = "api")
public class ApiProperties {
    private String url;
    private String key;
    private int timeout = 5000;  // 默认值
    // getters/setters
}

// Application.java
@EnableConfigurationProperties(ApiProperties.class)
@SpringBootApplication
public class Application { }

// MyService.java
@Service
public class MyService {
    private final ApiProperties apiProps;

    public MyService(ApiProperties apiProps) {
        this.apiProps = apiProps;
    }
}

方案三:@ConfigurationProperties + @ConfigurationPropertiesScan

// ApiProperties.java(同方案二)

// DbProperties.java
@ConfigurationProperties(prefix = "db")
public class DbProperties {
    private String url;
    private String username;
    // getters/setters
}

// Application.java
@ConfigurationPropertiesScan("com.example.config")
@SpringBootApplication
public class Application { }
// 无需手动注册,ApiProperties 和 DbProperties 会被自动发现

三、配置 Bean 的两种方式

1、方法一:在类上加 @Component 及其衍生注解(包括 @Configuration

@Repository@Service@Controller 本质就是 @Component,功能几乎相同,只是用于不同层次。从技术上讲,@Configuration 也是一种特殊的 @Component,但两者在 CGLIB 代理上存在重要区别:

@Configuration:Spring 会为其创建 CGLIB 子类,拦截所有 @Bean 方法的调用。
例如:

@Configuration
public class AppConfig {
    @Bean
    public A a() { return new A(); }
    @Bean
    public B b() { return new B(a()); }  // 调用 a() 实际返回容器中的单例 A
}

这里 b() 中调用 a() 不会重新创建 A 实例,而是从容器中获取已有的单例,从而保证 A 与 B 使用的是同一个对象。

@Component:不会被代理(除非手动添加 AOP)。如果在 @Component 类中写类似的 @Bean 方法:

@Component
public class MyComponent {
    @Bean
    public A a() { return new A(); }
    @Bean
    public B b() { return new B(a()); }  // 每次调用 a() 都会 new 一个新 A,破坏单例
}

这会导致 a() 被反复调用,每次生成新实例,从而破坏单例契约。

2、方法二:使用 @Configuration + @Bean

技术上讲,@Bean 可以放在任意由 Spring 管理的 Bean 中,但为了单例安全和语义清晰,始终将 @Bean 放在 @Configuration 类中。当 @Bean 定义在 @Configuration 类中时,Spring 会为该配置类生成 CGLIB 代理,保证 @Bean 方法之间的调用返回的是容器中的单例 Bean(即方法内部调用另一个 @Bean 方法时,不会重新创建新实例)。

3、两种方式对比

维度 @Component(及其派生注解) @Bean
标注位置 类级别 方法级别(通常位于 @Configuration 类中)
注册方式 隐式:通过类路径扫描(@ComponentScan)自动检测并注册 显式:在配置类中手动编写方法,明确返回实例
控制粒度 整个类作为一个 Bean 一个方法产生一个 Bean(可灵活定义返回类型、构造逻辑)
适用场景 我们自己编写的类,可以直接加注解 第三方类、需要自定义构造逻辑的类、需要条件注册的 Bean

简单记忆:
@Component 适合自己写的、通常只有一种实例的组件(如 Service、Repository、Controller)。
@Bean 适合需要精细控制实例创建过程的情况(如第三方库、带参数的构造、条件判断等)。

四、Bean 注入的两种方式

1、注解注入(字段/Setter 注入)

注解 默认注入方式 配合按名称 来源
@Autowired 按类型 需加 @Qualifier Spring
@Resource 按名称 直接用 name 属性 Java 标准(JSR-250)

2、构造器注入

① 当类只有一个构造函数时,Spring 会自动使用该构造函数完成依赖注入,无需任何注解。

@Service
public class UserService {
    private final UserRepository userRepository;

    // 唯一构造器,Spring 自动注入
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

② 如果类存在多个构造函数,则必须用 @Autowired 标注其中一个,明确告诉 Spring 使用哪个构造器。

@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final Notifier notifier;

    @Autowired  // 明确指定使用此构造器
    public OrderService(OrderRepository orderRepository, Notifier notifier) {
        this.orderRepository = orderRepository;
        this.notifier = notifier;
    }

    public OrderService(OrderRepository orderRepository) {
        this(orderRepository, null);  // 另一个构造器不会被 Spring 使用
    }
}

五、最佳实践示例

1、application.yml

redis:
  host: localhost
  port: 6379
  timeout: 2000ms

2、配置属性类

@ConfigurationProperties(prefix = "redis")   // 仅此注解
public class RedisProperties {
    private String host;
    private int port;
    private Duration timeout;
    // getters / setters
}

3、在启动类上添加 @ConfigurationPropertiesScan

@SpringBootApplication
@ConfigurationPropertiesScan   // 扫描当前包及子包下所有 @ConfigurationProperties 类
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

4、负责创建客户端 Bean 的配置类

@Configuration
public class RedisConfig {

    @Bean
    public JedisCluster jedisCluster(RedisProperties redisProps) {
        // 直接注入 RedisProperties,无需 @EnableConfigurationProperties
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        return new JedisCluster(
            Set.of(new HostAndPort(redisProps.getHost(), redisProps.getPort())),
            (int) redisProps.getTimeout().toMillis(),
            poolConfig
        );
    }
}

5、通过构造器注入使用

@Service
public class RedisService {

    private final JedisCluster jedisCluster;

    // 只有一个构造器 → Spring 自动注入 JedisCluster
    public RedisService(JedisCluster jedisCluster) {
        this.jedisCluster = jedisCluster;
    }

    public void set(String key, String value) {
        jedisCluster.set(key, value);
    }

    public String get(String key) {
        return jedisCluster.get(key);
    }
}

六、注意:@ConfigurationProperties 类不应使用 @Component@Configuration

官方文档明确反对这种做法。Spring Boot 参考文档(@ConfigurationProperties 章节)指出:

“You can also annotate your configuration property class with @Component to have it registered as a bean, but this is not recommended because it mixes configuration metadata with component scanning.”
(你也可以在配置属性类上标注 @Component 将其注册为 Bean,但不推荐,因为这样会把配置元数据与组件扫描混在一起。)

同样的理由也适用于 @Configuration,因为 @Configuration 本身就是一个 @Component

虽然将 @Component@Configuration 添加到 @ConfigurationProperties 类上在技术上可行,但不推荐。原因如下:

@ConfigurationProperties 类的本质:它只是一个数据载体(POJO),用于将外部配置(YAML/properties)绑定到结构化的字段上。它没有业务逻辑,也不负责创建其他 Bean。
@Component 的语义:表示该类是一个 Spring 管理的组件,通常包含业务逻辑(如 Service、Repository、工具类)。
@Configuration 的语义:表示该类是一个配置类,内部通常包含一个或多个 @Bean 方法,用于定义和组装 Bean。

如果在 @ConfigurationProperties 类上添加 @Component@Configuration,就等于告诉 Spring:“这个配置属性类也是一个业务组件或配置类”。这违反了单一职责原则,并会让代码阅读者感到困惑:这个类到底是用来承载配置的,还是用来做其他事情的?

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐