Spring 中的 @Configuration 与 @Component 差异:为何代理时机决定 Bean 生命周期行为

Spring 中的 @Configuration 与 @Component 差异:为何代理时机决定 Bean 生命周期行为

假设你在一个订单服务里,需要把两个 PaymentService 接口的实现注入到不同组件。你写了一个 PaymentConfig,用 @Bean 方法提供它们;又在另一个类 Helper 上加了 @Component,里面也用 @Bean 方法提供一些辅助类。上线后,你发现 Helper 里的 @Bean 方法每次调用都会创建一个新实例,而 PaymentConfig 中的 @Bean 方法却总能返回单例。更诡异的是,某些切面在 Helper 生成的 Bean 上失效了。这个现象背后,是 Spring 对 @Configuration@Component 采用了不同的内部处理策略------一个会生成代理,另一个不会。如果不理解这一点,你在配置类和普通组件之间切换时,就会踩到作用域、生命周期和 AOP 的隐性坑。

先记住一个最小模型:@Configuration 会被 Spring 当成一个完整的配置类,生成 CGLIB 代理(Full 模式);而 @Component 只是一个普通 Bean 的标记,即使它里面也写了 @Bean 方法,Spring 也不会对它做代理(lite 模式) 。代理意味着:你的配置对象被替换成了一个子类实例,Spring 会在调用 @Bean 方法前拦截,保证每次都走容器去获取或创建 Bean,从而确保单例语义、作用域代理和 BeanPostProcessor 能正确介入。

接下来,我们沿着这样的路径展开:先看两个模式在类加载、代理生成、@Bean 调用时机上怎么分工;然后用一个能跑的示例验证;最后讨论为什么 Spring 要保留两种模式,以及你在工程上怎么选。

1. 问题场景:为什么同一个 @Bean 方法,两种注解行为不同

先看一个最直接的差异。用最小代码分别验证两种类的内部调用结果:

java 复制代码
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

@Configuration
public class FullConfig {
    @Bean
    public MyBean myBean() {
        return new MyBean();
    }
    @Bean
    public UserOfMyBean userOfMyBean() {
        return new UserOfMyBean(myBean()); // 直接调用 myBean()
    }
}

@Component
public class LiteConfig {
    @Bean
    public MyBean myBean() {
        return new MyBean();
    }
    @Bean
    public UserOfMyBean userOfMyBean() {
        return new UserOfMyBean(myBean()); // 直接调用 myBean()
    }
}

public class MyBean { }
public class UserOfMyBean { 
    private final MyBean myBean;
    public UserOfMyBean(MyBean myBean) { this.myBean = myBean; }
    public MyBean getMyBean() { return myBean; }
}

public class Demo {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        ctx.register(FullConfig.class);
        ctx.refresh();
        Object u1 = ctx.getBean("userOfMyBean");
        Object m1 = ctx.getBean("myBean");
        boolean sameFull = ((UserOfMyBean)u1).getMyBean() == m1;
        System.out.println("Full 模式中 userOfMyBean 内的 myBean 与容器中的 myBean 相同? " + sameFull);

        AnnotationConfigApplicationContext ctx2 = new AnnotationConfigApplicationContext();
        ctx2.register(LiteConfig.class);
        ctx2.refresh();
        Object u2 = ctx2.getBean("userOfMyBean");
        Object m2 = ctx2.getBean("myBean");
        boolean sameLite = ((UserOfMyBean)u2).getMyBean() == m2;
        System.out.println("lite 模式中 userOfMyBean 内的 myBean 与容器中的 myBean 相同? " + sameLite);
        ctx.close();
        ctx2.close();
    }
}
  • 前置环境 :JDK 8+,Spring 5.x 或 6.x,只需要 spring-context 依赖。
  • 输入 :创建两个配置类,一个标注 @Configuration,一个标注 @ComponentDemo 中分别注册两个配置类并刷新容器,然后比较内部引用与容器单例是否相同。
  • 关键步骤 :在两个配置类中让一个 @Bean 方法内部调用另一个 @Bean 方法,这是触发差异的核心。
  • 预期输出Full 模式中 userOfMyBean 内的 myBean 与容器中的 myBean 相同? true,而 lite 模式中 ...? false。也就是说,Full 模式下内部调用返回同一单例,lite 模式下内部调用产生新对象,导致重复创建。
  • 适用场景 :演示 Spring 文档中关于 @Bean 方法内部调用的"标准用法";生产环境如果你在 @Component 中这样调用 @Bean,很可能招来对象重复、状态丢失。
  • 容易改错的地方 :忘记在 Demo 中关闭上下文会浪费资源;两个配置类中 Bean 名称冲突时会报错;如果使用 Spring Boot,通常用 @SpringBootApplication 扫描,但这里的 AnnotationConfigApplicationContext 更贴近容器本身,便于观察。

