ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

FactoryBean 机制详解:从BeanFactory误区到Spring容器定制对象创建

2026/10/2 21:11:42 拓冰建站 浏览量
FactoryBean 机制详解:从BeanFactory误区到Spring容器定制对象创建 1. FactoryBean 到底是干什么的——从 BeanFactory 的误会说起先说个很常见的事很多人第一次看到 FactoryBean 这个类名第一反应是这不就是 BeanFactory 的简写吗我在带团队评审代码的时候几乎每次提到 FactoryBean都会有人下意识把它当成 BeanFactory。其实这两个东西的关系用一句不太严谨但特别好记的话来概括BeanFactory 是 Spring 容器的总管家而 FactoryBean 是容器里一个特殊租户的定制生产线。BeanFactory 负责管理所有 Bean 的生命周期、依赖注入、作用域是整个 IoC 容器的基础设施接口。而 FactoryBean 本身也是一个 Bean但它是一个会生孩子的 Bean——当 Spring 容器扫描到一个实现了 FactoryBean 接口的类时它不会直接把该类本身作为 Bean 暴露给外部而是调用这个类的getObject()方法把返回的对象作为最终的 Bean 注册进容器。也就是说你在代码里Autowired的其实是getObject()的产物而不是 FactoryBean 实现类自身。这个机制解决了什么问题我举个实际例子假设你要把一个第三方连接池对象注入到业务代码里但这个第三方的连接池构造过程特别复杂需要设置十几个参数还要做一些初始化校验。如果让你每次都在Bean方法里写一堆初始化代码那代码会非常啰嗦而且多个地方需要复用的时候就只能复制粘贴。FactoryBean 就是为这种创建过程复杂、需要封装、需要复用的对象提供了一种标准化的定制创建方案。它把复杂的创建逻辑收敛到一个类里容器负责调用业务方只需要依赖最终产品对象就行。适合谁来读这篇文章我认为只要你在用 Spring Framework 写业务代码尤其是做中间件封装、二次开发、框架集成这类工作的同学都值得把 FactoryBean 吃透。它不像 IoC 和 AOP 那样人人都在讲但它在 Spring 内部被大量使用像mybatis-spring里的MapperFactoryBean、Apache Shiro里的ShiroFilterFactoryBean底层全都有它的影子。2. 核心接口拆解三个方法每一个都有讲究2.1 先看接口定义别急着写代码FactoryBean 接口的完整定义并不复杂核心就是三个方法。我直接贴出来public interface FactoryBeanT { // 返回由 FactoryBean 创建的对象实例 T getObject() throws Exception; // 返回该工厂创建对象的类型 Class? getObjectType(); // 返回该工厂创建的对象是否为单例 default boolean isSingleton() { return true; } }注意看从 Spring 5.0 开始isSingleton()有了默认实现默认返回true。这意味着如果你不关心对象的单例性完全可以不重写这个方法。但如果你创建的每个对象都应该是独立的那就要把这个方法改成返回false——这个细节后面我会专门讲因为它牵扯到一个非常隐蔽的坑。getObjectType()这个方法的返回值会影响 Spring 的依赖注入匹配。比如你有一个接口PaymentService它有两个实现类分别由两个 FactoryBean 来创建。如果getObjectType()实现得准确容器按类型注入时会精确匹配如果返回null或者返回错的类型Spring 只能退化成按名称匹配运气不好就直接报NoUniqueBeanDefinitionException或者NoSuchBeanDefinitionException。getObject()是整个接口的核心它承担了对象的真正创建过程。这个方法可以是任意复杂逻辑可以是反射、代理、动态字节码生成、远程调用什么都可以只要最终能返回一个非null实例。注意如果它返回nullSpring 不会立刻报错但如果你在容器初始化阶段就依赖这个 Bean那就会被BeanCreationException砸脸。2.2 为什么说 isSingleton() 藏着一个大坑我一开始接触 FactoryBean 的时候对这个方法的理解就是返回对象是否单例直到有一次排查线上问题才真正意识到这个方法的返回值不只是控制单例那么简单它还会影响创建时机。Spring 容器中单例 Bean 默认是容器启动时就立即创建的除非设置了lazy-init。如果 FactoryBean 的isSingleton()返回true那么getObject()会在容器初始化阶段被调用产品对象会被提前创建并缓存如果返回false那getObject()会推迟到每次获取该类型对象时才被调用而且每次调用都会生成一个新实例。这里有个非常容易踩的坑如果你在getObject()里做了很重的资源初始化比如开启了一个网络连接池但isSingleton()返回了false那你的连接池对象会被创建无数次资源瞬间被打爆。反之如果你的产品对象内部持有可变状态而且状态在不同的调用方之间需要隔离你却把isSingleton()返回true那就会导致不同调用方共享了同一个可变对象出现数据串味。我的建议是拿不准产品对象是否应该共享时先问自己一句——这个对象里面有没有非线程安全且需要隔离的状态如果答案是没有那就保持默认的true如果答案是有那必须改成false。2.3 getObjectType() 返回 null 的连锁反应getObjectType()如果你偷懒直接返回null大多数时候应用不会启动失败因为 Spring 在真正注入时还会通过getObject()去获取实例然后判断类型。但问题出在提前校验和自动装配匹配这两个环节。举个具体场景你把 FactoryBean 声明后又在另一个配置类里写了一个Autowired的ListPaymentService期望注入所有PaymentService类型的对象。如果getObjectType()返回nullSpring 在做类型扫描的时候没办法判断这个 FactoryBean 的产品是不是PaymentService结果就是这个对象会被遗漏不会出现在注入列表中。如果你用ApplicationContext.getBeansOfType(PaymentService.class)去手动获取同样会遇到这个问题那getObjectType()返回null的 FactoryBean 虽然已经注册了但因为类型未知所以不会出现在结果里。说白了这个方法的准确性直接决定了按类型查找的可靠性而按类型查找正是 Spring 容器最核心能力之一。所以我的经验是永远别让它返回null除非你完全清楚自己在干什么。3. 实操手写一个自定义 FactoryBean3.1 经典场景封装一个第三方配置加载器理论说再多不如直接上手写个例子。我用一个非常贴近真实业务场景的案例来演示假设公司内部有一个配置中心 SDK它提供了一个ConfigClient类使用前需要先调用ConfigClient.init(configCenterAddress, appName, env)做初始化然后才能调用getConfig(key)获取配置。这个初始化过程涉及网络连接、账号认证、环境判断而且整个进程内只应该初始化一次多个业务类要共享同一个实例。如果把初始化逻辑直接写在每个业务类里那就会导致重复代码、重复初始化还容易出现并发初始化的问题。用 FactoryBean 来做这件事简直再合适不过了。我先定义一个产品类public class ConfigClient { private String configCenterAddress; private String appName; private String env; public ConfigClient(String configCenterAddress, String appName, String env) { this.configCenterAddress configCenterAddress; this.appName appName; this.env env; // 模拟发起远程连接、权限校验等初始化动作 init(); } private void init() { System.out.println(ConfigClient 初始化完成连接地址: configCenterAddress); } public String getConfig(String key) { return mock-config-value- key; } }然后实现 FactoryBeanpublic class ConfigClientFactoryBean implements FactoryBeanConfigClient { private String configCenterAddress; private String appName; private String env; // setter 由 Spring 容器注入属性值 public void setConfigCenterAddress(String configCenterAddress) { this.configCenterAddress configCenterAddress; } public void setAppName(String appName) { this.appName appName; } public void setEnv(String env) { this.env env; } Override public ConfigClient getObject() throws Exception { return new ConfigClient(configCenterAddress, appName, env); } Override public Class? getObjectType() { return ConfigClient.class; } Override public boolean isSingleton() { return true; } }注意几个实现细节getObjectType()直接返回ConfigClient.class准确且高效isSingleton()返回true因为 ConfigClient 初始化很重必须全局单例setter 方法一定要留好这是让 Spring 容器注入配置参数的入口。3.2 两种注册方式XML 与 JavaConfig 的差异在 Spring Boot 大行其道的今天XML 配置确实少见了但很多遗留项目和公司内部框架还在用而且理解 XML 方式对理解 FactoryBean 的底层机制非常有帮助。我两种都写出来你感受一下差别。XML 方式bean idconfigClient classcom.example.config.ConfigClientFactoryBean property nameconfigCenterAddress valuehttp://config-center.internal/ property nameappName valueorder-service/ property nameenv valueprod/ /bean注意一个关键点这里bean的class指向的是ConfigClientFactoryBean而不是ConfigClient。Spring 容器在实例化这个 Bean 的时候会先创建一个ConfigClientFactoryBean实例然后发现它实现了 FactoryBean 接口于是调用getObject()获取产品对象。最终容器里以configClient这个名字注册的是getObject()返回的产品对象而不是工厂对象本身。JavaConfig 方式Configuration public class ConfigClientConfig { Bean public ConfigClientFactoryBean configClientFactoryBean( Value(${config.center.address}) String address, Value(${app.name}) String appName, Value(${app.env}) String env) { ConfigClientFactoryBean factoryBean new ConfigClientFactoryBean(); factoryBean.setConfigCenterAddress(address); factoryBean.setAppName(appName); factoryBean.setEnv(env); return factoryBean; } }这里有个非常容易被误解的点方法名明明叫configClientFactoryBean返回类型也是ConfigClientFactoryBean但你在其他类里Autowired却应该注入ConfigClient而不是ConfigClientFactoryBean。因为 Spring 在处理带Bean的方法时一样会检测返回值是不是 FactoryBean如果是就按 FactoryBean 的机制处理。方法名成了容器里的 Bean 名称但 Bean 的实际类型是产品类型。不信你可以加一行代码验证Component public class DemoService { Autowired private ConfigClient configClient; public void print() { System.out.println(configClient.getConfig(timeout)); } }这个DemoService启动起来不会报错说明configClient确实被注入了。而且如果你把注入类型改成ConfigClientFactoryBean你会发现启动直接报NoSuchBeanDefinitionException——因为工厂对象本身并没有作为 Bean 注册你想通过Autowired拿工厂对象是不行的。3.3 想拿工厂对象本身用 ApplicationContext.getBean(名字)等一下那如果我真的需要操作工厂对象比如我想动态改一下配置再重新获取产品对象该怎么办Spring 早就想好了它约定了一个特殊前缀。你可以通过applicationContext.getBean(configClient)来获取工厂对象本身。来看这段代码ApplicationContext ctx new AnnotationConfigApplicationContext(AppConfig.class); ConfigClient client ctx.getBean(configClient, ConfigClient.class); ConfigClientFactoryBean factory ctx.getBean(configClient, ConfigClientFactoryBean.class); System.out.println(client.getConfig(retry)); System.out.println(factory.getObjectType());在没有前缀的情况下getBean(configClient)返回的是getObject()的产品对象加上前缀Spring 才会返回被内部包装的 FactoryBean 实现类实例。这个约定看起来很简单但它是从 Spring 1.0 时代一直保留至今的底层约定理解它对排查那种我拿到了对象但怎么不是预期类型的诡异问题特别有帮助。4. 常见问题与排查技巧实录4.1 getObject() 返回 null 会发生什么这是我在给团队做代码审查时几乎必提的一个问题。如果getObject()返回了null而且这个 Bean 又不是懒加载容器在启动阶段就会抛异常完整错误信息大致是org.springframework.beans.factory.BeanCreationException: Error creating bean with name configClient defined in class ...: FactoryBean threw exception on object creation; nested exception is java.lang.NullPointerException但如果这个产品对象是懒加载的或者其他 Bean 提前引用了它那报错时机就不确定可能出现在启动阶段也可能出现在第一次调用业务功能的时候。这种延迟爆炸在线上最不好查。所以我给你一个硬性建议在getObject()方法的最后一定要显式对返回对象做一次Assert.notNull(result, ...)判断让问题尽早暴露在启动阶段。哪怕你觉得自己写的逻辑不可能返回 null也要加因为你无法控制后续维护者往里面塞了什么条件判断。4.2 为什么 Autowired 拿到的不是工厂对象不少新人问过我我明明定义了一个 XxxFactoryBean为什么Autowired到 XxxFactoryBean 类型的时候报错说没有这个 Bean这个问题的答案在前文其实已经提到了容器注册的是工厂生产的产品对象工厂对象本身并不是一个普通 Bean。但这里有一个更微妙的情况如果你在getObject()方法里返回的对象恰好就是工厂自身那Autowired工厂类型确实是能注入成功的因为产品对象和工厂类型一致。这种自引用式的 FactoryBean 虽然没什么实际意义但会在排查问题时造成极强的迷惑性。我建议遇到为什么这个 Bean 注入不进来的问题时第一反应不是去检查ComponentScan范围而是先去判断它是不是 FactoryBean 的产物。判断方法很简单在getBeanDefinitionNames()打印出来的 Bean 列表里找到那个名字再用前缀获取一次看看返回的类型和普通获取的类型是不是不同类。如果不同那基本可以确定是 FactoryBean 机制在起作用。4.3 FactoryBean 与 Bean 静态工厂方法的抉择很多人会问既然Bean方法也能定义复杂的创建逻辑那还用 FactoryBean 干什么一句话回答Bean方法是给当前配置类用的FactoryBean 是可复用可传递的。举一个更直白的例子如果我的配置中心客户端在多个不同的 Starter 里都要用到那我当然希望在每个 Starter 里都配置一遍Bean方法或者更优雅一点——直接把ConfigClientFactoryBean作为一个公共组件放到共享库里每个业务模块只声明配置参数就能复用同一套创建逻辑。FactoryBean 天生具备逻辑内聚、声明复用的能力而且可以被继承和扩展这是Bean方法不具备的。另外还有一个很实际的差异FactoryBean 因为实现了固定接口Spring 在容器初始化时可以对它做更结构化的处理比如在AbstractAutowireCapableBeanFactory内部有针对 FactoryBean 的专门处理分支而Bean方法本质还是走普通 Bean 的实例化流程。所以在对运行机制有较强依赖的框架代码里FactoryBean 反而更正统。4.4 排查技巧速查表现象可能原因排查方向Bean 注入报找不到类型getObjectType()返回类型不准确检查getObjectType()返回值确认与产品对象类型一致容器启动时抛 FactoryBean 异常getObject()内部抛错或返回 null在getObject()内加断言提前暴露问题获取不到工厂对象本身不理解前缀约定确认使用getBean(xxx)方式获取工厂本身单例对象状态互相污染isSingleton()返回了true但产品对象含可变状态重新评估产品对象是否应该共享按类型获取到的对象集合缺少某一项getObjectType()返回 null设置准确的getObjectType()Autowired工厂类型报错工厂对象本身未暴露为 Bean改为依赖产品对象或通过前缀获取这张表是我在实际支持多个项目时总结出来的基本覆盖了 FactoryBean 使用过程中的高频雷区。你现在可以把这张表截图存下来等真踩到坑了再回来对照。5. 我强烈建议的进阶视角理解 FactoryBean 在框架中的真实应用前面讲的都是我们如何自己实现 FactoryBean但其实真正让 FactoryBean 这个机制显得重要、并且在面试和框架源码阅读中被反复提到的原因是它在 Spring 生态中的大量底层应用。如果只看我们前文的示例你可能会觉得这不就是一个封装复杂创建的技巧吗那我只能说你还没看到它的真正威力。举一个典型例子mybatis-spring中的MapperFactoryBean。你平时写 MyBatis 只用写一个 Mapper 接口然后调用sqlSession.getMapper(UserMapper.class)但你有没有想过这个 Mapper 接口没有实现类它是怎么被注入到 Service 里的答案就是 MapperFactoryBean。它可以被定义为为一个接口生成动态代理对象每个 Mapper 接口对应一个 MapperFactoryBean。容器启动时Spring 会调用getObject()此时 mybatis-spring 会使用 SqlSession 创建一个 JDK 动态代理类把这个代理对象注册到容器中。所以你Autowired UserMapper的时候拿到的是一个代理实例它内部实际上会去调用 SqlSession 执行 SQL 操作。类似的还有 Spring 对 JNDI 数据源的封装、对 JMX 的 MBeanServerConnection 封装以及对各种远程服务RMI、Hessian的客户端代理封装。你会发现这些场景都有共同的特征对象不能简单通过构造函数创建需要经过某个中间过程而且创建过程是通用可复用的。FactoryBean 恰恰就是为这种场景而生的标准解法。另外一个进阶视角是看它在 Spring 容器内部的处理逻辑。你可以在AbstractBeanFactory类的源码中搜索getObjectForBeanInstance方法这个方法内部会判断 Bean 是否为 FactoryBean并根据是否带前缀来决定返回工厂对象还是产品对象。整个判断逻辑很精巧建议你去读一遍能极大提升你对 Spring 底层容器的理解。读到源码层面之后你就能自然理解为什么 FactoryBean 在一些场景下会被绕过。比如如果你的配置类里用了Bean返回一个普通对象而这个普通对象的类型恰好和某个 FactoryBean 的产品类型一致那在按类型注入时就可能出现两个候选 Bean的冲突报NoUniqueBeanDefinitionException。这种问题不把 FactoryBean 的机制搞清楚光靠试错很难定位。在我这几年的实践经验里有一个很深的体感理解 FactoryBean 是理解 Spring 内部为何有那么多神奇对象的关键一步。很多人觉得 Spring 是魔法其实 Spring 最大的优点恰恰是它把所有魔法点都收敛到了极其有限的几个扩展接口里FactoryBean 就是其中一扇门。你推开这扇门之后再去看那些框架的xxxFactoryBean就不再是看到一个类名而是看到了一条从声明到产品生成的清晰链路。最后再分享一个小技巧如果你在公司里做框架封装需要暴露给业务方一个可配置的复杂组件与其让业务方写一堆Bean初始化代码不如封装一个 FactoryBean 并提供几个setter参数再把参数通过配置中心下发。这样业务方的接入成本会大幅降低你也能把初始化逻辑、校验逻辑、异常处理统一收敛在一个类里。我在实战中靠这个做法把多个项目的配置客户端接入从每人写30行压缩到了配置5个参数维护成本直线下降。这个模式值得你尽快用起来。