1. 从BeanFactory开始——Spring工厂模式的原始底座
1.1 为什么说BeanFactory才是工厂模式的原型
接触Spring的开发者大概率都听过这句话——“Spring的核心就是一个Bean工厂”。这句话不是比喻,而是字面意义上的描述。Spring最底层、最基础的功能就是创建并管理对象,而这个创建与管理工作正是通过工厂模式实现的。BeanFactory就是这个工厂的标准接口,所有的对象在Spring容器中都以Bean的形式存在,开发者只管声明依赖关系,具体什么时候new、怎么new、怎么注入,全部由容器这个“超级工厂”负责。
如果你去看BeanFactory接口的源码,会发现它的方法列表非常克制:getBean、containsBean、isSingleton、getType这些,核心动作其实就一个——“根据名称或类型获取一个对象”。这个设计本质上就是工厂方法模式中“用一个工厂方法代替new操作”的典型体现。调用方不关心Bean是怎么来的、来自XML还是注解、是单例还是原型,只需要拿到一个可用的对象即可。
1.2 BeanFactory与工厂模式的对应关系
工厂模式有三种常见形态:简单工厂、工厂方法、抽象工厂。Spring并不仅仅套用了其中某一种,而是把三种形态都融进了自身的设计里。准确来说,BeanFactory本身更像一个“抽象工厂”——它定义了创建和管理对象的总接口,而具体的容器实现(比如XmlBeanFactory、AnnotationConfigApplicationContext)则承担了工厂方法模式中“具体工厂”的角色。每一个Bean的定义信息(BeanDefinition)就像是产品的配方,容器根据配方生产出对应的对象实例。
举个例子,当你在ApplicationContext中调用getBean("userService")时,背后发生的事情远远不是“new一个UserService”这么简单。容器会先获取UserService对应的BeanDefinition,解析构造函数,判断是否需要处理循环依赖,检查AOP切面是否需要生成代理对象,再考虑BeanPostProcessor是否要对实例进行增强。这一整套流程本质上就是一个高度定制化的工厂生产线,远比简单的new复杂得多,也强大得多。
2. Spring中工厂模式的几种常见落地形态
2.1 BeanFactory与ApplicationContext的差别
很多初学者分不清BeanFactory和ApplicationContext,这其实很正常,因为两者的关系就是“接口与实现增强”。BeanFactory是Spring容器的最底层规范,ApplicationContext在BeanFactory的基础上增加了国际化、事件发布、资源加载、自动扫描等能力。从工厂模式的角度看,两者都属于工厂,只是ApplicationContext是一个强化版工厂。
我在实际开发中几乎不会直接使用BeanFactory,因为它太底层了,连BeanPostProcessor的自动注册都需要手动处理。但理解BeanFactory依然是必要的,因为它是理解Spring容器生命周期的起点。ApplicationContext在启动时默认会实例化所有单例Bean,而BeanFactory则是在第一次调用getBean时才创建对象。这个差异在调试启动慢、排查初始化顺序问题时非常关键。
2.2 FactoryBean:让工厂里面套工厂
如果说BeanFactory是整个容器的总工厂,那么FactoryBean就是允许你在容器内部“自定义某个Bean的工厂”。
FactoryBean是Spring提供的一个接口,专门用于那些不能简单通过构造函数创建的Bean。典型的例子就是MyBatis的MapperFactoryBean——每个Mapper接口都是动态代理对象,根本不是普通的类实例,Spring没法直接用反射new出来,于是让FactoryBean来负责生成代理。你向容器请求“userMapper”这个Bean时,容器的逻辑是:发现这个Bean定义对应的是一个FactoryBean实现,于是调用该实现类的getObject()方法,把返回的对象交给你。
这里有一个很容易踩坑的地方。如果你用getBean("userMapper")去获取,得到的是FactoryBean创建的产品对象;但如果你写的是getBean("&userMapper"),得到的才是FactoryBean本身。这个“&”前缀是Spring约定俗成的特殊标记,新手经常会在这里被绕晕。
2.3 @Configuration和@Bean注解下的工厂思维
回到日常开发,最常见的工厂模式实践其实是@Bean注解。每一个被@Bean标注的方法,本质上就是一个工厂方法。方法名是Bean的名称,方法返回的对象就是产品。
比如你配置一个RestTemplate:
@Configuration public class RestConfig { @Bean public RestTemplate restTemplate() { return new RestTemplateBuilder() .setConnectTimeout(Duration.ofSeconds(3)) .build(); } }这个方法就是典型的简单工厂写法。Spring容器在启动时会调用restTemplate()方法,把返回值纳入容器管理。如果是通过启动类的@ComponentScan扫描到的配置类,Spring还会为这个配置类生成一个CGLIB代理,确保@Bean方法在同一个容器内只执行一次。
这里就有个容易忽略的细节:既然@Bean方法会被代理,那就不要在配置类内部直接调用另一个@Bean方法对应的“方法”本体,而应该通过依赖注入去拿Bean。否则你可能会发现代理逻辑没有生效,对象被多创建了一次。
3. 静态工厂、实例工厂与FactoryBean的实战选择
3.1 静态工厂方法的配置与局限
静态工厂方法是指通过某个类的静态方法来创建对象,这是Java老项目中比较常见的做法。Spring完全支持这种模式,只需要在XML或注解中指定工厂方法即可。
XLM配置时代是这样的:
<bean id="calendar" class="java.util.Calendar" factory-method="getInstance"/>换成注解后,你要做的就是自己去写@Bean方法,在方法内部调用静态工厂方法。比如创建UUID、创建ObjectMapper等工具类对象时,这种写法依然很实用。
静态工厂方法的优点是用起来简单、清晰,缺点是无法扩展——你不能对静态工厂的返回值做后续处理。所以后来Spring更推荐直接用@Bean方法,把创建逻辑写在方法内部,灵活性高得多。
3.2 实例工厂方法:工厂对象作为Bean
实例工厂方法的意思是:工厂本身也是一个Bean,通过工厂对象的实例方法来创建其他Bean。
听起来有点绕,我给你一个生活化的类比:静态工厂就像学校门口的自动打印机,你投硬币它出文件,不用管打印机本身;实例工厂就像找一家打印店,让店员帮你打印,你不但指定打印数量,还能让店员帮你装订、裁剪。
在Spring中实现实例工厂的形式很简单:
@Component public class CarFactory { public Car createBmw() { Car car = new Car(); car.setBrand("BMW"); car.setType("sedan"); return car; } } @Configuration public class CarConfig { @Autowired private CarFactory carFactory; @Bean public Car bmwCar() { return carFactory.createBmw(); } }这种写法的好处是,你可以在创建对象的同一个方法里加入复杂的业务逻辑,工厂对象自己也可以被注入其他依赖。它比静态工厂方法更灵活,比直接new更符合“依赖倒置”原则。
3.3 三种方案怎么选
静态工厂方法、实例工厂方法、FactoryBean,这三者看起来都能创建对象,但侧重点完全不同:
- 静态工厂方法:用于创建JDK自带的、有现成静态方法返回实例的工具类,比如Calendar、UUID、ObjectMapper。
- 实例工厂方法:适用于创建过程依赖其他Spring Bean、或者需要按不同条件返回不同子类型的场景。
- FactoryBean:适用于创建动态代理、生成字节码对象、或者Bean的创建过程不能被Spring常规扫描机制处理的场景,比如MyBatis的Mapper、Feign的Client。
我的经验是:日常业务代码里用@Bean就够了,根本不需要考虑FactoryBean;只有写框架级代码、或者集成第三方组件时,才需要动FactoryBean。
4. 生产中更常用的模式——策略工厂的搭建实践
4.1 场景设定:支付渠道选择的痛点
假设你正在做一个聚合支付模块,项目里有支付宝、微信、银行卡三种支付方式,每种支付方式有不同的参数组装、签名逻辑和下单接口。如果在一个Service里写if else判断渠道,后续每加一个渠道就要改动这个Service的代码,这明显违反了开闭原则。
这时候就可以用工厂模式来改造:定义统一的支付接口,每种支付渠道实现这个接口,再用一个工厂根据渠道编号返回对应的实现类。
public interface PaymentChannel { boolean supports(String channel); PayResult pay(PayRequest request); } @Component public class AlipayChannel implements PaymentChannel { @Override public boolean supports(String channel) { return "alipay".equals(channel); } @Override public PayResult pay(PayRequest request) { // 支付宝相关逻辑 return new PayResult("success", "支付宝下单成功"); } } @Component public class WechatChannel implements PaymentChannel { @Override public boolean supports(String channel) { return "wechat".equals(channel); } @Override public PayResult pay(PayRequest request) { // 微信相关逻辑 return new PayResult("success", "微信下单成功"); } }4.2 工厂类的两种实现方式
第一种是注入所有实现类到List中,遍历查找支持当前渠道的Bean:
@Component public class PaymentFactory { private final List<PaymentChannel> channels; public PaymentFactory(List<PaymentChannel> channels) { this.channels = channels; } public PaymentChannel getChannel(String channel) { return channels.stream() .filter(c -> c.supports(channel)) .findFirst() .orElseThrow(() -> new IllegalArgumentException("不支持的支付渠道: " + channel)); } }使用的时候是这样的:
@Service public class PaymentService { private final PaymentFactory paymentFactory; public PaymentService(PaymentFactory paymentFactory) { this.paymentFactory = paymentFactory; } public PayResult pay(String channel, PayRequest request) { PaymentChannel paymentChannel = paymentFactory.getChannel(channel); return paymentChannel.pay(request); } }第二种是用Map的方式,key是渠道编号,value是Bean实例:
@Component public class PaymentFactory { private final Map<String, PaymentChannel> channelMap; public PaymentFactory(Map<String, PaymentChannel> channelMap) { this.channelMap = channelMap; } public PaymentChannel getChannel(String channel) { PaymentChannel paymentChannel = channelMap.get(channel); if (paymentChannel == null) { throw new IllegalArgumentException("不支持的支付渠道: " + channel); } return paymentChannel; } }两种写法的区别在于定位Bean的方式不同。第一种依赖supports()方法去匹配,每次匹配都是遍历集合,效率上略低于Map的直接get。第二种直接用Map.get定位,速度更快,但对Bean的命名和配置有要求——Spring会把Map的key自动设置为Bean的名称,所以你的@Component注解里最好显式指定名称,比如@Component("alipay")。
4.3 扩展工厂时最舒服的一点
这种策略工厂最大的优势在于扩展性。新接入一个云闪付渠道时,只需要新建一个CloudPayChannel实现PaymentChannel,自动注册到容器,工厂不用改一行代码,调用方也不用改。这就是工厂模式与Spring容器结合后产生的化学反应——容器的自动装配机制帮我们完成了“注册”的动作,工厂本身只需要负责“选择”。
我在项目里常用的一个做法是把渠道编号定义成枚举,然后在实现类的supports方法里返回枚举的字符串值,避免魔法值散落各处。
5. 工厂模式与Spring生命周期的结合点
5.1 BeanPostProcessor:工厂产出前的最后一道工序
Spring容器的强大之处不在于“创建对象”,而在于“在创建对象的过程中提供横切能力”。BeanPostProcessor就是这种横切能力的核心参与方。它在Bean实例创建完成后、初始化方法执行前后回调,你可以在这时对Bean做代理、包装、属性修改等操作。
从工厂模式的角度理解,BeanPostProcessor就相当于“产品出厂前的质检员和改装师”。工厂生产出原始对象,质检员按需增加特性。AOP的实现正是基于这一点——Spring在BeanPostProcessor的回调中,判断这个Bean是否需要生成代理对象,如果需要就返回代理对象,不需要则返回原始对象。
如果你自己实现了一个BeanPostProcessor,一定不要忽略返回值的细节。BeanPostProcessor的postProcessAfterInitialization方法是有返回值的,你返回的如果是null,后续的处理器就会被跳过,这个坑会导致神秘的空指针问题。
5.2 SmartFactoryBean和延迟创建的设计取舍
Spring还提供了一个SmartFactoryBean接口给FactoryBean增加了isPrototype和isEagerInit等控制能力。isEagerInit返回false时,这个FactoryBean关联的Bean不会被容器启动时强行创建,而是等到第一次getBean时才实例化。
这在你的Bean创建成本较高、且不总是需要被容器启动逻辑触碰的场景下非常有用。我做过一个集成某个报表引擎的模块,其核心对象的创建需要加载大量模板文件,启动时初始化要花掉一两秒。用SmartFactoryBean把isEagerInit置为false后,应用可以快速启动,报表对象在用户访问时才构建。这是一种很典型的“延迟初始化”与工厂模式结合的设计。
5.3 ObjectProvider:工厂模式的“按需获取”变体
Spring从4.3版本开始引入了ObjectProvider,它的用法非常符合工厂模式的“按需获取”思想。当你注入一个ObjectProvider 时,你并不是直接拿到T对象,而是拿到一个“对象提供器”,你可以在需要的时机调用它的getObject()方法得到真正的Bean。
这个能力在处理可选依赖时特别顺手。比如某个功能强依赖Redis,但这个模块又要在没有Redis时降级运行:
@Service public class SomeService { private final ObjectProvider<RedisTemplate<String, String>> redisProvider; public SomeService(ObjectProvider<RedisTemplate<String, String>> redisProvider) { this.redisProvider = redisProvider; } public void doSomething() { RedisTemplate<String, String> redisTemplate = redisProvider.getIfAvailable(); if (redisTemplate != null) { // 使用Redis执行逻辑 } else { // 降级处理 } } }ObjectProvider本身也是Spring容器这个“大工厂”暴露给开发者的一种受控访问入口,它比直接注入Bean延迟了“拿到对象”的时机,给了更灵活的控制空间。
6. 常见问题与排查经验
6.1 在@Bean方法里直接new对象导致AOP失效
这个问题很典型。假设你有一个被@Transactional标注的Service,你在某个配置类里用@Bean方法创建它:
@Bean public UserService userService() { return new UserService(); }看起来没问题,但你会发现事务失效。原因在于容器中注册的是你自己new出来的UserService原始对象,而不是经过AOP代理的增强对象。Spring的AOP代理依赖BeanPostProcessor在Bean创建后对返回对象做包装,如果你的@Bean方法里自己new,等于绕过了代理的挂载。
解决方式很简单:UserService本身标注好@Component、@Service等注解,通过组件扫描让容器自己管理;如果必须显式用@Bean创建,那就注入它依赖的对象,让容器辅助完成装配,而不是自己在方法里new。
6.2 getBean("xxx")拿到了FactoryBean而不是产品对象
我们前面提过“&”前缀的问题。如果某个Bean是通过FactoryBean方式创建的,你又用相同名称去getBean,默认拿到的其实是FactoryBean.getObject()的返回值。想拿FactoryBean本身,要用“&”前缀。
如果你在一个项目的启动日志里看到两种类型的输出,比如“userMapper”和“&userMapper”,不要慌,这是正常的。Spring的BeanDefinition已经保留了这两者的注册信息。
6.3 @Bean方法会被调用多次?
这里分情况。如果@Bean定义在普通的非配置类里,或者配置类没有被Spring代理,那么每次调用@Bean方法都会执行方法体,产生新对象。如果配置类被CGLIB代理了,Spring会拦截@Bean方法调用并优先从容器中获取已有的Bean。
所以,如果你想在一个类中直接调用另一个@Bean方法来获取同一个Bean实例,注意你的配置类是否真正被代理。比如声明了@Configuration的配置类会被代理,方法内调用同一个类中其他@Bean方法时,能拿到同一个实例。而在加了@Configuration(proxyBeanMethods = false)的轻量模式下,就没有这种保证,每次调用都会创建新实例。
从工厂模式角度去理解这件事就更清晰了:proxyBeanMethods = true时,Spring为配置类创建了一个“代理工厂”,所有@Bean方法都走代理工厂分发;proxyBeanMethods = false时,配置类只是普通类,@Bean方法就是普通的对象创建方法。
6.4 策略工厂中Bean名称冲突导致Map注入异常
Spring将Bean注入Map时,键值默认是Bean名称。如果你的两个实现类不小心声明了相同的@Component名称,启动就会报BeanDefinitionOverrideException。这个问题在多人协作时经常发生,解决方法是规范命名,或者干脆在实现类里显式指定名称,与枚举值保持一致。
7. 从工厂到领域模型的扩展手法
7.1 工厂模式与构造器模式配合使用
如果你的对象创建参数较多,建议不要在工厂方法里堆太多参数。可以先在工厂方法内调用Builder或者构造器方法生成对象,再把工厂方法的职责限定在“决策”而不是“构建细节”上。比如:
@Component public class OrderFactory { public Order createOrder(OrderType type, Long userId, BigDecimal amount) { return Order.builder() .orderNo(generateOrderNo(type)) .userId(userId) .amount(amount) .status(OrderStatus.INIT) .build(); } }工厂负责“怎么选择正确的产品”,构造器负责“产品内部的完整性”,两者分工明确。这种写法的好处是测试友好——你可以单独测试工厂的决策逻辑,不需要真的去构造一个完整的复杂订单。
7.2 抽象工厂在跨模块协作中的体现
当你需要创建一组相互关联的产品对象时,抽象工厂就有用武之地了。比如你做一个报表系统,需要同时创建“PDF格式报表”和“Excel格式报表”,它们各自有对应的导出器和元数据解析器。这时可以定义ReportFactory接口,为每个格式提供一套完整的工厂实现。
不过说句实在话,在Spring项目里,抽象工厂用得并不多——因为Spring容器本身就是抽象工厂的典范实现。你声明接口,容器帮你选择实现,开发者真正手工写抽象工厂的场景非常少。更多时候我们把工厂收紧成单层策略工厂就够了,强行抽象反而增加理解成本。
7.3 用工厂模式隐藏第三方组件的复杂度
还有一种常见实践:用工厂模式封装对第三方SDK的接入。比如你得接入一个短信供应商的SDK,SDK要求先初始化Client、设置签名、预热连接池,然后才能发送短信。如果在业务代码里暴露这些初始化流程,谁调用谁都头疼。
用一个短息工厂:
@Component public class SmsClientFactory { private SmsClient smsClient; @PostConstruct public void init() { SmsConfig config = new SmsConfig(); config.setEndpoint("https://sms.example.com"); config.setAccessKey("xxx"); config.setSecretKey("xxx"); this.smsClient = new SmsClient(config); } public SmsClient getClient() { return smsClient; } }这样外部调用方只需要依赖SmsClientFactory,完全不用关心SDK的内部初始化细节。工厂模式在这里起到了“封装复杂创建过程”的作用,也让后续替换SDK供应商时,只需要改动工厂内部实现,业务层零感知。
8. 我的几点心得与踩坑总结
说实话,工厂模式是我在Spring项目里最常用、也最容易“用过头”的设计模式。自己刚工作那会儿,恨不得把所有Service都套上一层工厂,结果项目里全是绕来绕去的间接层,代码阅读成本急剧上升。
后来慢慢摸出两个判断标准:
- 如果你只是把一个类的创建过程简单包一层,而创建逻辑根本不会变化、也没有多种实现可选,那这个工厂大概率是多余的。Spring容器本身已经在管理Bean,你再去套工厂属于重复建设。
- 如果你确实有多个实现类需要根据运行时条件选择,或者创建过程需要依赖容器中的其他Bean、需要做复杂的初始化组装,那么工厂模式才值得用。
另一个让我印象深刻的点是:工厂模式与Spring的结合点,大多数场景不在“创建对象”本身,而在于“选择哪个对象”。Spring已经帮我们做好了创建,你只需要管好选择策略——这也是我在做支付、消息、文件存储这类多实现功能时的首选方案。
最后分享一个小技巧。当你用策略工厂时,一定要把工厂接口的入参设计成“稳定的上下文对象”,而不是一堆散参数。比如之前那个支付渠道的选择,传入的是一个PayRequest对象,而不是三个字符串参数。这样做的好处是,将来接口增加参数时,不需要改动工厂和所有实现类的签名,只改PayRequest本身即可。这个原则在工厂模式里尤其重要,却经常被忽略。
希望这些内容对你理解Spring中的工厂模式有帮助。如果后续遇到具体问题,比如FactoryBean不生效、策略工厂拿不到实现类、@Bean方法被重复调用,建议先回头看看类路径上有没有未扫描的包、Bean名称是否冲突、配置类有没有被CGLIB代理,大部分问题都出在这几个点上。