ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

字节面试 Spring 容器实例化Bean的方法,我这样回答直接给了37K

字节面试 Spring 容器实例化Bean的方法,我这样回答直接给了37K

一、面试现场:从一道题讲起

“Spring 容器实例化 Bean 的方法有哪些?”这是字节跳动后端面试中一道高频基础题。不少候选人只答出“无参构造 + @Bean”就停了,面试官继续追问“那 FactoryBean 和实例工厂的区别是什么?底层调用链怎么走?”往往就卡住了。真正能拿下 37K 的答案,不能只背八股,还需要把配置方式、注解驱动、源码调用链完整串起来。本文按面试现场的节奏,把这道题拆成“基础回答—进阶追问—源码验证”三个层次,帮你形成一套可以拿高分的表达。

在正式展开之前,先说明本文范围:只讨论 Spring 核心容器中的 Bean 实例化,不涉及 Spring Boot 自动装配的展开细节;源码以 Spring Framework 5.3.x 为例,6.x 原理相同。

二、先理清概念:Bean 实例化到底是什么

很多人把“实例化”和“初始化”混为一谈,面试时一开口就容易露怯。我们先把这两个词讲清楚。

2.1 BeanDefinition 是图纸,实例化是照着图纸造对象

Spring 容器启动时,会读取 XML、注解或 Java 配置,把每个 Bean 的描述信息封装成BeanDefinition。BeanDefinition 里保存了类名、作用域、构造参数、属性、初始化方法、销毁方法等元数据,但它本身并不是对象。所谓“实例化 Bean”,就是 Spring 根据 BeanDefinition 中的元数据,真正创建一个 Java 对象的过程。

可以用一个比喻:BeanDefinition 相当于建筑图纸,实例化相当于施工队按照图纸把房子盖出来。房子盖好之后,再进行内部装修,也就是“属性填充”和“初始化”。

2.2 实例化与初始化的区别

这是面试中非常容易被追问的点。Spring 创建 Bean 的完整流程通常可以概括为三步:

  • 实例化(Instantiation):调用构造器或工厂方法创建对象实例,此时对象的属性还未填充。
  • 属性填充(Populate):根据 BeanDefinition 中的属性信息,通过反射调用 setter 方法或字段注入,把依赖对象赋值进去。
  • 初始化(Initialization):执行 Aware 接口回调、@PostConstruct 方法、InitializingBean 的 afterPropertiesSet、自定义 init-method 等逻辑。

实例化只是第一步。我们可以用一张表对比:

阶段核心动作典型入口
实例化创建对象实例,不填充属性createBeanInstance()
属性填充注入依赖属性populateBean()
初始化执行回调与自定义初始化逻辑initializeBean()

如果面试官让你画出 Bean 的生命周期,把这三个阶段分清楚,是一个重要的加分项。

三、Spring 中实例化 Bean 的 4 类方法

回到面试题本身。可以从“配置方式”和“底层策略”两个维度来组织答案。先说配置方式,一共有以下四类常见手段。

3.1 构造器实例化(默认方式)

最基础的方式,通过类的构造器创建对象。默认使用无参构造器,也可以通过配置指定有参构造器。在 XML 中不指定工厂方法时,Spring 默认走构造器实例化。

示例:定义一个普通的 UserService 类。

public class UserService { private UserDao userDao; public UserService() { } public UserService(UserDao userDao) { this.userDao = userDao; } }

XML 配置如下:

<bean id="userService" class="com.example.UserService"> <constructor-arg ref="userDao"/> </bean>

如果不写 constructor-arg,Spring 会尝试查找无参构造器。如果类没有无参构造器,只有有参构造器,且没有通过配置告知 Spring 如何传参,启动时就会抛出 BeanCreationException。

注解方式更简单:类上标注 @Component,构造器上的 @Autowired 可以省略(当类只有一个构造器时,Spring 会自动用它注入)。

@Component public class UserService { private final UserDao userDao; public UserService(UserDao userDao) { this.userDao = userDao; } }

3.2 静态工厂方法实例化

