☰
BeanFactory与FactoryBean:用Fate类比理解Spring容器核心机制
2026/10/18 7:47:49 网站建设 项目流程

把 BeanFactory 和 FactoryBean 放在一块儿讲,是我给团队做 Spring 容器入门梳理时最头疼的一件事。名字只差一个字母,网上资料又多半各讲各的,新手看完经常抓头:“这俩到底是不是一回事?”直到我拿 Fate 里两套宝具机制做类比,大家才一下通了。

Fate 里红 A 的投影魔术和闪闪的王之财宝,恰好对应这两个概念的核心差异。闪闪的王之财宝是一个真品仓库,里面陈列着无数宝具,想要哪件就直接从宝库大门里取;红 A 的投影则完全不同,他手里并没有原典,每次都是靠魔力现场构筑复制品,用完就消散,需要了再投影一次。

把这套逻辑搬进 Spring:BeanFactory 是容器本身的顶级接口,负责所有 Bean 的注册、创建、查找和生命周期管理,相当于“宝物库本体”;FactoryBean 则是容器里的一个特殊成员,它不直接对外提供服务,而是作为一个“投影魔术师”存在——容器向外部暴露的,是它现造出来的产品对象。也就是说,一个管“库存”,一个管“生成”。

这篇文章适合两类读者:一是刚接触 Spring IoC 容器、对这两个概念傻傻分不清的人;二是做了几年开发但想弄明白 getBean 内部路径、FactoryBean 产品缓存机制的人。我会用 Fate 的类比打底,然后把所有结论落到代码和源码路径上,尽量让内容既好玩又实用。

1. 为什么想起来用 Fate 讲 Spring 容器

1.1 红 A 与闪闪:两种完全不同的取物方式

先界定一下两个角色的行为模式。

闪闪的“王之财宝”本质是一个收纳了无数真品宝具的宝物库。战斗时他背后出现金色的门,把一件件实体宝具射出去。这些宝具本身真实存在,而且早就收藏在仓库里。取用过程的核心动作是“打开门、拿库存”。整个过程没有“制造”的概念,有的只是“选取”和“输出”。

红 A 的“投影魔术”则是另一套逻辑。他开启固有结界“无限剑制”时,可以瞬间复制出自己见过的武器。每一次投影都是现场构筑,没有库存概念,原典在他手上只是一个“蓝图”。取用过程的核心动作是“读蓝图、现场造”。造出来的东西本质是复制品,用完也不再保留。

映射到 Spring 上,两者的分工非常清楚:

  • BeanFactory 侧:Bean 的定义(BeanDefinition)在容器启动时解析并注册好,单例 Bean 的实例在容器初始化阶段创建好放进单例池。调用方 getBean 时,容器从“库存”里取,或者按定义现场创建后放回库存。
  • FactoryBean 侧:容器里注册的是一个“工厂对象”,这个对象本身是 Bean。但当你用它的 BeanName 去 getBean 时,容器会调用它的 getObject() 方法,把现场生成的产品返回给你。

这个类比还能往下延伸。闪闪的库存里全是真品,所以他不担心“造不出”的问题,他关心的是“选哪件”;红 A 造出来的是复制品,所以他要关心“怎么造”以及“造完是不是要重新造”。对应到 Spring 里,BeanFactory 关心的是容器对 Bean 定义、依赖和生命周期的管理;FactoryBean 关心的是产品对象该如何构建、是不是要每次都新建。职责边界一下就清晰了。

1.2 BeanFactory 与 FactoryBean 到底在说什么

先把两个概念用一句话框死。

BeanFactory 是 org.springframework.beans.factory.BeanFactory 接口,是 Spring IoC 容器的根接口。它定义了 getBean、containsBean、isSingleton、getType 等基础方法,而 ApplicationContext 是它的子接口扩展出来的完整容器体系。通俗理解:BeanFactory 就是那个管理所有 Bean 的“王国”,是所有 Spring 容器的祖先。

FactoryBean 是 org.springframework.beans.factory.FactoryBean 接口。它本身是一个 Bean,但它不直接对外暴露自己,而是通过 getObject() 暴露“产品”。它更像一个“生产者”或“加工厂”。通俗理解:你写一个类实现 FactoryBean,把它注册进容器,别人 getBean 时拿到的是你 getObject() 返回的那个东西。

为什么容易混?两个原因。第一,名字都是 Bean 加 Factory,只是顺序不同,视觉上几乎一样;第二,在使用层面,普通 Bean 和 FactoryBean 产品在外观上没有区别——调用方都是 context.getBean("xxx"),拿到的都是对象,不看类型根本不知道背后是哪个机制在起作用。