这个例子说明:@Configuration 下 Spring 会让内部调用也走容器的单例,而 @Component 下内部调用就是普通 Java 方法,每次都执行 new

2. 整体框架:Full 模式与 lite 模式的接入点

Spring 容器解析配置类的核心发生在 ConfigurationClassPostProcessor------这是一个 BeanDefinitionRegistryPostProcessor,它在 Spring 生命周期的早期介入。整体可拆成三部分:

  1. 注册 :容器扫描或显式注册配置类,生成 BeanDefinition
  2. 解析ConfigurationClassPostProcessor 读取类上的注解,判断它是 Full 模式(有 @Configuration)还是 lite 模式(有 @Component@ComponentScan@Import@ImportResource 或类中存在 @Bean 方法等)。
  3. 代理 :若是 Full 模式,会生成 CGLIB 代理类并替换原 BeanDefinition 的 beanClass;若是 lite 模式,类本身不做代理。

之后,当容器创建任何一个 Bean 时,会经历多次 BeanPostProcessor 调用。关键点在于:代理是在 Bean 实例化之前就决定的 ,也就是容器在创建配置类 Bean 时就已经用了代理类。这个时机决定了后续所有 @Bean 方法调用都能被拦截,也决定了 BeanPostProcessor 能应用于被创建的 Bean。

下图展示了流程中最关键的节点:

text 复制代码
容器启动
  |
  扫描/注册候选类
  |
  ConfigurationClassPostProcessor 介入
  |  判断是否为配置类(@Configuration 或 @Component 且含 @Bean 方法)
  |  如果存在 @Configuration -> 标记为 FULL
  |  如果仅 @Component/普通类含 @Bean -> 标记为 LITE
  |
  如果是 FULL:用 CGLIB 代理类替换 BeanDefinition 的 beanClass
  |
  其他 Bean 的创建流程(实例化 -> 依赖注入 -> 初始化)
         |
         | 调用 @Bean 方法时:
         |   FULL 模式:代理拦截 -> 先查容器,没有再创建
         |   LITE 模式:直接执行方法体,new 新对象

这个框架里,两个关键角色是 ConfigurationClassPostProcessorConfigurationClassParser。前者是入口,后者负责读取注解和解析配置类。普通开发者很少直接接触它们,但理解"谁在何时拦截"是必要的。

3. 代理是本篇主角:为什么 CGLIB 代理能实现单例

Spring 对 Full 模式使用的代理不是 JDK 动态代理,而是 CGLIB 代理。CGLIB 能在运行时生成目标类的子类,覆写其中的 @Bean 方法。因为子类可以继承父类的所有非 final 方法,Spring 可以在覆写方法中加入逻辑:先查容器是否已有该名称的 Bean,若有直接返回,若没有才调用父类方法创建。

代理的拦截逻辑发生在配置类实例调用 @Bean 方法时,那个时机正好在 Bean 实例创建的过程之中。Spring 将配置类自身也视为一个 Bean,而它的代理类是在容器扫描该配置类后立即生成的,这样当其他 Bean 需要调用配置类的 @Bean 方法时,Bean 已经被代理替换了。

来看一个简化模型理解代理的"查-建"逻辑:

java 复制代码
// 伪代码,不可直接运行
// 假设 MyBean 是由 @Bean 方法返回的 Bean,容器维护单例池 singletonObjects
public class ConfigurationClassEnhancer {
    // 实际是生成子类,这里用口述说明
    public Object invokeBeanMethod(String beanName, Method method, Object[] args) {
        // 1. 先询问容器是否有该 Bean
        if (beanFactory.containsSingleton(beanName)) {
            return beanFactory.getBean(beanName); // 直接返回容器中的实例
        }
        // 2. 否则调用父类方法创建(即用户写的 new)
        Object result = super.invokeMethod(method, args);
        // 3. 注册或返回,但标准 @Bean 是创建后放入容器
        return result;
    }
}

伪代码 只帮助你理解"拦截后先查容器再决定创建"的流程,真实实现涉及 BeanFactory 内部多个方法,不能直接运行。

为什么用 CGLIB 而不是 JDK 代理?因为 JDK 代理只能基于接口,而配置类通常是普通类,CGLIB 通过生成子类能代理普通类。这也带来限制:如果配置类或 @Bean 方法是 final 的,CGLIB 无法覆写,Spring 就会退回 lite 模式行为(如果仍标记为 Full,启动时会报错或警告)。

4. 实现机制:Spring 如何判断 Full/lite,并生成代理

Spring 通过 ConfigurationClassUtils 判断:如果类上有 @Configuration,则标记 fullConfiguration = true;否则,如果检测到 @Bean 方法、@Component 等,就认为是 lite 配置类。这里有个重要细节:泛型 @Configuration(proxyBeanMethods = true) 是默认值 ,你可以显式设为 false 来关闭代理。

生成代理的入口在 ConfigurationClassPostProcessor 中。它的 enhanceConfigurationClasses 方法遍历所有配置类的 BeanDefinition,找到 full 模式的类,然后通过 ConfigurationClassEnhancer 生成增强子类,并用新的 BeanDefinition 替换原来的。这个动作发生在 Spring 容器生命周期的 invokeBeanFactoryPostProcessors 阶段,比常规 Bean 的实例化要早。

源码节选(Spring 5.3)说明了判断逻辑:

java 复制代码
// 源码节选,来自 ConfigurationClassUtils,不能独立运行
static boolean isFullConfigurationCandidate(AnnotationMetadata metadata) {
    return metadata.isAnnotated(Configuration.class.getName());
}
static boolean isLiteConfigurationCandidate(AnnotationMetadata metadata) {
    if (metadata.isAnnotated(Component.class.getName())) {
        return true;
    }
    // 还有其他判断,如 @ComponentScan、@Import、@Bean 方法等
    return metadata.hasAnnotatedMethods(Bean.class.getName());
}

这里的 metadata 是类元数据的载体,isFullConfigurationCandidate 直接检查是否存在 @Configuration。注意,@Component 本身就会触发 lite 判断,即使类中没有 @Bean 方法也会被当作文档里说的"lite"吗?严格说,@Component 类会变成一个普通 Bean,但它若含 @Bean 方法,那些方法所在类才被认为是 lite 配置类。Spring 在解析时,只有包含 @Bean 方法且不是 full 模式的类才会被提升为 lite 配置类并处理其中的 @Bean 方法。

ConfigurationClassEnhancer 生成代理时,使用 ASM 直接修改字节码,生成新类名如 FullConfig$$EnhancerBySpringCGLIB$$...。你可以在运行时通过 getClass() 查看类名来验证代理存在。

5. BeanPostProcessor 为什么只对代理 Bean 生效

BeanPostProcessor 是 Spring 中一个重要的扩展点,它能在 Bean 初始化前后插入自定义逻辑。你可能会认为:无论 Full 还是 lite,配置类本身也是 Bean,也应能被后处理器拦截。事实是:Full 模式的配置类因为被代理了,它的实例实际上是代理子类的实例,而 BeanPostProcessor 是在 Bean 实例化后调用的,代理类的创建和普通 Bean 一样,也会被后处理器作用于 postProcessBeforeInitializationpostProcessAfterInitialization。但 lite 模式的配置类如果没有代理,它同样会经历后处理器------不是说 lite 模式就完全不受后处理器影响。真正的区别在于:如果后处理器要拦截配置类内部对 @Bean 方法的调用,只有代理才能做到

