Spring6工厂模式全解析:BeanFactory、FactoryBean与ObjectProvider实战

发布时间:2026/9/26 7:22:28
Spring6工厂模式全解析:BeanFactory、FactoryBean与ObjectProvider实战 Spring6 这套笔记写到第 4 篇终于轮到工厂模式了。这个主题放在 IoC 容器的学习路径里特别合适因为 Spring 本身就是个超级工厂——BeanFactory 这个名字已经明说了它就是个工厂ApplicationContext 是增强版工厂。Spring 的整个 IoC 容器本质上是把“new 对象”的权力从调用方手里收走统一交给一个工厂来管理和发放。这个工厂不只负责创建还负责装配、初始化、代理、销毁。这篇笔记想把这层包装撕开从最简单的工厂模式讲起一路看到它在 Spring6 里的落地形态BeanFactory、FactoryBean、Bean 方法、ObjectProvider以及大家写业务代码时最常用到的“策略工厂”范式。不管你是在读 Spring 源码还是在项目里想要优雅地干掉 switch-case 分支创建逻辑这篇笔记都适用。1. 工厂模式为什么是 Spring 的底层逻辑1.1 三种工厂形态与 Spring 的映射不用急着背定义。你可以把工厂模式理解成一句话把“创建谁”和“怎么创建”从调用方手里拿过来交给一个专门的地方负责。调用方只关心“我要一个能用的东西”不关心它从哪来、怎么组装、是单例还是每次都新建。经典的书把工厂模式拆成了三类简单工厂一个工厂类里放 switch-case 或 if-else根据参数返回不同的产品。缺点是每加一个产品就要改工厂。工厂方法定义一个创建产品的抽象方法由子类决定具体创建哪一种。把“创建逻辑”下沉到子类。抽象工厂针对“产品族”的创建可以创建一组相互关联的产品对象而不是单个产品。如果把这三种形态映射到 Spring6 里你会发现 Spring 全是它们的影子简单工厂对应Bean 手动 if-else 或者 Map 收集工厂方法对应某个 Configuration 类的Bean方法抽象工厂对应 Spring 的自动配置机制——AutoConfiguration根据条件决定给你装配哪一组组件。下面这张表是我经常用来给学生做对照的工厂模式形态Spring 中的典型落点典型场景简单工厂Bean方法配合Qualifier、MapString, Interface注入按类型或名称从容器取 Bean工厂方法每个Configuration类里的Bean方法声明式定义 Bean 的创建过程抽象工厂Spring Boot AutoConfiguration Condition根据条件装配一组相关组件但真正值得注意的点是Spring 的工厂模式不是某一个类而是一整套容器机制。BeanFactory管生命周期FactoryBean管复杂对象的创建Bean方法管声明式装配ObjectProvider管延迟获取。四个机制玩明白了工厂模式在 Spring6 里你就通关了。1.2 Spring6 里工厂思想放在哪几个关键位置Spring6 的代码基线是 Java 17底层换成了 Jakarta EE 9 的命名空间但核心的工厂思想没变。我在读源码时发现有几个地方是理解整个容器的钥匙第一个是DefaultListableBeanFactory。这个类名很长但它就是 Spring 容器最核心的“大工厂车间”。它内部维护了beanDefinitionMapBean 定义的注册表和singletonObjects单例池每次getBean()的时候先查单例池没有再根据 BeanDefinition 去创建。整个流程走一遍就是工厂模式的工业化版本。第二个是FactoryBeanT接口。这个接口是给“创建过程特别复杂的 Bean”准备的。比如你创建一个 Redis 连接池希望在 Spring 启动后得到一个 JedisPool 实例这个实例的创建过程涉及一堆参数组装和校验直接在Bean方法里写会显得很乱这时可以实现FactoryBean把创建逻辑封装在getObject()里。第三个是Bean方法本身。很多人没意识到Configuration类里的每个Bean方法就是一个工厂方法。当你调用这个工厂方法时Spring 会介入拦截保证默认情况下同一个Bean方法只执行一次返回的都是同一个单例。这个特性就是 Spring6 中Configuration的proxyBeanMethods属性控制的——默认true用 CGLIB 代理拦截设为false则不做拦截每次调用方法都会新建对象。第四个是ObjectProviderT。这是 Spring 5.1 引入、在 Spring6 里被官方大力推荐的延迟工厂注入方式。你注入的不再是一个直接可用的 Bean而是一个“提供者”等实际要用了再通过getObject()或getIfAvailable()去容器里拿。这个设计对解决循环依赖和可选依赖非常有用。1.3 为什么不是所有 Bean 都必须走 FactoryBean说到这里你可能会问难道每个 Bean 都要手写一个 FactoryBean 吗不是的这是新手最容易误解的地方。FactoryBean是给少数“特殊创建逻辑”准备的接口大多数 Bean 用Component扫描或Bean方法就够了。你可以这样理解普通 Bean 是“流水线上标准化生产”的产品FactoryBean是“定制车间”里手工打造的产品。Spring 设计FactoryBean的初衷是让你在容器标准创建流程之外还能插入自定义的创建逻辑。那 Spring 自己内部用 FactoryBean 做了什么最有名的例子有两个ProxyFactoryBeanAOP 代理对象的创建。你定义切面后Spring 通过它生成代理对象。SqlSessionFactoryBeanMyBatis-Spring 集成时它负责创建SqlSessionFactory因为 SqlSessionFactory 的创建需要读取配置文件、解析 Mapper、注册类型别名逻辑量很大。所以在 Spring6 的项目里当你发现某个对象的创建过程特别复杂或者需要根据运行时配置动态决定返回类型时才值得考虑实现FactoryBean否则用Bean方法即可。2. Spring6 工厂模式核心接口与延迟获取机制解析2.1 BeanFactory 与 FactoryBean名字像、职责完全不同这是 Spring 里最容易混淆的两个名字面试也经常问。一句话区分BeanFactory 是容器的根接口管“所有 Bean 的获取与生命周期”FactoryBean 是一个 Bean管“某一个 Bean 的创建逻辑”。BeanFactory的使命很宏大提供getBean()、containsBean()、isSingleton()、getType()等方法是整个 IoC 容器对外的门面。FactoryBeanT的使命很具体它的泛型 T 代表它最终要创建的产品类型它本身也是一个 Bean注册在容器里但当别人向容器索要这个 Bean 时Spring 发现它是一个FactoryBean会调用它的getObject()把返回结果交给调用者。举个具体的例子。假设我写了一个OrderFactoryBean implements FactoryBeanOrder然后把它注册到容器。别人Autowired一个Order类型时容器检查发现当前处理的 Bean 是OrderFactoryBean于是调用它的getObject()拿到一个Order实例交给调用者。调用者感知不到OrderFactoryBean的存在。这里有一个经典陷阱如果你需要的是OrderFactoryBean本身而不是它生产的Order怎么办答案是使用前缀applicationContext.getBean(orderFactoryBean)。Spring 约定带前缀的 Bean 名返回的是工厂本身不带的返回的是工厂生产的产品。这个细节在实际排查问题时非常关键后面我会在问题章节详细讲。2.2 FactoryBean 的 getObject 与 getObjectType 设计意图FactoryBean接口总共三个抽象方法我来逐个拆解public interface FactoryBeanT { // 返回工厂生产的对象实例 T getObject() throws Exception; // 返回生产对象的类型用于类型匹配和依赖注入 Class? getObjectType(); // 是否单例true 表示容器只保留一个产品实例false 表示每次获取都是新对象 default boolean isSingleton() { return true; } }getObject()是核心你把复杂对象的创建过程写在这里返回一个 T 类型的实例。这个方法在容器启动或首次获取 Bean 时被调用。getObjectType()也很关键Spring 在做自动注入匹配时需要知道你的工厂到底能生产什么类型如果这里返回nullSpring 就不得不实例化工厂才能拿到产品类型可能会导致提前初始化或者类型推断失败。isSingleton()控制产品的缓存策略。默认返回true工厂生产的产品会放入单例池整个容器中只有一个如果你设置为false每次获取都会调用getObject()新建对象。这就相当于手动控制“产品”的作用域。这背后的设计意图是什么呢就是把“Bean 的创建细节”封装在一个可替换的工厂里让 IoC 容器只认FactoryBean接口。容器不需要知道你的产品是三元组装的还是反射创建的它只需要知道“这个工厂能生产某个类型的产品”就可以在你注入该类型的时候通过工厂把对象给你。2.3 ObjectProviderSpring6 中最推荐的延迟工厂注入方式ObjectProviderT是 Spring6 项目里值得重点掌握的一个接口。你可以在注入点使用它而不是直接注入目标对象Service public class OrderService { // 不再直接注入 Handler private final ObjectProviderPayHandler handlerProvider; public OrderService(ObjectProviderPayHandler handlerProvider) { this.handlerProvider handlerProvider; } public void pay() { // 需要时再获取 PayHandler handler handlerProvider.getIfAvailable(); handler.pay(); } }这样做的好处有三个第一延迟获取。容器启动时不会立即创建PayHandler而是注入一个“提供者”真正调用getIfAvailable()时才去容器拿实例。对于创建成本很高的 Bean或者依赖了运行时配置的 Bean这是非常实用的。第二可选依赖。getIfAvailable()在没有 Bean 时返回null比Autowired(required false)更优雅也更容易写默认值逻辑。第三规避循环依赖。两个 Bean 互相引用时如果一边改成注入ObjectProviderT容器的创建顺序就不需要同时满足双方了生命周期问题自然缓解。这个接口的功能远不止这些它还提供了getIfUnique()如果容器中只有一个才返回多个则返回 null、stream()获取所有实现、orderedStream()按Order排序获取所有实现。这几个方法在策略模式 工厂模式的场景下简直是为我们量身定做的工具。我见过很多项目还是老写法Autowired private ListPayHandler handlers;然后在方法里手动遍历。换成ObjectProvider后代码更简洁还能在运行时按需获取不再被“启动时就必须收集全部实现”的约束绑死。Spring6 官方文档在依赖注入章节也明确推荐了ObjectProvider建议你把老代码里的可选注入逐步换掉。2.4 Spring6 里编程式获取 Bean 的更稳姿势除了Autowired和ObjectProvider有时我们需要在非 Spring 管理的类里拿到 Bean。这时很多人直接注入ApplicationContext然后调用getBean()。这种做法能用但不够好。更好的姿势是注入ObjectProvider或使用ApplicationContext.getBeanProvider(ClassT)方法在 Spring6 里这是官方建议的编程式获取方式。原因在于getBeanProvider返回的是一个ObjectProviderT你可以用getIfAvailable、getIfUnique、stream这些方法代码读起来更清晰而且保持了延迟获取的特性。来看一段实用代码示例你有一个工具类里面需要拿到所有MessageHandler实现并按顺序处理消息Component public class MessageDispatcher { private final ObjectProviderMessageHandler handlerProvider; public MessageDispatcher(ObjectProviderMessageHandler handlerProvider) { this.handlerProvider handlerProvider; } public void dispatch(Message msg) { handlerProvider.orderedStream() .filter(handler - handler.supports(msg.getType())) .findFirst() .ifPresent(handler - handler.handle(msg)); } }orderedStream()会按实现类上的Order注解排序返回一个有序流。你可以通过supports()方法筛选出能处理当前消息的那个实现。这种写法比“注入 Map 然后遍历”更优雅也天然支持了开闭原则——新增一个MessageHandler实现类不需要修改MessageDispatcher的任何代码。3. 实操用工厂模式重构多渠道订单处理器3.1 场景与设计接口、注解、自动收集这一节我们抛开理论看一个我在订单中心项目里真实落地的工厂模式改造案例。场景很经典系统对接了多个支付渠道微信、支付宝、银联每种渠道的支付参数、签名算法、回调验签都不一致。最初代码写在一个支付服务类里用长长的 if-else 判断渠道类型public PayResult pay(PayRequest request) { if (wechat.equals(request.getChannel())) { // 微信支付逻辑 } else if (alipay.equals(request.getChannel())) { // 支付宝支付逻辑 } else if (unionpay.equals(request.getChannel())) { // 银联支付逻辑 } else { throw new UnsupportedOperationException(不支持的渠道); } }每接一个新渠道就要改动这个几百行的类测试回归范围越来越大。后来我彻底重构用“接口 注解 自动收集 工厂分发”的经典组合。设计如下定义一个PayHandler接口包含support(String channel)和pay(PayRequest request)方法。每种渠道写一个实现类用Component注册到容器。用Qualifier或 Map 注入把所有PayHandler的实现自动收集到工厂类里。工厂类根据request.getChannel()找到对应处理器并分发。这套设计的好处是新增渠道时只需要添加新的实现类工厂类不用改老代码不用改。这正是工厂模式在业务代码里价值最大的应用。3.2 核心代码实现与逐段说明先定义接口public interface PayHandler { // 当前处理器支持的支付渠道 String channel(); // 支付方法 PayResult pay(PayRequest request); }然后每个渠道实现自己的逻辑。这里以微信支付为例Component public class WechatPayHandler implements PayHandler { Override public String channel() { return wechat; } Override public PayResult pay(PayRequest request) { // 实际的微信支付调用 // 构造参数、签名、调用微信支付 API return PayResult.success(wechat, request.getOrderId()); } }支付宝和银联的实现类似只是渠道值不同、内部逻辑不同。接下来是工厂类的核心代码Component public class PayHandlerFactory { // 关键点通过 Map 注入容器会把所有 PayHandler 类型 Bean 收集进来key 是 Bean 名 private final MapString, PayHandler handlerMap; public PayHandlerFactory(MapString, PayHandler handlerMap) { this.handlerMap handlerMap; } public PayHandler getHandler(String channel) { return handlerMap.values().stream() .filter(handler - handler.channel().equals(channel)) .findFirst() .orElseThrow(() - new IllegalArgumentException(不支持的支付渠道: channel)); } }这里我要重点讲一个细节MapString, PayHandler的 key 是什么Spring 在注入 Map 时会把当前容器中所有匹配PayHandler类型的 Bean 的Bean 名作为 key把Bean 实例作为 value。Bean 名默认是类名的首字母小写比如wechatPayHandler、alipayPayHandler。所以如果你直接用handlerMap.get(wechat)是拿不到东西的因为 key 是类名映射后的名字不是我们定义的渠道名。我在代码里用values().stream()过滤channel()值就是为了避免依赖 Bean 名与渠道名的耦合约定。另外handlerMap通过构造器注入这也是 Spring6 官方推荐的注入方式。构造器注入能保证依赖完整不容易出现运行时的 NullPointerException也方便单元测试时手动传入 mock 的 Map。最后在调用侧只需要简单一句payHandlerFactory.getHandler(request.getChannel()).pay(request);从表面看这段代码只是少了 if-else但本质上是把“创建和分发”与“业务实现”彻底解耦了。以后新增渠道你就只管加实现类不用再动工厂。3.3 扩展点默认处理器与优雅降级线上运行一段时间后你大概率会遇到一个需求某些渠道暂时没有接入但用户又提交了请求与其直接抛异常不如友好降级。或者某些新渠道处于灰度验证阶段你想默认走一个兜底处理器。这时上面的orElseThrow就不合适了。我改造后的工厂逻辑如下public PayHandler getHandler(String channel) { return handlerMap.values().stream() .filter(handler - handler.channel().equals(channel)) .findFirst() .orElse(defaultHandler); }defaultHandler可以单独做成一个DefaultPayHandler它的内部逻辑是记录日志、发送告警、返回“渠道暂不支持”的提示。如果还要做得更精细可以结合配置中心动态指定某类渠道走默认处理器public PayHandler getHandler(String channel) { // 灰度期间指定渠道走到默认处理器 if (downgradeChannels.contains(channel)) { return defaultHandler; } return handlerMap.values().stream() .filter(handler - handler.channel().equals(channel)) .findFirst() .orElse(defaultHandler); }这个扩展并不复杂但它体现了工厂模式的另一个价值把变化收敛在工厂内部调用方完全无感知。你可以在轮询切换、灰度配置、故障降级这些需求上做文章而不会对业务代码造成冲击。3.4 落地时注意的 Spring 版本差异我踩过一个比较隐蔽的坑在 Spring Boot 2.x 和 Spring 6 之间MapString, Handler注入的行为是一致的——都是按 Bean 名注入。但在结合泛型接口时会有细微差别。假设你的接口定义带泛型public interface PayHandlerT extends PayRequest { PayResult pay(T request); }当你注入MapString, PayHandler时Spring 会尝试用ResolvableType做类型匹配。如果实现类的泛型参数能对上就顺利注入如果接口的泛型信息丢失比如实现了PayHandlerRawTypeSpring 在部分版本下可能因类型擦除而匹配失败。所以在用工厂模式 泛型接口时我建议你在实现类上明确写出泛型类型比如Component public class WechatPayHandler implements PayHandlerWechatPayRequest { // ... }这样 Spring 的ResolvableType机制能准确识别类型。如果是用反射做消息路由这类更复杂的泛型场景一定要测试容器启动时能否正常注入 Map。这个问题在 Spring6 里依然存在只是官方文档里没有特别强调。4. 常见问题与排查技巧实录4.1 案例一FactoryBean 类型推断失败导致转换异常有次在排查一个启动直接报错的问题时我发现了典型的FactoryBean类型推断陷阱。错误信息大概是org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named xxx is expected to be of type ActualType but the actual type was FactoryBeanType出现这个错误通常是因为你在Bean方法里返回了FactoryBeanT类型但调用的地方期望的是T类型。看下面这段问题代码Configuration public class AppConfig { Bean public OrderFactoryBean orderFactoryBean() { return new OrderFactoryBean(); } }注入方Autowired private Order order;容器启动后order实际是一个OrderFactoryBean对象而不是Order。这和我们前面的预期——FactoryBean 会返回产品——是矛盾的。原因在于直接在Bean方法里返回工厂实例Spring 会把orderFactoryBean当成普通 Bean 注册不会触发FactoryBean的处理逻辑。只有在 XML 配置或Component扫描方式注册FactoryBean时Spring 的BeanDefinition里会标记为 FactoryBean然后才能正常触发getObject()。这个坑的解法有两种。第一种是确保FactoryBean以组件形式注册不要用Bean直接注册第二种是修正注入方Autowired private ApplicationContext context; public void doSomething() { Order order context.getBean(Order.class); }但如果你真的需要在Bean方法里注册工厂而且想让 Spring 按工厂处理就得直接返回FactoryBean类型并让 Spring 感知到它。比较稳妥的做法是使用Component给OrderFactoryBean加注解然后依赖注入时直接索要产品类型。4.2 案例二Map 注入拿不到 Bean 或拿到代理对象用MapString, PayHandler注入策略时有个容易忽视的问题如果PayHandler实现类加了 AOP 切面比如事务、日志、权限Map 里存的是代理对象。这通常不会引起程序错误但如果你在工厂里用instanceof或获取注解来判断某类 Handler容易拿到代理对象的类而不是目标类。另外事务代理的channel()方法调用是没问题的AOP 拦截正常但如果你试图在工厂里通过handler.getClass().getAnnotation(...)获取自定义注解记得要处理代理类的情况。排查技巧在临时调试代码里打印handlerMap的每个 value 的 class看是不是包含$$EnhancerBySpringCGLIB$$字样如果包含你就知道它是代理对象。这时候访问目标注解应该用AopProxyUtils.getSingletonTargetClass(handler)而不是直接getClass()。还有一个常见情况是“Map 注入拿到空的集合”。多半是接口类型写得不一致比如实现在com.demo.handler包而注入的泛型是com.demo.other.PayHandler虽然类名一样但包路径不同Spring 不会匹配。建议把所有策略接口统一放到一个模块的 api 包里从结构上规避这种问题。4.3 案例三循环依赖与 ObjectProvider 的配套使用Spring6 里循环依赖的处理逻辑更严格了默认不允许循环依赖的场景越来越多。我在改造老项目时遇到过这样一个案例OrderService和PaymentService互相调用两个类依赖对方。按旧写法Service public class OrderService { private final PaymentService paymentService; public OrderService(PaymentService paymentService) { this.paymentService paymentService; } } Service public class PaymentService { private final OrderService orderService; public PaymentService(OrderService orderService) { this.orderService orderService; } }这种构造器注入的循环依赖在 Spring 6 里直接无法启动。解决思路不是去调allowCircularReferences而是从设计上打破循环。我用ObjectProvider改了一边Service public class PaymentService { private final ObjectProviderOrderService orderServiceProvider; public PaymentService(ObjectProviderOrderService orderServiceProvider) { this.orderServiceProvider orderServiceProvider; } }在方法里需要用到OrderService时再通过orderServiceProvider.getObject()获取。这样一来Spring 创建PaymentService时不需要立即创建OrderService循环依赖就解开了。这种方式比Lazy注解更干净因为Lazy是给整个注入点生成一个代理而ObjectProvider是真正延迟到方法调用时才获取语义更清晰调试起来也不会有代理干扰。4.4 工厂模式相关报错速查表我把这些年遇到的和工厂模式相关的经典报错整理成了一张表方便你出现问题时快速定位错误类型典型报错关键词常见原因解决思路类型不匹配BeanNotOfRequiredTypeExceptionBean方法返回了 FactoryBean调用方却期望产品类型改注册方式或使用前缀获取工厂空 Map 注入Map 为 empty接口包路径不一致、实现类未扫描到、泛型类型擦除检查包扫描范围与类型一致性NullPointerExceptionhandler 为 null工厂orElseThrow抛异常或 Map 注入失败加默认处理器或者改用getIfAvailable启动循环依赖The dependencies of some of the beans in the application context form a cycle构造器注入相互引用用 ObjectProvider 或重构职责边界FactoryBean 未被识别产品类型没有按工厂方式创建使用Bean注册 FactoryBean 未触发工厂逻辑使用Component注册或检查 Bean 定义ObjectProvider 为 nullNullPointerException in constructor忘记加Component注解或工厂未注入检查类是否被扫描到4.5 排查三个顺手技巧最后分享三个排查工厂模式相关代码的顺手技巧。第一个技巧启动时打印所有策略实现。在工厂类的构造器里临时加一行日志把收集到的实现类 list 打印出来。这样能最快确认你注入的 Map 到底有哪些 Bean类型是否如预期。日志打出来八成的“为什么我的 Map 空着”问题当场就能解决。public PayHandlerFactory(MapString, PayHandler handlerMap) { this.handlerMap handlerMap; System.out.println(收集到 PayHandler 实现: handlerMap.keySet()); }第二个技巧善用ApplicationContext.getBeanProvider。你不需要把整个ApplicationContext注入到工具类或静态方法里只需要在入口处拿到 provider然后传递它。这样既能延迟获取又保持了测试的便利性。第三个技巧遇到 FactoryBean 相关的问题先在 Spring 容器启动时打印getBeanDefinitionNames()查看被注册的 Bean 名。如果看到了名字里带前缀的特殊条目——注意在getBeanDefinitionNames()的结果里FactoryBean 本身的 Bean 名是普通名不带是getBean查询时才需要的语法。通过这个细节可以确认哪些 Bean 是工厂注册的。这个信息对判断问题非常关键。从我个人的实际经历来看工厂模式在 Spring6 里不是一个孤立的“知识考点”它几乎渗透在容器的每个角落——从BeanFactory这个根接口的名字到FactoryBean处理复杂对象创建再到ObjectProvider提供延迟获取能力一整条线串下来你对 Spring 的设计取舍就通透了。忘掉理论书上的三个分类抓住一个本质对象的创建权和控制权从调用方手里移交到容器手里调用方只对“得到的对象”负责创建过程永远可以替换、可以扩展。之后你在 Spring6 里写策略代码、看自动配置源码都会顺畅得多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询