有些对象的创建逻辑比较复杂,或者需要复用已有静态工厂方法,这时可以让 Spring 调用类的静态方法来创建 Bean。例如一个静态工厂类:

public class CarFactory { public static Car createCar() { return new Car(); } }

XML 配置使用 factory-method 属性:

<bean id="car" class="com.example.CarFactory" factory-method="createCar"/>

注意:class 指向的是工厂类,而不是最终返回的 Car。Spring 会调用 CarFactory.createCar() 静态方法,把返回值注册为 id 为 car 的 Bean。

注解方式中,可以在 @Bean 方法上直接返回对象,原理类似。

3.3 实例工厂方法实例化

静态工厂要求创建方法属于类本身,而实例工厂要求先创建工厂对象,再调用工厂对象的实例方法。因此需要配置两个 Bean:工厂 Bean 和目标 Bean。

public class CarFactory { public Car createCar() { return new Car(); } }

XML 配置:

<bean id="carFactory" class="com.example.CarFactory"/> <bean id="car" factory-bean="carFactory" factory-method="createCar"/>

与静态工厂的区别是:静态工厂用 class + factory-method,实例工厂用 factory-bean + factory-method。这点经常被问到,需要说清楚。

3.4 FactoryBean 接口

FactoryBean 是 Spring 提供的一种特殊 Bean。它本身会被 Spring 注册,但 getBean("xxx") 返回的不是 FactoryBean 实例,而是它 getObject() 方法返回的对象。如果要获取 FactoryBean 本身,需要在 name 前加&符号。

自定义 FactoryBean 示例:

@Component public class CarFactoryBean implements FactoryBean<Car> { @Override public Car getObject() throws Exception { return new Car(); } @Override public Class<?> getObjectType() { return Car.class; } @Override public boolean isSingleton() { return true; } }

当 getBean("carFactoryBean") 时,实际获取的是 Car 对象。如果 Spring 内部需要获取 FactoryBean 本身,例如处理依赖注入、生命周期回调时,会先拿到 FactoryBean 实例。

FactoryBean 与实例工厂方法的本质区别在于:FactoryBean 本身就是一个 BeanDefinition,Spring 会先实例化 FactoryBean,再调用 getObject() 获取目标对象;而实例工厂方法需要额外配置一个工厂 Bean 和 factory-method,工厂 Bean 和目标 Bean 是两个独立的 BeanDefinition。

3.5 @Bean 与 @Component 注解方式

现代 Spring 应用几乎都在使用注解驱动。@Component、@Service、@Repository、@Controller 等注解标记的类,会被组件扫描发现并注册为 BeanDefinition,之后同样需要实例化。@Bean 则用在 @Configuration 或 @Component 类的方法上,表示方法返回值是一个 Bean。

@Configuration public class AppConfig { @Bean public DataSource dataSource() { return new HikariDataSource(); } }

@Bean 实例化的底层,Spring 会通过反射调用该方法,把返回值作为 Bean。这里还涉及@Configuration 的 CGLIB 代理:当 @Configuration 类被代理后,同一个 @Bean 方法被多次调用时,不会重复创建对象,而是从容器缓存中获取已存在的单例 Bean。这个点后面追问部分会展开。

到这里,最常问的四类方法就回答完了。但面试官往往不是只听你报名字,而是要听源码。

四、源码层面:从 getBean 到 createBeanInstance

这一节是拉开差距的关键。面试时可以说:“从容器获取 Bean 时,最终会进入 AbstractAutowireCapableBeanFactory 的 createBeanInstance 方法,它会根据 BeanDefinition、构造器推断结果和实例化策略,选择构造器反射或 CGLIB 来创建实例。”

完整调用链大致如下:

AbstractApplicationContext.refresh() -> finishBeanFactoryInitialization() -> preInstantiateSingletons() -> getBean() -> doGetBean() -> createBean() -> doCreateBean() -> createBeanInstance()

4.1 确定构造器:determineConstructorsFromBeanPostProcessors

在 createBeanInstance 中,Spring 首先会检查 BeanDefinition 中是否配置了 Supplier、工厂方法或 factory-method。如果都没有,就进入构造器推断逻辑。

