☰
Spring工厂模式核心拆解:BeanFactory、FactoryBean与策略工厂实战
2026/10/20 10:13:22 网站建设 项目流程

很多Java开发者嘴上说着懂工厂模式,但真被问到Spring里工厂模式到底有几层、FactoryBean和BeanFactory有什么区别、@Bean方法背后的本质是什么,能把这三件事串起来讲清楚的人并不多。Spring中工厂模式不是教科书里的一个孤立设计模式,它就是容器本身的骨架,也是我们平时写业务代码绕不开的思维工具。这篇文章我从三个层面拆解——容器即工厂、FactoryBean这套扩展点、以及业务侧策略工厂的落地方式,最后附上我实际踩过的几个坑。

1. 工厂模式到底在解决什么问题:创建与使用的分离

1.1 先看没有工厂时,代码是怎么一步步腐化的

假设你在写一个订单支付服务,最开始只有支付宝,于是代码长这样:

public class OrderService { private AlipayService alipayService = new AlipayService(); public void pay(Order order) { alipayService.pay(order); } }

没过多久,产品说需要接入微信支付。你直接在OrderService里加了个if判断:

public class OrderService { public void pay(Order order, String channel) { if ("alipay".equals(channel)) { new AlipayService().pay(order); } else if ("wechat".equals(channel)) { new WechatService().pay(order); } } }

第三个月,银联、云闪付、海外信用卡全来了。OrderService变成了几百行的超级类,每个新渠道进来都要改这个核心类。这个过程的本质问题在于:创建对象的逻辑和使用对象的逻辑完全耦合在一起。调用方不仅要知道"我要支付",还要知道"我用哪个渠道支付""这个渠道的对象怎么创建""构造参数传什么"。这些都是调用方不该关心的。

1.2 工厂模式干的事情:把"怎么创建"藏起来

工厂模式的核心逻辑就是把new这个动作收敛到一个单独的类里。调用方只面向接口: 这里有两个简单的原则:

  • 调用方依赖接口,不依赖具体实现类;
  • 具体实现类的创建过程,统一由工厂负责;
  • 新增实现时,改动集中在工厂,调用方完全不用动。

这听起来像废话,但真到写代码的时候,很多人就忘了。工厂模式的本质价值,不是让你多用一层包装显得"专业",而是让系统的扩展点变得清晰。以后的每一次新增渠道,都只是"新增一个实现类+修改工厂",而不是"在所有使用的地方上翻下找"。

1.3 但Spring对工厂模式的理解,比Gof的经典定义深了一层

Gof的工厂模式解决的是"对象创建"问题,但Spring面对的其实是"对象装配"问题。Spring容器管理对象的整个生命周期——创建、依赖注入、初始化、代理增强、销毁。它比经典工厂多做三件事:

  • 按条件创建:根据配置、注解、类型、名称来决定创建哪个对象,用什么参数创建;
  • 管理依赖关系:A对象依赖B对象,Spring创建A之前会先把B创建好并注入进去;
  • 缓存与复用:默认单例,同一个对象只创建一次,拿到的都是同一份引用。

所以你会发现,Spring在"创建对象"这件事上做了两层抽象:一层是BeanFactory——它就是那个最朴素的工厂接口;另一层是ApplicationContext——在工厂之上做了大量增强。接下来重点说这两层。

2. Spring容器本身就是最大的工厂:从BeanFactory到ApplicationContext

2.1 BeanFactory:极简接口,五脏俱全

BeanFactory是Spring最底层的工厂接口,它的职责只有一个:提供Bean的注册与获取。核心方法就几个:

public interface BeanFactory { Object getBean(String name) throws BeansException; <T> T getBean(Class<T> requiredType) throws BeansException; <T> T getBean(String name, Class<T> requiredType) throws BeansException; boolean containsBean(String name); boolean isSingleton(String name) throws NoSuchBeanDefinitionException; String[] getAliases(String name); }

