一、@Configuration配置类
在 Spring Boot 中,@Configuration(配置类)是替代传统 Spring XML 配置文件(如applicationContext.xml)的核心手段。它采用Java Config的方式,将对象的创建、组装和依赖注入逻辑用 Java 代码清晰地表达出来。
1. 核心概念与本质
@Configuration声明在类上,告诉 Spring 容器:“这个类是一个 Bean 对象的生产工厂,里面包含了组件注册与配置的蓝图。”
@Configuration public class AppConfig { // 容器会读取这个类,并将其内部 @Bean 方法返回的对象注册到 IoC 容器中 }本质:
@Configuration底层继承了@Component注解。这意味着配置类本身也是一个被 Spring 容器管理的 Bean。
@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 进行注入。
@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= ...)调节容器行为。
// 默认为 true (Full 模式) @Configuration(proxyBeanMethods = true) public class FullConfig { // ... }Full 模式 vs Lite 模式
| 特性 | Full 模式 (true,默认) | Lite 模式 (false) |
| 原理 | 使用CGLIB为配置类生成动态代理类 | 不生成代理类,仅作为普通类处理 |
| 方法间互调 | 在 Bean 方法内部调用另一个@Bean方法时,总是从容器获取单例 | 每次调用都会直接执行普通方法,产生新的实例 |
| 启动性能 | 略慢(需生成 CGLIB 代理) | 更快,减少内存与启动消耗 |
| 适用场景 | 组件间存在内部依赖关系(组件 A 依赖组件 B 的单例) | 仅声明组件,组件之间无内部依赖调用 |
代码对比说明:
@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:配置文件中配置了指定属性时生效。@Bean @ConditionalOnProperty(name = "feature.email.enabled", havingValue = "true") public EmailService emailService() { return new EmailService(); }@ConditionalOnClass/@ConditionalOnMissingClass:类路径(Classpath)下存在/不存在指定类时生效。@ConditionalOnBean/@ConditionalOnMissingBean:容器中存在/不存在指定 Bean 时生效(非常适合做默认保底配置)。@Bean @ConditionalOnMissingBean(SearchService.class) public SearchService defaultSearchService() { return new DefaultSearchServiceImpl(); // 用户没配就用默认的 }
5. 配置类的模块化与组合
为了避免单一配置类过于庞大,通常需要拆分并进行组合。
1.@Import(引入外部配置或类)
用于直接将其他配置类、普通组件类、或者动态注册器注入容器。
@Configuration @Import({SecurityConfig.class, CacheConfig.class}) // 组合其他配置类 public class MainConfig { }2.@EnableConfigurationProperties(结合配置绑定)
将application.yml中的属性映射到实体类(@ConfigurationProperties),并在配置类中开启和使用。
方式1(推荐)
// 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:
// 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,你就不想加载跟短信相关的任何类以节省内存。
@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)选择性加载配置类。
@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 模式 - 会出问题)
@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 模式 - 安全)
@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写法:
@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出来的效果是一样的,而且更轻量。