← 返回题目列表

@Configuration 和 @Component 有什么区别?为什么 @Configuration 类会被 CGLIB 代理?

高频 中等 第 4 / 30 题 更新于 2026/07/26
ConfigurationCGLIBBeanproxyBeanMethods

简化版

@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 方法」时暴露:@ConfigurationFull 模式,Spring 用 CGLIB 生成子类代理重写所有 @Bean 方法,方法互调时代理拦截、直接返回容器里的单例(第一次才真正执行 new 并缓存),保证单例@Component 里的 @BeanLite 模式,无代理,互调就是普通 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 开关」。