举个例子,你想写一个后处理器,记录所有 @Bean 方法被调用的次数(比如为了监控配置创建开销)。如果配置类是 lite 模式,后处理器根本看不到内部调用,因为内部调用只是普通 Java 方法;而如果配置类是 Full 模式,代理会拦截方法,你的后处理器会看到两次调用。

更常见的是,Spring 内部的一些后处理器依赖代理来销毁 Bean 时执行清理。例如 CommonAnnotationBeanPostProcessor 会处理 @PreDestroy,它对两种模式都生效,因为它是针对 Bean 的,而不是针对配置方法的。所以"代理时机决定 BeanPostProcessor 是否生效"要区分对象:是配置类本身,还是配置类产生的 Bean。

下面用一段代码演示:在配置类实例化后判断它是不是代理类。

java 复制代码
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

@Configuration
public class ProxyDetectionConfig {
    @Bean
    public String message() { return "hello"; }

    public static class ProxyCheckPostProcessor implements BeanPostProcessor {
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            if (beanName.equals("proxyDetectionConfig")) {
                System.out.println(beanName + " 类名: " + bean.getClass().getName());
                System.out.println(beanName + " 是代理? " + (bean.getClass().getName().contains("CGLIB")));
            }
            return bean;
        }
    }

    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        ctx.register(ProxyDetectionConfig.class);
        ctx.addBeanFactoryPostProcessor(beanFactory -> beanFactory.addBeanPostProcessor(new ProxyCheckPostProcessor()));
        ctx.refresh();
        System.out.println(ctx.getBean(String.class));
        ctx.close();
    }
}
  • 前置环境:同上,不需要额外依赖。
  • 输入 :定义带 @Configuration 的类,注册一个后处理器来打印配置类实例的类名。
  • 关键步骤 :在 postProcessAfterInitialization 检查 beanName 为 proxyDetectionConfig 的 Bean,输出它的真实类名。
  • 预期输出 :你会看到类名包含 $$EnhancerBySpringCGLIB$$,证明代理已生效。
  • 适用场景:用于验证 Spring 是否代理了配置类;排查为什么某些 AOP 特性没有预期效果。
  • 容易改错的地方 :注册后处理的时机要选在 refresh 之前;如果你使用 Spring Boot,检测代码要放在 ApplicationRunner 里,但为了看到更底层,本例直接使用 AnnotationConfigApplicationContext;若你用的是 @Component,则类名不会出现 CGLIB。

6. 完整过程:Bean 创建时代理如何介入

用一个典型流程把上面的点串起来。假设你的 FullConfig 被扫描到,容器启动后有如下过程:

  • 第 0 步:Spring 读取 FullConfig 的元数据,判断带 @Configuration,标记 full。
  • 第 1 步:ConfigurationClassPostProcessor 被调用,它发现 FullConfig 需要增强,于是生成新的 BeanDefinition,其 beanClass 指向新生成的代理类,同时保持属性 source 等。
  • 第 2 步:容器继续实例化其它普通 Bean。在创建 UserOfMyBean 时,Spring 发现它的构造器需要 MyBean,于是解析依赖,发现容器的 BeanDefinition 中有 myBean 的定义(来自于 FullConfig 的 @Bean 方法)。于是容器去创建 myBean,但因为 FullConfig 还没初始化,容器会先实例化 FullConfig------此时实例化的是代理类。
  • 第 3 步:当执行 FullConfig 的 myBean() 方法创建 MyBean 时,代理拦截该方法,容器检查 singletonObjects,没有,于是调用父类方法 new MyBean(),并将结果注册到单例池。
  • 第 4 步:接着创建 UserOfMyBean,在构造中调用 fullConfig.myBean(),代理再次拦截,容器查出已存在 myBean,直接返回,不会再次执行 new

