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,一个标注@Component。Demo中分别注册两个配置类并刷新容器,然后比较内部引用与容器单例是否相同。 - 关键步骤 :在两个配置类中让一个
@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 生命周期的早期介入。整体可拆成三部分:
- 注册 :容器扫描或显式注册配置类,生成
BeanDefinition。 - 解析 :
ConfigurationClassPostProcessor读取类上的注解,判断它是 Full 模式(有@Configuration)还是 lite 模式(有@Component、@ComponentScan、@Import、@ImportResource或类中存在@Bean方法等)。 - 代理 :若是 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 新对象
这个框架里,两个关键角色是 ConfigurationClassPostProcessor 和 ConfigurationClassParser。前者是入口,后者负责读取注解和解析配置类。普通开发者很少直接接触它们,但理解"谁在何时拦截"是必要的。
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 一样,也会被后处理器作用于 postProcessBeforeInitialization 和 postProcessAfterInitialization。但 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 方法所在类都做代理?主要考虑三点:
- 性能:CGLIB 生成子类并在每次调用方法时做额外检查有开销,但在现代 JVM 中,由于内联等优化,开销很小。更多是类加载时的字节码生成耗时。
- 灵活性 :
@Configuration的代理要求方法非 final,且有默认构造(CGLIB 需要)。有些第三方类没有办法满足,因此 lite 模式允许不加@Configuration而只是使用@Bean,Spring 也会处理这些方法,但牺牲了"内部调用走容器"的保证。 - 兼容历史 :早期 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 计数,输出为1和1。 - 预期输出:
text
A 计数: 1
B 计数: 1
- 适用场景:诊断两个组件状态不同步问题。
- 容易改错的地方 :如果你在
componentA和componentB的方法内部直接注入参数ServiceCounter,例如ComponentA(@Autowired ServiceCounter counter),那么就不会出现重复创建,因为依赖是通过容器注入的。但这里内部调用绕过了依赖注入,是特意设计的陷阱。
排障时,可检查日志中的 bean 创建记录。如果看到同一个类创建了多次,或者有多个不同的实例但名字相同,就要检查你的配置类是否用了 lite 模式。
10. 常见误区与边界条件
- 误区一:
@Component中的@Bean方法也受@Bean方法所在类影响,但 Spring 也会解析它们。 对,Spring 会处理它们,但不会做代理,因此许多针对配置类的特性失效。 - 误区二:
@Configuration只能用于配置类。 实际上@Configuration本身也包含@Component元注解,所以它也是一个组件。 - 误区三:代理会导致方法调用开销巨大。 现代 JVM 可以内联这些检查,实际开销微乎其微;多数应用启动时间主要花费是 I/O。
- 误区四:
@Configuration(proxyBeanMethods=false)是一种推荐的性能调优。 它确实能减少启动时间,但会牺牲内部调用的一致性;只有当你的配置类没有内部调用时才推荐。 - 边界条件 :如果
@Bean方法返回final方法或类,CGLIB 可能无法代理,Spring 会向前兼容?实际上,如果类是 final,Spring 无法生成子类,会直接报错或降级为 lite,具体取决于版本。proxyBeanMethods=true时遇到 final 方法可能直接报错。 - 与 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. 面试/复盘问题
- 你如何理解"代理时机"?它会怎样影响 Bean 的创建过程?
- 为什么
@Component和@Configuration在处理@Bean方法时不同? - 你会在哪些场景下将
@Configuration的proxyBeanMethods设为 false?请给出一个真实需求。 - 如果你发现两个单例 Bean 实际上不是同一个实例,你会先用什么方法排查?
- 请尝试不使用 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 模块)
ConfigurationClassPostProcessor、ConfigurationClassEnhancer类。 - Spring Framework Reference(当前版本)中的 "Using the @Configuration annotation" 小节。
org.springframework.context.annotation包中注解文档(Java Doc)。