真正干活的是DefaultListableBeanFactory,它是Spring容器的默认实现类,注册了所有BeanDefinition,也维护了单例缓存池。你在ApplicationContext底层看到的最终工厂,其实就是它。

DefaultListableBeanFactory内部维护了几个核心结构:

  • beanDefinitionMap:存放Bean定义(Class、构造参数、属性值、初始化方法);
  • singletonObjects:单例对象缓存,就是一个ConcurrentHashMap;
  • beanDefinitionNames:按顺序记录Bean名字。

从数据结构上你就能看明白,一个对象在Spring容器里要经过"定义 -> 实例化 -> 缓存"三个阶段,和工厂模式里的"原料 -> 加工 -> 出货"完全对应。

2.2 ApplicationContext:站在工厂肩膀上的"超级工厂"

我们平时接触的AnnotationConfigApplicationContext、ClassPathXmlApplicationContext,本质上都是BeanFactory的子类或组合了BeanFactory的高级封装。那为什么不直接用BeanFactory?因为它太"裸"了,只提供最基础的能力。而一个Web应用还需要国际化、事件监听、资源文件加载、AOP自动代理。ApplicationContext在BeanFactory之上补齐了这些能力。

两者区别用表格看更清晰:

对比维度BeanFactoryApplicationContext
核心职责Bean的注册、获取、缓存容器全生命周期管理,额外支持事件、国际化、AOP
常见实现类DefaultListableBeanFactoryAnnotationConfigApplicationContext、ClassPathXmlApplicationContext
Bean加载时机按需加载默认启动时预实例化所有单例Bean
使用场景嵌入式、极简容器、底层框架绝大多数Spring/Spring Boot应用

注意表格里第三行——BeanFactory是按需创建,调用getBean时才实例化对象;而ApplicationContext在refresh()阶段就会把非懒加载的单例Bean全部提前创建。这也是为什么Spring Boot启动慢的原因之一,但换来的是第一次调用接口时响应更快。

2.3 refresh():一个浓缩的"工厂生产线"

AbstractApplicationContext.refresh()是整个Spring容器初始化的入口。我第一次读源码时很震撼,因为工厂模式的"生产过程"在这里被拆成了十几个清晰的步骤。我挑几个核心阶段:

  1. BeanDefinition加载:扫描@Component、@Bean、@Configuration等,把每个配置信息转成BeanDefinition注册进工厂;
  2. BeanFactory后置处理:执行BeanFactoryPostProcessor,典型的就是ConfigurationClassPostProcessor,它会解析@Configuration类里的@Bean方法;
  3. Bean实例化:遍历所有BeanDefinition,通过反射调用构造器创建实例,同时解决依赖注入;
  4. Bean后置处理与初始化:执行BeanPostProcessor,完成AOP代理、@Autowired注入等;执行InitializingBean、@PostConstruct等初始化回调;
  5. 单例缓存:创建完成的单例Bean放入singletonObjects。

这一整套流程就是工厂模式的教科书级实践:你告诉容器"我要什么Bean",容器去查配方(BeanDefinition)、调用生产线(反射+处理器)、最终交付成品(实例对象)。如果你正打算深入框架源码,我建议从refresh()入手,它比任何Gof例子都真实。

3. FactoryBean:Spring里最实用也最容易被误解的工厂扩展点

3.1 为什么"生产一个Bean"还不够,还要"生产Bean的工厂"?

到了Spring内部,事情就复杂了。绝大多数Bean可以用new加反射搞定,但有一种情况Spring自己搞不定——这个对象不是简单new出来的,它需要一整套创建逻辑。比如:

  • 通过ProxyFactory创建一个带有事务代理的Service对象;
  • 通过SqlSessionFactory创建MyBatis的Mapper代理对象;
  • 通过JedisPool配置创建一个连接池封装对象。

如果这些创建逻辑全写在@Configuration里的@Bean方法里,方法会变得很长,而且无法复用。Spring于是提供了一个专门的接口——FactoryBean<T>。你自己写一个类,告诉Spring"请把我的getObject()返回值当作一个Bean来管理"。

