
来聊聊Spring Cloud里的工厂模式。这不是一篇纯八股文而是我在实际微服务项目里把工厂模式真正落地后的一些经验总结。如果你写过多渠道对接、多支付方式、多消息推送这类代码你大概率经历过if-else一长串写到怀疑人生的时刻工厂模式就是针对这类问题的解药。这篇文章我会从最基础的工厂模式概念讲起然后重点落到Spring Cloud环境下工厂模式怎么设计、代码怎么写、坑在哪里最后再给你一套面试官爱听的回答思路。无论你是准备面试、还是项目里正好要重构这段逻辑这篇都值得收藏慢慢看。1. 先搞明白Spring Cloud场景下为什么还要工厂模式1.1 工厂模式到底解决什么问题很多人对设计模式有个误解觉得它就是一套花架子写的时候多此一举。其实设计模式的核心就一句话把变化的部分封装起来让不变的稳定部分复用。工厂模式解决的正是对象创建这个环节的耦合问题。举个生活里的例子。你去餐厅吃饭你不用关心后厨是怎么洗菜、切菜、炒菜的你只需要跟服务员说来一份宫保鸡丁。服务员把你的需求传给后厨后厨根据菜单做出来端给你。这个服务员的角色就是工厂。调用者不直接依赖具体菜品怎么做只依赖一个统一的接口——点菜。放在Java世界里也一样。如果你的业务代码里到处是new AlipayService()、new WechatPayService()表面上看没什么问题但一旦某个渠道的构造方式变了或者新增了一个渠道你就得把所有调用点翻出来改一遍。工厂模式就是要消灭这种硬编码的创建过程把选择哪一个实现类的逻辑收敛到一个地方统一管理。1.2 从if-else地狱到工厂模式的一次重构我前几年接手过一个支付网关项目那代码写得真是经典。核心方法是这样的public PayResult pay(String channel, Order order) { if (alipay.equals(channel)) { // 50行支付宝逻辑 } else if (wechat.equals(channel)) { // 50行微信逻辑 } else if (unionpay.equals(channel)) { // 50行银联逻辑 } else { throw new UnsupportedOperationException(不支持的渠道); } }这个方法的致命问题在于每次加渠道都要动这个核心方法它有无数个调用方一不小心就把别的渠道搞挂了。每个分支里的逻辑越来越长很快就会变成几百行的上帝方法。没法单独测试某个渠道想测银联你得把前面的分支全走一遍。如果不同渠道的处理流程有细微差异比如有的要加签名、有的要验签这个方法的复杂度会指数级上升。我们的重构思路其实特别朴素把每一种支付渠道封装成一个独立的策略类然后用一个工厂统一管理和获取。调用方只需要传入一个channel标识就能拿到对应的渠道处理器。这套思路用术语来说就是策略模式简单工厂的组合。Spring Cloud项目天然支持这种玩法因为Spring容器本身就是个巨大的工厂我们要做的只是把业务策略Bean交给Spring托管再由一个门面类对外提供获取入口。1.3 三种工厂形态在Spring Cloud里应该选哪种工厂模式在经典书里被分成三种形态简单工厂、工厂方法、抽象工厂。很多人背概念很熟一到项目里就不知道选哪个我直接给你说结论。形态核心思想适合场景Spring Cloud下的落地方式简单工厂一个工厂类根据参数返回不同产品产品种类少、创建逻辑简单用MapString, Strategy存Bean一个方法返回策略工厂方法每个产品对应一个工厂类由工厂子类决定实例化哪个类产品种类多、每种产品有差异化创建流程定义ProcessorFactory接口每种渠道实现一个工厂类抽象工厂一个工厂创建一整族相关的产品对象产品族概念清晰比如不同支付渠道的请求工厂、响应工厂微服务里相对少见碰到多产品族联动时才会用到从我见过的真实项目看Spring Cloud体系下90%以上的场景用简单工厂策略模式就够了。原因很简单Spring容器本身已经帮我们完成了对象的创建和管理我们写的工厂本质上是一个分发器——根据一个字符串关键字从Spring容器里取出对应的Bean返回给调用方。完全没有必要在Spring之外再手工new来new去那样反而会绕过Spring的代理增强导致AOP切面失效。2. 工厂模式与Spring Cloud结合的落地思路2.1 把创建对象这件事交给Spring容器如果你已经用Spring Boot开发过一段时间你会发现一个有意思的现象我们平时根本不new对象而是通过Autowired、Resource把Bean注进来。这就是Spring控制反转IoC的核心思想——对象创建的权利交给容器。在Spring Cloud微服务里这个特性更加重要。因为你的对象往往不只是普通类而是被各种组件包装过的标注了FeignClient的远程调用客户端被Transactional包裹的事务代理被HystrixCommand或SentinelResource包装的容错代理被RabbitListener标记的消息监听器。这些都要在Spring容器里才能正常工作。如果你在代码里直接new一个Feign接口的实例你会得到一个悲伤的NullPointerException因为它内部的InvocationHandler是JDK动态代理生成的根本没法手动实例化。所以在Spring Cloud环境里我们写工厂模式的第一原则就是工厂不负责创建对象只负责从容器里获取对象。这个思想转变很关键很多从传统Java转过来的同学上来就在工厂里写new XXX()这其实是把Spring的优势白白浪费了。2.2 基于注解的自动注册机制我用过最舒服的写法是用自定义注解ApplicationContextAware实现Bean的自动注册。思路是这样的定义一个注解比如PayChannel(alipay)。每个支付渠道实现类标上这个注解。工厂类实现ApplicationContextAware在setApplicationContext方法里扫描所有带这个注解的Bean放进一个MapString, Strategy。这套机制的好处是开闭原则贯彻得特别彻底。你新增一个渠道只需要写一个实现类、标上注解工厂代码一行都不用改。系统会自动发现它、注册它、在调用时返回它。我之前见过不少团队用手工注册的方式factory.register(alipay, new AlipayService()); factory.register(wechat, new WechatService());也不是不行但每加一个渠道都得改工厂代码漏了就是线上事故。自动注册这种方式把注册这个动作也自动化了更符合Spring Cloud的自动化运维理念。2.3 和Feign客户端组合策略接口指向远程服务Spring Cloud项目里很多业务动作其实是调用远程服务完成的。比如你要对接多个物流平台每个平台都有对应的Feign客户端。这时候工厂模式的作用就更明显了。你定义一个统一的策略接口public interface LogisticsStrategy { String getChannel(); LogisticsTrack queryTrack(String expressNo); }然后每个物流平台的Feign客户端实现这个接口FeignClient(name shunfeng-service, url ${sf.url}) public interface ShunfengFeignClient extends LogisticsStrategy { Override GetMapping(/api/track) LogisticsTrack queryTrack(String expressNo); }工厂根据渠道码返回对应的Feign客户端Bean调用方不需要知道对方是HTTP调用还是本地实现。这就是策略模式的价值它隐藏了能力从哪里来的细节让上层业务只关心做什么不关心怎么做。我在实际项目里还见过一种更高级的玩法把工厂本身暴露成Spring Cloud服务让其他微服务通过远程调用获取策略执行结果。这种设计适合策略非常多、且很多服务都要用的情况。不过大多数项目没必要搞这么重一个公共的utils模块或者common包就够用了。3. 从零实现一套可复用的支付渠道工厂3.1 定义策略接口与统一结果模型咱们直接上干货我以支付渠道对接为例写一套完整的代码。这套代码我改过很多遍已经是比较成熟的样子了你可以直接抄到项目里改改就能用。首先定义统一结果模型。正常情况下每个支付渠道返回的报文格式都不一样但我们在上层只关心成功没成功、订单号是多少、第三方流水号是多少、失败原因是什么。所以要定义一个统一的返回体public class PayResult { private boolean success; private String channel; private String orderNo; private String thirdPartyTradeNo; private String errorMsg; public static PayResult success(String channel, String orderNo, String thirdPartyTradeNo) { PayResult result new PayResult(); result.success true; result.channel channel; result.orderNo orderNo; result.thirdPartyTradeNo thirdPartyTradeNo; return result; } public static PayResult failure(String channel, String orderNo, String errorMsg) { PayResult result new PayResult(); result.success false; result.channel channel; result.orderNo orderNo; result.errorMsg errorMsg; return result; } // getter / setter 省略 }然后是策略接口public interface PayStrategy { String getChannel(); PayResult pay(PayRequest request); PayResult refund(PayRequest request); }这里我加了一个getChannel()方法返回该策略支持的渠道标识。这个方法很有用它让工厂可以自动完成注册而不需要额外配置。3.2 三个渠道实现类与注册逻辑接下来是具体渠道实现。以支付宝为例PayChannel(alipay) Component public class AlipayStrategy implements PayStrategy { Autowired private AlipayClient alipayClient; Override public String getChannel() { return alipay; } Override public PayResult pay(PayRequest request) { // 调用支付宝SDK构建请求参数发起支付 // 把结果转成统一PayResult try { AlipayTradePagePayRequest pagePayRequest new AlipayTradePagePayRequest(); pagePayRequest.setBizContent({\out_trade_no\:\ request.getOrderNo() \}); String body alipayClient.pageExecute(pagePayRequest).getBody(); return PayResult.success(alipay, request.getOrderNo(), alipay_trade_no); } catch (Exception e) { return PayResult.failure(alipay, request.getOrderNo(), e.getMessage()); } } Override public PayResult refund(PayRequest request) { // 退款逻辑 return PayResult.success(alipay, request.getOrderNo(), refund_no); } }微信支付的实现类套路差不多就是PayChannel(wechat)内部调用微信SDK的接口。银联也一样。这里我不贴完整的重复代码了写多了反而显得水重点是理解这个结构每个渠道一个类类内部复杂度自己消化对外暴露的统一接口完全一样。3.3 工厂类的核心代码与Bean管理细节接下来是重头戏——工厂类。我先写一个基于ApplicationContextAware的版本这是我最推荐的写法Component public class PayStrategyFactory implements ApplicationContextAware { private static final MapString, PayStrategy STRATEGY_MAP new ConcurrentHashMap(); Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { MapString, PayStrategy beansOfType applicationContext.getBeansOfType(PayStrategy.class); beansOfType.values().forEach(strategy - STRATEGY_MAP.put(strategy.getChannel(), strategy)); } public PayStrategy getStrategy(String channel) { PayStrategy strategy STRATEGY_MAP.get(channel); if (strategy null) { throw new IllegalArgumentException(不支持的支付渠道: channel); } return strategy; } }有几个关键点需要说明第一用的是ConcurrentHashMap而不是HashMap。虽然初始化一般发生在应用启动阶段没有并发问题但谁知道后续运行时会不会有人动态往Map里加东西。用线程安全的Map多花不了几个字节图个安心。第二getBeansOfType方法会按照类型拿到所有该类型的Bean包括子类、代理类。只要是实现了PayStrategy接口的Bean都会被捞出来。这一点在Spring Cloud里特别重要因为Feign客户端生成的代理对象也是Bean同样能被扫到。第三工厂类本身是单例的Spring默认就是单例不要给它加Scope(prototype)。这个工厂在整个应用生命周期内只需要一个实例就够了。第四获取不到策略时千万不要返回null。返回null会把问题推给调用方调用方忘了判空就是空指针。这里主动抛异常把问题暴露在最早的时间点这对定位线上问题非常有帮助。调用方的代码就非常清爽了Service public class PaymentService { Autowired private PayStrategyFactory strategyFactory; public PayResult pay(PayRequest request) { PayStrategy strategy strategyFactory.getStrategy(request.getChannel()); return strategy.pay(request); } }业务逻辑里完全看不到if-else了以后加渠道、改渠道都只是在PayStrategy的继承体系里做文章。4. 进阶技巧与避坑指南4.1 用Map批量注入替代手写注册除了ApplicationContextAware的方案Spring还提供了一种更简洁的注入方式——直接把所有策略注入到一个Map里。这招是我后来才发现的代码量少了很多Component public class PayStrategyFactory { private final MapString, PayStrategy strategyMap; public PayStrategyFactory(MapString, PayStrategy strategyMap) { this.strategyMap strategyMap; } public PayStrategy getStrategy(String channel) { PayStrategy strategy strategyMap.get(channel); if (strategy null) { throw new IllegalArgumentException(不支持的支付渠道: channel); } return strategy; } }注意这里的Map的key是Bean的名字默认是类名首字母小写比如alipayStrategy、wechatStrategy。你可以在Component(alipay)里指定Bean名字这样Map的key就跟你业务里的渠道码一致了。我对比过这两种方式的差异ApplicationContextAware方式多写几个方法但工厂内部自己维护Map对外的构造器很干净适合策略数量多、渠道码与Bean名不一致的场景。Map注入方式代码最简洁Spring容器自动把同一接口的所有实现类注入进来适合渠道码规范、Bean名可以对齐的场景。两种方案我都用过谈不上谁绝对好。如果你做的是公共组件将来可能被别人以starter的形式引入我建议用第一种因为第一种不依赖Bean命名规范getChannel()返回什么就注册什么语义更明确。4.2 配置驱动策略选择与兜底策略在实际微服务项目里策略的选择往往还跟配置中心相关。比如灰度发布的时候你可能想让某个渠道先切一部分流量到新逻辑上。这时候纯靠工厂模式还不够需要结合配置中心动态调整策略选择。一个比较实用的做法是引入兜底策略的概念Component public class PayStrategyFactory { private final MapString, PayStrategy strategyMap; Value(${pay.default-channel:alipay}) private String defaultChannel; public PayStrategy getStrategy(String channel) { PayStrategy strategy strategyMap.get(channel); if (strategy null) { strategy strategyMap.get(defaultChannel); } if (strategy null) { throw new IllegalArgumentException(不支持的支付渠道: channel , 且默认渠道未配置); } return strategy; } }请求方传了一个不认识的渠道码时不至于直接报错而是走默认渠道。这在对接上游系统时特别有用——上游可能传过来一个老版本的渠道码但你们已经下线了兜底策略能保证业务不中断。还有一种更灵活的做法是结合RefreshScope和配置中心实现动态切换。比如A/B测试时让10%的请求走新渠道、90%走老渠道这个比例放在配置中心里改配置就能生效不用重新发版。思路就是工厂类加上RefreshScope注解工厂内部根据配置中心的权重值做路由选择。这种玩法在Spring Cloud Alibaba的Nacos配置中心下非常好用推荐有需求的同学试试。4.3 我踩过的坑Bean提前加载、循环依赖、并发初始化聊点实战中真正让人头疼的问题。第一个坑是Bean提前加载导致的循环依赖。如果你的策略实现类里注入了PayStrategyFactory而工厂类又通过getBeansOfType扫描所有策略Bean很可能出现循环依赖。我遇到过的情况是AlipayStrategy里注入了工厂工厂又要获取所有PayStrategy的实现Spring一启动就报BeanCurrentlyInCreationException。解决思路有两个一是让策略类不要反向依赖工厂把对这个工厂的依赖改成Lazy注解注入延迟到实际调用时才创建完整Bean二是用ObjectProvider来安全地获取Bean。一般我更推荐第一种因为Lazy最简单直接就是一行注解的事。第二个坑是getStrategy方法的并发问题。如果工厂类内部不只是简单地返回Map里的值而是每次实时去Spring容器里getBean在高并发下会有性能瓶颈。我之前一版代码就是每次调用都去容器里获取Bean压测时发现吞吐上不去后来改成启动时一次性把Bean捞出来存Map性能问题立刻消失。微服务场景下每个请求都是QPS级别的容器的getBean调用会触发各种后置处理器逻辑千万别放在热路径上。第三个坑是策略实现类内部状态被并发修改。策略Bean默认是单例的也就是所有请求共享同一个实例。如果你的策略类里有可变状态字段比如当前处理中的订单号那并发请求就会互相覆盖数据。正确做法是策略类内部无状态所有方法参数都从入参传递。这个概念跟静态工具类有点类似写的时候一定要克制住往成员变量里塞业务数据的冲动。5. 面试官视角工厂模式在SpringCloud里的高频考点5.1 一看就知道背过八股文的回答这几年Java面试设计模式几乎成了标配问题。每次面试候选人我都能听到近乎一模一样的回答简单工厂模式是根据参数的不同返回不同类的实例工厂方法模式是定义了一个创建对象的接口让子类决定实例化哪一个类抽象工厂模式是围绕一个超级工厂创建其他工厂。这种回答背得很流利但在我这里过不了关。因为它只回答了是什么没有回答为什么。面试官问工厂模式真正想了解的是你有没有在复杂业务里用过它、怎么用的、解决了什么问题。我给准备面试的同学一个忠告别把设计模式当知识点背要把设计模式当工具用。你把工厂模式在Spring Cloud项目里的应用场景讲清楚比背三遍概念都有用。5.2 让面试官眼前一亮的实战细节如果你想在面试中脱颖而出可以从这几个角度展开第一讲清楚Spring容器本身就是一个工厂模式的体现。ApplicationContext负责创建和管理所有Bean你写的业务工厂只是在Spring工厂之上做了另一层抽象。这样讲既能展示你对框架原理的理解又能说明你为什么不在工厂里直接new对象。第二把工厂模式和Spring Cloud的组件结合着讲。比如远程调用层怎么用策略模式切换不同的Feign客户端配置中心怎么动态改变策略选择网关层怎么做动态路由——这些都是工厂模式在微服务里更高级的玩法。第三主动聊一些扩展点。比如你可以说面对新增的支付渠道我只需要新增一个实现类打上注解系统会自动注册完全不需要修改现有代码然后进一步说说你们是怎么做自动注册的。这种细节比空洞的概念描述有说服力得多。第四如果可以顺手提一下跟其他模式的组合。工厂模式经常和策略模式、模板方法模式、单例模式配合使用。搭配合适是112的效果如果滥用就会变成过度设计。5.3 手撕题从简单工厂到Spring容器工厂不少公司面试会有手写代码环节。如果让手写一个简单工厂大多数人写出来是这样的public class PayServiceFactory { public static PayService create(String type) { if (alipay.equals(type)) { return new AlipayService(); } else if (wechat.equals(type)) { return new WechatService(); } return null; } }这个没问题但如果你能在这个基础上进一步优化面试官会高看你一眼。比如用Map消除if-elsepublic class PayServiceFactory { private static final MapString, PayService SERVICES new HashMap(); static { SERVICES.put(alipay, new AlipayService()); SERVICES.put(wechat, new WechatService()); } public static PayService create(String type) { return SERVICES.get(type); } }如果再进一步结合Spring的InitializingBean或者注解实现自动注册那面试官基本能确认你是真写过项目的。面试看的不是代码多花哨而是你有没有从能跑就行进化到好维护才行的工程意识。6. 扩展工厂模式之外Spring Cloud还藏着哪些设计模式聊完工厂模式我顺手把Spring Cloud体系里其他设计模式也梳理了一遍。你会发现微服务框架本身就是设计模式的集大成者。模板方法模式在Spring Cloud里非常常见。比如AbstractFeignClient就是固定了远程调用的骨架把具体的编码逻辑交给子类完成。我们在写统一API封装时也可以借鉴这种思想一个抽象类定义整个流程具体步骤延迟到子类实现。代理模式在Spring Cloud里到处可见。Feign本质上是JDK动态代理RestTemplate也被LoadBalancerInterceptor代理。AOP切面在微服务里被用来做日志、鉴权、限流这些都是代理模式的实战应用。观察者模式对应的是Spring的事件监听机制比如ApplicationEvent配合EventListener做业务解耦。在微服务里本地事务发事件、异步监听处理这类场景非常多。适配器模式体现在不同组件之间的整合上。Spring Cloud把不同的注册中心适配成统一的DiscoveryClient接口这就是典型的适配器思维。策略模式在前面已经讲了很多它跟工厂模式是天然的好搭档。调用方用工厂拿策略策略内部用模板方法固定流程再配合代理模式做增强一套组合拳下来代码既不臃肿也不散乱。状态模式在订单状态机里非常常见。微服务里订单状态流转、审批流程流转都可以用状态模式避免大量的switch-case判断。说这些不是为了让你一口气全用上而是想告诉你一个道理设计模式的价值在于复用前人总结出来的优秀实践不在于数量多。在Spring Cloud项目里工厂模式策略模式这对组合已经能覆盖很多业务场景。其他模式按需引入千万别为了用而用否则代码复杂度不减反增。我自己做项目多年最深的感受是设计模式不是面试背的题是真正能帮你省时间的工具。工厂模式尤其如此它解决的问题是Java开发里最常见的痛点之一——对象创建的耦合。Spring Cloud把对象管理的复杂度接管了但你仍然需要一套清晰的业务策略组织方式。把工厂模式用好了你的代码会看起来特别通透新同学接手也容易看懂。如果你正在重构一个if-else特别多的老模块不妨先试试这套思路应该会让你对设计模式这件事有新的认识。