而 lite 模式中,容器会像处理普通类一样创建 LiteConfig 的实例,不是代理。当 UserOfMyBean 的构造调用 liteConfig.myBean() 时,方法体直接执行,新 MyBean 被创建。这个对象与容器管理的 myBean 是两个不同的实例,而容器原来管理的那个在第一次被 @Bean 方法创建时已经放在单例池里。因此出现了重复。

用时序图表示:

text 复制代码
容器             FullConfig 代理            singletonObjects
 |                    |                       |
 |-- 创建 UserBean -->|                       |
 |                    |-- 调用 myBean()------->|
 |                    |                       |-- 查无,没有
 |                    |-- super.myBean()       |  -> new MyBean()
 |                    |-- 注册 MyBean--------->|  (存入)
 |                    |                       |
 |-- 构造参数需要 myBean -->|                    |
 |                    |-- 调用 myBean()------->|
 |                    |                       |-- 查有,返回单例
 |<-- 返回单例 -------|                       |

容器             LiteConfig (无代理)         singletonObjects
 |                    |                       |
 |-- 创建 UserBean -->|                       |
 |                    |-- 调用 myBean()------->|
 |                    |                       |-- 查无,但代理没拦,方法直接 new
 |                    |-- new MyBean()         |  (新建,不是容器管理的)
 |                    |                       |
 |-- 构造参数需要 myBean -->|                    |
 |                    |-- 调用 myBean()------->|
 |                    |-- new MyBean()         |  (又新建一个)
 |<-- 这个新对象 -----|                       |

这也解释了为什么 @Configuration 能保证单例语义:代理确保了 @Bean 方法的调用都走容器。

7. 两种模式的设计取舍:性能、灵活性与兼容

为什么 Spring 不把所有的 @Bean 方法所在类都做代理?主要考虑三点:

  1. 性能:CGLIB 生成子类并在每次调用方法时做额外检查有开销,但在现代 JVM 中,由于内联等优化,开销很小。更多是类加载时的字节码生成耗时。
  2. 灵活性@Configuration 的代理要求方法非 final,且有默认构造(CGLIB 需要)。有些第三方类没有办法满足,因此 lite 模式允许不加 @Configuration 而只是使用 @Bean,Spring 也会处理这些方法,但牺牲了"内部调用走容器"的保证。
  3. 兼容历史 :早期 Spring 没有 lite 的概念,后来为了支持与 @Component 混用而引入了 lite。如果你迁移代码,可能会见到 proxyBeanMethods = false 的写法,那是一种主动放弃代理的节流手段。

设计上,@Configuration(proxyBeanMethods = false) 会强制 lite 模式,即使类上有 @Configuration。这样可以在不需要内部调用依赖注入时才用,提高启动速度。但要注意,一旦你关闭代理,内部调用就变成普通方法,可能引发重复创建;因此一般只用于那些不依赖内部调用的纯配置类。

下面用一个表对比两种模式:

特性 Full 模式(@Configuration 默认) lite 模式(@Component 或 proxyBeanMethods=false)
类级别代理 是,CGLIB
@Bean 方法内部调用 方法会被拦截,返回容器单例(或按要求作用域) 直接执行方法体,可能多次创建新实例
对 final 类/方法的支持 有限制,final 方法不能被覆写 无额外限制
BeanPostProcessor 能否作用于内部调用 能间接影响(代理方法可触发相关处理) 不能拦截内部调用
启动开销 稍高(生成代理类) 更低
典型使用 标准配置类,需要内部依赖注入或作用域代理 轻量配置、第三方类嵌套或只向外暴露 @Bean 方法

第二张表针对是否写明 proxyBeanMethods 的场景:

场景 选择 原因
配置类内部调用其他 @Bean Full(默认) 保证单例、作用域、生命周期
配置类不依赖内部调用,只是为了集中定义 @Bean Lite(proxyBeanMethods=false) 减少启动开销,降低代理覆盖范围
使用 @Component 且想提供 @Bean Lite(强制) 因为无法通过标准注解切换到 Full,除非改成 @Configuration
配置类有 final 方法 proxyBeanMethods=false 或不用 @Configuration 否则启动报错

