
Spring 实例化 bean 有几种方式这个问题我在面试候选人的时候基本是必问的。说实话能一口气把构造器、静态工厂、实例工厂说完的人不少但如果接着问一句“FactoryBean 算不算一种方式”或者“注解配置算哪种实例化思路”很多人就开始支支吾吾了。这道题难不在“背出几种方式”而在于你能不能把这几种方式放在同一个体系里去理解它们本质上都是“把创建对象的控制权交给 Spring 容器”这个思路的具体落地。这篇我把自己的理解完整梳理一遍从最基础的 XML 配置时代讲到 Spring Boot 里常见的注解写法顺带把实例化和属性注入、Bean 生命周期之间的关系也理清楚。我的建议是把它当作一块垫脚石真正看懂以后再去碰三级缓存和手写 IOC 的源码会顺畅得多。1. 先别急着背答案弄清楚“实例化”到底指什么很多回答一道这种问题就露怯不是因为他不知道那几种写法而是因为他把“实例化”“初始化”“属性注入”三个概念混在一起讲。面试官问的是实例化他回答的却是整个 Bean 创建过程听起来就是不精准。所以第一步先把概念的边界划清楚。1.1 实例化、初始化、属性注入三者不是一回事在 Spring 框架里“实例化”特指对象内存创建的那一刻也就是通过构造器或者工厂方法完成new这个动作的阶段。这一步做完对象是存在的但它的属性是空的、依赖还没进来也不符合一个能正常工作的状态。紧接着的下一个阶段叫“属性填充”也就是 Spring 把配置好的依赖项、配置值通过 setter 或者字段反射等方式赋值进去。这一步做完对象才具备完整的依赖关系。再往下才轮到“初始化”包括执行InitializingBean.afterPropertiesSet()、自定义的init-method以及BeanPostProcessor的前后处理逻辑。所以你看整条链路是“实例化 → 属性填充 → 初始化”。很多人说“Bean 加载过程”的时候其实把这三者打包在一起说的这本身没错但一旦讨论的是“有哪几种实例化方式”就必须把注意力锁定在第一步上。1.2 搞清楚 BeanDefinition 才是理解实例化的钥匙顺着刚才的思路既然实例化是“创建对象”的动作那 Spring 是怎么知道该创建谁、用哪种方式创建的呢答案在BeanDefinition里。每一个受 Spring 管理的 Bean容器里都对应一份 BeanDefinition它记录了这个 Bean 的类路径、作用域、懒加载标记、初始化方法名、工厂类名、工厂方法名等等。实例化方式的差异其实就体现在 BeanDefinition 里面几个字段的组合上有没有指定factory-bean、有没有指定factory-method、class 指向是普通类还是 FactoryBean 实现类。Spring 底层的InstantiationStrategy就是根据这几个字段来决定走哪条路的。所以与其去背“有四种方式”不如反过来理解那四种方式就是 BeanDefinition 信息组合出来的四种典型形态。2. Spring 实例化 Bean 的五大核心方式说到根上Spring 容器的任务就是帮我们“生产对象实例”。它生产对象的方式主要有五种下面逐一拆开讲。2.1 构造器实例化最常用也最容易被忽略的方案第一种方式是构造器实例化也是 Spring 默认使用的方案。容器根据 BeanDefinition 中声明的类信息通过反射调用它的构造器来创建对象大多数情况下调用的是无参构造器。bean iduserService classcom.example.service.UserService/这一段 XML 在 Spring 底层干的事大体等价于Class? clazz Class.forName(com.example.service.UserService); UserService instance (UserService) clazz.getDeclaredConstructor().newInstance();执行到这里对象已经创建出来了但属性还是空的。后续的属性填充、AOP 代理的生成都是在实例化之后才进行的。所以这个阶段你要特别小心一件事如果你的类里写了有参构造器却没有显式声明无参构造器Spring 默认是无参构造器实例化的时候就会直接报错提示找不到合适的构造器。在 Spring Boot 时代构造器实例化以两种常见面目出现。一种是 Component 等注解配合组件扫描让容器自动注册并创建 Bean另一种是通过 Configuration 类里的 Bean 方法方法内部返回一个 new 出来的对象同样是构造器实例化的路子。2.2 静态工厂方法不经过构造器的创建方式第二种是静态工厂方法实例化。有些场景下我们不希望外部直接 new 对象而是通过类的静态方法来获取实例比如 JDK 里LocalDateTime.now()、DecimalFormat.getNumberInstance()这类写法。Spring 也支持把这种创建逻辑托管给容器方式就是配置factory-method。bean iddateTime classjava.time.LocalDateTime factory-methodnow/这段配置的含义是Spring 不再去调用LocalDateTime的构造器而是去执行它的静态方法now()来获得实例。Java 配置里对应的写法同样灵活Bean public DateFormat dateFormat() { return DateFormat.getDateInstance(); }这里getDateInstance()是DateFormat的一个静态工厂方法返回一个具体实例。Bean方法体内部的实现本质上是静态工厂思路的自然延伸——创建逻辑由方法控制容器只负责调用方法并接管返回的对象。在实际项目里静态工厂方式最常见的使用场景是接入第三方工具类。你不想改别人的源码又希望统一由容器管理实例就可以在自己的配置类里包一层静态工厂方法调用。这点在旧系统改造、老代码迁移到 Spring Boot 的过程中特别实用。重点是如果静态工厂方法需要参数怎么办XML 里用constructor-arg传参Java 配置里直接在方法形参体现两者逻辑一样。2.3 实例工厂方法先有工厂 Bean再有产品 Bean第三种方式是实例工厂方法也就是通过一个已有的工厂对象来生成目标对象。它与静态工厂的区别在于调用工厂方法的主体是“对象”而不是“类”。所以配置上需要先定义一个工厂 Bean然后目标 Bean 通过factory-bean指向它。public class CarFactory { public Car create(String brand) { return new Car(brand); } }bean idcarFactory classcom.example.factory.CarFactory/ bean idcar factory-beancarFactory factory-methodcreate constructor-arg valueBMW/ /bean在这里Spring 的创建顺序是先保证carFactory这个 Bean 被实例化出来再调用它的create方法得到car。如果工厂 Bean 还没实例化那目标 Bean 自然拿不到所以容器内部存在依赖关系。这种模式现在看起来老气但在一些遗留系统里仍然能看到比如业务中像流水号生成器、数据库连接工厂这些对象依然保留着这种老式风格。它的优点在于创建逻辑可以在工厂类里复用参数可以动态传。缺点就是配置繁琐写起来不直观所以在新代码里我一般不太建议用它除非是为了兼容遗留接口。2.4 FactoryBean 扩展点Spring 留给你的高级自定义方案第四种是 FactoryBean 接口。很多人会把“工厂 Bean”和“实例工厂方法”混起来但这两者差别很大。FactoryBean 是 Spring 容器提供的一个扩展接口你可以实现getObject()方法来定义复杂对象的创建逻辑。Spring 容器会为实现了 FactoryBean 的类创建两个“物件”一个叫beanName获取 FactoryBean 本身另一个是 beanName获取的是getObject()返回的实际产品对象。Component public class CarFactoryBean implements FactoryBeanCar { Override public Car getObject() { return new Car(BYD); } Override public Class? getObjectType() { return Car.class; } Override public boolean isSingleton() { return true; } }这样做的意义在框架级项目中体现得最明显。我们常用的 MyBatis-Spring里面的MapperFactoryBean就是通过这种机制把原本无法直接交给 Spring 管理的 Mapper 接口变成容器的 Bean。你平时写接口只定义了抽象方法但通过 FactoryBean 的getObject()容器能得到一个动态代理对象从而让Autowired注入 Mapper 成为可能。这是扩展点的问题它不是“额外的实例化方式”而是把实例化过程完全交给你自定义。深入理解之后你再去围观 Spring 源码里那些 FactoryBean 实现就很容易猜到作者的意图了。2.5 注解与 JavaConfig 时代的实例化思路严格来说注解扫描和 Bean 配置并没有突破前面几种方式的底层逻辑它们只是换了“配置描述”的载体。过去在 XML 里写bean class.../现在用Component过去用bean factory-method.../来声明静态工厂现在在 Configuration 类里写 Bean 方法返回对象。底层走的还是构造器或者静态工厂方法。但从代码组织角度它们带来了一次大的思维转变Bean 的定义从集中式 XML 配置变成了分散在业务类上的声明式注解或者集中在配置类里的方法。Spring Boot 时代绝大多数 Bean 的实例化都是通过 Component 扫描 Bean 方法完成的我们不需要再关心 XML 的繁琐标签维护成本大大降低。值得一提的还有ConditionalOnMissingBean这类条件装配。它本身不决定实例化方式但决定了一个 Bean 是否要实例化以及是否用默认实例。理解这部分读 Spring Boot 自动配置源码会轻松很多。3. 实例化时 Spring 底层到底做了什么面试到了这个环节追问就开始了你说的构造器实例化Spring 底层是怎么执行的反射过程有几步三级缓存和实例化有什么关系这几个问题答上来这道题才算真正过关。3.1 反射创建对象的完整链条Spring 的构造器实例化过程本质上是一条反射链路容器拿到 BeanDefinition 里的 beanClassName → 通过类的类加载器加载这个类 → 得到Class?对象 → 解析出合适的构造器 → 执行newInstance得到原始对象。在这个链条里有一个容易忽略的点Spring 不是简单用clazz.newInstance()去创建对象的。它会根据容器里已经配置的依赖关系决定到底调用哪个构造器、每个构造器参数怎么获取。这就是ConstructorResolver要做的事。比如你定义了一个构造函数UserService(UserMapper userMapper)Spring 在实例化时发现容器里有 UserMapper 的 Bean就会把它当作构造器参数传进去。所以有参构造器实例化和无参构造器实例化在 Spring 看来是两种复杂度差异很大的任务。无参构造器简单粗暴直接new有参构造器需要先解析参数引用涉及其他 Bean 的创建这就引出了依赖注入的先后顺序问题。3.2 三级缓存里藏着“提前实例化”的痕迹Spring 解决单例 Bean 循环依赖的核心是三级缓存很多讲循环依赖的文章都会提到三级缓存但它和实例化是什么关系说得往往不够透。三级缓存的三层 Map 分别存的是一级缓存singletonObjects里放的是完整成品对象二级缓存earlySingletonObjects里放的是刚实例化完、尚在属性填充阶段的“早期引用”三级缓存singletonFactories里放的是单例工厂对象通过它可以拿到这个早期引用。关键点就在这里什么情况下 Bean 会出现在三级缓存答案是“实例化完成、但属性填充还没做完”的时候。在默认单例情况下Spring 实例化完一个 Bean 后会提前把一个ObjectFactory放进三级缓存目的是如果其他 Bean 在循环引用时引用到了它可以返回这个半成品。注意这个机制只解决“构造器是默认无参构造器”这类的循环依赖场景。如果循环依赖里涉及构造器注入比如 A 的构造器需要 B、B 的构造器需要 A那三级缓存也救不了因为实例化 A 之前就需要 B可 B 还没开始实例化。这就是网上经常说的“构造器注入无法解决循环依赖”这个结论的根源。3.3 实例化顺序谁先谁后谁依赖谁Spring 容器创建 Bean 遵循依赖优先原则。A 依赖 B那 B 必然先被实例化。如果是构造器注入那 B 完整创建完以后A 才能进入构造器参数解析的环节如果是 setter 注入A 可以先实例化进入属性填充阶段再去创建 B这也是循环依赖能被绕过一大部分的原因。在实际跑一个大项目的时候你经常会看到启动日志里 Bean 创建顺序并不是代码里定义的顺序而是按依赖关系拓扑排序后得到的顺序。这是 Spring 的拓扑排序在处理不复杂但很值得理解。遇到“启动缓慢”“Bean 创建链路过长”这类问题知道这个顺序逻辑才能精准定位。4. 实例化、属性注入与 Bean 生命周期的完整脉络4.1 一个 Bean 从出生到死亡的完整阶段一张经典的生命周期图大概包含BeanDefinition 解析 → 实例化 → 属性填充 → Aware 回调 → BeanPostProcessor 前置处理 → InitializingBean / init-method → BeanPostProcessor 后置处理 → 使用 → DisposableBean / destroy-method。实例化只是中间很小的一段。但这一小段决定了整条链路能不能启动。举个例子如果你的类里只定义了有参构造器且参数又是容器里不存在的 Bean实例化会直接失败如果实例化成功但属性填充阶段找不到依赖报的又是另一种错了。所以定位问题先要分清阶段不然会浪费时间在错误的方向上排查。4.2 属性注入的三种方式与实例化如何衔接属性注入方式最常听到的是构造器注入、Setter 注入、Autowired 字段注入。这里和实例化最相关的是构造器注入。它发生在实例化阶段Spring 在构造器参数解析时就会去寻找依赖 Bean。注意如果依赖 Bean 是单例的它必须已经存在或者在本次创建中被立即创建出来。所以构造器注入对 Bean 的创建顺序最严格。Setter 注入和 Autowired 字段注入则发生在实例化完成之后的属性填充阶段。此时对象已经诞生依赖还没进来。一个容器制造的半成品能出现在内存里这就是为什么循环依赖时可以通过早起引用暴露来兜底的原因。顺便提醒一句如果你实现BeanPostProcessor来处理某个 Bean一定要意识到postProcessBeforeInitialization触发时这个 Bean 的属性已经填充完毕。如果你在这个阶段发现某些属性是 null那要排查属性填充阶段而不是实例化阶段。5. 实操真实项目里怎么选实例化方案面试答完原理真正干活时该怎么选我的经验如下。5.1 常规业务代码尽量用注解声明式写法现在的项目基本都是 Spring Boot我默认的写法是业务类上直接标Service、Repository、Controller让容器自动扫描、自动装配。这是最省心的方式实例化方式走的是构造器反射配合构造器注入无妖无魔。如果你需要生产第三方库的对象比如RestTemplate、ObjectMapper、RedisTemplate就在配置类里用 Bean 包装方法内部怎么写创建逻辑都行这本质上是前面说的工厂思路。5.2 外部类与第三方库接入时的工厂模式选型外部类的典型特征是不能加注解无法被自动扫描。这个时候静态工厂方法和实例工厂方法就派上用场了。我的参考建议是对象的创建不需要依赖容器里其他 Bean优先用静态工厂方法逻辑简单。对象的创建依赖容器中的其他资源用实例工厂方法或将创建逻辑放在 Configuration 类里通过 Bean 处理。如果要在创建前后干更多事比如动态代理、每次调用时计算上下文、按条件返回不同实现考虑实现 FactoryBean。5.3 FactoryBean 在框架级设计里的典型场景如果你在写自己的组件或轮子一定会喜欢 FactoryBean。它能屏蔽复杂创建过程让使用方只关心接口类型。一个真实的场景是这样框架需要为不同用户生成不同扩展实现但接口本身没有具体类那你就可以通过 FactoryBean 动态生成代理对象并把每个目标类包装成不同名字的 Spring Bean。MyBatis 的 Mapper 扫描、Ribbon 的负载均衡器接口、Feign 的 Client 工厂等等都在演示这条思路。所以如果你想深入读框架源码FactoryBean 是绕不开的类型。6. 常见问题与排查心得实录这部分整理几个我实际遇到、也常被问到的问题方便你自查。问题原因解决方案Bean 创建失败No default constructor found类里定义了有参构造器但没写无参构造器Spring 默认无参反射创建补一个无参构造器或者用 Autowired 标注有参构造器报 BeanCurrentlyInCreationException循环依赖且涉及构造器注入或非单例作用域改为 setter/字段注入或者 Lazy 延迟其中一个依赖配置了 factory-method 仍然创建失败静态工厂方法的访问权限不是 public或方法签名与配置不一致确认方法为 public且参数个数、类型与constructor-arg匹配Autowired 注入到接口时提示多个候选 Bean容器里有多个该接口的实现使用 Primary 指定主候选或 Qualifier 指定名称想获取 FactoryBean 本身却拿到产品对象对 FactoryBean 名字直接 getBean在 beanName 前加前缀例如carFactoryBeanBean 方法内静态工厂方法参数无法从容器取Bean 方法形参解析基于容器依赖如果传了字面值需要 Value使用 Value 注入字面参数或方法体内手动获取参数6.1 构造器里不要写耗时或异常逻辑一个很容易踩的坑把一些费时操作直接写在构造器里。实例化阶段发生异常整个容器启动就中断了而且排查麻烦。构造器里应该只做必要的赋值和不可变对象的创建任何涉及外部 IO、网络调用、线程启动的逻辑放到 afterPropertiesSet 或监听器里更安全。这一点不仅适用于 Spring也是写 Java 类的基本素养。6.2 手写一个极简 IoC 来验证你的理解如果读源码读不进去我特别推荐你试着用一个最小实现去模拟 Spring 的实例化过程。大概只需要三步写一个注解MyComponent用扫描器收集类路径下带这个注解的类写一个容器类遍历收集结果通过无参构造器反射创建实例并放进 Map写一个字段注入逻辑扫描带MyAutowired的字段从容器里拿出依赖对象通过反射赋值。做一遍以后你对实例化阶段和属性填充阶段的理解会完全不一样。当年我自己在做这种 mini 版容器的时候最大的收获不是复刻了一个轮子而是搞明白了一个朴素的道理Spring 所做的一切都是在“构造对象”这件事上叠加了更多的控制能力。实例化方式再多本质还是反射和工厂思想的不同组合。7. 顺着实例化还能往哪走在真实团队里能讲清楚实例化方式的同事写扩展点的时候通常也比别人稳。因为 Spring 的 AOP、事务、配置装配本质都是在 Bean 创建链条的某个位置插入逻辑。你理解了实例化就等于拿到了这段链条的入口钥匙。如果你现在正在准备面试我的建议是答到这三种就结束构造器、静态工厂、实例工厂然后把 FactoryBean 单拎出来谈扩展点体验最后用三级缓存里“实例化完成即暴露工厂”的例子证明你不止懂语法还知道 Spring 的运行时行为。这样回答层次感和区分度都出来了。这篇先写到这里。后续如果时间允许我可以把“手写一个 IoC 容器”的过程完整复现一遍那种从零搭 Bean 创建链路的体验比背十个面试题都来得扎实。