一、@Configuration配置类
在 Spring Boot 中,@Configuration(配置类) 是替代传统 Spring XML 配置文件(如 applicationContext.xml)的核心手段。它采用 Java Config 的方式,将对象的创建、组装和依赖注入逻辑用 Java 代码清晰地表达出来。
1. 核心概念与本质
@Configuration 声明在类上,告诉 Spring 容器:"这个类是一个 Bean 对象的生产工厂,里面包含了组件注册与配置的蓝图。"
java
@Configuration
public class AppConfig {
// 容器会读取这个类,并将其内部 @Bean 方法返回的对象注册到 IoC 容器中
}
本质:
@Configuration底层继承了@Component注解。这意味着配置类本身也是一个被 Spring 容器管理的 Bean。
java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface Configuration {
}
2. 声明与注册 Bean(@Bean 的用法)
在配置类内部,最核心的用法是结合 @Bean 注解来声明组件。
-
@Component作用于类 :告诉 Spring,"这个类 是我写的,请帮我实例化它并放进容器。" -
@Bean作用于方法 :告诉 Spring,"请执行这个方法 ,并把方法返回的对象放进容器。"
基础定义与参数自动注入
在 @Bean 方法中,方法的返回值 会作为注册到容器中的 Bean 实例,方法名默认作为 Bean 的唯一标识(Bean ID)。
如果方法带有参数,Spring 会自动从容器中查找匹配的 Bean 进行注入。
java
@Configuration
public class DatabaseConfig {
// 声明一个 DataSource 类型的 Bean,Bean ID 默认为 "dataSource"
@Bean
public DataSource dataSource() {
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
ds.setUsername("root");
ds.setPassword("secret");
return ds;
}
// 方法参数 (DataSource ds) 会由 Spring 容器自动注入上面定义的 dataSource
@Bean
public JdbcTemplate jdbcTemplate(DataSource ds) {
return new JdbcTemplate(ds);
}
}
属性与生命周期控制
通过辅助注解,可以精准控制 Bean 的行为:
| 注解 / 参数 | 描述 | 示例 |
|---|---|---|
@Bean(name = "...") |
自定义 Bean 的名称(可指定多个别名) | @Bean(name = {"myService", "aliasService"}) |
@Scope |
作用域:singleton(单例,默认)或 prototype(原型/多例) |
@Scope("prototype") |
@Primary |
存在多个同类型 Bean 时,优先注入该 Bean | @Primary |
@Lazy |
延迟初始化(首次使用时才创建 Bean,而非启动时) | @Lazy |
initMethod / destroyMethod |
指定 Bean 的初始化方法和销毁钩子方法 | @Bean(initMethod = "init", destroyMethod = "close") |
3. 两种代理模式:proxyBeanMethods (Full 与 Lite 模式)
这是 @Configuration 最底层也最重要的机制之一。
可以通过 @Configuration(proxyBeanMethods = ...) 调节容器行为。
java
// 默认为 true (Full 模式)
@Configuration(proxyBeanMethods = true)
public class FullConfig {
// ...
}
Full 模式 vs Lite 模式
| 特性 | Full 模式 (true,默认) | Lite 模式 (false) |
|---|---|---|
| 原理 | 使用 CGLIB 为配置类生成动态代理类 | 不生成代理类,仅作为普通类处理 |
| 方法间互调 | 在 Bean 方法内部调用另一个 @Bean 方法时,总是从容器获取单例 |
每次调用都会直接执行普通方法,产生新的实例 |
| 启动性能 | 略慢(需生成 CGLIB 代理) | 更快,减少内存与启动消耗 |
| 适用场景 | 组件间存在内部依赖关系(组件 A 依赖组件 B 的单例) | 仅声明组件,组件之间无内部依赖调用 |
代码对比说明:
java
@Configuration(proxyBeanMethods = true) // Full 模式
public class AppConfig {
@Bean
public UserDao userDao() {
return new UserDao();
}
@Bean
public UserService userService() {
// 直接调用 userDao() 方法,Spring 代理拦截该方法,直接从 IoC 容器拿单例,绝不会 new 两次!
return new UserService(userDao());
}
}
4. 条件装配(Conditional Configuration)
Spring Boot 的灵魂在于按需加载。
结合 @ConditionalOn* 系列注解,可以让配置类或某个 @Bean 仅在满足特定条件时才生效。
常用条件注解汇总
-
@ConditionalOnProperty:配置文件中配置了指定属性时生效。java@Bean @ConditionalOnProperty(name = "feature.email.enabled", havingValue = "true") public EmailService emailService() { return new EmailService(); } -
@ConditionalOnClass/@ConditionalOnMissingClass:类路径(Classpath)下存在/不存在指定类时生效。 -
@ConditionalOnBean/@ConditionalOnMissingBean:容器中存在/不存在指定 Bean 时生效(非常适合做默认保底配置)。java@Bean @ConditionalOnMissingBean(SearchService.class) public SearchService defaultSearchService() { return new DefaultSearchServiceImpl(); // 用户没配就用默认的 }
5. 配置类的模块化与组合
为了避免单一配置类过于庞大,通常需要拆分并进行组合。
1. @Import(引入外部配置或类)
用于直接将其他配置类、普通组件类、或者动态注册器注入容器。
java
@Configuration
@Import({SecurityConfig.class, CacheConfig.class}) // 组合其他配置类
public class MainConfig {
}
2. @EnableConfigurationProperties(结合配置绑定)
将 application.yml 中的属性映射到实体类(@ConfigurationProperties),并在配置类中开启和使用。
方式1(推荐)
java
// 1. 定义配置属性类
@ConfigurationProperties(prefix = "sms")
public class SmsProperties {
private String apiKey;
private String secret;
// getters and setters...
}
// 2. 在配置类中启用绑定并使用
@Configuration
@EnableConfigurationProperties(SmsProperties.class)
public class SmsConfig {
@Bean
public SmsClient smsClient(SmsProperties properties) {
return new SmsClient(properties.getApiKey(), properties.getSecret());
}
}
方式2:
java
// 1. 定义配置属性类
@Component
@ConfigurationProperties(prefix = "sms")
public class SmsProperties {
private String apiKey;
private String secret;
// getters and setters...
}
// 2. 在配置类中启用绑定并使用
@Configuration
public class SmsConfig {
@Bean
public SmsClient smsClient(SmsProperties properties) {
return new SmsClient(properties.getApiKey(), properties.getSecret());
}
}
这两种方式的区别:
方式2的这种方式(直接加 @Component)完全可以,并且在日常的业务代码开发中非常常见! 代码不仅能跑通,而且在单一业务系统里也是一种很便捷的写法。
既然方式2的写法能跑通,为什么 Spring Boot 官方还要搞一个 @EnableConfigurationProperties 呢?
这主要涉及到 组件封装设计原则 、条件触发 以及 开发第三方框架(Starter) 时的场景限制。
下面我为你详细梳理这两种方式的适用场景和底层设计考量:
(1). 为什么官方推荐用 @EnableConfigurationProperties?
场景一:开发第三方 Starter(最核心的原因)
如果你在写一个给别人用的 sms-spring-boot-starter,你的包名可能是 com.yourcompany.sms,而使用者的主程序包名是 com.user.app。
-
如果你用
@Component: 使用者的 Spring Boot 默认只会扫描com.user.app目录下的组件。你的SmsProperties带有@Component也不会被扫描到,导致注入失败。 -
如果你用
@EnableConfigurationProperties(SmsProperties.class): 你可以在你的自动配置类(SmsAutoConfiguration)上使用这个注解。只要自动配置类被加载,它就会强制 把SmsProperties注册为 Bean。这种方式完全摆脱了@ComponentScan(包扫描)的限制。
场景二:做到真正的"按需加载"与条件装配
在底层框架设计中,通常希望组件是完全解耦且按需加载的。
假设用户没有在 application.yml 里配置 sms.enabled=true,你就不想加载跟短信相关的任何类以节省内存。
java
@Configuration
@ConditionalOnProperty(name = "sms.enabled", havingValue = "true")
@EnableConfigurationProperties(SmsProperties.class)
public class SmsConfig {
// 只有在 sms.enabled=true 时,SmsConfig 才会生效。
// SmsConfig 生效了,SmsProperties 才会被注册为 Bean。
}
如果你在 SmsProperties 上直接加了 @Component,那么无论有没有开启短信功能,Spring 启动时都会扫描并无脑实例化这个配置类对象,这就违背了 Spring Boot "按需加载"的优雅设计。
场景三:保持 POJO 的纯粹性(解耦)
SmsProperties 本质上只是一个装载数据的 POJO(纯 Java 对象)。从架构设计的洁癖角度来看,把它打上 @Component 的烙印,就意味着它和 Spring 的 IoC 容器深度绑定了。使用 @EnableConfigurationProperties 可以让实体类只保留 @ConfigurationProperties(纯粹声明属性前缀),由配置类去决定何时将它纳入 Spring 容器。
(2). 总结与最佳实践
其实,随着 Spring Boot 的演进,现在处理配置绑定有三种流派,可以根据你的实际场景来选择:
| 方式 | 写法特征 | 适用场景 |
|---|---|---|
| 方式 1:包扫描(你的写法) | @Component + @ConfigurationProperties |
普通业务模块。自己项目里随便写,简单粗暴,直接注入。 |
| 方式 2:显式启用(官方经典) | @ConfigurationProperties + 配置类上的 @EnableConfigurationProperties |
开发通用组件 / Starter 。需要规避包扫描限制,或者需要与其他 @Conditional 条件联合按需加载。 |
| 方式 3:配置树扫描(Spring Boot 2.2+ 新特性) | POJO 上只写 @ConfigurationProperties,然后在启动类加上 @ConfigurationPropertiesScan |
大型业务系统 。统一管理所有配置类,既不用写 @Component,也不用在每个配置类上写 Enable 注解。 |
3. @Profile(多环境隔离)
根据当前激活的 Profile(如 dev、test、prod)选择性加载配置类。
java
@Configuration
@Profile("dev") // 仅在 spring.profiles.active=dev 时生效
public class DevDatabaseConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder().build(); // 使用内存数据库
}
}
6. Spring Boot 自动配置原理扩展
当你在编写可复用的 Starter 模块时,配置类的注册机制有所不同:
-
Spring Boot 2.7 前 :在
META-INF/spring.factories 中配置配置类。 -
Spring Boot 3.x 及以上 :在
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中按行写入配置类的全路径名。 -
使用
@AutoConfiguration(Spring Boot 2.7+ 引入)替代标准的@Configuration作为自动配置类的入口,配合@AutoConfigureBefore/@AutoConfigureAfter控制加载顺序。
7. 最佳实践清单
-
单功能原则 :按业务模块拆分配置类(如
RedisConfig、SecurityConfig、SwaggerConfig),避免产生"超级配置类"。 -
善用
proxyBeanMethods = false:若@Bean方法之间不需要互相调用来保证单例,建议设置为false提升系统启动速度。 -
优先使用参数注入 :在
@Bean方法定义中,直接将依赖写入方法参数,利用 Spring 自动装配,减少定义不必要的内部字段。 -
合理使用
@ConditionalOnMissingBean:在封装公共 SDK/组件时,始终用该注解留出拓展口,方便业务侧自定义覆盖。
二、@Component + @Bean的组合
在 @Component 类中使用 @Bean,和在 @Configuration 类中使用 @Bean,底层有着非常核心且致命的区别。
还记得我们在第一讲提到的 "Full 模式" 和 "Lite 模式" 吗?这就是它们最大的差异。
核心区别:是否生成代理对象保证单例
-
@Configuration+@Bean(Full 模式): Spring 会使用 CGLIB 给这个配置类生成一个代理子类。当你在一个@Bean方法中调用另一个@Bean方法时,代理对象会拦截这个调用,去 Spring 容器里拿单例对象。 -
@Component+@Bean(Lite 模式): Spring 不会 生成代理类。类就是一个普通的 Java 类。如果你在内部发生方法互调,就是纯粹的 Java 方法执行,会每次都new出一个全新的对象,从而破坏 Spring 默认的单例机制!
代码对比:
假设我们有两个 Bean:UserDao 和 UserService(UserService 依赖 UserDao)。
场景 1:使用 @Component(Lite 模式 - 会出问题)
java
@Component
public class AppComponents {
@Bean
public UserDao userDao() {
System.out.println("UserDao 被创建了!");
return new UserDao();
}
@Bean
public UserService userService() {
// 【注意这里】直接调用了 userDao() 方法
return new UserService(userDao());
}
}
其实这个方法,idea层就会编译报错了!
运行结果 :控制台会打印 两次 "UserDao 被创建了!"。
-
Spring 扫描到
@Bean userDao(),调用一次,注册到容器。 -
Spring 扫描到
@Bean userService(),执行内部的userDao()方法,由于没有代理拦截,这就是普通 Java 方法调用,又new了一个全新的UserDao。后果 :
UserService里注入的UserDao,和 Spring 容器里管理的那个UserDao,不是同一个对象!
场景 2:使用 @Configuration(Full 模式 - 安全)
java
@Configuration
public class AppConfig {
@Bean
public UserDao userDao() {
System.out.println("UserDao 被创建了!");
return new UserDao();
}
@Bean
public UserService userService() {
// 这里的 userDao() 调用会被 Spring 拦截
return new UserService(userDao());
}
}
运行结果 :控制台只打印 一次 "UserDao 被创建了!"。
在执行 userService() 时,Spring 的代理类发现你要调用 userDao(),它会说:"等一下,容器里已经有这个单例了,我直接把容器里的给你,别再 new 了。"
那么,什么时候可以用 @Component + @Bean?
如果你定义的多个 @Bean 之间完全独立,不需要互相调用,那么使用 @Component + @Bean 是完全没问题的,甚至更好!
因为不需要生成 CGLIB 代理,Spring 的启动速度会稍微快一点,内存消耗也少一点。这种写法在 Spring 源码内部(比如一些自动配置类)经常被使用。
为了避免掉坑,最佳实践建议:
如果你在 @Component 中定义 @Bean,遇到需要依赖其他 Bean 的情况,坚决不要用方法调用,而是用参数注入。
正确的 @Component + @Bean 写法:
java
@Component
public class AppComponents {
@Bean
public UserDao userDao() {
return new UserDao();
}
// 通过参数 userDao 让 Spring 自动注入,而不是去调用 userDao() 方法
@Bean
public UserService userService(UserDao userDao) {
return new UserService(userDao);
}
}
总结:
@Component确实可以和@Bean一起用(Lite 模式)。只要你不通过"直接调用方法"的方式来注入依赖,它和
@Configuration出来的效果是一样的,而且更轻量。