ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

BeanFactory与FactoryBean:Spring容器和工厂Bean的职责差异

BeanFactory与FactoryBean:Spring容器和工厂Bean的职责差异

说个挺有意思的现象:我带过的新人里,几乎每个第一次看到BeanFactory和FactoryBean这两个名字时,都会愣一下。这两个接口从拼写上只差一个前缀,但从职责上看完全是两码事。不少人学Spring学到一半,就是被这种"双胞胎命名"卡住的。这篇是Spring极简入门教程的第四篇,前几篇聊过依赖注入、Bean生命周期这些基础,这篇单独把BeanFactory和FactoryBean拎出来掰开揉碎讲清楚,顺便把源码调用链也走一遍。

这篇内容适合两类人:一类是刚学完IoC基础、开始接触Spring内部结构的初学者;另一类是已经在用SpringBoot写业务、但一直没有系统搞懂容器底层逻辑的开发者。看完你会明白,为什么有的对象可以直接@Autowired拿回来,有的对象拿到手之后跟你想象中的"那个类"不一样。

1. 两个接口,名字只差一个前缀,职责差出十万八千里

先解决最基础的困惑:BeanFactory和FactoryBean到底分别是什么?

简单粗暴地记:BeanFactory是容器本身,FactoryBean是容器里的一个Bean。一个是管理所有Bean的"总后台",另一个只是被管理的众多Bean中比较特殊的一种。名字像,是因为FactoryBean这个名字里包含了Bean和Factory两个词,而BeanFactory正好是这两个词的顺序调换。顺序一变,含义全变,这在Spring里属于经典命名迷惑点。

1.1 为什么这两个名字能让人绕晕

很多时候我们学习一个新东西,最难的不是知识点本身,而是知识点之间的"名字撞车"。BeanFactory和FactoryBean就是这样一对。

我打个比方。如果把Spring容器比作一家餐厅,那么BeanFactory就是这家餐厅的后厨调度中心,所有菜品的制作流程、原材料管理、出餐顺序都由这个调度中心控制。而FactoryBean更像后厨里的一台自动咖啡机,它本身是后厨的一件设备,但它的作用不是给自己炒菜,而是按一下按钮就吐出一杯做好的咖啡。容器想知道"今天有多少设备、每个设备能生产什么",而咖啡机关心的是"怎么把咖啡豆变成一杯拿铁"。这两者的身份完全不同,但名字确实太像了。

理解了这层关系,再看getBean方法的行为就会豁然开朗。正常情况下,从容器里拿一个Bean,拿到的就是配置里那个类的实例。但如果配置的是一个FactoryBean,容器会优先调用它的getObject()方法,把生产出来的那个对象返回给你,而不是返回FactoryBean本身。这个"偷梁换柱"的设计,就是这门课的核心。

1.2 用一张表看清职责边界

我自己学习的时候,喜欢把容易混淆的东西放在一张表里硬记。这里也给你整理了一份:

对比项BeanFactoryFactoryBean
身份容器根接口,是整个IoC容器的最顶层抽象一个普通Bean,只不过它实现了工厂方法逻辑
核心方法getBean()、containsBean()、isSingleton() 等getObject()、getObjectType()、isSingleton()
谁来创建它由容器自己创建,所有Bean都归它管由容器创建,但它的"产品"由它的 getObject() 决定
典型实现DefaultListableBeanFactoryProxyFactoryBean、MapperFactoryBean 等
与普通Bean的关系它是Bean的"管理者"它本身也是Bean,被BeanFactory管理
获取方式一般通过 ApplicationContext 间接使用getBean("&name") 才能拿到FactoryBean本身

这张表我建议你截图保存。后面的内容,基本都是围绕这张表展开的。

2. BeanFactory:Spring容器内部那个"总开关"

先来讲BeanFactory。它是Spring IoC容器最底层的接口,整个Spring框架的依赖注入、生命周期管理,都是从这一个接口开始向外扩展的。

2.1 BeanFactory到底管哪些事