我个人建议用一句话记:BeanFactory 是容器,FactoryBean 是容器里的一个特殊 Bean,它的使命是生成别的 Bean。

2. BeanFactory:闪闪的王之财宝

2.1 容器里到底存了“宝具”吗

说到库存,先得澄清一个容易误解的点:BeanFactory 里到底存了什么?

容器启动时并不会把所有 Bean 实例都创建好。它先做的事是解析配置(XML、注解、Java 配置类),把每个 Bean 的定义转成 BeanDefinition,注册到 beanDefinitionMap 这个 ConcurrentHashMap 里。BeanDefinition 是“蓝图”,记录了这个 Bean 的类名、作用域、依赖、初始化方法、是否懒加载等元信息。你可以把它理解成闪闪宝物库里的一张“藏品清单”,清单上写的是每件宝具的名字、出处和存放位置,但宝具实体还没摆出来。

真正的“宝具”实例,存放在单例池 singletonObjects 里。这个池子同样是一个 Map(ConcurrentHashMap<String, Object>),只有当某个单例 Bean 被创建之后,实例才会放进去。以后再取,直接命中缓存,不会再创建。

强调一下:不是所有 Bean 都会进入宝物库。原型(prototype)Bean 每次 getBean 都会重新创建,容器并不缓存它的实例。这一点对应闪闪的宝物库里全是“可复用的真品”,而原型 Bean 更像红 A“用完即弃的投影”。所以判断一种 Bean 是“库存”还是“投影”,先看它的 scope 就够了。

2.2 getBean 的完整路径:从请求到交付

理解了存储结构,再走一遍 getBean 流程就顺了。以 DefaultListableBeanFactory 为例,简化路径如下:

  1. 调用 getBean(String name) 或 getBean(Class type),进入 AbstractBeanFactory.doGetBean。
  2. 先从 singletonObjects 查缓存,命中直接返回。涉及循环依赖时,还会查 earlySingletonObjects 和 singletonFactories 这两层缓存。
  3. 未命中,则根据 name 拿到 BeanDefinition。
  4. 根据 BeanDefinition 判断 scope。单例走创建单例的流程;原型走 createBean 后直接返回;自定义 scope 走对应策略。
  5. 创建时执行 doCreateBean:实例化对象、属性填充(依赖注入)、执行 Aware 回调、BeanPostProcessor 前置和后置处理、初始化方法。
  6. 单例 Bean 创建完成后放进 singletonObjects,下次直接取。

这段流程里最关键的一个认知是:BeanFactory 的职责不是“给你库存”,而是“确保你拿到的每个 Bean 都是正确、完整、及时创建的”。就算实例已经创建过了,也是经过容器管理的对象,不是简单 new 一个东西扔给你。

如果你手动用过 DefaultListableBeanFactory(不依赖 ApplicationContext),大概长这样:

DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(Message.class); bf.registerBeanDefinition("message", builder.getBeanDefinition()); Message msg = bf.getBean(Message.class); System.out.println(msg.getContent());

这一段能跑通,你就理解了“注册定义 → 按需创建 → 缓存单例”的最小闭环。当然实际项目中没人这样裸用 BeanFactory,我们日常用的 ApplicationContext 在内部包装了更多功能,比如事件发布、资源加载、国际化,但底层核心还是这套路径。

2.3 单例创建时机:宝物库是“摆出来”还是“藏在深处”

另一个容易踩坑的点是单例 Bean 的创建时机。

默认情况下,ApplicationContext 在启动阶段就会调用 preInstantiateSingletons(),把非懒加载的单例 Bean 全部创建好。这就是为什么项目启动时很多初始化逻辑会执行——不是因为你调用了它,而是容器启动时就把“宝物库大门”打开,把所有非懒加载的单例都陈列出来了。

如果某个 Bean 配置了 lazy-init="true" 或者 @Lazy,它就不会在启动时创建,而是等到第一次 getBean 时才创建。这就像闪闪的宝物库里有些宝具存放在更深的隔间,不主动去“开锁”就不拿出来。

理解这个时机,对排查“为什么启动时我的 Bean 初始化了两次”“为什么我把 lazy-init 改成 true 后启动变快了但第一次调用变慢了”这类问题很有帮助。实际经验是:不要把 @Lazy 当成性能优化神器随便加,它会推迟错误的暴露时机,让本该在启动期暴露的配置问题延迟到运行期才爆出来,排查成本反而更高。