Spring 会遍历所有 SmartInstantiationAwareBeanPostProcessor,其中最关键的是AutowiredAnnotationBeanPostProcessor,它负责处理 @Autowired 构造器。默认情况下,@Autowired 标注的构造器优先被选中;如果类只有一个构造器,即使没有 @Autowired 也会被选中。

确定构造器后,Spring 还可能通过ConstructorResolver解析构造器参数,处理参数名称、@Value 注解、循环依赖等情况。

4.2 两种实例化策略

Spring 提供了两种实例化策略,接口为InstantiationStrategy:

  • SimpleInstantiationStrategy:基于 JDK 反射,调用 Constructor.newInstance() 创建对象。
  • CglibSubclassingInstantiationStrategy:继承 SimpleInstantiationStrategy,当需要方法注入(Lookup Method Injection、Replace Method)时,使用 CGLIB 动态生成子类,否则仍走反射。

默认策略是 CglibSubclassingInstantiationStrategy。实例化 Bean 时,如果没有特殊需求,最终会调用 BeanUtils.instantiateClass(),通过反射执行构造器。

4.3 实例化策略的底层:JDK 反射与 CGLIB

普通情况下,Spring 优先使用 JDK 反射创建对象,性能较好;只有检测到 BeanDefinition 中定义了 MethodOverride(例如 lookup-method、replaced-method),才使用 CGLIB 生成子类,因为此时需要在子类中覆盖方法实现。

创建对象的入口代码可以简化为:

// 简化的 createBeanInstance 判断逻辑 if (instanceSupplier != null) { return obtainFromSupplier(instanceSupplier, beanName); } if (factoryMethodName != null) { return instantiateUsingFactoryMethod(beanName, mbd, args); } Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName); if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR) { return autowireConstructor(beanName, mbd, ctors, args); } return instantiateBean(beanName, mbd);

注意:这是源码逻辑的简化表达,实际代码有更多判断。面试时只要能准确说出这个判断顺序,就已经超过了大多数候选人。

五、完整可运行示例

下面给出一个同时演示构造器、静态工厂、实例工厂和 FactoryBean 的 Spring Boot 项目示例,方便读者本地运行验证。

项目结构:

com.example.demo ├── DemoApplication.java ├── config/AppConfig.java ├── bean/Car.java ├── bean/UserDao.java ├── bean/UserService.java ├── factory/CarFactory.java ├── factory/CarFactoryBean.java └── runner/BeanRunner.java

Car 实体类:

public class Car { private String brand = "BYD"; public String getBrand() { return brand; } public void setBrand(String brand) { this.brand = brand; } }

静态工厂与实例工厂:

public class CarFactory { public static Car createStaticCar() { return new Car(); } public Car createInstanceCar() { return new Car(); } }

FactoryBean 实现:

public class CarFactoryBean implements FactoryBean<Car> { @Override public Car getObject() throws Exception { return new Car(); } @Override public Class<?> getObjectType() { return Car.class; } }

配置类,演示多种注册方式:

@Configuration public class AppConfig { // 1. 构造器实例化:通过 @Component 扫描 // 2. @Bean 方式 @Bean public Car beanStyleCar() { return new Car(); } // 3. 静态工厂 @Bean public Car staticFactoryCar() { return CarFactory.createStaticCar(); } // 4. 实例工厂:先注册工厂 Bean,再调用方法 @Bean public CarFactory carFactory() { return new CarFactory(); } @Bean public Car instanceFactoryCar(CarFactory carFactory) { return carFactory.createInstanceCar(); } // 5. FactoryBean @Bean public CarFactoryBean carFactoryBean() { return new CarFactoryBean(); } }

测试运行:

@Component public class BeanRunner implements CommandLineRunner { @Autowired private ApplicationContext context; @Override public void run(String... args) { System.out.println(context.getBean("beanStyleCar").getClass()); System.out.println(context.getBean("staticFactoryCar").getClass()); System.out.println(context.getBean("instanceFactoryCar").getClass()); System.out.println(context.getBean("carFactoryBean").getClass()); System.out.println(context.getBean("&carFactoryBean").getClass()); } }

输出显示,前四个对象都是 Car 类型,最后一个以&开头获取到的是 CarFactoryBean 本身。这个例子能很直观地验证 FactoryBean 的取 Bean 规则。

六、面试高频追问与回答模板

这道题往往不会只停留在“有哪些方法”,面试官会继续往下挖掘。下面整理几个高频追问。

6.1 构造器注入和 Setter 注入怎么选?

回答要点:优先使用构造器注入。

