Spring Boot 配置属性,90% 的人都用错了这几个注解
在 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
@Componentto 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:“这个配置属性类也是一个业务组件或配置类”。这违反了单一职责原则,并会让代码阅读者感到困惑:这个类到底是用来承载配置的,还是用来做其他事情的?
更多推荐

所有评论(0)