3. FactoryBean:红 A 的投影魔术

3.1 为什么需要 FactoryBean:有些宝具不能直接收藏

普通 Bean 用 Class 定义就能创建,但现实里有一类对象没法简单地 new 出来。比如基于接口生成代理对象、需要读取外部配置后动态构造的对象、需要池化管理的连接对象。把这些逻辑写在一个普通 Bean 的构造方法里不合适,写在 XML 的 factory-method 里又太死板,尤其当产品类型是接口时,配置阶段根本不知道运行时具体实现是谁。

FactoryBean 就是为这种场景设计的。它的思路是:你只管把“工厂”注册成 Bean,至于产品怎么造,全在 getObject() 里写。容器负责管理工厂本身的生命周期,同时在有人 getBean 时调用 getObject() 生产产品。这样做的好处是,产品对象的构建细节被完全封装起来,对调用方透明。

框架里最典型的例子是 MyBatis 的 Mapper 扫描机制:每个 Mapper 接口最终拿到的实现,本质上是动态代理对象,而生产这个代理对象的,正是内部使用的 FactoryBean 体系。平时我们写配置时经常遇到的 SqlSessionFactoryBean 也是 FactoryBean 的实现,它的 getObject() 负责构建完整的 SqlSessionFactory。

所以不要觉得 FactoryBean 是冷门概念。你每天都在使用它,只不过大多数时候是框架替你实现的,你没有直接写代码而已。理解它的原理,能让你在框架排查中看得更深。

3.2 FactoryBean 的三个方法:投影的核心步骤

FactoryBean 接口只有三个方法需要实现:

方法作用Fate 类比
getObject()返回产品对象投影术的“构造”动作
getObjectType()返回产品类型投影术的“蓝图预览”
isSingleton()产品是否单例投影一次存库,还是每次现场造

getObjectType 不是可有可无的。容器在做类型匹配时,比如 @Autowired 按类型注入、getBean(Class) 查找,必须知道这个工厂生产的东西是什么类型。如果 getObjectType 返回空或者类型不准,容器就无法完成类型匹配,甚至可能注入失败。

isSingleton 的语义也容易理解:返回 true 时,容器会把 getObject() 产出的对象缓存起来,下次直接复用;返回 false 时,每次 getBean 都会重新调用 getObject(),产生全新对象。这个标记只影响产品,不影响工厂本身——工厂本身始终是容器管理的一个 Bean。

3.3 一个完整示例:从零写一个投影工厂

为了看清楚机制,我写一个带参数的示例。假设业务里需要创建 Message 对象,但 Message 的内容要由前缀和后面的内容拼接出来,而且每次获取都要重新生成(投影模式)。

先建产品类:

public class Message { private final String content; public Message(String content) { this.content = content; } public String getContent() { return content; } }

再写工厂类:

import org.springframework.beans.factory.FactoryBean; public class MessageFactoryBean implements FactoryBean<Message> { private String prefix; private String content; public void setPrefix(String prefix) { this.prefix = prefix; } public void setContent(String content) { this.content = content; } @Override public Message getObject() throws Exception { return new Message(prefix + content); } @Override public Class<?> getObjectType() { return Message.class; } @Override public boolean isSingleton() { return false; } }

用 Java 配置类注册:

@Configuration public class BeanConfig { @Bean public MessageFactoryBean message() { MessageFactoryBean factory = new MessageFactoryBean(); factory.setPrefix("Greeting: "); factory.setContent("Hello from FactoryBean"); return factory; } }

使用:

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(BeanConfig.class); Message msg = ctx.getBean("message", Message.class); System.out.println(msg.getContent());

这里要注意一个重要细节:@Bean 方法返回类型是 MessageFactoryBean,但 getBean("message") 得到的却是 Message,而不是 MessageFactoryBean。这正是 FactoryBean 在背后干的活。如果你真的需要拿到工厂本身,必须用 & 前缀:

MessageFactoryBean factory = ctx.getBean("&message", MessageFactoryBean.class);

这个 & 你记成“取工厂本体”就可以了。下面第四章会展开讲容器是如何处理这个分支的。

4. 投影结果放进财宝库:两种机制如何合体

4.1 容器是如何识别 FactoryBean 的:getObjectForBeanInstance

容器在 getBean 时,拿到一个 BeanDefinition 后会发现这个 Bean 的类型是 FactoryBean。此时它不会直接把工厂实例返回给调用方,而是走一段特殊逻辑:getObjectForBeanInstance。