BeanFactory定义了容器最基本的操作规范。打开Spring源码,你会看到它的方法并不多,但每一个都精确到点子上:

  • getBean(String name):根据名字获取Bean实例
  • getBean(Class<T> requiredType):根据类型获取Bean实例
  • containsBean(String name):判断容器中是否存在指定名字的Bean
  • isSingleton(String name):判断指定Bean是否是单例
  • isPrototype(String name):判断指定Bean是否是原型模式
  • getType(String name):获取指定Bean的类型
  • getAliases(String name):获取指定Bean的所有别名

注意观察,这些方法都围绕着一个核心动作:获取Bean。也就是说,BeanFactory的本质职责就是管理Bean的创建、获取、类型判断和关系维护。你在代码里写的所有@Service、@Component、@Repository,最终都是被这样一个接口的实现类创建出来的。

我自己刚接触源码时有个感受:原来Spring Boot里那些看起来很高级的自动配置、条件装配,地基其实都是这个看起来有点"朴素"的接口。它方法不多,但每一条都是后面所有扩展机制的起点。

2.2 为什么实际项目里到处是ApplicationContext

有个问题很多新手会问:既然BeanFactory是根接口,为什么平时写代码都是直接用ApplicationContext?

原因很简单,ApplicationContext是BeanFactory的子接口,它在BeanFactory的基础上扩展了更多功能。如果把BeanFactory比作一台只能出品的咖啡机,那ApplicationContext就是一台自带磨豆、打奶泡、保温、显示菜单的多功能一体机。BeanFactory只负责Bean的创建和获取,而ApplicationContext还加入了:

  • 国际化消息支持
  • 事件发布与监听机制
  • 资源加载与访问
  • 自动注册BeanPostProcessor、BeanFactoryPostProcessor
  • 多个配置文件与多个容器的整合能力

还有一个关键区别:BeanFactory默认是延迟加载的,也就是说你调用getBean时才真正创建对象;而ApplicationContext在启动阶段就会把大部分单例Bean预先创建好。这个设计差异会直接影响应用的启动速度和首次访问速度。

在实际工程里,ApplicationContext几乎成了"容器"的代名词,但你心里要清楚:它只是继承了BeanFactory的扩展版容器。面试的时候如果只答出"ApplicationContext是BeanFactory的子接口",那只是背了个定义;能把上面这几点差异说清楚,才算真懂。

3. FactoryBean:专门用来"造Bean的Bean"

名字里带Factory的Bean,它的定位很特殊。一个普通的Bean,容器拿到类定义之后直接反射创建实例就完事了。但是某些对象没法这么简单地创建,比如:需要动态生成代理对象、需要读取外部配置来初始化、需要连接第三方系统才能构建。这时候就需要FactoryBean出马。

3.1 三个方法决定了它的全部行为

FactoryBean接口一共就三个关键方法,理解了这三个方法,整个机制就通了:

public interface FactoryBean<T> { T getObject() throws Exception; Class<?> getObjectType(); boolean isSingleton(); }

第一个是getObject(),这是核心中的核心。容器发现你配置的Bean实现了FactoryBean接口后,会把创建对象的权力交给你,调用你的getObject()方法去生产对象。你在这个方法里想怎么造都行:可以直接new,可以走代理工厂,也可以解析一段复杂的配置后再返回。

第二个是getObjectType(),用于告诉容器:"我这个工厂生产出来的产品是什么类型"。这个方法不是随便写写的,容器在类型匹配、依赖注入判断、提前初始化时都需要读取这个返回值。

第三个是isSingleton(),返回true表示这个工厂生产出来的产品以单例形式缓存,容器只创建一次;返回false则表示每次获取都会重新调用getObject()生成一个新对象。

说到这里,我建议你动手敲一下代码验证。因为这几个方法的关系,光看文档是记不住的,真正写一个FactoryBean跑起来,比读十篇博客都管用。

3.2 什么时候才值得自己写一个FactoryBean

有一种常见误解是:项目里想创建复杂对象,就应该用FactoryBean。其实不是。@Configuration+@Bean方法已经能覆盖绝大多数"手动构建对象"的场景。那FactoryBean特有的价值在哪?

我认为有两个场景是它无法替代的:

一是需要把创建过程封装成可复用的组件。比如封装一个SDK客户端,你希望别的服务只要注入一个接口类型,就能自动获得一个配置好的客户端实例。用FactoryBean可以把创建逻辑和业务逻辑彻底隔离。

二是你需要对Spring隐藏返回对象的真实类型。最常见的就是代理对象。你配置的上层依赖是一个接口,实际返回的是一个代理实现,代理对象的具体类可能是字节码动态生成的,这时候FactoryBean的getObjectType()可以返回接口类型,让容器按接口来管理依赖关系,而真正的代理类对容器透明。

这个设计思路,后来慢慢演变成了很多框架底层集成的标准姿势。你看MyBatis的Mapper注入、Spring对RMI和JNDI的封装,背后都有FactoryBean的影子。

4. 手写一个FactoryBean,亲眼看看getBean返回的到底是谁

道理讲了一堆,不如直接上手跑一遍。我带你在代码里把这个机制彻底验证清楚。

4.1 先造一个产品类和它的工厂

我这边的模拟场景是:创建一个日期对象,但不直接指定具体值,而是通过一个字符串参数去解析。这个"通过字符串解析出Date对象"的过程,就交给FactoryBean来做。

先写产品类,很简单:

public class Order { private String orderNo; private Date createTime; // getter、setter 略 }

然后写FactoryBean:

public class OrderFactoryBean implements FactoryBean<Order> { private String orderNo; private String dateStr; public void setOrderNo(String orderNo) { this.orderNo = orderNo; } public void setDateStr(String dateStr) { this.dateStr = dateStr; } @Override public Order getObject() throws Exception { Order order = new Order(); order.setOrderNo(orderNo); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); order.setCreateTime(sdf.parse(dateStr)); return order; } @Override public Class<?> getObjectType() { return Order.class; } @Override public boolean isSingleton() { return true; } }

重点来了:注册这个FactoryBean的时候,class写的是OrderFactoryBean,但容器返回给你的产品是Order。

<bean id="order" class="com.example.factorybean.OrderFactoryBean"> <property name="orderNo" value="SN20240601"/> <property name="dateStr" value="2024-06-01 10:30:00"/> </bean>

4.2 注册进容器并验证&前缀

接下来用一段代码跑一下:

public class FactoryBeanDemo { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("factorybean-demo.xml"); // 这里返回的是Order对象,不是OrderFactoryBean Order order = context.getBean("order", Order.class); System.out.println(order.getOrderNo()); System.out.println(order.getCreateTime()); // 用 & 前缀才能拿到FactoryBean本身 OrderFactoryBean factoryBean = context.getBean("&order", OrderFactoryBean.class); System.out.println(factoryBean.getObjectType()); } }

看到没有,同一个bean名字,前面加不加&,拿到的类型完全不同。getBean("order")拿到的是getObject()的产品,getBean("&order")拿到的才是FactoryBean本身。

这个&的规则,我第一次接触的时候觉得特别反直觉,为什么非要加个符号?后来想明白了:因为在Spring里,getBean传名字的时候,默认语义就是"给我那个产品的Bean",如果你还想拿到工厂本身,就需要有个额外的约定符号来区分。"&"在Java的字符串要求里不会和合法的bean名称冲突,所以被用作这个约定。这个设计虽说不算优雅,但胜在实用。

5. 跟一遍源码链路,搞懂它俩是怎么配合的

前面验证了现象,接下来从源码角度把这个过程看清楚。很多人觉得源码难啃,其实这条链路并不长,关键是找对入口。

5.1 getBean在这条链路上干了什么

当调用context.getBean("order")时,真正执行的是AbstractBeanFactory.doGetBean()方法。它会经历这么几步:

第一,根据名字获取BeanDefinition。BeanDefinition记录了类的全路径、作用域、属性值、初始化方法等等所有元信息。你会看到,配置里的property就是在这里被解析的。

第二,处理循环依赖与作用域。如果发现当前Bean是单例且在创建中,Spring会通过三级缓存机制提前暴露早期引用;如果是原型作用域,则不做缓存,每次新建。

第三,调用createBean()创建实例。你可能以为这里会直接去new一个Order,但容器会先检查这个Bean是否实现了FactoryBean接口。这个判断发生在getObjectForBeanInstance()方法里。

5.2 getObjectForBeanInstance和FactoryBeanRegistrySupport

简化后的逻辑大概是这样的:

// AbstractBeanFactory.getObjectForBeanInstance 的简化逻辑 protected Object getObjectForBeanInstance(Object beanInstance, String name, ...) { if (BeanFactoryUtils.isFactoryDereference(name)) { // 如果beanName带了 & 前缀,直接返回FactoryBean本身 return beanInstance; } if (!(beanInstance instanceof FactoryBean)) { // 如果当前实例不是FactoryBean,直接返回 return beanInstance; } // 核心:从FactoryBean中取出产品对象 return getObjectFromFactoryBean((FactoryBean<?>) beanInstance, ...); }

再往下走一步,getObjectFromFactoryBean在FactoryBeanRegistrySupport这个类里。它做了两件事:如果isSingleton()返回true,会尝试从缓存里取,缓存没有才调用getObject(),并把结果放回缓存;如果isSingleton()返回false,则每次直接调用getObject(),不做缓存。

这条链路总结成一句话:容器先创建FactoryBean本身,再根据注入的名字判断要不要取出它的产品。整个过程都发生在AbstractBeanFactory和FactoryBeanRegistrySupport的协作中,并不需要你去背源码,但理解了这个顺序,以后遇到"Bean还没创建完就报错"这类问题,你会更容易定位到原因。

6. 那些藏在框架里的FactoryBean,以及我踩过的坑

到了应用层面,你会发现平时写的业务代码里很少直接声明一个FactoryBean,但它其实无处不在。

6.1 常见框架中的FactoryBean影子

先举几个典型的例子,帮你建立"原来这些都是FactoryBean"的映射感。

MyBatis和Spring整合时,每个Mapper接口最终都是通过MapperFactoryBean注册进容器的。它实现FactoryBean,getObject()返回的是JDK动态代理生成的Mapper实现类。你在Service里注入Mapper接口,拿到手的就是这个代理产品,而不是MapperFactoryBean本身。

Spring对远程调用、消息监听这类组件做封装时,也喜欢用FactoryBean。比如ProxyFactoryBean,它为指定接口生成AOP代理对象,getObject()返回的是经过切面增强后的代理。你看配置的时候会看到一个target属性,其实target才是业务类,返回的产品是包裹了一层拦截器的代理。

还有一个典型的:JndiObjectFactoryBean,它把JNDI查找和对象获取的过程也封装成了工厂Bean。不过这个在现在的Spring Boot项目里很少见了,知道即可。

6.2 实战中需要注意的几个细节

最后分享几个我实际踩过的坑。

第一个坑:getObjectType()和实际返回类型不一致。早期我在一个项目里写FactoryBean,getObjectType()返回了接口类型,但getObject()返回的却是实现类类型。容器在做类型匹配注入时,偶尔会出现类型不匹配的诡异报错。后来我养成了习惯:getObjectType()老实返回getObject()的真实类型,除非有特殊目的,否则不要两边各说各话。

第二个坑:isSingleton()返回false但没意识到每次注入都会重建对象。有个模块用了FactoryBean,上线后发现每次注入的实例都不是同一个,排查了半天,最后发现isSingleton()被写成了false。对某些包含状态的对象来说,这种配置直接会导致数据不一致。

第三个坑:&前缀在配置中的表现。你在Java配置类里注册FactoryBean时,方法名就是beanName,获取工厂本身依然要用&beanname。另外要注意,@Autowired是按类型注入的,如果想注入工厂本身,类型要写成FactoryBean<Xxx>,否则Spring会优先注入产品对象。这个细节我见过好几个同事吃过亏。

操作上还有个建议:项目里如果只是创建一两个复杂对象,优先用@Bean方法就够了,没必要上FactoryBean。但如果你在做框架封装、组件复用,或者需要隐藏产品的创建细节,FactoryBean依然是Spring提供的最正统的扩展点。理解好这两个接口的区别,不仅是为了过面试,更是为了以后读框架源码时,不会被这两个名字绕昏头。

RELATED READING

延伸阅读

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