  • 构造器注入能保证依赖不可变,字段可以用 final 修饰,对象创建完成即可用,依赖更明确。
  • Setter 注入适合可选依赖或可能出现循环依赖的场景,但会暴露可变状态。
  • Spring 官方推荐:强制依赖用构造器注入,可选依赖用 Setter 注入。

6.2 循环依赖时 Spring 如何实例化 Bean?

循环依赖发生在属性填充阶段,而不是实例化阶段。Spring 解决单例循环依赖依赖三级缓存:

  • 一级缓存 singletonObjects:存放完全初始化好的 Bean。
  • 二级缓存 earlySingletonObjects:存放提前暴露的 Bean 实例,尚未填充属性。
  • 三级缓存 singletonFactories:存放 ObjectFactory,用于生成早期引用。

实例化完成但还未填充属性时,Spring 会把当前 Bean 包装成 ObjectFactory 放入三级缓存。当 B 依赖 A 时,可以从三级缓存获取 A 的早期引用,从而打破循环。需要注意的是,构造器注入的循环依赖无法用三级缓存解决,因为构造器阶段还没完成实例化,无法提前暴露。这也是推荐构造器注入但也要注意循环依赖的原因。

6.3 FactoryBean 和 BeanFactory 的区别

这个问题专门用来考察概念是否扎实。两者虽然名字相近,但定位完全不同。

  • BeanFactory:是 Spring 容器的顶层接口,负责管理 Bean 的创建、获取、依赖注入和生命周期。
  • FactoryBean:是一种特殊的 Bean,本身也会注册到容器中,但它对外暴露的是 getObject() 返回的对象。

简单记:BeanFactory 是“容器”,FactoryBean 是“能生产对象的工厂 Bean”。当面试官问“二者区别”时,先明确一个是基础设施,一个是特殊 Bean,再补一句“获取 FactoryBean 本身要加&前缀”,会显得理解很深。

6.4 @Bean 和 @Component 的区别

这是注解驱动场景下的高频追问,回答时可以从作用对象和产生方式两个角度切入。

  • 作用对象不同:@Component 标注类,通过类路径扫描注册 Bean;@Bean 标注方法,通过方法返回值注册 Bean。
  • 适用场景不同:自己写的类、能直接通过组件扫描管理的,优先用 @Component;引入第三方库对象、需要自定义创建逻辑或统一管理多态实例时,用 @Bean。
  • 底层行为:@Bean 方法在 @Configuration 代理类中,Spring 会拦截重复调用,保证单例 Bean 只创建一次。

可以总结成一句话:@Component 是“扫描类生成 Bean”,@Bean 是“调用方法生成 Bean”。

七、总结:如何把这道题答出高分

把整道题串起来,面试时可以用下面的顺序组织回答:

  • 先给定义:先说 Bean 实例化是 Spring 根据 BeanDefinition 创建对象实例的过程,并区分实例化和初始化。
  • 再列方法:从构造器实例化、静态工厂、实例工厂、FactoryBean 到 @Bean/@Component,把配置方式和注解驱动都点到。
  • 最后讲源码:说明调用入口 createBeanInstance()、构造器推断、两种实例化策略,以及 JDK 反射与 CGLIB 的选择依据。
  • 补充追问:主动提到循环依赖、三级缓存、FactoryBean 与 BeanFactory 的区别,展示知识深度。

这样回答既有广度又有深度,不会停留在八股背诵。建议本地把第五章示例跑一遍,再把第四章调用链画一遍,这样面试时表达会更自然、更经得起追问。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进