8. 完整示例二:作用域代理和 Lite 模式的区别

除了单例,作用域(如 @Scope("prototype"))也能体现差异。在 Full 模式下,每次调用 @Bean 方法但请求的是原型 Bean,代理会返回一个新实例;但如果你在 lite 模式内部调用,每次调用同样新建------看起来区别不大。但真正的区别在于,如果你希望内部调用时每次都从容器获取原型实例(例如同一个原型被注入多次),Full 模式可以保证每次调用都走作用域逻辑,而 lite 模式内部调用只是普通方法,不会经过 ScopedProxy 的处理。

这里代码演示:在 @Component 中通过 @Bean 返回同一个原型 Bean,再在另一个 @Bean 方法中注入两次。

java 复制代码
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;

@Component
public class LiteProtoConfig {
    @Bean
    @Scope(BeanDefinition.SCOPE_PROTOTYPE)
    public PrototypeBean prototypeBean() {
        return new PrototypeBean();
    }
    @Bean
    public PrototypeUser prototypeUser() {
        // 注意:这里直接调 prototypeBean() 会得到两个不同实例
        return new PrototypeUser(prototypeBean(), prototypeBean());
    }
}

public class PrototypeBean { }
public class PrototypeUser {
    private final PrototypeBean first, second;
    public PrototypeUser(PrototypeBean first, PrototypeBean second) {
        this.first = first;
        this.second = second;
    }
    public boolean isSame() { return first == second; }
}

public class Demo2 {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        ctx.register(LiteProtoConfig.class);
        ctx.refresh();
        PrototypeUser user = ctx.getBean(PrototypeUser.class);
        System.out.println("Lite 模式中两次注入的是否同一个实例?" + user.isSame());
        ctx.close();
    }
}

如果把类上的 @Component 改为 @Configuration,则两次调用会被代理,它们会从容器中分别获取原型实例,所以仍是不同实例。但如果你在 Full 模式下把一个原型 Bean 直接注入到另一个单例 Bean 的构造中,你会得到一个原始原型实例;如果通过方法参数注入,才会注入代理。总之,这里重点展示内部调用导致多次 new 的现象,并不区分原型。

这个例子的意义在于:不管作用域是什么,lite 模式下内部调用永远是新创建,而且不会经过容器的作用域代理机制。当你在 lite 模式下需要作用域代理时,必须通过 ObjectProvider@Lookup 注入,而不能直接调用方法。

预期输出:

text 复制代码
Lite 模式中两次注入的是否同一个实例?false

因为每次调用都执行 new,两个参数引用了不同实例。如果换成 Full 模式,同样会得到 false,因为原型本来就要求不同,但如果把 prototypeBean() 改为单例,Full 会为两次调用返回同一实例,而 lite 会返回两个不同的。这个对比留给读者自行试验。

9. 完整示例三:生产排障 ------ 从日志定位重复创建

在生产环境,你可能遇到某个 Bean 状态被意外重置,或者数值对不上。排查方法之一是开启 Spring 的 debug 日志,看 Creating shared instance of singleton bean 出现的次数。下面用一个简单例子模拟:先定义 CounterService,它是一个简单的计数器,期望被注入到两个组件,但其中一个是 lite 模式配置,导致两个组件拿到不同实例。

java 复制代码
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ServiceCounter {
    private static final Logger log = LoggerFactory.getLogger(ServiceCounter.class);
    private int count = 0;
    public int next() { return ++count; }
}

@Component
public class BadConfig {
    @Bean
    public ServiceCounter counter() { return new ServiceCounter(); }
    @Bean
    public ComponentA componentA() { return new ComponentA(counter()); }
    @Bean
    public ComponentB componentB() { return new ComponentB(counter()); } // 这里导致创建第二个
}

public class ComponentA {
    private final ServiceCounter counter;
    public ComponentA(ServiceCounter c) { this.counter = c; }
    public int next() { return counter.next(); }
}

public class ComponentB {
    private final ServiceCounter counter;
    public ComponentB(ServiceCounter c) { this.counter = c; }
    public int next() { return counter.next(); }
}

