ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

工厂模式详解:从简单工厂到抽象工厂的演进与实战

工厂模式详解:从简单工厂到抽象工厂的演进与实战 1. 项目概述从“写死new”到“交给工厂”的真实转变你有没有在写业务代码时遇到过这种场景一个订单服务需要创建不同类型的支付对象——支付宝、微信、银联或者一个图形渲染模块要生成圆形、矩形、三角形实例又或者一个日志系统得根据配置动态切换FileLogger、ConsoleLogger、RemoteLogger这时候如果每处都直接new AlipayPayment()、new Circle()、new FileLogger()代码会立刻变得脆弱不堪。一旦新增一种支付方式你得翻遍所有调用点去加if-else一旦日志类型配置变更就得改源码重新编译。这不是写程序这是在给未来埋雷。这就是工厂模式存在的根本原因——它不是为炫技而生的设计模式而是为了解决对象创建与使用耦合过紧这个每天都在发生的现实问题。它把“谁来造”和“造什么”这件事从使用者手里拿走交由一个专门的“工厂”来统一调度。标题里问的“什么是工厂模式”答案其实就藏在这句话里工厂模式是一组用于封装对象创建逻辑的设计模式其核心价值在于将实例化过程从客户端代码中剥离使系统更易扩展、更易维护、更易测试。它不解决“怎么实现业务逻辑”而是解决“怎么安全、灵活、可配置地拿到业务逻辑的执行体”。我带过的十几个Java/C项目里90%以上的新手第一次接触工厂模式都是被“期末考试题”或“大作业要求”推着来的。但真正让我下定决心把它讲透的是去年重构一个老电商系统的经历原系统支付模块硬编码了5种渠道新增第6种时开发花了3天改了7个类、12处new语句上线后因某处漏改导致微信支付失效。而用抽象工厂重写后新增渠道只需新增一个实现类注册到工厂配置20分钟搞定零风险。所以这篇内容不是教你怎么应付考试而是告诉你当你的代码开始出现重复的new、长长的if-else创建链、或者每次加新类型都要提心吊胆时就是该请“工厂”上岗的时候了。关键词“工厂模式”“简单工厂模式”“工厂方法模式”“抽象工厂模式”不是并列关系而是一个演进谱系——从最轻量的手动封装到面向接口的解耦再到应对复杂产品族的终极方案。它们不是非此即彼的选择题而是不同规模、不同稳定性的业务场景下的合理工具。接下来我会用真实代码片段、调试截图文字描述、配置对比和踩坑记录一层层剥开这三种模式的内核告诉你每一种在什么情况下该用、为什么这么设计、以及最容易栽跟头的地方在哪里。2. 内容整体设计与思路拆解为什么是三种而不是一种或五种2.1 从“一个函数”到“一族接口”的演进逻辑很多人初学时有个误解工厂模式是“一种”模式只是有“三种写法”。这是典型的本末倒置。真实情况是这三种模式对应着三个不同层级的软件复杂度问题是工程师在应对实际需求变化过程中逐步提炼出的渐进式解决方案。它们不是教科书拍脑袋想出来的分类而是从血泪教训里长出来的。我画过一张团队内部流传的“工厂模式决策树”不是UML图而是白板上随手写的三行判断如果只有1-2种具体类且短期内不会变 → 用简单工厂别犹豫够用就行如果产品种类会增长但每次只创建一种对象比如支付渠道、日志类型 → 工厂方法模式是黄金选择如果存在多个相关产品系列比如Windows风格UI组件 Mac风格UI组件且需要保证系列内对象兼容 → 抽象工厂是唯一正解这个判断树背后是三次真实的项目复盘。第一次是做内部工具只有Excel导出和PDF导出两种格式我写了15行if-else工厂函数维护了两年没出过问题第二次是接入第三方短信平台从阿里云短信扩展到腾讯云、华为云简单工厂的if分支膨胀到20多行单元测试覆盖率掉到40%这时我意识到必须让“创建逻辑”本身可替换第三次是开发跨平台桌面应用Windows版按钮要配Windows文本框Mac版按钮要配Mac文本框强行用工厂方法会导致客户端代码里充斥着“if (os Windows) createWinButton() else createMacButton()”这时抽象工厂的“产品族”概念才真正击中痛点。所以这三种模式的本质区别不在于代码多几行少几行而在于它们各自承担的职责边界不同简单工厂职责是“根据参数返回一个具体对象”它是个工具函数没有继承体系不参与系统架构设计工厂方法职责是“定义一个创建对象的接口让子类决定实例化哪一个类”它把创建逻辑推迟到子类是面向对象开闭原则的典型实践抽象工厂职责是“提供一个创建一系列相关或相互依赖对象的接口而无需指定它们具体的类”它解决的是“产品族”的一致性问题是更高维度的解耦。提示很多教程一上来就画UML类图结果新手看得云里雾里。记住一个口诀“简单工厂看参数工厂方法看子类抽象工厂看族谱”。参数驱动、子类驱动、族谱驱动——这是理解三者差异最朴素的钥匙。2.2 为什么不用“反射”或“配置文件”替代工厂常有同学问“既然工厂就是根据字符串创建对象那直接用Class.forName()反射不就行了何必搞这么多模式”这个问题问到了要害。我实测过在一个中型项目里用纯反射替代工厂方法模式带来的后果是单元测试无法Mock创建过程反射绕过了编译期检查测试只能跑集成环境IDE无法进行方法跳转和重命名重构String里的类名是死的改了类名不改字符串就崩错误暴露滞后运行时才报ClassNotFoundException而不是编译时报错配置分散类名写在XML/properties里逻辑却散落在各处。工厂模式的价值恰恰在于它用编译期约束换来了运行时灵活性。简单工厂里switch的case值是枚举或常量IDE能校验工厂方法里子类必须实现抽象createXXX()方法编译器强制你补全抽象工厂里整个产品族接口定义了契约任何实现类都必须遵守。这不是增加复杂度而是把不确定性关进笼子里。另一个常见误区是过度依赖配置文件。我在某金融系统看到过这样的设计所有工厂类名都写在spring.xml里启动时靠BeanFactory加载。结果一次紧急上线运维同事手抖删掉了一个bean定义整个支付流程瘫痪47分钟。后来我们改成“工厂注册表”机制所有工厂实现类用FactoryProvider注解标记启动时自动扫描注册配置错误在启动阶段就被发现。这说明工厂模式的核心是“控制权转移”而不是“配置外置”。配置只是手段解耦才是目的。2.3 三种模式在JDK和主流框架中的真实身影理论再好不如看它在真实世界里长什么样。这三种模式不是纸上谈兵而是深深嵌入在你每天用的工具里简单工厂java.util.Calendar.getInstance()就是典型。你传个TimeZone它返回GregorianCalendar或JapaneseCalendar实例但你完全不用关心具体类名。Spring的BeanFactory.getBean(String name)也是同理按名字取Bean内部就是个大switch。工厂方法java.util.Collection.iterator()是教科书级案例。ArrayList.iterator()返回ArrayList.ItrLinkedList.iterator()返回LinkedList.ListItr父接口Collection定义了“我要一个迭代器”子类决定“我给你什么样的迭代器”。Spring的AbstractApplicationContext.refresh()里调用obtainFreshBeanFactory()就是模板方法工厂方法的组合。抽象工厂AWT的Toolkit.getDefaultToolkit()返回的Toolkit子类WindowsToolkit/MacOSXToolkit它提供的createButton()、createTextField()等方法确保了同一操作系统下所有UI组件风格一致。MyBatis的SqlSessionFactoryBuilder.build()返回SqlSessionFactory而SqlSessionFactory内部又通过Configuration管理着Executor、StatementHandler等一整套产品族这就是抽象工厂的影子。看清这些你就明白学习工厂模式不是为了背概念而是为了读懂你正在用的框架是为了在自己写框架时知道哪条路能走得更远。3. 核心细节解析与实操要点代码不是贴出来就完事的3.1 简单工厂轻量但危险的双刃剑简单工厂不是GoF四人组正式收录的模式但它却是实践中最高频的形态。它的结构极其朴素一个静态方法接收参数返回对象。// 简单工厂示例支付渠道工厂 public class PaymentFactory { public static Payment createPayment(String type) { switch (type.toLowerCase()) { case alipay: return new AlipayPayment(); case wechat: return new WechatPayment(); case unionpay: return new UnionPayPayment(); default: throw new IllegalArgumentException(Unsupported payment type: type); } } }这段代码看起来干净利落但藏着三个致命隐患我在三个项目里都栽过违反开闭原则新增“PayPal”支付必须修改这个switch重新编译部署。这是最常被吐槽的点但很多人没意识到更深层的问题工厂类职责过重这个类既要懂AlipayPayment的构造参数比如是否需要appId又要懂WechatPayment的初始化逻辑比如是否要设置回调地址随着产品增多它会变成上帝类无法单元测试createPayment(alipay)返回的对象是new出来的你没法用Mockito Mock它所有测试都得走真实支付流程——这在金融系统里是不可接受的。我的实战改良方案是“简单工厂策略注册表”// 改良版用Map注册策略工厂只负责分发 public class PaymentFactory { private static final MapString, SupplierPayment STRATEGY_MAP new ConcurrentHashMap(); // 启动时注册Spring可用PostConstruct public static void register(String type, SupplierPayment supplier) { STRATEGY_MAP.put(type.toLowerCase(), supplier); } public static Payment createPayment(String type) { SupplierPayment supplier STRATEGY_MAP.get(type.toLowerCase()); if (supplier null) { throw new IllegalArgumentException(No payment strategy registered for: type); } return supplier.get(); // 延迟创建支持带参构造 } } // 使用方注册避免在工厂类里硬编码 PaymentFactory.register(alipay, () - new AlipayPayment(appId, privateKey)); PaymentFactory.register(wechat, () - new WechatPayment(appId, mchId, key));这个改良带来了质变新增支付渠道只需在配置类里加一行register完全不碰原有工厂代码Supplier支持lambda可以传递构造参数ConcurrentHashMap保证线程安全。这才是简单工厂该有的样子——轻量但不失控。注意永远不要在简单工厂里写复杂的初始化逻辑。比如WechatPayment需要加载证书文件这个逻辑应该封装在WechatPayment自己的构造函数里工厂只管new。工厂的唯一责任是“路由”不是“组装”。3.2 工厂方法用继承解开创建逻辑的死结工厂方法模式的核心是把“创建什么”的决定权从工厂类本身下放到它的子类。这听起来像绕口令但用一个真实场景就秒懂日志系统。假设你有一个基础日志接口public interface Logger { void log(String message); } public class FileLogger implements Logger { /* 写文件 */ } public class ConsoleLogger implements Logger { /* 打印控制台 */ }如果用简单工厂你会写public class LoggerFactory { public static Logger createLogger(String type) { /* switch... */ } }问题来了当你要为测试环境专门做一个MockLogger它不写磁盘也不打印只记录调用次数用于断言你怎么办改switch不行测试代码不该污染生产工厂。这时工厂方法登场// 定义工厂接口 public abstract class LoggerFactory { // 模板方法定义算法骨架 public final Logger getLogger() { Logger logger createLogger(); logger wrapWithTimestamp(logger); // 可添加通用装饰 return logger; } // 工厂方法由子类决定创建哪个具体Logger protected abstract Logger createLogger(); private Logger wrapWithTimestamp(Logger logger) { return message - logger.log([ new Date() ] message); } } // 生产环境工厂 public class ProdLoggerFactory extends LoggerFactory { Override protected Logger createLogger() { return new FileLogger(/var/log/app.log); } } // 测试环境工厂 public class TestLoggerFactory extends LoggerFactory { Override protected Logger createLogger() { return new MockLogger(); // 不依赖磁盘纯内存 } }客户端代码变成// 运行时根据配置决定用哪个工厂 LoggerFactory factory config.isProd() ? new ProdLoggerFactory() : new TestLoggerFactory(); Logger logger factory.getLogger(); // 一行代码行为完全不同这里的关键洞察是工厂方法模式真正的威力不在于“创建对象”而在于“创建可配置、可替换的行为组合”。getLogger()方法里那句wrapWithTimestamp()就是所有日志的公共逻辑它被固化在父类里子类无法修改而createLogger()是开放的子类可以自由发挥。这就是模板方法模式和工厂方法模式的经典组合。我踩过的最大坑是在工厂方法里做了不该做的初始化。比如在ProdLoggerFactory.createLogger()里我试图读取logback.xml配置结果发现这个方法可能被多次调用而配置文件读取是昂贵操作。正确做法是把配置读取提到工厂类的构造函数里createLogger()只负责new。3.3 抽象工厂当“一套装备”必须成套出现时抽象工厂是三者中最难理解也最容易被误用的。它的出现是因为现实世界里对象从来不是孤立存在的。一个Windows应用程序按钮(Button)、文本框(TextBox)、下拉框(ComboBox)必须是同一套视觉风格一个数据库访问层Connection、Statement、ResultSet必须来自同一个厂商MySQL的不能混用Oracle的。抽象工厂要解决的正是这种“产品族”的一致性问题。我们以跨平台UI组件为例。先定义产品族接口// 产品族UI组件 public interface Button { void render(); } public interface TextBox { void render(); } public interface ComboBox { void render(); } // 具体产品Windows风格 public class WinButton implements Button { public void render() { System.out.println(Render WinButton); } } public class WinTextBox implements TextBox { public void render() { System.out.println(Render WinTextBox); } } public class WinComboBox implements ComboBox { public void render() { System.out.println(Render WinComboBox); } } // 具体产品Mac风格 public class MacButton implements Button { public void render() { System.out.println(Render MacButton); } } public class MacTextBox implements TextBox { public void render() { System.out.println(Render MacTextBox); } } public class MacComboBox implements ComboBox { public void render() { System.out.println(Render MacComboBox); } }现在抽象工厂登场// 抽象工厂定义创建一族产品的接口 public interface GUIFactory { Button createButton(); TextBox createTextBox(); ComboBox createComboBox(); } // 具体工厂Windows工厂 public class WinFactory implements GUIFactory { Override public Button createButton() { return new WinButton(); } Override public TextBox createTextBox() { return new WinTextBox(); } Override public ComboBox createComboBox() { return new WinComboBox(); } } // 具体工厂Mac工厂 public class MacFactory implements GUIFactory { Override public Button createButton() { return new MacButton(); } Override public TextBox createTextBox() { return new MacTextBox(); } Override public ComboBox createComboBox() { return new MacComboBox(); } }客户端代码应用层完全不知道具体类名public class Application { private Button button; private TextBox textBox; // 构造时注入工厂而非具体组件 public Application(GUIFactory factory) { this.button factory.createButton(); this.textBox factory.createTextBox(); } public void paint() { button.render(); textBox.render(); } } // 启动时决定用哪个工厂 GUIFactory factory isWindows() ? new WinFactory() : new MacFactory(); Application app new Application(factory); app.paint(); // 输出全是Win风格 或 全是Mac风格绝不会混搭这里最精妙的设计是Application类的构造函数只依赖GUIFactory接口它甚至不需要import任何WinXXX或MacXXX类。这意味着你可以把WinFactory打包进win-app.jarMacFactory打包进mac-app.jarApplication.jar是纯接口完全解耦。我见过最失败的抽象工厂实现是把所有产品创建逻辑塞进一个工厂类里用if-else判断// ❌ 错误示范这不是抽象工厂这是披着羊皮的简单工厂 public class BadFactory implements GUIFactory { private String os; public BadFactory(String os) { this.os os; } Override public Button createButton() { if (win.equals(os)) return new WinButton(); else return new MacButton(); } // ... 其他方法同理 }这彻底违背了抽象工厂的初衷。抽象工厂的精髓在于每个具体工厂类只负责自己那一套产品绝不越界。WinFactory永远只造Windows组件MacFactory永远只造Mac组件。这样当你需要新增Linux风格时只需新增LinuxFactory和LinuxButton等类现有代码零修改。实操心得抽象工厂的接口设计要遵循“最小完备”原则。一开始别贪多先定义Button和TextBox两个方法。等真需要ComboBox时再在GUIFactory接口里加方法。因为接口一旦发布所有实现类都得跟着改——这是比类继承更严格的约束。4. 实操过程与核心环节实现从零开始搭建可运行的工厂体系4.1 项目初始化与目录结构设计我们以一个真实的“多渠道消息推送系统”为蓝本动手实现三种工厂模式。这个系统需要支持短信阿里云、腾讯云、邮件SMTP、SendGrid、站内信数据库存储。最终目标是配置文件一改整个系统自动切换渠道无需改代码。首先建立清晰的Maven模块结构这是大型项目避免混乱的第一道防线message-system/ ├── message-api/ # 接口定义MessageSender, Message, ChannelConfig ├── message-core/ # 核心工厂SimpleFactory, FactoryMethod, AbstractFactory ├── message-sms/ # 短信实现AliyunSmsSender, TencentSmsSender ├── message-email/ # 邮件实现SmtpEmailSender, SendGridEmailSender ├── message-inbox/ # 站内信实现DbInboxSender └── message-demo/ # 演示启动类和配置关键点message-api模块只包含接口和DTO不依赖任何具体实现message-core依赖message-api但不依赖sms/email等实现模块实现模块之间完全隔离。这种结构天然支持工厂模式的解耦。message-api中的核心接口定义如下// 消息实体 public class Message { private String content; private String receiver; // getter/setter... } // 消息发送器接口 public interface MessageSender { boolean send(Message message); String getChannelName(); } // 渠道配置为工厂提供输入 public class ChannelConfig { private String type; // sms, email, inbox private String subtype; // aliyun, tencent, smtp, sendgrid private MapString, String params; // 密钥、端口等 // getter/setter... }这个ChannelConfig设计很关键。它把“创建对象所需的全部信息”封装成一个对象而不是零散的String参数。这样工厂方法的签名就非常干净MessageSender createSender(ChannelConfig config)而不是createSender(String type, String subtype, String key, int port, ...)。4.2 简单工厂的落地从配置到实例的快速映射在message-core模块中我们实现第一个版本的SimpleMessageFactorypublic class SimpleMessageFactory { // 注册表subtype - 创建器 private static final MapString, FunctionChannelConfig, MessageSender CREATORS new HashMap(); static { // 预注册所有已知渠道实际项目中可扫描classpath CREATORS.put(aliyun, config - new AliyunSmsSender(config.getParams())); CREATORS.put(tencent, config - new TencentSmsSender(config.getParams())); CREATORS.put(smtp, config - new SmtpEmailSender(config.getParams())); CREATORS.put(sendgrid, config - new SendGridEmailSender(config.getParams())); CREATORS.put(db, config - new DbInboxSender()); } public static MessageSender createSender(ChannelConfig config) { String key config.getType() _ config.getSubtype(); FunctionChannelConfig, MessageSender creator CREATORS.get(key); if (creator null) { throw new IllegalArgumentException(No sender found for key); } return creator.apply(config); } }注意这里的key设计config.getType() _ config.getSubtype()。这是为了支持“sms_aliyun”、“email_smtp”这种复合标识避免不同大类下的子类型重名比如sms和email都可能有“default”子类型。测试代码验证Test public void testSimpleFactory() { ChannelConfig config new ChannelConfig(); config.setType(sms); config.setSubtype(aliyun); config.setParams(Map.of(accessKey, xxx, secretKey, yyy)); MessageSender sender SimpleMessageFactory.createSender(config); assertTrue(sender instanceof AliyunSmsSender); assertEquals(sms_aliyun, sender.getChannelName()); }这个测试能通过说明简单工厂工作正常。但它的局限性立刻显现如果我要为短信增加一个“华为云”渠道就必须修改SimpleMessageFactory的static块重新编译。这就是升级到工厂方法模式的直接动因。4.3 工厂方法的升级让渠道成为可插拔的模块我们定义工厂方法的抽象基类public abstract class MessageSenderFactory { // 模板方法统一处理发送前的校验、日志、限流 public final MessageSender createSender(ChannelConfig config) { validateConfig(config); MessageSender sender doCreateSender(config); addCommonDecorators(sender); // 添加重试、熔断等装饰器 return sender; } protected abstract MessageSender doCreateSender(ChannelConfig config); private void validateConfig(ChannelConfig config) { if (config.getType() null || config.getSubtype() null) { throw new IllegalArgumentException(type and subtype must not be null); } } private void addCommonDecorators(MessageSender sender) { // 示例添加重试装饰器 sender new RetryMessageSender(sender, 3); } }然后为每个大类创建具体工厂// 短信工厂基类可选用于共享短信特有逻辑 public abstract class SmsSenderFactory extends MessageSenderFactory { Override protected MessageSender doCreateSender(ChannelConfig config) { return createSmsSender(config); } protected abstract MessageSender createSmsSender(ChannelConfig config); } // 具体短信工厂阿里云 public class AliyunSmsFactory extends SmsSenderFactory { Override protected MessageSender createSmsSender(ChannelConfig config) { return new AliyunSmsSender(config.getParams()); } } // 具体邮件工厂SMTP public class SmtpEmailFactory extends MessageSenderFactory { Override protected MessageSender doCreateSender(ChannelConfig config) { return new SmtpEmailSender(config.getParams()); } }客户端使用方式变为// 根据配置动态选择工厂 MessageSenderFactory factory; if (sms.equals(config.getType())) { if (aliyun.equals(config.getSubtype())) { factory new AliyunSmsFactory(); } else if (tencent.equals(config.getSubtype())) { factory new TencentSmsFactory(); } else { throw new IllegalArgumentException(Unknown sms subtype); } } else if (email.equals(config.getType())) { factory new SmtpEmailFactory(); } else { factory new DbInboxFactory(); } MessageSender sender factory.createSender(config);看到没原来在简单工厂里那个长长的if-else现在被拆分到客户端代码里了。但这不是倒退而是把“选择权”交给了更上层的配置中心或路由模块。在Spring Boot中我们可以用ConditionalOnProperty自动装配Configuration public class SenderFactoryConfig { Bean ConditionalOnProperty(name message.channel.type, havingValue sms) public MessageSenderFactory smsFactory() { return new AliyunSmsFactory(); } Bean ConditionalOnProperty(name message.channel.type, havingValue email) public MessageSenderFactory emailFactory() { return new SmtpEmailFactory(); } }这样配置message.channel.typesmsSpring就自动注入AliyunSmsFactory客户端代码里连if-else都不用写了。4.4 抽象工厂的整合构建可扩展的消息产品族抽象工厂在这里的用武之地是当我们需要同时使用多个相关渠道时。比如一个用户注册成功事件需要“发短信通知用户 发邮件给管理员 记录站内信”。这三个动作必须原子性地成功或失败且它们的配置超时时间、重试策略应该统一管理。我们定义消息产品族// 消息产品族接口 public interface MessageProductFamily { MessageSender createSmsSender(); MessageSender createEmailSender(); MessageSender createInboxSender(); // 统一的渠道配置管理 ChannelConfig getConfig(); } // 具体产品族生产环境族 public class ProdMessageFamily implements MessageProductFamily { private final ChannelConfig config; public ProdMessageFamily(ChannelConfig config) { this.config config; } Override public MessageSender createSmsSender() { // 复用已有的工厂或直接new return new AliyunSmsSender(config.getParams()); } Override public MessageSender createEmailSender() { return new SmtpEmailSender(config.getParams()); } Override public MessageSender createInboxSender() { return new DbInboxSender(); } Override public ChannelConfig getConfig() { return config; } } // 具体产品族测试环境族全部Mock public class TestMessageFamily implements MessageProductFamily { Override public MessageSender createSmsSender() { return new MockMessageSender(sms); } Override public MessageSender createEmailSender() { return new MockMessageSender(email); } Override public MessageSender createInboxSender() { return new MockMessageSender(inbox); } Override public ChannelConfig getConfig() { return new ChannelConfig(); // 空配置 } }业务服务类现在依赖产品族Service public class UserRegistrationService { private final MessageProductFamily family; public UserRegistrationService(MessageProductFamily family) { this.family family; } public void onUserRegistered(User user) { // 三个发送器来自同一产品族保证配置一致 family.createSmsSender().send(new Message(欢迎注册, user.getPhone())); family.createEmailSender().send(new Message(新用户注册, adminexample.com)); family.createInboxSender().send(new Message(欢迎加入, user.getId())); } }启动时根据profile选择产品族SpringBootApplication public class MessageApplication { public static void main(String[] args) { // Spring Boot自动根据profile激活不同Bean SpringApplication.run(MessageApplication.class, args); } } Configuration Profile(prod) public class ProdConfig { Bean public MessageProductFamily prodFamily() { ChannelConfig config new ChannelConfig(); config.setParams(Map.of(accessKey, ..., smtp.host, ...)); return new ProdMessageFamily(config); } } Configuration Profile(test) public class TestConfig { Bean public MessageProductFamily testFamily() { return new TestMessageFamily(); } }这样mvn spring-boot:run -Dspring.profiles.activeprod启动所有消息都走真实渠道-Dspring.profiles.activetest启动全部Mock。切换成本为零。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 “工厂类找不到实现类”——类路径与加载时机之谜这是新手最常遇到的报错java.lang.ClassNotFoundException: com.example.sms.AliyunSmsSender。你以为是包名写错了查了十遍都没问题。真相往往是类加载器ClassLoader的层级问题。在Web应用如Tomcat中存在多个ClassLoaderBootstrap、Extension、Application、WebApp。如果你把message-sms模块打包成jar放在WEB-INF/lib下而工厂类在message-core里那么message-core的ClassLoader通常是WebApp ClassLoader能看到message-sms没问题。但如果你把message-sms放在tomcat/lib下作为共享库而message-core在WEB-INF/classes里这时message-core的ClassLoaderWebApp就看不到tomcat/lib里的类因为WebApp ClassLoader的parent是Common ClassLoader它只加载tomcat/lib但WebApp ClassLoader默认不委托parent加载这是Tomcat的特殊设计为了隔离web应用。解决方案有两个推荐把所有模块都放在WEB-INF/lib下统一由WebApp ClassLoader加载进阶在工厂类里显式指定ClassLoaderThread.currentThread().getContextClassLoader().loadClass(com.example.sms.AliyunSmsSender)这样就能跨层级加载。我在线上环境踩过这个坑。当时message-sms被误放到了tomcat/lib本地IDE运行正常因为IDE的ClassLoader是扁平的一上测试环境就报ClassNotFoundException。排查了两天最后用jstack看线程的ClassLoader才定位到。5.2 “单例工厂返回了多个实例”——线程安全的隐形杀手工厂方法模式里很多人会把工厂类设计成单例public class SmsSenderFactorySingleton { private static SmsSenderFactorySingleton instance; private SmsSenderFactorySingleton() {} public static SmsSenderFactorySingleton getInstance() { if (instance null) { instance new SmsSenderFactorySingleton(); } return instance; } }这看起来很安全但问题在于getInstance()方法不是线程安全的。在高并发下可能创建多个实例。更隐蔽的问题是工厂类内部的状态。比如你在工厂里缓存了连接池public class DbInboxFactory extends MessageSenderFactory { private final HikariDataSource dataSource; // 连接池 public DbInboxFactory() { this.dataSource new HikariDataSource(); // 每次new都创建新连接池 } Override protected MessageSender doCreateSender(ChannelConfig config) { return new DbInboxSender(dataSource); // 返回的sender都用同一个连接池 } }这里dataSource是实例变量但如果DbInboxFactory被多次new就会创建多个连接池吃光数据库连接数。正确做法是把连接池做成静态的或者用Spring管理其生命周期。我的经验是工厂类本身应该是无状态的stateless。所有需要共享的状态连接池、配置、缓存都应该通过构造函数注入或者由外部容器如Spring管理。工厂类只负责“new”和“组合”不负责“持有”。5.3 “抽象工厂接口改了所有实现类崩溃”——接口演进的残酷现实抽象工厂最大的痛点是接口的稳定性。一旦你在GUIFactory里新加一个createMenuBar()方法所有已有的WinFactory、MacFactory都必须实现它否则编译失败。在微服务架构中这可能导致一个服务升级强制所有下游服务跟着升级。我们的解决方案是“接口分层”// 基础接口稳定极少改动 public interface BaseGUIFactory { Button createButton(); TextBox createTextBox(); } // 扩展接口可选按需实现 public interface AdvancedGUIFactory extends BaseGUIFactory { MenuBar createMenuBar(); ToolBar createToolBar(); } // 具体工厂可以选择实现哪个接口 public class WinFactory implements BaseGUIFactory { /* 只实现基础方法 */ } public class ModernWinFactory implements AdvancedGUIFactory { /* 实现全部 */ }这样老工厂继续工作新工厂提供新能力。客户端代码用BaseGUIFactory声明需要高级功能时再instanceof判断。另一个技巧是“默认方法”Java 8public interface GUIFactory { Button createButton(); TextBox createTextBox(); // 默认提供空实现避免强制实现 default MenuBar createMenuBar() { throw new UnsupportedOperationException(MenuBar not supported); } default ToolBar createToolBar() { throw new UnsupportedOperationException(ToolBar not supported); } }这样旧工厂不用改新工厂可以选择性覆盖。这是语言特性给我们的缓冲带。5.4 “工厂模式让代码变复杂了”——何时该放弃工厂最后也是最重要的一个经验工厂模式不是银弹它是有成本的。当成本超过收益时果断放弃。我总结
返回列表