简化后的判断规则是:

  • 如果请求的是普通 name(不带 ),并且 bean 是 FactoryBean,则先获取工厂实例,再调用 getObject() 返回产品对象。
  • 如果请求的 name 带 前缀,则直接返回工厂本身。
  • 如果 bean 不是 FactoryBean,无论名字带不带 ,都返回原来的 bean。

这段逻辑在 AbstractBeanFactory.getObjectForBeanInstance 里,它是 FactoryBean 机制的核心枢纽。源码不长,但信息量很大。建议读源码时重点看它如何使用 factoryBeanObjectCache 做产品缓存。

factoryBeanObjectCache 是容器里专门缓存 FactoryBean 产品对象的 Map。当 isSingleton() 返回 true 时,getObject 的结果会放进这个缓存;下次再取直接从缓存拿,不会再调用 getObject()。返回 false 时不缓存,每次都是新的。注意这个缓存和普通 Bean 的单例池是独立的,两者不要混为一谈。

有了这个底层认知,你就能明白为什么 getBean 两次拿到的是同一个产品还是不同的产品,关键不在 Bean 的 scope,而在 FactoryBean 的 isSingleton。产品缓存的地点和机制与普通单例 Bean 完全不同。

4.2 框架里常见的 FactoryBean 使用场景

不需要自己写的场景里,最常见的两个:

第一个是 MyBatis 整合场景。你配置一个 @MapperScan,然后注入一个 Mapper 接口,背后就是 FactoryBean 在生成代理对象。容器里并没有一个真正的 Mapper 实现类的 Bean,只有对应的 FactoryBean,产品对象每次通过 getObject() 用 JDK 动态代理生成,调用方拿到的接口实现其实是代理。

第二个是 AOP 代理创建。Spring AOP 在需要生成代理对象时,会构造一个 ProxyFactoryBean(也是 FactoryBean 的实现),用它来生产最终的代理。产品类型是目标接口或类,工厂本身在容器里只是一个加工节点。

工作里如果你遇到“注入的类型明明存在却失败”“同一个 Bean 名字拿到的类型和声明的接口对不上”这类问题,很可能就是背后有 FactoryBean 参与。这时候不要慌,先在断点里看 getObjectForBeanInstance 是怎么走的,再决定是排查工厂还是排查产品。

4.3 FactoryBean 与 @Configuration 生产 Bean 的区别

有人会问:直接用一个 @Bean 方法返回对象不就好了,为什么还要 FactoryBean?

答案是杠杆不同。@Bean 方法在容器启动时执行一次,返回的产品对象就是 Bean 本身,容器不管构建细节;而 FactoryBean 将“构建细节”封装在一个实现了特定接口的类里,容器能感知它的存在,能参与它的生命周期,还能控制产品缓存策略。

FactoryBean 真正不可替代的地方,是它既可以做到延迟构建——产品只有在第一次 getBean 时才调用 getObject(),又可以让容器在不知道产品具体类型的情况下完成注册。这在框架整合类场景中尤其重要,因为你没法在配置阶段就确定产品的真实类型和构建参数。

如果只是需要在一个方法里拼接一个对象,不要强行上 FactoryBean。会用哪种没有绝对对错,但理解了二者的边界以后,选择会清晰很多。

5. 实战避坑与排查笔记

5.1 坑一:getObjectType 不准导致按类型注入失败

场景:自己写了一个 FactoryBean,getObject() 返回的是接口的代理对象,但 getObjectType() 返回了实现类,或者直接返回 null。然后某个 Service 里用 @Autowired 按接口类型注入这个产品。

结果:启动报错,提示找不到对应类型的 Bean,或者 no unique bean of type。原因在于容器按类型匹配候选 Bean 时,依赖的是 getObjectType 的返回值。如果这个值缺失或不准,容器就无法把工厂产品当成候选。

排查思路很简单:断点打到 getObjectForBeanInstance,或者顺着 AbstractApplicationContext 的类型查找逻辑走一遍,看 type 是从哪来的。修法是让 getObjectType() 尽量精确返回产品接口类型。如果产品是代理对象、运行时类型是内部类,不要用 obj.getClass() 去推断,直接写接口的 Class 对象更稳。

5.2 坑二:isSingleton 与产品缓存的关系被误解

场景:FactoryBean 的 isSingleton() 返回 false,但某处 getBean 两次,发现两个对象完全一样。你以为是缓存问题,其实是别的地方把产品对象存下来了,比如单例池缓存了另一个引用,或者产品被设置到其他 Bean 的属性里。

