很多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之上补齐了这些能力。
两者区别用表格看更清晰:
| 对比维度 | BeanFactory | ApplicationContext |
|---|---|---|
| 核心职责 | Bean的注册、获取、缓存 | 容器全生命周期管理,额外支持事件、国际化、AOP |
| 常见实现类 | DefaultListableBeanFactory | AnnotationConfigApplicationContext、ClassPathXmlApplicationContext |
| Bean加载时机 | 按需加载 | 默认启动时预实例化所有单例Bean |
| 使用场景 | 嵌入式、极简容器、底层框架 | 绝大多数Spring/Spring Boot应用 |
注意表格里第三行——BeanFactory是按需创建,调用getBean时才实例化对象;而ApplicationContext在refresh()阶段就会把非懒加载的单例Bean全部提前创建。这也是为什么Spring Boot启动慢的原因之一,但换来的是第一次调用接口时响应更快。
2.3 refresh():一个浓缩的"工厂生产线"
AbstractApplicationContext.refresh()是整个Spring容器初始化的入口。我第一次读源码时很震撼,因为工厂模式的"生产过程"在这里被拆成了十几个清晰的步骤。我挑几个核心阶段:
- BeanDefinition加载:扫描
@Component、@Bean、@Configuration等,把每个配置信息转成BeanDefinition注册进工厂; - BeanFactory后置处理:执行
BeanFactoryPostProcessor,典型的就是ConfigurationClassPostProcessor,它会解析@Configuration类里的@Bean方法; - Bean实例化:遍历所有
BeanDefinition,通过反射调用构造器创建实例,同时解决依赖注入; - Bean后置处理与初始化:执行
BeanPostProcessor,完成AOP代理、@Autowired注入等;执行InitializingBean、@PostConstruct等初始化回调; - 单例缓存:创建完成的单例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厂商、不同短信服务商的适配。
我在好几个项目里把这种模式叫"策略工厂"。它的本质是三层解耦:
- 接口层:定义行为,比如
PayChannel.pay(); - 策略层:每个实现负责一种具体行为;
- 工厂层:只在容器初始化时做一次映射构建,运行时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的依赖注入机制都是清晰可推导的。这套"接口 + 自描述 + 工厂分发"的写法学好之后,面对任何"按类型做不同处理"的需求,你都不会再头大了。