public class Demo3 {
    private static final Logger log = LoggerFactory.getLogger(Demo3.class);
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        ctx.register(BadConfig.class);
        ctx.refresh();
        ComponentA a = ctx.getBean(ComponentA.class);
        ComponentB b = ctx.getBean(ComponentB.class);
        ctx.getBean(ServiceCounter.class); // 强制初始化 counter?不一定,因为 counter 没被注入到别的
        System.out.println("A 计数: " + a.next());
        System.out.println("B 计数: " + b.next());
        ctx.close();
    }
}
  • 前置环境 :注意这里没有 @Scope("singleton"),但默认是单例。
  • 输入:运行 Demo3,观察输出。
  • 关键步骤 :在同一个 @Component 类里,两个不同的 @Bean 方法各调用一次 counter(),因此产生了两个 ServiceCounter 的单例 Bean。但实际上 Spring 只注册了一个名为 counter 的 Bean,第一次调用创建后放入容器,第二次调用因为无代理又创建了另一个,并未放入容器且未覆盖原来的。因此 a.next()b.next() 分别对不同的 counter 计数,输出为 11
  • 预期输出
text 复制代码
A 计数: 1
B 计数: 1
  • 适用场景:诊断两个组件状态不同步问题。
  • 容易改错的地方 :如果你在 componentAcomponentB 的方法内部直接注入参数 ServiceCounter,例如 ComponentA(@Autowired ServiceCounter counter),那么就不会出现重复创建,因为依赖是通过容器注入的。但这里内部调用绕过了依赖注入,是特意设计的陷阱。

排障时,可检查日志中的 bean 创建记录。如果看到同一个类创建了多次,或者有多个不同的实例但名字相同,就要检查你的配置类是否用了 lite 模式。

10. 常见误区与边界条件

  1. 误区一:@Component 中的 @Bean 方法也受 @Bean 方法所在类影响,但 Spring 也会解析它们。 对,Spring 会处理它们,但不会做代理,因此许多针对配置类的特性失效。
  2. 误区二:@Configuration 只能用于配置类。 实际上 @Configuration 本身也包含 @Component 元注解,所以它也是一个组件。
  3. 误区三:代理会导致方法调用开销巨大。 现代 JVM 可以内联这些检查,实际开销微乎其微;多数应用启动时间主要花费是 I/O。
  4. 误区四:@Configuration(proxyBeanMethods=false) 是一种推荐的性能调优。 它确实能减少启动时间,但会牺牲内部调用的一致性;只有当你的配置类没有内部调用时才推荐。
  5. 边界条件 :如果 @Bean 方法返回 final 方法或类,CGLIB 可能无法代理,Spring 会向前兼容?实际上,如果类是 final,Spring 无法生成子类,会直接报错或降级为 lite,具体取决于版本。proxyBeanMethods=true 时遇到 final 方法可能直接报错。
  6. 与 BeanPostProcessor 的关系:一个 Bean 创建后,会经过后处理的 before/after 方法;代理类本身也是由 Spring 创建,因此代理对象也能被后处理。但如果你希望在配置类内部拦截,需要后处理作用于整个 FactoryBean?实际上,后处理器是拦截 Bean 实例的,而代理在 Bean 实例化后才生成;没有后处理器能拦截到代理类自身的方法调用,除非那个 Bean 本身就是 FactoryBean。

11. 生产实践建议与排障清单

选择建议 :当你要定义一个独立的配置模块,内部有交叉引用,请使用 @Configuration;当你在一个组件类中附带提供少量 @Bean(比如在某个 Service 里加个辅助 Bean),且不引用当前类的其它 @Bean,可以安全使用 lite。

排障清单

症状 可能原因 检查方向
同名单例被多次实例化 在 lite 模式内部调用了 @Bean 看类是否有 @Configuration;开 debug 日志看创建次数
切面不生效或代理失效 某些 Bean 是 lite 生成,未做 AOP 代理 检查类标注;尝试改为 @Configuration;或用 @Scope(proxyMode=...)
启动报错"Cannot subclass final class" @Bean 方法所在类 final 去掉 final 或设置 proxyBeanMethods=false
配置类上的 @PostConstruct 没执行 可能是代理造成的?实际上代理不会跳过初始化,但如果你把 @Configuration 当成普通 @Component 用可能混入问题 检查 bean 名称和是否被扫描