反方向也常见:isSingleton() 返回 true,但每次调 getObject() 里都打印了新时间戳,以为没走缓存。其实产品确实被缓存了,时间戳不同是因为产品内部记录了创建时间,但你拿到的始终是同一个实例,只是字段值在别处被修改了。

理解这个机制的关键是:FactoryBean 的产品缓存只发生在容器内部的 factoryBeanObjectCache,外部任何地方都不能绕过它去“再生成”产品。想验证缓存是否生效,直接在 getObject() 里打印一行日志,再看启动和运行期日志输出次数即可。

5.3 坑三:& 前缀与工厂自身注入

场景:你想注入 FactoryBean 本身,结果注入进来的是产品对象。这个坑几乎是每个用到自定义 FactoryBean 的人都会踩一次。

记住区分:getBean("xxx") 拿产品,getBean("&xxx") 拿工厂。在依赖注入时同理,用 @Qualifier("&xxx") 可以强制拿工厂。但更推荐的做法是:把工厂作为独立 Bean 注入到需要它的地方,而不是依赖 前缀去做依赖注入,因为 前缀在表达式场景下容易被解析出歧义。

还有一个隐蔽问题:如果某个 FactoryBean 参与循环依赖,而你又想在另一个 Bean 里注入它自身,循环依赖处理时可能会拿到产品而不是工厂。这时候需要检查 Bean 的类型判断逻辑,必要时让 getObjectType() 返回真实产品类型,避免容器误判。

5.4 常见问题速查表

问题现象可能原因解决建议
getBean 拿到的是产品而不是工厂没用 前缀使用 getBean("&beanName")
按接口类型注入失败getObjectType 返回值不准或为 null精确返回产品接口类型
FactoryBean 产品每次都一样或不一样isSingleton 配置影响产品缓存按需求设置 true 或 false
启动时 FactoryBean 的 getObject 执行了单例产品在 getBean 时才创建检查是否有早期依赖触发
工厂本身被注入了产品注入时用了产品名字用工厂类注入或 前缀

5.5 调试 FactoryBean 时的心得

我在实际排查中,最常用的一招是直接写一个临时测试,绕过 Spring 的注解扫描,手动构造 DefaultListableBeanFactory 并注册 FactoryBean。这样可以把 getBean 的路径完全暴露在断点下,看到每一步返回什么,比在完整项目里翻调用栈省力得多。

如果觉得 debug 级别日志太吵,可以只开 org.springframework.beans.factory 包下的 DEBUG 日志。Spring 在创建 Bean、调用 getObject 等关键节点都有日志输出,顺着日志里的 beanName 找,基本能定位问题。这个方法我自己用了很久,效果稳定。

6. 最后,说说我自己的经验和习惯

6.1 我判断要不要用 FactoryBean 的三个问题

我第一次真正理解 FactoryBean,不是在读文档的时候,而是在排查一个 Mapper 注入失败问题时。当时 debug 了很多层,最后发现容器里注册的确实是一个 FactoryBean,而我一直在按普通 Bean 的视角找问题。从那以后,我只要看到类型对不上、或者 getBean 结果和预期不一致,第一反应就是去确认这个 Bean 是不是 FactoryBean 家族。

日常开发中,如果代码里要写一个工厂类,我会先问自己三个问题:这个产品是不是要延迟产生?产品创建逻辑是否复杂到不适合放在构造方法里?是否需要在容器不知晓具体类型的情况下完成注册?三个问题只要有一个是肯定的,FactoryBean 就是不二选择;如果只是简单拼装,@Bean 方法足够。

6.2 一个容易被忽略的调试小技巧

还有一个关于 & 前缀的冷知识:在 XML 配置时代,& 是写在 name 属性里的,比如 ref="&myFactory"。到了注解和 Java 配置时代,很多人已经没见过这个写法了。但 getBean("&") 仍然有效,这是和底层实现保持一致的接口。建议所有框架使用者都记住这个特性,说不定排查问题时就是救命稻草。

把 BeanFactory 和 FactoryBean 放进 Fate 的框架里,我自己的收获是:很多东西不是记不住,而是没有一个能承载其结构的“叙事”。王之财宝对应存量,投影魔术对应生成,一旦把容器和工厂的职责边界映射到这两个角色上,后面所有源码细节都能顺着这个比喻长出来。

这篇内容没有打算把所有源码贴一遍。我更希望大家带着这个类比去读 org.springframework.beans.factory 包下的源码,你会发现,原来闪闪和红 A 早就把答案写在了接口设计里。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询