3.2 一个最简单的FactoryBean,三个方法一个都不能少

@Component public class ExampleFactoryBean implements FactoryBean<MessageService> { @Override public MessageService getObject() { // 这里可以放任何复杂的创建逻辑 return new RocketMessageService(); } @Override public Class<?> getObjectType() { return MessageService.class; } @Override public boolean isSingleton() { return true; } }

三个方法很好记:

  • getObject():工厂生产的产品;
  • getObjectType():产品的类型,Spring做类型匹配时要靠它;
  • isSingleton():这个产品是否为单例。返回true时,产品会被缓存到factoryBeanObjectCache;返回false时,每次getBean都会调用一次getObject()。

从容器视角看,ExampleFactoryBean本身是"工厂",它产出的MessageService才是"产品"。Spring在管理的时候非常有技巧:容器里同时存在两个东西,一个叫exampleFactoryBean,一个叫exampleFactoryBean对应的产品。产品在用户那一边,工厂在容器内部。

3.3 通过&前缀拿到FactoryBean本体

这是个冷门但超级实用的知识。你在业务代码里做了@Autowired MessageService,Spring会去调用exampleFactoryBean.getObject()把产品注入进来。但如果你想拿到ExampleFactoryBean本身怎么办?用&前缀:

// 拿到的是产品 MessageService service = applicationContext.getBean("exampleFactoryBean", MessageService.class); // 拿到的是工厂本体 ExampleFactoryBean factory = applicationContext.getBean("&exampleFactoryBean", ExampleFactoryBean.class);

这个&是从AbstractBeanFactory.FACTORY_BEAN_PREFIX常量来的。我记得第一次看到这个用法时觉得非常怪异,后来才理解,因为BeanName命名空间里需要区分"工厂"和"产品",&就是Spring钦定的分隔符。

3.4 FactoryBean和@Bean方法,什么时候用哪个?

很多人会问:@Bean方法也能定义创建逻辑,为什么还需要FactoryBean?我的判断标准如下:

维度FactoryBean@Bean方法
创建逻辑归属独立类,逻辑内聚方法,和配置类耦合
复用性可被多个容器配置复用复用需要继承配置类,纠缠
创建逻辑复杂时适合,可拆多个方法容易变成长方法
日常业务Bean不推荐,过度设计推荐
框架级组件推荐,比如MyBatis、Redis较少

一个直观的例子,如果你需要封装一个"创建数据库连接池"的逻辑,让它既能用在开发配置,又能用在生产配置,FactoryBean是更好的选择——因为你能把创建逻辑写进一个可复用类的内部。如果只针对单一场合,@Bean方法就足够了。

4. 业务代码里落地工厂模式:从if-else大军到策略工厂

4.1 我见过的最常见的"假工厂"

很多人觉得自己写了工厂:搞一个PayChannelFactory,里面还是满屏if-else:

@Component public class PayChannelFactory { public PayChannel getChannel(String channelCode) { if ("alipay".equals(channelCode)) { return new AlipayChannel(); } else if ("wechat".equals(channelCode)) { return new WechatChannel(); } throw new IllegalArgumentException("unsupported channel: " + channelCode); } }

逻辑是比之前收敛了,但如果新增渠道,你依然要改getChannel方法。这只是把散落在全项目的if-else搬到了同一个类里,不是真正的开闭原则。真正的扩展点在于让Spring容器自动收集所有实现类,让代码自己找"该用哪个"。

4.2 配合容器自动注入:让产品代码零改动

我先定义一个自描述的接口,每个实现类自己报上名来:

public interface PayChannel { String channelCode(); void pay(PaymentRequest request); }

然后两个实现类分别标上@Component:

@Component public class WechatPayChannel implements PayChannel { @Override public String channelCode() { return "wechat"; } @Override public void pay(PaymentRequest request) { /* 具体逻辑 */ } } @Component public class AlipayPayChannel implements PayChannel { @Override public String channelCode() { return "alipay"; } @Override public void pay(PaymentRequest request) { /* 具体逻辑 */ } }

最后工厂类在构造器里接收Spring自动收集的List<PayChannel>,转成Map:

@Component public class PayChannelFactory { private final Map<String, PayChannel> channelMap; public PayChannelFactory(List<PayChannel> channels) { this.channelMap = channels.stream() .collect(Collectors.toMap(PayChannel::channelCode, Function.identity())); } public PayChannel getChannel(String code) { PayChannel channel = channelMap.get(code); if (channel == null) { throw new IllegalArgumentException("unsupported channel: " + code); } return channel; } }

以后新增一个UnionPayChannel,加一个类,实现channelCode()返回"unionpay",完事。PayChannelFactory一行不用改,调用方一行不用改。为什么能做到?因为Spring容器在启动时就把所有PayChannel的实现类收集起来注入进了工厂,新增实现类 = 自动注册。这套组合拳比单独写工厂强太多了,因为Spring本身就是一个天然的对象收集器。

4.3 类似的场景远不止支付渠道

这套"自描述接口 + Map收集 + 工厂分发"的模式,几乎可以套用到任何"按类型做分发"的业务:

  • 消息推送:短信、邮件、AppPush、站内信,每个实现对应一个typeCode;
  • 文件解析:Excel、CSV、PDF,每个解析器对应一个文件类型;
  • 任务调度:不同类型的异步任务,每种任务一个Handler;
  • 外部对接:不同物流商、不同OSS厂商、不同短信服务商的适配。

我在好几个项目里把这种模式叫"策略工厂"。它的本质是三层解耦:

  1. 接口层:定义行为,比如PayChannel.pay();
  2. 策略层:每个实现负责一种具体行为;
  3. 工厂层:只在容器初始化时做一次映射构建,运行时O(1)分发。

4.4 这个方法同样适合没有Spring的场景

如果你在用纯Java写一个小工具,没有Spring容器,不要慌。把"容器自动收集"换成"注册表手动注册"就行:

public class PayChannelRegistry { private static final Map<String, PayChannel> CHANNELS = new HashMap<>(); static { register(new AlipayChannel()); register(new WechatChannel()); } public static void register(PayChannel channel) { CHANNELS.put(channel.channelCode(), channel); } public static PayChannel get(String code) { return CHANNELS.get(code); } }

本质思路一模一样,只是少了容器的自动扫描能力。所以在Spring里,工厂模式的核心价值被放大了,因为Spring帮你做了最繁琐的"收集"这项工作,你只需要定义映射规则即可。

5. 四个高频坑:从Autowired歧义到FactoryBean缓存陷阱

5.1 坑一:一个接口多个实现,Autowired直接启动失败

上面策略工厂的例子,前提是你得把多个实现注入到集合里。但如果你不小心直接在Service里这样写:

@Autowired private PayChannel payChannel;

Spring启动直接报NoUniqueBeanDefinitionException。解决方式有四种:

  • @Primary:在某个实现类上标注,被选为默认实现;
  • @Qualifier:注入时指定Bean名称,适合明确说明要哪个;
  • @Resource(name=...):按名称注入;
  • 构造器注入 + 集合:像我们之前工厂那样注入List<Bean>,而不是单个Bean。

我实际项目的习惯是:凡是有多个实现类的接口,一律禁止在业务代码里直接注入单个接口对象,全部走工厂。原因很简单——写@Qualifier虽然能跑,但你等于把"实现选择"散落到了各个使用点,以后想切换实现就要改多处,又走回老路了。

5.2 坑二:在@Bean方法内部手动new对象,代理和生命周期全废

我有一个同事写过这样的代码:

@Configuration public class AppConfig { @Bean public PayChannel payChannel() { return new WechatPayChannel(); // 问题所在 } }

表面上看没问题,Bean确实是交付给容器了。但注意:如果这个类里有被@Transactional/@Cacheable修饰的方法,手动new出来的对象完全不会被代理,因为事务、缓存这些全是靠AOP代理对象实现的。你以为加了事务,实际根本没有。正确写法是让Spring管理这个类的实例化:

@Configuration public class AppConfig { @Bean public PayChannel payChannel() { // 这里new可以,因为WechatPayChannel本身不需要代理 // 但如果你需要代理,必须注入一个被容器管理的实例 return wechatPayChannel; // 通过构造器或方法参数注入 } }

一个更常见的场景是在@Configuration类里调用另一个@Bean方法:

@Bean public PayService payService() { return new PayService(payChannel()); // 直接方法调用 }

在@Configuration类中,payChannel()返回的其实是代理对象,容器能保证单例;但如果这个配置类改成普通类(比如加了@Component而不是@Configuration),那payChannel()就是普通方法调用,每次都会new一个,单例就破了。这里面水很深,建议遵循一个简单原则:Bean之间的依赖优先用构造器注入,而不是在@Bean方法里手动传参。

5.3 坑三:FactoryBean的getObjectType返回null,类型匹配直接失效

Spring在做@Autowired类型注入时,需要判断FactoryBean产出的产品类型。如果getObjectType()返回null或错误的类型,容器就无法按类型进行自动装配,甚至报BeanNotOfRequiredTypeException。

想象一个场景:某个FactoryBean生成的代理对象,类型是个接口的实现,你直接返回getObjectType()为null,后面所有依赖这个产品的Bean在启动时全部失败。这种问题的坑在于,启动时报错的信息往往很隐晦,经常是"expected at least 1 bean which qualifies as autowire candidate",或者某个类型不匹配的异常。排查时一定要优先去检查FactoryBean的实现,确认getObjectType()返回的是对外暴露的接口类型,而不是内部实现类的类型。

5.4 坑四:把有状态对象交给单例工厂,多线程环境下直接出事故

工厂本身一般是单例的,这也意味着如果工厂里保存了状态,就会出现线程安全问题。比如我在一个项目里见过这样的代码:

@Component public class FileParseFactory { private String currentFileName; // 状态! public FileParser getParser(String fileType) { currentFileName = parserConfig.getFileName(); return parserMap.get(fileType); } }

两个请求同时进来,currentFileName被互相覆盖,解析逻辑拿到的文件名全乱了。原则很简单:工厂类最好是无状态的,如果真需要在解析过程中传递上下文,应该把上下文作为方法参数传给产品对象,而不是放在工厂属性里。我一般在写完工厂类之后都会自问一句:这个类里的字段是不是全都是不可变的?只要有一个可变的,就要警惕并发问题。

5.5 一个关于FactoryBean缓存的补充认知

关于FactoryBean你会看到一些容易出现认知偏差的地方。当isSingleton()返回true时,产品对象会被缓存到factoryBeanObjectCache中;当返回false时,每次获取都会重新调用getObject()。因此,如果你的产品持有数据库连接、Socket等资源,一定要想清楚isSingleton()该返回什么。返回true意味着所有调用方共享一个产品实例,有状态就有并发风险;返回false则每次创建,可能撑爆连接池。正确的做法通常是:为"单例安全"的产品返回true,为"每次需要独立实例"的产品返回false,并且明确在代码注释里写清楚理由。

我在实际排查过的一个案例里,某个框架自定义的FactoryBeanisSingleton()返回true,但产品对象内部持有可变的RequestId字段,结果并发请求全部串号。查了半天才发现是产品和工厂的生命周期理解有偏差。所以说,FactoryBean不仅是一个创建工具,更是一个生命周期声明器,它的三个方法合在一起才定义了产品的完整语义。

如果你想把项目里的if-else分发逻辑重构掉,我建议先从策略工厂这套入手,它足够简单,又不依赖Spring太深的知识。等你把容器自动收集玩熟了,再回去看BeanFactory和FactoryBean,会发现整个Spring的依赖注入机制都是清晰可推导的。这套"接口 + 自描述 + 工厂分发"的写法学好之后,面对任何"按类型做不同处理"的需求,你都不会再头大了。

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

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

立即咨询