@Configuration 和 @Component 有什么区别?为什么 @Configuration 类会被 CGLIB 代理?
简化版
@Configuration 和 @Component 都能让类被扫描成 Bean,但 @Configuration 类里的 @Bean 方法有个特殊能力:方法之间互相调用时,返回的是同一个单例 Bean。这是因为 Spring 会用 CGLIB 给 @Configuration 类生成代理,拦截 @Bean 方法调用——第二次调用同一个 @Bean 方法时,代理直接从容器返回已有单例,而不是重新执行方法 new 一个新对象。这叫 Full 模式(proxyBeanMethods=true,默认)。而 @Component 里的 @Bean 方法没有这层代理(Lite 模式),方法间调用就是普通 Java 调用,每次都真的执行、产生新对象,破坏单例。
详细版
核心区别:@Bean 方法间调用的行为:
@Configuration // Full 模式,被 CGLIB 代理
public class AppConfig {
@Bean
public A a() { return new A(); }
@Bean
public B b() {
return new B(a()); // 调用 a() —— 返回的是容器里那个单例 A!
}
@Bean
public C c() {
return new C(a()); // 又调用 a() —— 还是同一个单例 A(代理拦截)
}
}
// a() 只被真正执行一次,b、c 拿到的是同一个 A 实例
如果把 @Configuration 换成 @Component(Lite 模式):
@Component // Lite 模式,无 CGLIB 代理
public class AppConfig {
@Bean public A a() { return new A(); }
@Bean public B b() { return new B(a()); } // a() 是普通方法调用,new 一个新 A!
@Bean public C c() { return new C(a()); } // 又 new 一个新 A!
}
// a() 被执行三次(作为容器 Bean 一次 + b/c 各调一次),产生 3 个不同的 A 实例
Full 模式 vs Lite 模式:
| 维度 | Full(@Configuration,proxyBeanMethods=true) | Lite(@Component 里的 @Bean,或 proxyBeanMethods=false) |
|---|---|---|
| 是否 CGLIB 代理 | 是 | 否 |
| @Bean 方法间调用 | 返回容器单例(拦截) | 普通 Java 调用(每次 new 新对象) |
| 保证单例 | 是 | 否(方法间调用会破坏) |
| 启动开销 | 略高(生成代理) | 低 |
⚠️ Spring Boot 2.2+ 给很多内部配置类加了
proxyBeanMethods=false(切到 Lite 模式),因为如果@Bean方法之间不互相调用,就不需要 CGLIB 代理,省去代理生成开销、加快启动。所以proxyBeanMethods=false是个启动优化——前提是你的@Bean方法不互调。
完整版教学
一、两者的表面相同与本质不同
@Configuration 本身就带了 @Component(点开源码能看到),所以它也是一个 Bean、也会被组件扫描。表面上,用 @Configuration 还是 @Component 声明配置类,里面的 @Bean 都能注册 Bean,看起来一样。
相同:都能被扫描成 Bean,里面的 @Bean 方法都能注册 Bean
不同:@Configuration 类会被 CGLIB 代理(Full 模式)
@Component 类不会(Lite 模式)
→ 差异只在"@Bean 方法之间互相调用"时才暴露
关键认知:这个区别只在「一个 @Bean 方法内部调用了另一个 @Bean 方法」时才有影响。如果你的 @Bean 方法都是独立的、不互相调用,那用哪个注解结果一样。理解「差异的触发条件」,才知道什么时候必须用 @Configuration。
二、问题的根源:@Bean 方法互调时的单例陷阱
设想没有任何代理,@Bean 方法就是普通 Java 方法。那么 b() 里调 a(),就是一次普通方法调用,会执行 return new A() 造一个新对象:
Lite 模式(无代理),容器初始化时:
调用 a() 注册 Bean A → new A() 得到 A@1(容器里的单例)
调用 b() 注册 Bean B → b() 内部调 a() → 又 new A() 得到 A@2(新对象!)
调用 c() 注册 Bean C → c() 内部调 a() → 再 new A() 得到 A@3(又一个新对象!)
结果:容器里的 A 是 A@1,但 B 持有 A@2、C 持有 A@3 —— 三个不同的 A,单例被破坏!
这是个隐蔽的 bug:你以为整个应用共享一个 A 单例,实际 B、C 拿到的是各自 new 出来的副本。如果 A 有状态(如计数器、连接),就会出现「改了一个 A 另一个看不到」的诡异问题。@Configuration 的 CGLIB 代理就是来堵这个漏洞的。
三、CGLIB 代理如何保证单例
@Configuration 被 Spring 用 CGLIB 生成一个子类代理,这个代理重写了所有 @Bean 方法,加了一层拦截逻辑:
代理后的 a() 方法逻辑(伪代码):
a() {
if (容器里已有 Bean "a") {
return 容器里的单例 a; // 已存在,直接返回,不执行原方法
} else {
A instance = super.a(); // 第一次,才真正执行原方法 new A()
容器缓存这个 instance;
return instance;
}
}
于是 Full 模式下:
b() 内部调 a() → 走代理 → 发现容器已有 a → 返回同一个单例 A@1
c() 内部调 a() → 走代理 → 同样返回 A@1
结果:A、B、C 拿到的都是 A@1,单例得到保证 ✓
代理拦截了「方法调用」这个动作:第一次调用真正执行方法体,之后的调用直接返回缓存的单例。这就是为什么 @Configuration 里 @Bean 方法互调也能保证单例——CGLIB 代理把「普通方法调用」变成了「从容器取单例」。
四、为什么用 CGLIB 而不是 JDK 动态代理
一个高频追问:为什么这里用 CGLIB?因为 CGLIB 是基于继承(生成子类)的代理,能代理没有实现接口的类:
配置类通常不实现任何接口 → JDK 动态代理(基于接口)无法代理它
CGLIB 生成配置类的子类 → 能重写 @Bean 方法 → 可以拦截
所以 @Configuration 的代理必须用 CGLIB,不能用 JDK 动态代理
这也解释了两个限制:① @Configuration 类不能是 final(CGLIB 要继承它生成子类,final 类无法被继承);② @Bean 方法不能是 final 或 private(子类无法重写 final/private 方法,拦截不到)。如果你把配置类或 @Bean 方法写成 final,CGLIB 代理会失效或报错。这是「为什么配置类不能 final」的底层原因。
五、proxyBeanMethods 属性:Full 与 Lite 的开关
@Configuration 有个属性 proxyBeanMethods,控制是否走 CGLIB 代理:
@Configuration(proxyBeanMethods = true) // 默认,Full 模式,有 CGLIB 代理
@Configuration(proxyBeanMethods = false) // Lite 模式,无代理,省启动开销
选择依据很简单——看你的 @Bean 方法之间是否互相调用:
proxyBeanMethods = true(Full):
需要:@Bean 方法之间互相调用,要保证拿到的是单例
代价:CGLIB 代理有生成和调用开销,启动稍慢
proxyBeanMethods = false(Lite):
适用:@Bean 方法彼此独立、不互调
好处:跳过 CGLIB 代理,启动更快、内存更省
代价:若不小心互调了,会破坏单例
Spring Boot 2.2+ 大量内部自动配置类用了 proxyBeanMethods = false,因为它们的 @Bean 方法基本不互调,关掉代理能显著加快启动(尤其在类很多时)。这是一个「知道自己不互调,就关掉代理换启动速度」的优化。
六、实战建议与选型
| 场景 | 用什么 |
|---|---|
| 配置类,@Bean 方法互相调用 | @Configuration(默认 Full,保证单例) |
| 配置类,@Bean 方法不互调、追求启动速度 | @Configuration(proxyBeanMethods = false) |
| 普通业务组件顺便声明个 @Bean | @Component + @Bean(Lite,注意别互调) |
实践准则:只要是「配置类」就用 @Configuration(默认 Full 最安全,不会踩单例陷阱);只有在明确知道 @Bean 不互调、且非常在意启动性能时,才用 proxyBeanMethods = false。别用 @Component 来放会互调的 @Bean 方法——那是最容易踩坑的写法。
记忆钩子:「@Configuration 被 CGLIB 代理(Full 模式),@Bean 方法互调返回容器单例;@Component 的 @Bean 无代理(Lite),互调会 new 新对象破坏单例;proxyBeanMethods=false 关代理省启动,前提是方法不互调;配置类不能 final、@Bean 不能 final/private」。
七、常见误区与追问
- 误区:@Configuration 和 @Component 完全一样。 只在「@Bean 方法之间互相调用」时不同——@Configuration 有 CGLIB 代理保证返回单例,@Component 没有会 new 新对象。
- 误区:@Bean 方法互调没问题,反正都是单例。 只有 Full 模式(@Configuration)才保证;Lite 模式下互调是普通方法调用,每次真的执行产生新对象,破坏单例。
- 误区:proxyBeanMethods=false 只是个无关紧要的开关。 它关掉 CGLIB 代理能显著加快启动(Spring Boot 内部大量使用),但前提是 @Bean 方法不互调,否则会引入单例 bug。
- 误区:配置类可以是 final。 不行,CGLIB 要继承配置类生成子类代理,final 类无法被继承;@Bean 方法也不能是 final/private。
- 追问:@Configuration 为什么用 CGLIB 而非 JDK 动态代理? 配置类通常不实现接口,JDK 动态代理(基于接口)无法代理它;CGLIB 基于继承生成子类,能重写 @Bean 方法实现拦截。
- 追问:Full 模式的代理是怎么保证单例的? CGLIB 子类重写 @Bean 方法,调用时先查容器有没有该 Bean,有就直接返回单例、不执行原方法,第一次才真正 new 并缓存。
- 追问:什么时候该用 proxyBeanMethods=false? @Bean 方法之间彼此独立不互相调用、且追求启动性能时;这样跳过 CGLIB 代理生成,启动更快、更省内存。
八、加强记忆
@Configuration 和 @Component 的区别只在「一个 @Bean 方法内部调用另一个 @Bean 方法」时暴露:@Configuration 是 Full 模式,Spring 用 CGLIB 生成子类代理重写所有 @Bean 方法,方法互调时代理拦截、直接返回容器里的单例(第一次才真正执行 new 并缓存),保证单例;@Component 里的 @Bean 是 Lite 模式,无代理,互调就是普通 Java 方法调用,每次真的执行产生新对象、破坏单例。用 CGLIB 而非 JDK 动态代理,是因为配置类通常不实现接口(所以配置类不能 final、@Bean 不能 final/private,否则子类无法继承/重写)。proxyBeanMethods=false 能关掉代理省启动开销(Spring Boot 2.2+ 内部大量用),前提是 @Bean 方法不互调。实践准则:配置类一律用 @Configuration(默认 Full 最安全),别拿 @Component 放会互调的 @Bean。一句话「@Configuration 靠 CGLIB 代理让 @Bean 互调返回单例,@Component 无代理会 new 新对象,proxyBeanMethods 是 Full/Lite 开关」。