遇到问题优先查看:Spring 日志是否提示"ConfigurationClassEnhancer"相关错误;打印 bean 类名看是否带 CGLIB。

12. 面试/复盘问题

  1. 你如何理解"代理时机"?它会怎样影响 Bean 的创建过程?
  2. 为什么 @Component@Configuration 在处理 @Bean 方法时不同?
  3. 你会在哪些场景下将 @ConfigurationproxyBeanMethods 设为 false?请给出一个真实需求。
  4. 如果你发现两个单例 Bean 实际上不是同一个实例,你会先用什么方法排查?
  5. 请尝试不使用 IDE 调试,说明如何通过查看一个 Bean 实例的 class 来判断它是否被 Spring 代理。

13. 总结:一张图区分模式与选择

`可以用文本图表示:

text 复制代码
选择配置类注解
  |
  |--- 需要内部调用其他 @Bean 方法,并希望由容器管理?
  |       |--- 是 -> 使用 @Configuration(Full 模式)-> Spring 生成 CGLIB 代理 -> 拦截内部调用
  |       |--- 否 -> 可以使用 @Component 或 @Configuration(proxyBeanMethods=false)(lite 模式)-> 无代理
  |
  |--- 切面或后处理器需要拦截内部调用?
  |       |--- 是 -> 推荐 Full 模式,因为代理能让你看到内部调用
  |       |--- 否 -> lite 亦可

核心要点回顾:

  • 代理时机:Spring 在创建配置类 Bean 之前就决定是否生成 CGLIB 子类,Full 模式在创建配置类实例时已经是代理类。
  • CGLIB 代理 :通过覆写 @Bean 方法,在调用时先查容器,从而保证单例、作用域、生命周期拦截得以生效。
  • lite 模式 :没有代理,@Bean 方法只是普通方法,内部调用会导致对象重复创建。
  • BeanPostProcessor:它应用于 Bean 实例,与配置类代理无直接关系;但代理能让你自定义后处理器观察到内部调用。
  • 何时选择 :大多数应用配置都适合用 @Configuration;只有当你确定不需要内部调用协作时才关闭代理或用 @Component

最后记住一句:在 @Bean 方法内直接调用另一个 @Bean 方法,如果你不想要重复对象,请使用 @Configuration,并确保代理可用。

参考资料

  • Spring Framework Documentation: Core - "Bean Overview" 与 "Java-based Container Configuration"。
  • Spring Framework 源码(spring-context 模块)ConfigurationClassPostProcessorConfigurationClassEnhancer 类。
  • Spring Framework Reference(当前版本)中的 "Using the @Configuration annotation" 小节。
  • org.springframework.context.annotation 包中注解文档(Java Doc)。
相关推荐
qq_452396231 小时前
第十二篇:《数据采集:Grafana Alloy、Fluent Bit、Vector 的选型与配置》
java·贪心算法·grafana
明月_清风1 小时前
字符串匹配四大经典算法:BF、RK、BM、KMP 到底有什么区别?
后端·算法
卷无止境1 小时前
测试全绿,功能能跑,代码却烂到没法上线:AI编程助手留下的十个坑
后端·python
珍珠先生1 小时前
P16 · IDEA 启动报错:找不到或无法加载主类
java
子非鱼a1 小时前
【WEB】[RoarCTF 2019]Easy Java
java·开发语言
西峰u1 小时前
电商项目测试报告
java·项目测试报告
lemon_sjdk2 小时前
各种Binding(1)
java·javafx·绑定·源码解析
李少兄2 小时前
记一次 IDEA 打开 Vue 项目后目录“闪现即消失“ 问题排查
java·vue.js·intellij-idea
明月_清风2 小时前
多模式字符串匹配:Trie 与 AC 自动机
后端·算法