腾讯二面:@Bean 与 @Component 用在同一个类上,会怎么样?
一、面试场景还原:这道题到底在问什么
先还原一个典型的腾讯二面现场。面试官已经问完 Spring IOC、循环依赖、Bean 生命周期这些常规问题,你回答得都比较顺。这时候他突然抛出一句:
“你说 @Component 是类级注解,@Bean 是方法级注解。那我问你,如果把 @Bean 和 @Component 用在同一个类上,会发生什么?”
很多候选人的第一反应是懵的。因为平常写代码,几乎没有人会把这两个注解写在一起,甚至第一感觉是“这写法根本不存在”。其实这正是这道题的陷阱所在:题目本身有歧义。面试官真正想考察的,不是你能不能背出某个标准答案,而是:
你是否真正理解 @Bean 与 @Component 的作用层级差异;
你是否知道 @Bean 的 @Target 限制,能不能一眼看出“把 @Bean 写在类上”是非法写法;
你是否了解 @Component 类中定义 @Bean 方法时,Spring 的Lite 模式(轻量模式)机制;
你是否理解 Full 模式与 Lite 模式的本质区别,以及 CGLIB 代理在其中扮演的角色。
在正式展开之前,先把核心结论摆出来,方便你建立全局认知:
如果“用在同一个类上”指的是把 @Bean 直接标注在类上,那么这段代码根本编译不过。因为 @Bean 的 @Target 是 METHOD 和 ANNOTATION_TYPE,不包含 TYPE,编译器会直接报错。
如果“用在同一个类上”指的是类上标 @Component、方法上标 @Bean,那么代码可以正常编译运行。此时类本身会被组件扫描注册为一个 Bean,而 @Bean 方法的返回值也会被注册为另一个 Bean,容器里会出现两个甚至更多 Bean。这种类属于 Spring 的Lite 模式,不会被 CGLIB 代理增强。
Lite 模式最大的坑:该类内部 @Bean 方法之间互相调用时,走的是普通 Java 方法调用,不会经过容器代理。因此被调用的 @Bean 方法可能被重复执行,产生多个实例,破坏单例语义。
下面我们从基础概念开始,一层层把这个问题拆开讲透。
二、基础概念:@Bean 和 @Component 到底是什么
要想把这道面试题回答漂亮,首先必须把两个注解的定义、作用和底层机制彻底搞清楚。很多同学写了好几年 Spring,却从来没认真看过这两个注解的源码定义,这正是面试失分的根源。
2.1 @Component 的定义与作用
先看 @Component 的源码定义,这里是 Spring 5.x 的版本。我故意保留完整注释结构,方便你观察它的 @Target:
java
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Indexed public @interface Component { /** * The value may indicate a suggestion for a logical component name, * to be turned into a Spring bean in case of an autodetected component. * @return the suggested component name, if any (or empty String otherwise) */ String value() default ""; }从定义可以读出三个关键信息:
作用目标:ElementType.TYPE,也就是类、接口、枚举。这决定了它只能写在类型声明上,不能写在方法、字段或局部变量上。
保留策略:RetentionPolicy.RUNTIME,运行时保留,Spring 可以通过反射读取。
唯一属性:value,用于指定 Bean 名称。如果不指定,默认 Bean 名称是类名的首字母小写形式,比如 UserService 对应 userService。
@Component 是“通用组件”标记。它本身不携带业务语义,它的三个派生注解才表达具体语义:
@Service:标注业务逻辑层组件;
@Repository:标注数据访问层组件,并为持久层异常提供翻译机制;
@Controller:标注 Web 控制层组件。
这三个注解内部都标注了 @Component,因此只要类路径扫描开启,它们都会被 Spring 容器自动发现并注册。以 @Service 为例:
java
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface Service { @AliasFor(annotation = Component.class) String value() default ""; }这里涉及到一个重要概念:注解的“元注解”传递。Spring 在扫描类的时候,并不是只看类上有没有直接标 @Component,而是会递归检查类上的注解、以及注解上的注解。因此标了 @Service 的类,同样会被识别为候选组件。这个机制由 TypeFilter 和 MetadataReader 配合完成,后文源码部分会详细展开。
所以 @Component 的定位非常明确:它是类级标记,作用是让类被组件扫描机制自动识别并注册为 Spring Bean。这是一种“隐式声明”,即你不需要写任何配置代码,只要把注解贴上去,容器就能自动发现它。
2.2 @Bean 的定义与作用
再看 @Bean 的源码定义。同样保留了关键注释:
java
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Bean { @AliasFor("name") String[] value() default {}; @AliasFor("value") String[] name() default {}; /** * Are dependencies to be injected via conventional autowiring? */ @Deprecated boolean autowireCandidate() default true; /** * The optional name of a method to call on the bean instance during * initialization. Not commonly used, given that the method may be * called programmatically directly within the body of a Bean-annotated * method. */ String initMethod() default ""; /** * The optional name of a method to call on the bean instance upon * closing the application context. */ String destroyMethod() default AbstractBeanDefinition.INFER_METHOD; }关键信息如下:
作用目标:ElementType.METHOD 和 ElementType.ANNOTATION_TYPE。这就是整篇文章讨论的第一个重点:@Bean 可以写在方法上,也可以写在自定义注解定义上(让该自定义注解具备 @Bean 语义),但绝对不能直接写在类上。
value 与 name 互为别名(通过 @AliasFor 关联),可以指定一个或多个 Bean 名称。不指定时,默认使用方法名作为 Bean 名称。
initMethod 和 destroyMethod:用于指定初始化方法和销毁方法。其中 destroyMethod 的默认值是 AbstractBeanDefinition.INFER_METHOD,即让 Spring 自动推断,当方法名为 close 或 shutdown 时自动作为销毁回调。
@Bean 的定位与 @Component 完全不同:它是方法级标记,作用是把方法的返回值注册为 Spring Bean。它表达的是一种“显式声明”,即容器不负责扫描和推断,而是把方法当作一个“工厂方法”,把方法执行的产物交给容器管理。
典型用法是在 @Configuration 类中声明第三方库对象:
java
@Configuration public class DataSourceConfig { @Bean public DataSource dataSource() { HikariDataSource dataSource = new HikariDataSource(); dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/demo"); dataSource.setUsername("root"); dataSource.setPassword("123456"); return dataSource; } }这里你无法修改 HikariDataSource 的源码去贴 @Component,因为它是第三方类库的类。这时 @Bean 就派上了用场:通过工厂方法把第三方对象纳入 Spring 容器管理。
2.3 两者最本质的区别
把两个注解放在一起对比,差别非常清晰。这里用一张表格总结:
| 对比维度 | @Component | @Bean |
|---|---|---|
| 作用层级 | 类级别 | 方法级别 |
| @Target 范围 | TYPE | METHOD、ANNOTATION_TYPE |
| 注册方式 | 隐式扫描 | 显式工厂方法 |
| 典型场景 | 自己编写的业务类 | 第三方类、多实例、条件装配 |
| 默认 Bean 名称 | 类名首字母小写 | 方法名 |
| 能否直接写在同一位置 | 不能写在方法上 | 不能写在类上 |
理解了这个表格,就抓住了这道面试题的第一层得分点:两个注解本来就不是一个层级的东西,根本不存在“同时标注在同一个位置”的规范写法。所谓的“用在同一个类上”,实际是一个口语化的表述,必须具体问题具体分析。
三、问题拆解:同一个类上的三种可能解读
面试官问出“@Bean 与 @Component 用在同一个类上会怎么样”时,由于中文表达的歧义性,至少存在以下三种理解。高水平的回答应该先把歧义拆开,再逐一给出结论,这样既体现了严谨性,也展示了知识广度。
3.1 解读一:@Bean 直接标注在类上
这是最字面、最天真的理解:写一个类,类上同时有 @Bean 和 @Component 两个注解,像这样:
java
@Bean @Component public class UserService { // ... }如果你这么写,答案非常干脆:编译失败。因为 @Bean 的 @Target 中不包含 ElementType.TYPE。这是注解处理器和编译器共同约束的,跟你写 @Override 在类上是一个道理。IDE 会立刻提示错误,Maven 或 Gradle 构建时也会在编译阶段直接终止。
所以这一层答案是送分的。如果你连 @Target 都不知道,面试官心里基本已经把你归到“只会用不会看源码”的那一类了。
3.2 解读二:@Component 类中定义 @Bean 方法
第二种理解最接近面试官的真实意图:类上标 @Component,类内部某个方法标 @Bean,像这样:
java
@Component public class MixedConfig { @Bean public DataSource dataSource() { return new DriverManagerDataSource(); } @Bean public JdbcTemplate jdbcTemplate() { return new JdbcTemplate(dataSource()); } }这种代码是可以编译、可以运行的。运行后会发生以下几件事:
MixedConfig 这个类本身,因为贴了 @Component,会被包扫描发现并注册为一个 Bean,名称是 mixedConfig。
dataSource() 方法因为贴了 @Bean,它的返回值会被注册为名为 dataSource 的 Bean。
jdbcTemplate() 方法的返回值会被注册为名为 jdbcTemplate 的 Bean。
所以容器里总共会出现三个 Bean:mixedConfig、dataSource、jdbcTemplate。这就回答了“同一个类上”最粗暴的疑问:类本身是一个 Bean,方法产物又是另外的 Bean。
但事情远没有这么简单。这种写法触发了 Spring 的Lite 模式(轻量配置模式)。在 Lite 模式下,@Bean 方法之间的相互调用不会经过 CGLIB 代理,上面的 dataSource() 在 jdbcTemplate() 内部被调用时,就是一次普通的 Java 方法调用,会重新创建一个 DataSource 对象,而不是从容器中取单例。这是本道面试题最深层的考点,后文会重点剖析。
3.3 解读三:类同时被 @Component 和 @Configuration 标注
第三种理解更偏“坑中坑”:一个类同时标注 @Component 和 @Configuration。为什么要把 @Configuration 扯进来?因为面试中经常有候选人把 @Component 和 @Configuration 混为一谈,认为它们都是“组件”,可以随便替换。实际上它们对应的处理模式完全不同。
java
@Component @Configuration public class AmbiguousConfig { @Bean public DataSource dataSource() { return new DriverManagerDataSource(); } }这种写法的结果是:以 @Configuration 为准。Spring 在判断配置类模式时,会优先检查 @Configuration 注解。只要类上有 @Configuration,无论是否还有 @Component,它都被判定为Full 模式(完整配置模式),并对其进行 CGLIB 代理增强。此时 @Component 的存在并不会改变处理逻辑,只是显得冗余且具有误导性。
不过这里有个细节值得注意:@Configuration 注解本身被 @Component 元注解标注了。看源码:
java
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface Configuration { @AliasFor(annotation = Component.class) String value() default ""; boolean proxyBeanMethods() default true; }所以一个 @Configuration 类天然就是一个 @Component 组件。也正因为如此,@Configuration 类本身也会被注册为一个 Bean。但它在容器中的角色和普通 @Component 完全不同:它会被 Spring 当作“配置元数据载体”来特殊处理。
把三种解读汇总成一张决策表,是这道题回答的结构骨架:
| 解读 | 代码形态 | 结果 | 容器行为 |
|---|---|---|---|
| 解读一 | @Bean 直接标在类上 | 编译失败 | 编译阶段直接拦截,Spring 容器无法启动 |
| 解读二 | @Component 类中定义 @Bean 方法 | 正常编译运行 | 类本身与所有 @Bean 方法返回值都会被注册;类按 Lite 模式处理,无 CGLIB 代理,内部相互调用可能重复创建实例 |
| 解读三 | 类同时标 @Component 和 @Configuration | 正常编译运行 | 以 @Configuration 为准,按 Full 模式处理,类被 CGLIB 代理增强,@Bean 单例语义得到保证,@Component 显得冗余 |
上面这张表已经把题目可能覆盖的三条路径全部收拢。接下来进入这道题最深层的部分:@Component 类中定义 @Bean 方法为何会触发 Lite 模式,以及 Full 模式与 Lite 模式在源码层面的本质差异。这也是腾讯二面真正能拉开差距的地方。
四、核心机制:Full 模式与 Lite 模式的本质区别
前面反复提到两个词:Full 模式和 Lite 模式。它们是 Spring 对“配置类”的内部标记,直接决定了 @Bean 方法是否会被代理增强。搞清楚这两个模式,就抓住了这道面试题最硬核的得分点。
4.1 什么是 Full 模式和 Lite 模式
Spring 在处理配置类时,会为 BeanDefinition 打上一个属性标记,属性值是以下两者之一:
CONFIGURATION_CLASS_FULL:完整配置模式。该类会通过 CGLIB 生成子类代理,@Bean 方法之间的内部调用会被拦截,从而保证 @Bean 方法的单例语义。
CONFIGURATION_CLASS_LITE:轻量配置模式。类本身也会被注册,但不会生成 CGLIB 代理,@Bean 方法之间的调用就是普通 Java 方法调用,容器只管注册每个 @Bean 方法返回的对象。
通俗地说:Full 模式是一个“被 Spring 加了智能代理”的配置类,而 Lite 模式是一个“普通类,只是顺带提供了工厂方法”。
4.2 判定逻辑:一个类如何被标记为 Full 或 Lite
判定入口在 ConfigurationClassUtils.checkConfigurationClassCandidate 方法中。核心逻辑简化后如下:
java
public static boolean checkConfigurationClassCandidate( BeanDefinition beanDef, MetadataReader metadataReader) { AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); Map<String, Object> config = metadata.getAnnotationAttributes(Configuration.class.getName()); if (config != null && !Boolean.FALSE.equals(config.get("proxyBeanMethods"))) { beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL); } else if (config != null || isConfigurationCandidate(metadata)) { beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE); } else { return false; } return true; }把这段逻辑翻译成人话:
如果类上有 @Configuration,并且 proxyBeanMethods 不是显式地等于 false,就标记为 Full 模式。
如果类上有 @Configuration 但 proxyBeanMethods = false,或者类上虽然没有 @Configuration、但满足“配置候选”条件,就标记为 Lite 模式。
两条都不满足,说明它不是配置类,直接跳过。
所谓“配置候选”,由 isConfigurationCandidate 方法判断:类上只要有 @Component、@ComponentScan、@Import、@ImportResource 中的任意一个,或者类中存在 @Bean 方法,都会被认定为候选。
所以,本文 3.2 节中的 @Component 类,正是因为没有 @Configuration,却又有 @Bean 方法,于是被判定为 Lite 模式。这就是“同一个类上”写法的底层归宿。
4.3 Full 模式:CGLIB 代理如何保证单例
Full 模式的核心武器是 ConfigurationClassEnhancer。它会为 @Configuration 类生成一个 CGLIB 子类,子类重写所有非 final 的 @Bean 方法。这样,当外部从容器获取 Bean 时,走的其实是代理对象,而代理会在方法调用链上做文章。
方法调用被 BeanMethodInterceptor 拦截,核心逻辑简化如下:
java
public Object intercept(Object enhancedConfigInstance, Method beanMethod, Object[] beanMethodArgs, MethodProxy cglibMethodProxy) throws Throwable { String beanName = BeanAnnotationHelper.determineBeanNameFor(beanMethod); if (isCurrentlyInvokedFactoryMethod(beanMethod)) { return cglibMethodProxy.invokeSuper(enhancedConfigInstance, beanMethodArgs); } return this.beanFactory.getBean(beanName); }关键是 isCurrentlyInvokedFactoryMethod 这个判断。Spring 用一个 ThreadLocal 标记“当前正在执行的工厂方法”。当容器第一次调用 @Bean 方法创建 Bean 实例时,该标记为 true,于是走上半段逻辑,真正执行方法体,拿到新对象;而如果这个调用发生在另一个 @Bean 方法内部,比如 jdbcTemplate() 方法里调用 dataSource(),此时标记不是“当前工厂方法”,于是走下半段逻辑,直接从容器里取已经存在的 dataSource 单例。
这样一来,不论 @Bean 方法被同一个配置类内部调用多少次,返回的都是容器里的同一个单例对象,单例语义得到保证。
4.4 proxyBeanMethods=false 的意义
Spring 5.2 之后,@Configuration 增加了 proxyBeanMethods 属性,允许显式关闭代理增强:
java
@Configuration(proxyBeanMethods = false) public class LiteConfig { @Bean public DataSource dataSource() { return new DriverManagerDataSource(); } @Bean public JdbcTemplate jdbcTemplate() { return new JdbcTemplate(dataSource()); } }此时虽然类上有 @Configuration,但因为 proxyBeanMethods = false,它仍然被判定为 Lite 模式。这么设计的目的非常明确:用代理保证单例是有成本的。CGLIB 代理会生成子类、拦截方法调用,启动时更慢,也要求配置类不能是 final、配置方法不能是 private 或 final。如果你的配置类里 @Bean 方法之间没有相互调用,就不需要代理,关闭它可以换来更轻量的启动过程。
所以,面试中如果能把 proxyBeanMethods=false 点出来,面试官会立刻意识到你不仅知道结论,还知道结论背后的权衡。
五、源码走读:@Bean 方法从注册到调用
只看结论还是不够。接下来沿着 Spring 启动流程,把 @Bean 方法从“被发现”到“被注册”再到“被调用”的过程串一遍。这样你回答时就不是背答案,而是带着源码主线去解释。
5.1 入口:ConfigurationClassPostProcessor
Spring 容器在 refresh 阶段会执行 BeanFactoryPostProcessor,其中最核心的一个就是 ConfigurationClassPostProcessor。它负责两件事:
找出所有的配置类候选;
调用 ConfigurationClassParser 解析这些类上的 @ComponentScan、@Import、@Bean 等元数据。
也正是这个处理器内部会调用前文提到的 ConfigurationClassUtils.checkConfigurationClassCandidate,先给候选类打上 Full 或 Lite 标记。
5.2 解析与注册:@Bean 方法如何变成 BeanDefinition
标记完成后,ConfigurationClassParser 会把每个 @Bean 方法包装成一个 BeanMethod 对象。随后 ConfigurationClassBeanDefinitionReader 通过 loadBeanDefinitionsForBeanMethod,把这些方法注册成一个个 BeanDefinition。
这里有一个非常关键的设计:@Bean 方法注册出的 BeanDefinition,其 factoryBeanName 指向配置类本身,factoryMethodName 指向对应的方法名。也就是说,容器并不直接 new 出这个 Bean,而是“从某个配置类实例上调用某个工厂方法”来创建它。
对于 Full 模式,容器持有的配置类实例是 CGLIB 代理对象;对于 Lite 模式,就是普通实例。差别最终体现在:调用工厂方法时,有没有经过 BeanMethodInterceptor 的拦截。
5.3 Lite 模式为什么会有坑
把上面的流程对齐到 3.2 节的 MixedConfig,就能清楚看到问题:
容器启动时,先创建 mixedConfig 这个 Bean;因为它是 Lite 模式,创建的就是普通实例,没有代理。
随后容器要创建 jdbcTemplate,于是调用 mixedConfig.jdbcTemplate() 工厂方法。
这个方法内部又调用了 this.dataSource()。由于没有 CGLIB 代理,这次调用就是普通 Java 方法调用,直接 new DriverManagerDataSource(),不会去容器拿单例。
结果就是:容器里的 dataSource 是一个对象,而 jdbcTemplate 内部持有的 dataSource 是另一个对象,两个实例并存。这不是语法错误,但违反了我们对单例的预期,埋下了非常隐蔽的 Bug。
六、实战验证:把结论跑出来
面试最怕“只听说过,没跑过”。下面用三个实验把前面的结论逐一验证,你完全可以整理成一个本地 Demo 自己跑一遍,回答时信手拈来。
6.1 实验一:@Component + @Bean 破坏单例
先写一个 @Component 配置类,并用计数器记录 dataSource() 到底被调用了几次:
java
@Component public class MixedConfig { private static final AtomicInteger COUNT = new AtomicInteger(0); @Bean public DataSource dataSource() { System.out.println("dataSource() 被调用,第 " + COUNT.incrementAndGet() + " 次"); return new DriverManagerDataSource(); } @Bean public JdbcTemplate jdbcTemplate() { System.out.println("jdbcTemplate() 被调用"); return new JdbcTemplate(dataSource()); } }再用主程序获取容器中的两个 Bean 并做身份比较:
java
public class LiteModeDemo { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MixedConfig.class); JdbcTemplate jdbcTemplate = context.getBean(JdbcTemplate.class); DataSource dataSource = context.getBean(DataSource.class); System.out.println("容器中的 dataSource 是否等于 jdbcTemplate 内部的 dataSource:"); System.out.println(dataSource == jdbcTemplate.getDataSource()); context.close(); } }控制台输出会清楚显示:dataSource() 被调用了两次,且最后的相等判断为 false。这就是 Lite 模式单例被“悄悄破坏”的实锤证据。
6.2 实验二:@Configuration Full 模式保证单例
把上面的 @Component 改成 @Configuration,其余代码完全不变,再运行:
java
@Configuration public class FullConfig { private static final AtomicInteger COUNT = new AtomicInteger(0); @Bean public DataSource dataSource() { System.out.println("dataSource() 被调用,第 " + COUNT.incrementAndGet() + " 次"); return new DriverManagerDataSource(); } @Bean public JdbcTemplate jdbcTemplate() { System.out.println("jdbcTemplate() 被调用"); return new JdbcTemplate(dataSource()); } }这次控制台里 dataSource() 只会被调用一次,最终相等判断为 true。因为 CGLIB 代理拦住了 jdbcTemplate() 内部的 dataSource() 调用,直接从容器拿到了同一个单例。
6.3 实验三:@Configuration(proxyBeanMethods=false) 又回到 Lite 行为
再把 FullConfig 改为 @Configuration(proxyBeanMethods = false),运行结果会和实验一一样:dataSource() 被调用两次,身份比较为 false。这说明即使写了 @Configuration,只要明确关闭 proxyBeanMethods,Spring 就不会做代理增强。
三个实验连起来,正好把“Full 模式、Lite 模式、proxyBeanMethods=false”这条线完整闭环。
七、面试现场:如何组织一个满分答案
有了前面的知识储备,最后把这道题的回答框架固化下来。腾讯二面时间有限,回答要有层次、有亮点,而不是把所有细节一股脑倒出来。
7.1 四步答题法
第一步,先澄清歧义:先说明“用在同一个类上”至少有两种主流理解:一是把 @Bean 标在类上,二是类标 @Component、方法标 @Bean。这一步体现了严谨。
第二步,快速给结论:第一种编译失败,原因在 @Target;第二种正常运行,类和方法产物都会被注册,进入 Lite 模式。
第三步,切入核心机制:重点讲 Full 模式与 Lite 模式,说明 @Configuration 触发 CGLIB 代理、@Component 只是普通类,并点出内部调用重复创建实例的坑。
第四步,给出实践建议:配置类统一用 @Configuration;如果 @Bean 方法之间没有互相调用,可以用 @Configuration(proxyBeanMethods = false) 减少代理开销;不要为了省事在 @Component 里写 @Bean 方法。
7.2 一句话高分开场
“这道题要先拆歧义:@Bean 标在类上根本编译不过,因为它的 @Target 不含 TYPE;如果是类标 @Component、方法标 @Bean,那它会进入 Lite 模式,类本身和 @Bean 返回值都会被注册,但内部方法互调不会走代理,可能破坏单例——这正是 @Configuration 的 Full 模式要用 CGLIB 增强的原因。”
7.3 高频追问与应对
追问 1:@Component 和 @Configuration 都能注册 Bean,为什么还要区分?
回答:@Component 只是让类被扫描注册;@Configuration 除了注册,还会触发 Full 模式代理,保证 @Bean 方法之间的调用返回容器单例。两者语义不同,不能随意替换。
追问 2:为什么 Spring 5.2 要引入 proxyBeanMethods?
回答:CGLIB 代理有启动开销和限制(不能是 final 类、方法不能是 private/final)。很多配置类里 @Bean 方法互不调用,不需要代理。引入 proxyBeanMethods=false 让开发者按需选择,兼顾灵活性和性能。
追问 3:Lite 模式下重复创建实例,会有什么实际影响?
回答:如果 @Bean 返回的是有状态对象(如数据源、连接池、缓存),重复创建会导致资源浪费、连接数翻倍、配置不一致;如果对象被注册为单例并被其他地方引用,还会出现“容器里的实例和业务持有的实例不是同一个”的隐蔽 Bug。
八、总结
这道腾讯二面题看似冷门,实际上串联了 Spring 注解定义、配置类解析、Full/Lite 模式、CGLIB 代理、BeanDefinition 注册、单例语义等一整套核心知识。能把这道题讲透,说明你不是只会用 Spring,而是真正理解了它的设计取舍。
核心结论回顾:
@Bean 不能标在类上,@Target 只允许 METHOD 和 ANNOTATION_TYPE;
@Component 类中定义 @Bean 方法会进入 Lite 模式,类和方法返回值都会被注册;
Lite 模式下 @Bean 方法互调不走代理,可能重复创建实例;
@Configuration 默认触发 Full 模式,通过 CGLIB 代理保证 @Bean 单例;
@Configuration(proxyBeanMethods = false) 可显式关闭代理,回到 Lite 行为;
生产实践:配置类统一用 @Configuration,@Component 只用于普通组件。