
把 BeanFactory 和 FactoryBean 放在一块儿讲是我给团队做 Spring 容器入门梳理时最头疼的一件事。名字只差一个字母网上资料又多半各讲各的新手看完经常抓头“这俩到底是不是一回事”直到我拿 Fate 里两套宝具机制做类比大家才一下通了。Fate 里红 A 的投影魔术和闪闪的王之财宝恰好对应这两个概念的核心差异。闪闪的王之财宝是一个真品仓库里面陈列着无数宝具想要哪件就直接从宝库大门里取红 A 的投影则完全不同他手里并没有原典每次都是靠魔力现场构筑复制品用完就消散需要了再投影一次。把这套逻辑搬进 SpringBeanFactory 是容器本身的顶级接口负责所有 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 里。这个池子同样是一个 MapConcurrentHashMapString, Object只有当某个单例 Bean 被创建之后实例才会放进去。以后再取直接命中缓存不会再创建。强调一下不是所有 Bean 都会进入宝物库。原型prototypeBean 每次 getBean 都会重新创建容器并不缓存它的实例。这一点对应闪闪的宝物库里全是“可复用的真品”而原型 Bean 更像红 A“用完即弃的投影”。所以判断一种 Bean 是“库存”还是“投影”先看它的 scope 就够了。2.2 getBean 的完整路径从请求到交付理解了存储结构再走一遍 getBean 流程就顺了。以 DefaultListableBeanFactory 为例简化路径如下调用 getBean(String name) 或 getBean(Class type)进入 AbstractBeanFactory.doGetBean。先从 singletonObjects 查缓存命中直接返回。涉及循环依赖时还会查 earlySingletonObjects 和 singletonFactories 这两层缓存。未命中则根据 name 拿到 BeanDefinition。根据 BeanDefinition 判断 scope。单例走创建单例的流程原型走 createBean 后直接返回自定义 scope 走对应策略。创建时执行 doCreateBean实例化对象、属性填充依赖注入、执行 Aware 回调、BeanPostProcessor 前置和后置处理、初始化方法。单例 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-inittrue 或者 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 FactoryBeanMessage { 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 不准导致按类型注入失败场景自己写了一个 FactoryBeangetObject() 返回的是接口的代理对象但 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 属性里的比如 refmyFactory。到了注解和 Java 配置时代很多人已经没见过这个写法了。但 getBean() 仍然有效这是和底层实现保持一致的接口。建议所有框架使用者都记住这个特性说不定排查问题时就是救命稻草。把 BeanFactory 和 FactoryBean 放进 Fate 的框架里我自己的收获是很多东西不是记不住而是没有一个能承载其结构的“叙事”。王之财宝对应存量投影魔术对应生成一旦把容器和工厂的职责边界映射到这两个角色上后面所有源码细节都能顺着这个比喻长出来。这篇内容没有打算把所有源码贴一遍。我更希望大家带着这个类比去读 org.springframework.beans.factory 包下的源码你会发现原来闪闪和红 A 早就把答案写在了接口设计里。