设计模式面试
设计模式面试
综述
【简单】什么是设计模式?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:5 min | 🏷 标签:设计模式 / 概念
💎 关键结论
设计模式是针对常见软件设计问题的可复用解决方案,是前人总结的最佳实践模板。它不是现成代码,而是一种设计思想,指导如何组织类和对象解决特定场景的问题。
⚡记忆卡片
- 口诀:模式不是代码,是解题思路
- 关键词:可复用 / 设计思想 / 特定场景 / 最佳实践
- 链路:常见问题 → 前人实践 → 提炼为模式 → 指导类与对象的组织方式
📖 核心知识
- 本质:软件工程中针对常见问题的可复用解决方案,是经过大量工程实践验证的设计经验结晶。
- 形态:不是可以直接拷贝的代码,而是解决某类问题的思路模板——描述在什么场景下、用什么样的类结构、如何协作。
- 价值:统一团队设计语言(说"用策略模式"即可对齐意图)、避免重复踩坑、为代码评审提供共同的评判标准。
- 边界:模式必须匹配真实场景才有效,脱离问题硬套模式会造成过度设计。
🔀 发散问题
- Q:设计模式和架构模式是一回事吗? → 不是。设计模式关注类与对象级别的协作结构(23 种 GoF 模式),架构模式关注系统级结构(MVC、微服务、分层),粒度不同但思想同源。
- Q:学设计模式该从哪里入手? → 先掌握 SOLID 原则再学模式——模式本质是原则的具体落地,见本文档「什么是面向对象五大原则(SOLID)?」。
【简单】有哪些经典的设计模式?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:5 min | 🏷 标签:设计模式 / GoF 23 模式
💎 关键结论
经典设计模式指 GoF《设计模式》一书总结的 23 种模式,按意图分三类:创建型管"对象如何创建"、结构型管"对象如何组合"、行为型管"对象如何协作"。
⚡记忆卡片
- 口诀:创建造对象、结构拼骨架、行为管协作,6+7+11=23
- 关键词:GoF 23 种 / 创建型 / 结构型 / 行为型
- 链路:对象创建 → 对象组合成结构 → 结构间协作通信
📖 核心知识
经典设计模式通常指 GoF(Gang of Four)《设计模式》一书中总结的 23 种模式,分为三大类:
创建型模式(6 种)——提供创建对象的机制,提升已有代码的灵活性和可复用性,回答"对象如何创建":
| 模式 | 一句话要点 |
|---|---|
| 单例模式 (Singleton) | 全局唯一实例 |
| 简单工厂模式 (Simple Factory) | 工厂类根据参数创建不同产品,集中封装创建逻辑(不属于 GoF 23 种) |
| 工厂方法模式 (Factory Method) | 子类决定创建哪个对象 |
| 抽象工厂模式 (Abstract Factory) | 创建相关或依赖的产品族 |
| 建造者模式 (Builder) | 分步构建复杂对象 |
| 原型模式 (Prototype) | 克隆生成对象 |
结构型模式(7 种)——介绍如何将对象和类组装成较大的结构,并保持结构的灵活和高效,回答"对象如何组合":
| 模式 | 一句话要点 |
|---|---|
| 代理模式 (Proxy) | 控制对象访问 |
| 装饰模式 (Decorator) | 动态添加职责 |
| 适配器模式 (Adapter) | 接口转换,兼容不匹配类 |
| 桥接模式 (Bridge) | 抽象与实现分离,独立变化 |
| 组合模式 (Composite) | 树形结构表示整体-部分 |
| 外观模式 (Facade) | 为子系统提供统一接口 |
| 享元模式 (Flyweight) | 共享细粒度对象,节省内存 |
行为型模式(11 种)——负责对象间的高效沟通和职责委派,回答"对象如何协作":
| 模式 | 一句话要点 |
|---|---|
| 模板方法模式 (Template Method) | 算法骨架,子类实现步骤 |
| 策略模式 (Strategy) | 算法可替换 |
| 观察者模式 (Observer) | 一对多通知依赖者 |
| 状态模式 (State) | 状态改变行为 |
| 职责链模式 (Chain of Responsibility) | 请求沿链传递,多处理器 |
| 命令模式 (Command) | 请求封装为对象,支持操作队列 |
| 迭代器模式 (Iterator) | 顺序访问聚合元素 |
| 中介者模式 (Mediator) | 封装对象间交互,降低耦合 |
| 访问者模式 (Visitor) | 不改变元素类前提下增加新操作 |
| 备忘录模式 (Memento) | 保存和恢复对象状态 |
| 解释器模式 (Interpreter) | 定义并解释文法 |
🔀 发散问题
- Q:为什么按创建型/结构型/行为型分类? → 对应设计三个基本问题:对象从哪来(创建)、对象怎么拼(结构)、对象怎么动(行为)。面试先报分类再报模式,结构感更强。
- Q:简单工厂为什么不在 23 种之内? → GoF 成书时未收录,它属于工厂方法的一种简化特例,但因实用被广泛讨论,见本文档「什么是简单工厂模式?」。
- Q:23 种都要背吗? → 高频约 10 种(单例、工厂、建造者、代理、装饰器、适配器、模板方法、策略、观察者、职责链),其余掌握意图与一句典型场景即可。
📚 延伸阅读:Design Patterns - Wikipedia
【简单】什么是面向对象五大原则(SOLID)?⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:8 min | 🏷 标签:设计模式 / 设计原则
💎 关键结论
SOLID 是面向对象设计的五大原则:单一职责、开闭、里氏替换、接口隔离、依赖倒置。本质是控制变化的影响面——SRP 是地基,DIP/ISP 是手段,LSP 是契约,最终收敛到 OCP:新功能靠扩展而非修改存量实现。
⚡记忆卡片
- 口诀:单一开闭里氏换,接口隔离靠倒转
- 关键词:SRP / OCP / LSP / ISP / DIP
- 链路:SRP 拆职责 → DIP/ISP 面向抽象 → LSP 保契约 → 达成 OCP 可扩展
📖 核心知识
| 原则 | 缩写 | 一句话内涵 |
|---|---|---|
| 单一职责 | SRP | 一个类只做一件事,只有一个变化的原因 |
| 开闭原则 | OCP | 对扩展开放,对修改关闭 |
| 里氏替换 | LSP | 子类可替换父类而不破坏程序正确性 |
| 接口隔离 | ISP | 接口最小化,客户端不依赖无用方法 |
| 依赖倒置 | DIP | 依赖抽象,不依赖具体实现 |
五者不是并列清单而是一条主线:SRP 是地基(先拆出清晰职责),DIP 和 ISP 是实现手段(面向小接口抽象、由容器注入),LSP 是契约保障(继承关系名实相符),最终收敛到 OCP——新增功能通过扩展而非修改存量代码实现。面试时讲出这条主线,远比逐条背定义有说服力。
🔬 扩展知识
详情
- 【L3】源码级实证:
- SRP:Spring 中
BeanDefinition承载 Bean 元数据、BeanFactory负责实例化、BeanPostProcessor负责扩展点,职责清晰拆分;反例是一个XXXService同时干校验、落库、发消息。 - DIP:Spring 的
@Autowired注入的永远是接口类型,由 IoC 容器决定实现;DefaultListableBeanFactory依赖BeanDefinition抽象而非具体类。 - OCP:Servlet 规范通过
Filter接口让你不改容器源码即可插入过滤逻辑;MyBatis 插件机制对Executor做拦截,同样是 OCP。 - LSP:JDK
Properties继承Hashtable却要求 String 键值,Collections.unmodifiableList()对修改操作抛异常,都是经典 LSP 破坏案例。 - ISP:JDK
Collection拆分为List/Set/Queue,Iterable独立出来供 for-each 使用,避免大接口。
- SRP:Spring 中
- 【L3】工程权衡:SOLID 有设计成本——过早追求 OCP 会引入大量接口/工厂/注入,3 人小项目的 CRUD 模块直接写反而更高效;正确姿势是先让代码工作,发现变化点后再向 SOLID 重构(配合 Rule of Three:第三次出现类似变化才抽象)。
- 【L3】“职责”的粒度如何界定?“职责”本质是变化的原因(axis of change):当且仅当两个需求由不同角色/不同节奏提出时,它们才是不同职责。实践中以“这个类会因为几个不同的原因被修改”来度量,超过两个就值得拆分;但把每个方法都拆成类同样违反 SRP 初衷。
- 【L3】里氏替换与多态的关系:多态只是语法机制,LSP 是行为契约——子类可以增强但不能收缩父类承诺(不能抛父类未声明的异常、不能收紧前置条件、不能放宽后置条件)。典型症状是调用方被迫写
if (obj instanceof 某子类)做特判,此时继承已名存实亡,应重新抽象或改用组合。 - 【L4】依赖倒置的代价:接口膨胀、跳转链变长、调试与 mock 成本上升。叶子级、稳定的基础设施类(如
LocalDate、工具类)可直接依赖;真正需要 DIP 的是易变的业务协作点(存储、支付、消息等外部依赖),这也是六边形架构“端口-适配器”的理论依据。 - 【L3】场景演练:团队里
OrderService承担创建订单、校验库存、计算优惠、调用支付、发送通知,共 30 余个方法、2000+ 行代码,每次需求只改一小块却要全量回归,还曾因改支付逻辑导致通知发送出 bug。这是 SRP 违反的典型案例——一个类有 5 个变化原因,修改影响面无法收敛。拆法:优惠计算抽为策略族(规则独立演化)、支付调用走 DIP 抽象接口(换渠道不动主流程)、通知改事件发布(下单只发OrderCreatedEvent)。取舍:拆分会增加类数量和协作成本,建议按“变化频率最高、事故最多”的模块优先拆,不必一次到位。
🔀 发散问题
- Q:SOLID 和迪米特法则、合成复用原则什么关系? → SOLID 是五大核心原则,LoD、CRP 是补充性原则,共同构成面向对象设计准则体系,见本文档「什么是迪米特法则?」「什么是合成复用原则?」。
- Q:违反 SOLID 的代码长什么样? → 上帝类、if-else 巨方法、空实现接口等都有识别信号,见本文档「如何识别代码中违反 SOLID 原则的案例?」。
【中等】如何识别代码中违反 SOLID 原则的案例?⭐⭐⭐⭐
🎯 目标等级:L2-L3 | ⏱ 建议用时:10 min | 🏷 标签:设计模式 / 设计原则 / 代码坏味道
💎 关键结论
识别能力比背诵定义更重要:单一职责看“一个类是否因多个原因被修改”,开闭看“新增是否要改老代码”,里氏看“子类是否收窄契约”,接口隔离看“是否被迫空实现”,依赖倒置看“高层是否直接 new 具体类”。
⚡记忆卡片
- 口诀:职责散、开闭改、里氏崩、接口空、倒置 new
- 关键词:变化原因 / 存量修改 / 契约收窄 / 空实现 / 硬编码 new
- 链路:识别信号 → 定位违反的原则 → 说明后果 → 给出重构方案
📖 核心知识
面试常给一段代码让你指出问题,识别能力比背诵定义更重要:
| 原则 | 典型违反案例 | 识别信号 | 修正方向 |
|---|---|---|---|
| 单一职责 | UserService 里既有登录校验又有发送邮件、导出报表 | 类名宽泛(XXXManager)、方法间毫无关联 | 按变化原因拆类 |
| 开闭原则 | 新增支付渠道要改 PayService 里的一大串 if-else | 每次新增都修改老代码 | 策略 + 工厂,新增实现不改存量 |
| 里氏替换 | 子类重写父类方法抛出新异常/收窄入参,替换后调用方崩溃 | 子类覆写后行为语义变了 | 重新抽象接口,或改用组合 |
| 接口隔离 | 实现类被迫空实现/抛 UnsupportedOperationException | 接口大而全(IXXXAll) | 拆分为角色化小接口 |
| 依赖倒置 | Service 里直接 new MysqlUserDao() | 高层代码出现具体类的 new | 面向接口编程 + DI 注入 |
答题技巧:先指出违反哪条原则,再说后果(耦合、扩展需改存量、难以测试),最后给重构方案(通常落到“面向接口 + 组合 + 依赖注入”)。
一句话总结:SOLID 的本质是控制变化影响面——违反它的代码,改起来总会牵一发动全身。
🔬 扩展知识
详情
- 【L3】识别手段工程化:资深工程师不靠肉眼,靠信号量化——类超过 500 行/方法超过 30 个触发 SRP 复查;SonarQube 等工具的重复度、圈复杂度、扇出(fan-out)指标能定位坏味道;git blame 看“一个文件是否被多个不相干的需求反复修改”是 SRP 违反的最硬证据。
- 【L3】原则冲突的取舍:严格 DIP(一切皆接口)会造成接口膨胀;严格 OCP(一切可插拔)会过度设计。真实决策标准是变化频率:只变过一次的分支,if-else 比策略模式成本更低;识别坏味道后要给出“值不值得重构”的判断,而不是机械套原则。
- 【L3】坏味道组合拳:实战中坏味道很少单独出现——违反 SRP 的上帝类往往伴随违反 DIP(内部大量
new具体实现)和长参数列表,回答时应串起来讲,体现系统性视角。 - 【L3】上帝类(God Class)与特性依恋(Feature Envy):上帝类的信号是类名宽泛(
XXXManager/XXXHelper)、方法间无内聚、行数巨大;特性依恋的信号是某方法大量读取另一个类的字段。拆解手法是 Extract Class / Move Method,把数据与操作它的方法挪到一起,恢复内聚。 - 【L3】散弹式修改(Shotgun Surgery)与发散式变化(Divergent Change)是 SRP 违反的一对镜像:前者是一个需求要改散落在多个类里的代码(如加个字段要改 DTO、Mapper、Service 三处),后者是一个类因多个不相干需求反复被改。前者做内聚归拢(Move Class/Inline),后者按变化原因拆分。
- 【L4】重构时机判断:依据是变化频率 × 修改成本——稳定的配置类即使大而全也不需要拆;正在快速演化的核心域才值得投入。同时遵循 Rule of Three:第一次出现就抽象往往是投机式设计,第三次重复出现时再重构,避免过早优化。
- 【L3】场景演练:评审中看到
public class ExcelReportService extends ReportService,父类模板方法依次调用loadData()、render()、export();子类重写export()时先调父类export()再追加水印逻辑,且抛出了父类未声明的IOException。这里叠加两个坏味道:一是 LSP 破坏——子类抛出新异常收窄契约,调用方按父类类型处理会漏捕获;二是模板方法被破坏性重写——子类调super.export()追加行为,说明它不想替换该步骤而是想增强,应改用组合(装饰器/钩子方法)而非继承。重构方向:父类拆出afterExport()钩子供子类追加;水印做成装饰器包装导出结果,与文件格式解耦。
🔀 发散问题
- Q:违反 SOLID 和坏味道是一回事吗? → 不完全。坏味道(重复代码、长方法、长参数列表)是表层症状,SOLID 违反是深层病因;识别坏味道后应追溯到违反了哪条原则,再决定重构手法。
- Q:怎么用模式修正 SOLID 违反? → 开闭违反常用策略+工厂,里氏违反常用组合替代继承,见本文档「实际业务场景中如何选型设计模式?」。
【简单】什么是迪米特法则?⭐
🎯 目标等级:L2 | ⏱ 建议用时:5 min | 🏷 标签:设计模式 / 设计原则
💎 关键结论
迪米特法则(Law of Demeter,LoD)又称最少知识原则:一个对象应当对其他对象有尽可能少的了解,即只与直接的朋友通信,不与陌生人说话,避免 a.getB().getC().doSomething() 式链式穿透。
⚡记忆卡片
- 口诀:只和熟人说话,不跟陌生人打交道
- 关键词:最少知识 / 直接朋友 / 降低耦合
- 链路:减少了解 → 只依赖直接协作者 → 内部结构变化不外泄
📖 核心知识
- 定义:一个对象应当对其他对象有尽可能少的了解,只与直接的朋友(成员变量、方法参数、方法内创建的对象)通信。
- 违反信号:链式调用穿透多层对象,如
order.getCustomer().getAddress().getCity()——中间任一结构变化都会波及调用方。 - 落地方式:由被穿透对象自己提供封装方法(如
order.getCity()),把内部结构对调用方隐藏。 - 本质:与封装思想一脉相承,目的是降低类间耦合,控制变化的传播范围。
🔀 发散问题
- Q:迪米特法则和单一职责的区别? → SRP 管“一个类该做几件事”,LoD 管“一个类该知道多少别人”,前者管内聚、后者管耦合,两者相辅相成。
- Q:链式调用(Builder)算违反 LoD 吗? → 不算。Builder 链返回的是同一建造者自身,没有穿透陌生对象;LoD 限制的是跨对象的知识获取。
【简单】什么是合成复用原则?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:5 min | 🏷 标签:设计模式 / 设计原则
💎 关键结论
合成复用原则(Composite Reuse Principle)核心是:优先使用对象组合(has-a)而不是类继承(is-a)来实现复用。继承破坏封装且编译期固化,组合可运行时动态替换、耦合更低;只有严格的 is-a 关系且子类不改变父类行为时才用继承。
⚡记忆卡片
- 口诀:组合优于继承,继承只认真 is-a
- 关键词:has-a / is-a / 破坏封装 / 运行时组合
- 链路:继承绑定实现细节 → 父类变更波及子类 → 改用组合持有引用 → 动态替换行为
📖 核心知识
- 为什么优先组合
- 继承破坏封装:子类依赖父类实现细节,父类变更可能影响子类。
- 组合更灵活:可在运行时动态组合对象、改变行为,耦合度低。
- 符合开闭原则:通过组合已有对象扩展新功能,无需修改原有代码。
- 何时用继承:只有当类之间确实存在严格的“is-a”关系,且子类不会改变父类行为时才考虑继承。
- 记忆点:组合优于继承,耦合低更灵活,继承只用于真正的 is-a。
🔀 发散问题
- Q:哪些模式体现了合成复用原则? → 策略、装饰器、桥接、组合模式都是“持有引用替代继承”的典型,见本文档「什么是策略模式?」。
- Q:组合优于继承是绝对的吗? → 不是。模板方法模式恰是靠继承固定骨架的,流程稳定且需共享状态时继承更合适,见本文档「什么是模板方法模式?」。
创建型模式
【简单】什么是单例模式?⭐⭐⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:设计模式 / 创建型模式
💎 关键结论
单例模式确保全局只有唯一实例:私有构造函数 + 静态 getInstance() 方法返回缓存对象。它本质是全局状态管理,真正的价值在提供全局协调点而非省创建开销;现代 Spring 项目通常交给容器管理而非手写。
⚡记忆卡片
- 口诀:私构造、静方法、缓存引用全局一份
- 关键词:唯一实例 / 私有构造函数 / getInstance / 全局协调点
- 链路:隐藏构造函数 → 静态方法创建并缓存 → 后续调用返回同一实例
📖 核心知识
单例模式 (Singleton) 确保全局只有唯一实例。
代码结构

单例(Singleton)类声明了一个名为 getInstance 的静态方法,返回其所属类的同一个实例。单例的构造函数必须对客户端(Client)代码隐藏,调用 getInstance 方法必须是获取单例对象的唯一方式。
所有单例的实现都包含以下两个相同的步骤:
- 将默认构造函数设为私有,防止其他对象使用单例类的
new运算符。 - 新建一个静态构建方法作为构造函数。该函数会“偷偷”调用私有构造函数来创建对象,并将其保存在一个静态成员变量中。此后所有对于该函数的调用都将返回这一缓存对象。
应用场景
- 配置管理类:系统配置信息全局共享,只需加载一次,避免重复读取配置。
- 日志记录器:避免重复创建文件句柄或网络连接,保证日志写入的一致性和性能。
- 线程池:全局管理线程资源,避免频繁创建销毁线程,提升系统效率。
- Spring Bean 默认作用域:IoC 容器中多数 Bean 设计为单例,减少对象创建开销,方便依赖注入。
- 缓存管理器:全局缓存数据,避免多个缓存实例造成数据不一致和内存浪费。
- 运行时环境类:如 Java 的
Runtime类,代表应用程序的运行环境,天然单例。
🔬 扩展知识
详情
- 【L3】单例的本质是“全局状态管理”:需要单例的真正原因通常不是“创建开销”,而是需要一个全局协调点(如
Runtime、SecurityManager)。如果只是为了省对象创建开销,单例是伪需求——对象创建在现代 JVM 上极其廉价。 - 【L3】滥用反模式(高频考点):
- 破坏可测试性:单例是隐式全局依赖,单测无法替换,只能用反射 hack 或 PowerMock;正确做法是“逻辑单例、实现注入”——类本身是普通类,由 Spring 容器保证只有一个实例。
- 隐藏依赖:方法签名里看不出依赖,调用
Config.getInstance()让依赖关系隐形,重构时难以追踪。 - 并发陷阱:单例常被误当作“线程安全”的同义词,实际上单例持有可变状态时仍需自行同步。
- 【L3】源码实证:
java.lang.Runtime(饿汉式静态字段)、java.awt.Desktop.getDesktop()、java.util.logging.LogManager.getLogManager();Spring 中BeanFactory本身即容器内单例,Bean 默认 singleton 作用域由DefaultSingletonBeanRegistry的三级缓存管理。 - 【L4】选型经验:现代 Spring 项目中几乎不手写 GoF 单例,而是把类交给容器管理(scope=singleton),既保留全局唯一又保留可注入、可 mock、可 AOP 的能力;手写单例只在无容器的工具库/基础设施代码中出现。
- 【L3】Spring 的单例和 GoF 单例是一回事吗?不是。GoF 单例靠私有构造器 + 静态方法保证 JVM 内唯一;Spring 单例是容器作用域内唯一(同一个 BeanDefinition 一个实例),通过
DefaultSingletonBeanRegistry.getSingleton()的 ConcurrentHashMap 缓存实现,构造器公开、可被容器反射创建、可被 AOP 代理。本质差异:GoF 单例是代码层硬约束,Spring 单例是容器层策略,后者更灵活可测。 - 【L3】单例和静态工具类(
StringUtils)怎么选?静态工具类适用于无状态纯函数,无生命周期、无法实现接口、不能被 mock;单例适用于有状态或需要被替换的场景(如缓存管理器、连接池)。判别标准:未来是否需要依赖注入/接口抽象/状态维护。 - 【L4】什么场景下主动反对使用单例?三个典型信号:单例里存可变业务状态(并发灾难)、单例被当作依赖注入的替代品(可测性崩塌)、多租户/多实例场景(如每个租户需要独立配置,静态单例根本无法表达)。此时应改为容器管理的普通 Bean 或显式传参。
- 【L3】场景演练:单测中需要替换配置中心的地址,但
AppConfig是手写饿汉式单例(private static final AppConfig INSTANCE = new AppConfig()),测试无法注入 mock,只能用反射把静态字段 hack 掉,测试代码里出现大量Field.setAccessible(true)。这是手写单例破坏可测试性的典型代价。重构分两步:先把AppConfig改成普通类、配置值通过构造器注入、由 Spring 容器保证单例语义;再在测试中直接 new 一个传入测试配置即可,无需任何反射。核心教训:把“唯一实例”的语义交给容器管理,业务代码保持可注入、可替换。
🔀 发散问题
- Q:单例有哪几种实现?如何保证线程安全? → 饿汉、懒汉、DCL、静态内部类、枚举五种,重点在 DCL 的 volatile 与枚举防反射,见本文档「单例模式有哪几种实现?如何保证线程安全?」。
- Q:单例和工厂模式矛盾吗? → 不矛盾,工厂方法可以返回缓存的同一实例(工厂方法不保证每次创建新对象),两者可结合。
【困难】单例模式有哪几种实现?如何保证线程安全?⭐⭐⭐⭐⭐
🎯 目标等级:L3-L4 | ⏱ 建议用时:15 min | 🏷 标签:设计模式 / 创建型模式 / 并发
💎 关键结论
五种主流实现:饿汉式、懒汉式、双重检查锁定(DCL)、静态内部类、枚举单例。线程安全核心是:类加载机制(饿汉/内部类)、volatile 禁重排 + 双重检查(DCL)、JVM 枚举特殊处理(枚举);推荐静态内部类或枚举。
⚡记忆卡片
- 口诀:饿汉早、懒汉悬、DCL 要 volatile、内部类躺赢、枚举最安全
- 关键词:类加载器锁 / volatile 禁重排 / 懒加载 / 防反射序列化
- 链路:创建时机(类加载 vs 首次访问)→ 并发控制 → 防反射/序列化攻击 → 选型
📖 核心知识
饿汉式(线程安全,类加载时初始化)
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}懒汉式(线程不安全,需改进)
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}双重检查锁定(线程安全,推荐)
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}静态内部类(线程安全,推荐)
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}枚举单例
public enum Singleton {
INSTANCE;
public void bizMethod() {
// 一些业务逻辑方法
}
}
// 使用
Singleton singleton = Singleton.INSTANCE;
singleton.bizMethod();各写法对比:饿汉式类加载即创建(利用类加载器机制天然线程安全),可能浪费但简单;静态内部类兼得懒加载与安全(Holder 类首次被访问才加载);枚举最简洁且《Effective Java》推荐,但不支持延迟加载且不能继承。
🔬 扩展知识
详情
- 【L3】为什么要防反射/序列化/克隆攻击?
- 反射:
setAccessible(true)后调用私有构造器可创建第二个实例,需在构造器中检测已创建则抛异常(枚举天然免疫,JVM 禁止反射创建枚举实例)。 - 序列化:反序列化会绕过构造器生成新对象,需实现
readResolve()返回已有实例(枚举天然免疫)。 - 类加载器:不同 ClassLoader 加载同名类会产生多个“单例”,需显式指定类加载器。
- 反射:
- 【L3】DCL 为什么必须加 volatile?
instance = new Singleton()分三步:分配内存 → 初始化对象 → 引用赋值。指令重排后其他线程可能拿到“已赋值但未初始化”的对象导致 NPE;volatile 通过禁止重排 + happens-before 保证可见性。 - 【L3】静态内部类方案的线程安全靠 JVM 类加载机制:内部类
Holder只有在getInstance()首次被调用时才会被加载,而类的初始化阶段由 JVM 加锁保证线程安全(《Java 虚拟机规范》规定<clinit>执行同步)。与饿汉式的差异仅在于加载时机:饿汉式在外部类加载时就创建实例,内部类方案实现了真正的懒加载。 - 【L4】枚举单例为什么是《Effective Java》首推写法?枚举在 JVM 层面禁止反射调用其构造器(
Constructor.newInstance()对枚举类型直接抛IllegalArgumentException),序列化机制对枚举也有特殊处理(按 name 查找已有实例),因此天然免疫反射和序列化攻击。代价是:不支持懒加载、不能继承、无法被 mock,不适合需要依赖注入的场景。 - 【L4】单例在分布式/多容器场景下还成立吗?不成立,单例的“唯一”仅在单个 JVM、单个 ClassLoader 内有效。分布式部署下每台机器各有一个实例,全局协调需求要用分布式锁/注册中心(如 ZooKeeper、Redis)实现;同一 JVM 多 ClassLoader(如 Tomcat 多应用)也会产生多实例,此时单例语义失效。
- 【L3】Spring 中的单例:IoC 容器单例 Bean 存于
DefaultSingletonBeanRegistry的三级缓存(singletonObjects 等),getSingleton()中双重检查 + 加锁保证并发安全;三级缓存的作用是解决循环依赖中的提前暴露;注意单例 Bean 持有可变成员状态时仍需自行保证线程安全。
🏭 实战场景
详情
- 双实例故障排查(典型生产案例口径):生产上出现“缓存穿透防护失效”——
CacheGuard是 DCL 单例,内部用ConcurrentHashMap缓存热点 key,但压测时发现不同请求拿到了不同实例,防护计数完全对不上。排查方向按概率排序:一是类加载器问题——该单例同时被业务 ClassLoader 和某个中间件/热部署 ClassLoader 加载,产生双实例(Tomcat 应用重部署是典型诱因);二是 DCL 实现漏了volatile,但这种情况通常表现为偶发 NPE 而非双实例;三是反序列化生成了新实例。修复上优先统一类加载器来源 + 补readResolve(),根治方案是把缓存管理交给 Spring 容器单例 Bean,彻底消除手写单例。
⚠️ 常见误区
详情
常见误区:
- ❌ “单例模式就是线程安全的” → 单例只保证实例唯一,实例持有的可变状态仍需自行同步;高并发下操作单例内部集合照样要加锁。
- ❌ “DCL 只要两次判空就行,volatile 可加可不加” → 没有 volatile,
new的指令重排会让其他线程拿到未初始化完成的对象,这是 DCL 正确性的前提而非优化。 - ❌ “枚举单例支持懒加载和继承扩展” → 枚举实例在类加载时即创建,不支持懒加载;枚举也不能继承、难以 mock,需要 DI 的场景应交给容器管理。
- ❌ “静态内部类和饿汉式完全一样” → 两者线程安全机制同源(类加载器锁),但加载时机不同:内部类方案是真正的懒加载,饿汉式在外部类加载时就创建。
🔀 发散问题
- Q:Spring 的单例和 GoF 单例是一回事吗? → 不是,一个是代码层硬约束(JVM 内唯一),一个是容器层策略(同一 BeanDefinition 一个实例),见本文档「什么是单例模式?」的扩展知识。
- Q:Spring 三级缓存和单例什么关系? → 三级缓存存储的是容器单例 Bean,其中第三级缓存的 ObjectFactory 用于解决循环依赖中的提前暴露,详见本文档「说说 JDK 和 Spring 源码中用到了哪些设计模式?」。
【中等】什么是简单工厂模式?⭐⭐
🎯 目标等级:L2-L3 | ⏱ 建议用时:8 min | 🏷 标签:设计模式 / 创建型模式
💎 关键结论
简单工厂模式通过一个工厂类根据不同参数返回不同产品实例,将创建逻辑集中封装,客户端无需了解具体实现。它是对象创建型模式,但不属于 23 种 GoF 设计模式;代价是新增产品要改工厂的分支逻辑。
⚡记忆卡片
- 口诀:一个工厂一把 switch,参数进、产品出
- 关键词:集中创建 / 参数路由 / 非 GoF / 违反开闭
- 链路:客户端传参 → 工厂分支判断 → 返回对应产品实例
📖 核心知识
简单工厂模式 (Simple Factory) 通过一个工厂类根据参数创建不同产品对象,将创建逻辑集中封装,客户端无需了解具体实现。
代码骨架

简单工厂模式通常是定义一个工厂类,这个类可以根据不同变量返回不同类的产品实例。
简单工厂模式是一种对象创建型模式。但是简单工厂模式不属于 23 种 GoF 设计模式之一。
结构要点:工厂类持有分支逻辑(if-else/switch/注册表),产品类实现统一接口;创建逻辑只存于工厂一处。
🔬 扩展知识
详情
- 【L3】优缺点权衡:优点是创建逻辑集中封装、客户端解耦;缺点是新增产品必须修改工厂的分支代码,违反开闭原则——这正是它与工厂方法模式的本质区别。JDK
DriverManager.getConnection()按 URL 返回不同数据库的 Connection,即类简单工厂。 - 【L3】与静态工厂方法的关系:《Effective Java》推荐的静态工厂方法(如
Integer.valueOf()、List.of())与简单工厂思想同源——用方法替代构造器创建对象,可命名、可缓存、可返回子类型;区别在于前者是类自身的方法,后者是独立工厂类。 - 【L3】量化选型:产品类型 ≤ 3 个且创建逻辑只有一行 new 时,简单工厂(甚至直接 new)更合适;产品多且需配置/缓存/校验时再升级工厂方法,见本文档「工厂模式和抽象工厂模式有什么区别?」。
🔀 发散问题
- Q:简单工厂和工厂方法怎么选? → 产品稳定且少用简单工厂,预期持续新增产品用工厂方法(新增只加类不改存量),见本文档「什么是工厂方法模式?」。
【中等】什么是工厂方法模式?⭐⭐⭐⭐
🎯 目标等级:L2-L3 | ⏱ 建议用时:10 min | 🏷 标签:设计模式 / 创建型模式
💎 关键结论
工厂方法模式在父类中提供创建对象的方法,让子类决定实例化的类型:一个产品对应一个工厂类,新增产品只新增类、不改存量,符合开闭原则。典型落地:Collection.iterator()、SLF4J LoggerFactory、Spring FactoryBean。
⚡记忆卡片
- 口诀:一产品一工厂,子类定类型
- 关键词:多态创建 / 开闭原则 / FactoryBean / SPI
- 链路:定义工厂方法 → 子类重写决定产品 → 客户端面向抽象使用
📖 核心知识
工厂方法模式 (Factory Method) 由子类决定创建哪个对象。
- 工厂模式中,增加一种产品类,就要增加一个工厂类:因为每个工厂类只能创建一种产品的实例。
- 工厂模式遵循“开放-封闭原则”:新增一种产品并不需要修改原有类,仅仅是扩展。
代码结构

- 产品(Product)将会对接口进行声明。对于所有由创建者及其子类构建的对象,这些接口都是通用的。
- 具体产品(Concrete Products)是产品接口的不同实现。
- 创建者(Creator)类声明返回产品对象的工厂方法。该方法的返回对象类型必须与产品接口相匹配。
- 你可以将工厂方法声明为抽象方法,强制要求每个子类以不同方式实现该方法。或者,你也可以在基础工厂方法中返回默认产品类型。
- 注意,尽管它的名字是创建者,但它最主要的职责并不是创建产品。一般来说,创建者类包含一些与产品相关的核心业务逻辑。工厂方法将这些逻辑处理从具体产品类中分离出来。打个比方,大型软件开发公司拥有程序员培训部门。但是,这些公司的主要工作还是编写代码,而非生产程序员。
- 具体创建者(Concrete Creators)将会重写基础工厂方法,使其返回不同类型的产品。
注意,并不一定每次调用工厂方法都会创建新的实例。工厂方法也可以返回缓存、对象池或其他来源的已有对象。
应用场景
- 日志框架:SLF4J 通过
LoggerFactory获取日志记录器,具体实现(Logback、Log4j)由工厂方法动态决定,客户端无需感知底层。 - 数据库连接池:如 HikariCP、Druid 通过
DataSource工厂创建和管理数据库连接,隐藏连接创建、池化、销毁等复杂逻辑。 - Java 集合框架:
Collection.iterator()是一个工厂方法,返回具体迭代器(如ArrayList.Itr),客户端统一遍历集合,不关心内部结构。 - Spring IoC 容器:
BeanFactory和ApplicationContext根据配置(XML、注解)创建和管理 bean 实例,是工厂模式的典型实现。 - JDBC API:
DriverManager.getConnection()根据 URL 返回不同数据库的 Connection 对象,类似简单工厂,屏蔽驱动差异。
🔬 扩展知识
详情
- 【L3】SPI 与工厂的关系:JDK
ServiceLoader、Dubbo 的扩展点加载本质上都是“工厂方法 + 配置文件”,把“创建哪个实现”的决策从代码硬编码转移到配置,符合开闭原则。 - 【L3】Spring 中的体现:
FactoryBean是工厂方法模式的典型落地——getObject()由子类决定返回什么对象(如SqlSessionFactoryBean生产 SqlSessionFactory)。 - 【L3】滥用反模式:为每个只有一个实现的产品都建一个工厂类,造成类数翻倍却没有任何扩展收益;更隐蔽的误用是工厂方法里塞入业务逻辑(如创建后顺手做初始化/注册),使工厂演变为上帝类。
- 【L4】量化视角:创建逻辑只有
new XXX()一行且产品类型 ≤ 3 个时,简单工厂(甚至直接 new)更合适;产品超过 5 个、创建过程有配置/缓存/校验逻辑时,工厂方法的扩展收益才开始体现。 - 【L3】工厂方法和抽象工厂怎么选?工厂方法面向单一产品,通过子类继承决定创建谁;抽象工厂面向产品族,通过组合保证一组产品配套。升级信号:出现“产品 A 和产品 B 必须配套”的约束(如 MySQL 的 Connection 不能配 Oracle 的 Statement),此时单产品工厂会产生非法组合,需要抽象工厂保证族内一致性。
- 【L3】
Collection.iterator()为什么算工厂方法模式?工厂方法的本质是把对象创建委派给子类/实现类决定:ArrayList.iterator()返回内部类Itr,LinkedList返回自己的迭代器实现,客户端面向Iterator接口编程而不感知具体实现。注意它与简单工厂的差别:工厂方法是多态创建(每个集合自己决定),简单工厂是集中式创建(一个工厂类 switch)。 - 【L4】Spring 中
FactoryBean和普通 Bean 有什么区别?普通 Bean 由容器通过反射直接实例化;FactoryBean是“生产 Bean 的 Bean”,getObject()的返回值才是目标对象(getBean("x")返回产品,getBean("&x")返回工厂自身)。必须用的场景:目标对象的创建过程无法用构造器表达,如 MyBatis 的SqlSessionFactoryBean生产SqlSessionFactory。 - 【L3】场景演练:导出模块支持 Excel/CSV 格式,代码里
ExportService用 if-else 判断 format 参数后直接new ExcelExporter()/new CsvExporter(),且创建后还要各自设置不同的默认配置、注册到导出记录表。新增 PDF 格式时,除了新增类还要改ExportService。这里有两个层次的问题:创建逻辑(new + 默认配置 + 注册)与路由逻辑(if-else)混在业务类里,违反 OCP。拆法:先把“创建 + 初始化”收敛到工厂(每种格式一个工厂方法,或简单工厂 + 注册表),再把路由改为Map<String, ExporterFactory>查表;同时评估规模——若格式长期只有三种且不再扩展,简单工厂已够用,不必升级到每个格式一个工厂类的工厂方法体系,避免过度设计。
🔀 发散问题
- Q:工厂方法和抽象工厂的区别? → 一个工厂生产一个产品(继承)vs 一个工厂生产一簇配套产品(组合),见本文档「工厂模式和抽象工厂模式有什么区别?」。
- Q:工厂方法和建造者怎么选? → 工厂关注“创建哪个类型”,建造者关注“分步组装复杂对象”,见本文档「什么是建造者模式?」。
【中等】什么是抽象工厂模式?⭐⭐
🎯 目标等级:L2-L3 | ⏱ 建议用时:8 min | 🏷 标签:设计模式 / 创建型模式
💎 关键结论
抽象工厂模式用于创建相关或依赖的产品族:一个工厂接口声明一组创建方法,每个具体工厂只生产同一变体的配套产品,保证族内一致性;切换产品族只需更换工厂。
⚡记忆卡片
- 口诀:一厂一族,配套不串味
- 关键词:产品族 / 配套约束 / 更换工厂切换族
- 链路:定义产品族接口 → 具体工厂绑定变体 → 客户端面向抽象工厂使用
📖 核心知识
抽象工厂模式 (Abstract Factory) 用于创建相关或依赖的产品族。
代码骨架

- 抽象产品(Abstract Product)为构成系列产品的一组不同但相关的产品声明接口。
- 具体产品(Concrete Product)是抽象产品的多种不同类型实现。所有变体(维多利亚/现代)都必须实现相应的抽象产品(椅子/沙发)。
- 抽象工厂(Abstract Factory)接口声明了一组创建各种抽象产品的方法。
- 具体工厂(Concrete Factory)实现抽象工厂的构建方法。每个具体工厂都对应特定产品变体,且仅创建此种产品变体。
- 尽管具体工厂会对具体产品进行初始化,其构建方法签名必须返回相应的抽象产品。这样,使用工厂类的客户端代码就不会与工厂创建的特定产品变体耦合。客户端(Client)只需通过抽象接口调用工厂和产品对象,就能与任何具体工厂/产品变体交互。
应用场景
- 多数据库支持(DAO 层):为 MySQL、Oracle 等数据库分别实现用户、订单等 DAO 对象,切换数据库时只需更换工厂,保证各操作类兼容。
- 框架集成:Spring 的
BeanFactory可视为抽象工厂变体,按环境(开发/生产)创建配置相关的 Bean 组。
🔬 扩展知识
详情
- 【L3】新增一个产品维度(族内加新品类)是所有工厂接口都加方法,存量具体工厂都要改——抽象工厂对“新增维度”不友好,对“新增变体”友好;选型时先确认变化的方向是哪个。
- 【L4】Spring 中的近似体现:
FactoryBean族 + Profile 按环境切换一组配套 Bean,可看作抽象工厂思想的容器化落地——用配置/条件装配替代了手写工厂类。 - 【L3】族内配套约束的真实性:只有当“错配会出错”(如 MySQL Connection 配 Oracle Statement 编译/运行不过)时,抽象工厂的约束才有价值;若产品间无强耦合,用多个独立工厂更灵活。
🔀 发散问题
- Q:抽象工厂和工厂方法的区别? → 一个生产一簇配套产品(组合)vs 一个生产单一产品(继承),见本文档「工厂模式和抽象工厂模式有什么区别?」。
【中等】工厂模式和抽象工厂模式有什么区别?⭐⭐⭐
🎯 目标等级:L2-L3 | ⏱ 建议用时:10 min | 🏷 标签:设计模式 / 创建型模式
💎 关键结论
工厂方法:一个工厂生产一个产品,通过继承创建;抽象工厂:一个工厂生产一簇产品,通过组合创建,强调产品之间的配套约束。升级信号是出现“多个产品必须配套”的约束。
⚡记忆卡片
- 口诀:工厂方法单品靠继承,抽象工厂一族靠组合
- 关键词:单产品 vs 产品族 / 继承 vs 组合 / 配套约束
- 链路:单产品创建 → 出现配套约束 → 升级为产品族工厂
📖 核心知识
| 维度 | 工厂方法 | 抽象工厂 |
|---|---|---|
| 产品范围 | 一个工厂生产一个产品 | 一个工厂生产一簇产品 |
| 创建方式 | 通过继承(子类重写工厂方法) | 通过组合(客户端持有工厂实例) |
| 关注点 | 把创建决策延后到子类 | 保证产品族内配套一致 |
| 扩展方向 | 新增产品只加子类,符合开闭 | 新增变体容易,新增产品维度困难 |
两者不互斥:抽象工厂内部的每个创建方法,本身就可以用工厂方法实现。
🔬 扩展知识
详情
- 【L3】升级信号:出现“产品 A 和产品 B 必须配套”的约束(如 MySQL 的 Connection 不能配 Oracle 的 Statement),此时单产品工厂会产生非法组合,需要抽象工厂保证族内一致性;反之产品间无耦合时不要硬上抽象工厂。
- 【L4】演化路径:简单工厂(一个工厂 switch)→ 工厂方法(一产品一工厂)→ 抽象工厂(一族一工厂),本质是创建逻辑随“产品维度 × 变体维度”增长而逐步抽象,与消灭 if-else 的思路同源,见本文档「如何用设计模式消除大量 if-else 硬编码?」。
🔀 发散问题
- Q:什么信号说明该升级到抽象工厂? → 出现跨产品的配套约束、单产品工厂可能产生非法组合时,见本文档「什么是抽象工厂模式?」的扩展知识。
【中等】什么是建造者模式?⭐⭐⭐
🎯 目标等级:L2-L3 | ⏱ 建议用时:10 min | 🏷 标签:设计模式 / 创建型模式
💎 关键结论
建造者模式用于分步构建复杂对象:Builder 定义构造步骤,Director 定义调用顺序,同一构建过程可创建不同表示。与工厂的区别:工厂侧重“创建哪个类型”,建造者侧重“产品内部结构的分步组装”,典型落地是 Lombok @Builder 和 StringBuilder。
⚡记忆卡片
- 口诀:分步建、链式调、Director 管顺序
- 关键词:分步构建 / 链式调用 / 构造器爆炸
- 链路:多可选参数 → 构造器爆炸 → 分步 setter → 最后 build 出产品
📖 核心知识
建造者模式 (Builder) 用于分步构建复杂对象。
代码骨架

- 建造者(Builder)接口声明在所有类型建造者中通用的产品构造步骤。
- 具体建造者(Concrete Builders)提供构造过程的不同实现。具体建造者也可以构造不遵循通用接口的产品。
- 产品(Products)是最终生成的对象。由不同建造者构造的产品无需属于同一类层次结构或接口。
- 主管(Director)类定义调用构造步骤的顺序,这样你就可以创建和复用特定的产品配置。
- 客户端(Client)必须将某个建造者对象与主管类关联。一般情况下,你只需通过主管类构造函数的参数进行一次性关联即可。此后主管类就能使用建造者对象完成后续所有的构造任务。但在客户端将建造者对象传递给主管类制造方法时还有另一种方式。在这种情况下,你在使用主管类生产产品时每次都可以使用不同的建造者。
应用场景
- Lombok @Builder
- StringBuilder
- 快餐套餐组合
- HttpClient 配置构建
java.lang.StringBuilder#append()(非同步)java.lang.StringBuffer#append()(同步)java.nio.ByteBuffer#put()(还有CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer和DoubleBuffer)java.lang.Appendable的所有实现
与工厂模式区别
- 工厂:一次性创建,侧重产品类型
- 建造者:分步创建,侧重产品内部结构
🔬 扩展知识
详情
- 【L3】建造者解决的原始痛点是构造器爆炸:多可选参数时构造函数重载呈指数增长,telescoping constructor 可读性极差;Builder 用分步 setter + 最后
build()取代,且可在build()中集中做参数合法性校验。 - 【L3】Director 在现代代码中常被省略:链式调用(fluent API)让客户端自己决定步骤顺序,Lombok
@Builder即是无 Director 的简化形态;Director 的价值在于固化“合法的构建顺序”,如构建 HTTP 请求必须先设 host 再设 path。 - 【L3】不可变对象的好搭档:Builder 收集参数后在
build()里一次性构造不可变产品(final 字段),避免 setter 暴露导致对象半初始化状态泄漏。 - 【L4】量化视角:构造参数 ≥ 5 个且可选组合多时才值得引入 Builder;参数少于 4 个直接构造函数 + 命名参数更简单,见本文档「实际业务场景中如何选型设计模式?」。
🔀 发散问题
- Q:StringBuilder 和 StringBuffer 都算建造者吗? → 都算,append 链式累积、toString 产出产品;区别仅在同步与否。见本文档「什么是简单工厂模式?」对比不同创建模式的分工。
- Q:建造者和装饰器都“层层包装”,区别在哪? → 建造者最终产出一个新产品对象,装饰器增强原对象功能且不改变类型,见本文档「什么是装饰器模式?」。
【中等】什么是原型模式?⭐⭐
🎯 目标等级:L2-L3 | ⏱ 建议用时:8 min | 🏷 标签:设计模式 / 创建型模式
💎 关键结论
原型模式用于克隆生成对象:原型类需实现 Cloneable 接口并把 Object.clone() 提升为 public。Java 中 Cloneable 接口就是立即可用的原型模式;识别方法是存在 clone/copy 类方法。
⚡记忆卡片
- 口诀:实现 Cloneable、重写 clone,克隆代替 new
- 关键词:Cloneable / Object.clone / 浅拷贝 vs 深拷贝
- 链路:创建成本高或需保留现场 → 克隆现有对象 → 修改副本
📖 核心知识
原型模式 (Prototype) 用于克隆生成对象。
原型模式主要用于对象的复制,它的核心是类图中的原型类 Prototype。Prototype 类需要具备以下两个条件:
- 实现 Cloneable 接口。在 Java 语言有一个 Cloneable 接口,它的作用只有一个,就是在运行时通知虚拟机可以安全地在实现了此接口的类上使用 clone 方法。在 Java 虚拟机中,只有实现了这个接口的类才可以被拷贝,否则在运行时会抛出 CloneNotSupportedException 异常。
- 重写 Object 类中的 clone 方法。Java 中,所有类的父类都是 Object 类,Object 类中有一个 clone 方法,作用是返回对象的一个拷贝,但是其作用域 protected 类型的,一般的类无法调用,因此,Prototype 类需要将 clone 方法的作用域修改为 public 类型。
代码骨架

- 原型(Prototype)接口将对克隆方法进行声明。在绝大多数情况下,其中只会有一个名为
clone克隆的方法。 - 具体原型(Concrete Prototype)类将实现克隆方法。除了将原始对象的数据复制到克隆体中之外,该方法有时还需处理克隆过程中的极端情况,例如克隆关联对象和梳理递归依赖等等。
- 客户端(Client)可以复制实现了原型接口的任何对象。
应用场景
使用示例: Java 的 Cloneable(可克隆)接口就是立即可用的原型模式。
任何类都可通过实现该接口来实现可被克隆的性质。
java.lang.Object#clone()(类必须实现java.lang.Cloneable接口)
识别方法:原型可以简单地通过 clone 或 copy 等方法来识别。
🔬 扩展知识
详情
- 【L3】浅拷贝 vs 深拷贝:
Object.clone()默认浅拷贝——只复制字段值,引用类型字段仍指向同一对象;深拷贝需递归克隆关联对象(或序列化往返实现)。忽略这点是原型模式最高频的 bug 来源。 - 【L3】
Cloneable的怪异性:它是标记接口(无任何方法),clone 的实际实现在Object中(native 方法),未实现Cloneable时 clone 抛CloneNotSupportedException;《Effective Java》建议谨慎使用,替代方案是拷贝构造函数或序列化。 - 【L4】何时克隆比 new 划算:对象初始化成本高(如从数据库/远程加载大配置)且需多个相似副本时;另一个典型是“保存现场”——先克隆再修改,失败可回退原对象,与备忘录思想相通,见本文档「什么是备忘录模式?」。
🔀 发散问题
- Q:原型模式和建造者模式的区别? → 原型复制已有对象,建造者分步从零构建,见本文档「什么是建造者模式?」。
结构型模式
【中等】什么是适配器模式?⭐⭐⭐
🎯 目标等级:L2-L3 | ⏱ 建议用时:10 min | 🏷 标签:设计模式 / 结构型模式
💎 关键结论
适配器模式用于接口转换,兼容不匹配的类:适配器实现客户端接口并封装服务对象,把调用转换为服务对象能理解的形式。转换过程藏于幕后,被封装的对象感知不到适配器的存在;典型落地是 InputStreamReader、Arrays.asList()。
⚡记忆卡片
- 口诀:接口不匹配,包一层转翻译
- 关键词:接口转换 / 封装被适配者 / 兼容存量
- 链路:客户端接口 ≠ 服务接口 → 适配器实现前者封装后者 → 调用转换后透传
📖 核心知识
适配器模式 (Adapter) 用于接口转换,兼容不匹配类。
适配器模式通过封装对象将复杂的转换过程隐藏于幕后。被封装的对象甚至察觉不到适配器的存在。
代码骨架
适配器实现了其中一个对象的接口,并对另一个对象进行封装。

- 客户端(Client)是包含当前程序业务逻辑的类。
- 客户端接口(Client Interface)描述了其他类与客户端代码合作时必须遵循的协议。
- 服务(Service)中有一些功能类(通常来自第三方或遗留系统)。客户端与其接口不兼容,因此无法直接调用其功能。
- 适配器(Adapter)是一个可以同时与客户端和服务交互的类:它在实现客户端接口的同时封装了服务对象。适配器接受客户端通过适配器接口发起的调用,并将其转换为适用于被封装服务对象的调用。
- 客户端代码只需通过接口与适配器交互即可,无需与具体的适配器类耦合。因此,你可以向程序中添加新类型的适配器而无需修改已有代码。这在服务类的接口被更改或替换时很有用:你无需修改客户端代码就可以创建新的适配器类。
应用场景
java.util.Arrays#asList()java.util.Collections#list()java.util.Collections#enumeration()java.io.InputStreamReader(InputStream)(返回Reader对象)java.io.OutputStreamWriter(OutputStream)(返回Writer对象)
🔬 扩展知识
详情
- 【L3】两种形式:对象适配器(组合,持有被适配者引用,Java 主流做法)与类适配器(继承,同时继承目标接口与被适配者,受 Java 单继承限制较少用)。
- 【L3】适配器 vs 装饰器 vs 代理:三者结构都是包装,区别看意图——适配器改变接口(让不兼容的能合作),装饰器不改接口只增强功能,代理不改接口只控制访问,见本文档「装饰器、适配器、代理、桥接这四种设计模式有什么区别?」。
- 【L4】防腐层(ACL):DDD 中对接外部/遗留系统的适配层本质是适配器模式的架构化应用——在边界处把外部模型翻译成本域模型,隔离外部模型变化对核心域的冲击。
- 【L3】场景演练:接入第三方 SDK 时直接让业务代码调 SDK API,SDK 升级改接口后所有调用点全部爆炸;正确做法是定义本域接口 + 一个适配器实现(封装 SDK 调用),SDK 升级只改适配器一处。适配器要放在依赖方向的外围,禁止核心域反向依赖 SDK 类型。
🔀 发散问题
- Q:适配器和桥接的区别? → 适配器是事后补救(已有不兼容接口),桥接是事前设计(预防抽象与实现耦合),见本文档「什么是桥接模式?」。
【简单】什么是桥接模式?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:5 min | 🏷 标签:设计模式 / 结构型模式
💎 关键结论
桥接模式将抽象与实现分离,使二者可以独立变化:抽象部分持有实现部分接口的引用,而非用继承绑定。典型识别特征是“控制实体与多个平台之间的明确区分”,Java 中最经典的代表是 slf4j 的桥接 jar 包。
⚡记忆卡片
- 口诀:抽象拿引用,实现单独变
- 关键词:抽象与实现分离 / 引用替代继承 / 多维度变化
- 链路:抽象部分 → 持有实现接口引用 → 实现部分独立演化
📖 核心知识
桥接模式 (Bridge) 用于抽象与实现分离,独立变化。
代码骨架

- 抽象部分(Abstraction)提供高层控制逻辑,依赖于完成底层实际工作的实现对象。
- 实现部分(Implementation)为所有具体实现声明通用接口。抽象部分仅能通过在这里声明的方法与实现对象交互。
- 抽象部分可以列出和实现部分一样的方法,但是抽象部分通常声明一些复杂行为,这些行为依赖于多种由实现部分声明的原语操作。
- 具体实现(Concrete Implementations)中包括特定于平台的代码。
- 精确抽象(Refined Abstraction)提供控制逻辑的变体。与其父类一样,它们通过通用实现接口与不同的实现进行交互。
- 通常情况下,客户端(Client)仅关心如何与抽象部分合作。但是,客户端需要将抽象对象与一个实现对象连接起来。
应用场景
桥接模式在处理跨平台应用、支持多种类型的数据库服务器或与多个特定种类(例如云平台和社交网络等)的 API 供应商协作时会特别有用。
桥接可以通过一些控制实体及其所依赖的多个不同平台之间的明确区别来进行识别。
Java 中桥接模式应用最经典的代表无疑是日志组件 slf4j 的桥接 jar 包。
假如,你正在开发应用程序所调用的组件当中已经使用了 common-logging,这时你需要 jcl-over-slf4j.jar 把日志信息输出重定向到 slf4j-api,slf4j-api 再去调用 slf4j 实际依赖的日志组件。这个过程称为桥接。下图是官方的 slf4j 桥接策略图:

🔀 发散问题
- Q:桥接和适配器怎么区分? → 桥接是设计期预防(把多维度拆开各自演化),适配器是事后兼容(让已存在的不兼容接口协作),见本文档「什么是适配器模式?」。
- Q:JDBC 算桥接吗? → 算典型体现:DriverManager/Connection 抽象与 MySQL/Oracle 驱动实现分离,换数据库只换驱动不改业务代码。
【简单】什么是组合模式?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:5 min | 🏷 标签:设计模式 / 结构型模式
💎 关键结论
组合模式用树形结构表示整体-部分关系:组件接口统一描述叶节点与容器,客户端无差别对待单个对象与组合对象,容器收到请求后递归委派给子项目。
⚡记忆卡片
- 口诀:叶子干活、容器分发、客户端一视同仁
- 关键词:树形结构 / 统一组件接口 / 递归委派
- 链路:定义组件接口 → 叶节点实现基础操作 → 容器递归委派子节点
📖 核心知识
组合模式 (Composite) 用于树形结构表示整体-部分。
代码骨架

- 组件(Component)接口描述了树中简单项目和复杂项目所共有的操作。
- 叶节点(Leaf)是树的基本结构,它不包含子项目。一般情况下,叶节点最终会完成大部分的实际工作,因为它们无法将工作指派给其他部分。
- 容器(Container)——又名“组合(Composite)”——是包含叶节点或其他容器等子项目的单位。容器不知道其子项目所属的具体类,它只通过通用的组件接口与其子项目交互。容器接收到请求后会将工作分配给自己的子项目,处理中间结果,然后将最终结果返回给客户端。
- 客户端(Client)通过组件接口与所有项目交互。因此,客户端能以相同方式与树状结构中的简单或复杂项目交互。
应用场景
- 文件系统:目录(文件夹)包含文件或子目录,统一提供获取大小、删除等操作,客户端无差别对待单个文件或整个目录。
- 缓存组合:如 Spring Cache 的
CompositeCacheManager,组合多个缓存管理器,统一处理缓存操作。
🔀 发散问题
- Q:组合模式和访问者模式怎么配合? → 访问者常在组合树上遍历操作,不改变元素类就能新增操作,见本文档「什么是访问者模式?」。
- Q:组织权限树、菜单树适合组合模式吗? → 适合,都是典型的整体-部分递归结构,统一接口后增删节点无需改调用方。
【中等】什么是装饰器模式?⭐⭐⭐
🎯 目标等级:L2-L3 | ⏱ 建议用时:10 min | 🏷 标签:设计模式 / 结构型模式
💎 关键结论
装饰器模式用于动态添加职责:装饰类与被装饰类实现同一接口,装饰类持有被装饰对象引用并在委派前后叠加行为,可多层叠加。Java IO 流与 Collections.synchronizedXXX() 是最经典落地。
⚡记忆卡片
- 口诀:同接口、持引用、包一层加一层
- 关键词:动态增强 / 递归组合 / IO 流 / 能力正交
- 链路:基础部件 → 装饰器逐层包装 → 客户端面向统一接口使用
📖 核心知识
装饰模式 (Decorator) 用于动态添加职责。
代码骨架

- 部件(Component)声明封装器和被封装对象的公用接口。
- 具体部件(Concrete Component)类是被封装对象所属的类。它定义了基础行为,但装饰类可以改变这些行为。
- 基础装饰(Base Decorator)类拥有一个指向被封装对象的引用成员变量。该变量的类型应当被声明为通用部件接口,这样它就可以引用具体的部件和装饰。装饰基类会将所有操作委派给被封装的对象。
- 具体装饰类(Concrete Decorators)定义了可动态添加到部件的额外行为。具体装饰类会重写装饰基类的方法,并在调用父类方法之前或之后进行额外的行为。
- 客户端(Client)可以使用多层装饰来封装部件,只要它能使用通用接口与所有对象互动即可。
应用场景
java.io.InputStream、OutputStream、Reader和Writer的所有代码都有以自身类型的对象作为参数的构造函数。java.util.Collections;checkedXXX()、synchronizedXXX()和unmodifiableXXX()方法。javax.servlet.http.HttpServletRequestWrapper和HttpServletResponseWrapper
🔬 扩展知识
详情
- 【L3】源码实证(精确到类):
BufferedInputStream继承自FilterInputStream(基础装饰器),构造器强制传入另一个InputStream,read()先委派给in再叠加缓冲;Collections.unmodifiableList()返回UnmodifiableList包装类,所有写操作抛异常;SpringTransactionAwareCacheDecorator给缓存装饰事务感知能力,MyBatis 的Cache装饰器链(SynchronizedCache包LruCache包PerpetualCache)也是经典嵌套。 - 【L3】选型权衡(装饰器 vs 代理):两者结构几乎同构,差异在意图与生命周期:装饰器增强功能、由客户端显式传入被包装对象、可叠加;代理控制访问、代理自己创建/管理目标对象、客户端无感知。结构一样时看“谁创建目标、目的是增强还是控制”。
- 【L3】滥用反模式:装饰层次过深导致
new A(new B(new C(stream)))可读性差、调试栈冗长;每层都持有引用增加内存开销。工程上用建造者/工厂封装组装过程(如 IO 工具类统一构建),把嵌套藏在边界内。 - 【L4】量化视角:需要自由组合的能力超过 3 种时才值得引入;若组合固定只有一种,直接继承/子类化更简单(但要警惕子类组合爆炸:n 个能力用继承需要 2ⁿ 个子类)。
- 【L3】装饰器和继承都能“增强功能”,为什么优先选装饰器?继承是编译期静态增强,能力组合靠类爆炸(缓冲 + 压缩 + 加密需要三个组合就需 7 个子类);装饰器是运行期动态组合,能力正交、自由叠加、可随时拆除。代价是多层包装增加调用链长度和调试复杂度,因此能力维度 ≥ 2 且需自由组合时才值得。
- 【L3】Java IO 流为什么选装饰器而不是每个组合一个类?IO 能力维度(缓冲/压缩/加密/对象化)与数据源维度(文件/网络/字节数组)是笛卡尔积关系,硬编码组合会造成类爆炸;装饰器把能力正交化:数据源提供基础流,能力装饰器自由叠加(
new ObjectInputStream(new BufferedInputStream(new FileInputStream(...)))),新增能力只需加一个装饰器类。 - 【L4】Servlet 中用
HttpServletRequestWrapper装饰请求的典型场景是请求体重复读取:包装类在构造时把 body 缓存为字节数组,重写getInputStream()每次返回新的ByteArrayInputStream,解决日志过滤器读掉 body 后 Controller 读不到的问题;Spring 提供的 ContentCachingRequestWrapper 同理。 - 【L3】场景演练:报表导出接口需要按租户动态叠加:压缩、加密、水印。现有代码用继承实现了 6 个子类,新需求“部分租户只要压缩+水印”上线后发现没有对应子类,只能临时又加了一个类,子类已膨胀到 9 个。这是典型的“能力组合爆炸”:压缩/加密/水印三个能力用继承表达需要穷举 2³=8 种组合。重构方案:定义
ReportWriter接口,基础实现输出原始内容,压缩/加密/水印各做一个装饰器,运行时按租户配置动态叠加;组装逻辑放在工厂/Builder 中,避免业务代码里出现多层 new 嵌套。同时提醒:装饰顺序有语义(先加密再压缩 ≠ 先压缩再加密),需在组装处显式约定并写清注释。
🔀 发散问题
- Q:装饰器和代理结构相同,怎么区分使用是否恰当? → 看目标对象由谁创建、意图是增强还是控制,见本文档「什么是代理模式?」。
- Q:装饰器和建造者的区别? → 装饰器增强已有对象,建造者分步产出新对象,见本文档「什么是建造者模式?」。
【中等】什么是外观模式?⭐⭐
🎯 目标等级:L2-L3 | ⏱ 建议用时:8 min | 🏷 标签:设计模式 / 结构型模式
💎 关键结论
外观模式为复杂子系统提供统一接口:外观了解如何重定向请求、如何操作子系统内部的活动部件,客户端只需调外观一个入口。典型落地:SLF4J 日志门面、Spring JdbcTemplate、DriverManager。
⚡记忆卡片
- 口诀:子系统复杂,门面一个口
- 关键词:统一入口 / 简化调用 / 只编排不下沉
- 链路:子系统对象多且调用有顺序约束 → 外观封装编排 → 客户端单点调用
📖 核心知识
外观模式 (Facade) 为子系统提供统一接口。
代码骨架

外观(Facade)提供了一种访问特定子系统功能的便捷方式,其了解如何重定向客户端请求,知晓如何操作一切活动部件。
创建附加外观(Additional Facade)类可以避免多种不相关的功能污染单一外观,使其变成又一个复杂结构。客户端和其他外观都可使用附加外观。
复杂子系统(Complex Subsystem)由数十个不同对象构成。如果要用这些对象完成有意义的工作,你必须深入了解子系统的实现细节,比如按照正确顺序初始化对象和为其提供正确格式的数据。
子系统类不会意识到外观的存在,它们在系统内运作并且相互之间可直接进行交互。
客户端(Client)使用外观代替对子系统对象的直接调用。
应用场景
- JDBC 驱动管理:
DriverManager屏蔽不同数据库驱动的加载和连接创建细节,客户端通过统一接口获取连接。 - Spring JdbcTemplate:封装连接获取、语句执行、结果集处理、异常转换,提供简洁的数据库操作入口。
- SLF4J 日志门面:为 Logback、Log4j 等日志框架提供统一 API,客户端面向门面编程,底层可随时切换。
🔬 扩展知识
详情
- 【L3】源码实证(精确到类):
JdbcTemplate.execute()把“获取连接 → 创建 Statement → 执行 → 处理结果集 → 关闭释放 → SQLException 转 DataAccessException”整个子系统流程收在一个方法里,调用方只写回调;ApplicationContext可视为对BeanFactory+ 事件机制 + 资源加载 + 国际化多个子系统的外观;SLF4J 的LoggerFactory.getLogger()屏蔽绑定发现与具体实现差异。 - 【L3】选型权衡(外观 vs 适配器 vs 中介者):外观简化子系统对外的入口(多对一、单向简化);适配器让不兼容的接口协作(转换已有接口);中介者集中多个同事对象间的网状交互(双向协调)。判别口诀:简化入口用外观,转换接口用适配器,协调交互用中介者。
- 【L3】滥用反模式:外观类越加越多变成新的上帝类(“万能 Service”);外观屏蔽过度导致高级用户无法绕过,只能再开旁路接口。工程实践:外观只做编排不做业务逻辑,复杂逻辑仍下沉到子系统;同时保留子系统接口的可见性,让特殊场景可以绕过外观。
- 【L4】量化视角:子系统对象 ≥ 3 个、调用方需理解调用顺序/初始化约束时才值得封装;只有一两个对象的“门面”纯属多一层间接。
- 【L4】外观模式和聚合服务(BFF/应用服务层)的关系:微服务架构中的 BFF(Backend For Frontend)层本质就是外观——把订单、库存、用户等多个下游服务的调用编排成一个面向前端的接口,屏蔽下游的调用顺序、容错、数据组装细节。差别在于外观是类级别,BFF 是服务级别,设计约束一致:只编排不下沉业务规则。
- 【L3】SLF4J 为什么叫日志门面?SLF4J API 对业务代码是外观——统一入口、屏蔽 Logback/Log4j 差异;而对具体日志实现则通过
StaticLoggerBinder/SPI 绑定,绑定层本质是适配器(如log4j-over-slf4j把 Log4j API 适配到 SLF4J)。同一套机制在不同视角下呈现两种模式,说明模式分类看意图不看结构。 - 【L4】什么时候应该拒绝“加一个门面统一入口”的提案?当门面只是把调用原样转发、没有任何简化或编排价值时,它只增加跳转成本;当子系统接口本身已经足够简单稳定(如 JDK 集合 API),再包一层只会造成双重维护。外观的价值在“简化复杂度”,复杂度没被简化就是伪外观。
- 【L3】场景演练:业务方接入短信服务需要:选供应商(阿里云/腾讯云)、模板报备、签名管理、频控、失败重试、降级切换,现在每个业务方都自己拼这套流程,已有 6 个业务方各写了一份,且有一家漏了频控被供应商封了号。这是外观模式的教科书场景——子系统对象多(供应商 SDK、模板服务、频控组件、重试组件)、调用有顺序约束(先报备再发送)、出错后果重。方案:建
SmsFacade.send(scene, params)统一入口,内部编排模板选择→频控校验→供应商路由→重试降级;同时保留直连供应商 SDK 的能力作为旁路。取舍:门面要收敛“必须经过的逻辑”(频控/审计),可选项(如自定义模板)留接口参数,避免门面膨胀成上帝类。
🔀 发散问题
- Q:外观和中介者都是“集中协调”,区别在哪? → 外观简化子系统对外(单向),中介者集中同事对象间交互(双向),见本文档「什么是中介者模式?」。
【中等】什么是享元模式?⭐⭐
🎯 目标等级:L2-L3 | ⏱ 建议用时:8 min | 🏷 标签:设计模式 / 结构型模式
💎 关键结论
享元模式通过共享细粒度对象来最小化内存消耗:把可共享的内在状态抽成享元缓存复用,各异的外在状态由客户端传入。唯一目的是省内存,没有内存压力时无需引入;Integer.valueOf() 的缓存池是最经典落地。
⚡记忆卡片
- 口诀:内在共享存缓存,外在传入配情景
- 关键词:内在状态 / 外在状态 / 享元工厂 / 省内存
- 链路:大量相似对象占内存 → 拆内在/外在状态 → 工厂缓存内在状态 → 按需取用
📖 核心知识
享元模式 (Flyweight) 共享细粒度对象,节省内存。
代码骨架

- 享元模式只是一种优化。在应用该模式之前,你要确定程序中存在与大量类似对象同时占用内存相关的内存消耗问题,并且确保该问题无法使用其他更好的方式来解决。
- 享元(Flyweight)类包含原始对象中部分能在多个对象中共享的状态。同一享元对象可在许多不同情景中使用。享元中存储的状态被称为“内在状态”。传递给享元方法的状态被称为“外在状态”。
- 情景(Context)类包含原始对象中各不相同的外在状态。情景与享元对象组合在一起就能表示原始对象的全部状态。
- 通常情况下,原始对象的行为会保留在享元类中。因此调用享元方法必须提供部分外在状态作为参数。但你也可将行为移动到情景类中,然后将连入的享元作为单纯的数据对象。
- 客户端(Client)负责计算或存储享元的外在状态。在客户端看来,享元是一种可在运行时进行配置的模板对象,具体的配置方式为向其方法中传入一些情景数据参数。
- 享元工厂(Flyweight Factory)会对已有享元的缓存池进行管理。有了工厂后,客户端就无需直接创建享元,它们只需调用工厂并向其传递目标享元的一些内在状态即可。工厂会根据参数在之前已创建的享元中进行查找,如果找到满足条件的享元就将其返回;如果没有找到就根据参数新建享元。
应用场景
使用示例: 享元模式只有一个目的:将内存消耗最小化。如果你的程序没有遇到内存容量不足的问题,则可以暂时忽略该模式。
享元模式在核心 Java 程序库中的示例:
识别方法:享元可以通过构建方法来识别,它会返回缓存对象而不是创建新的对象。
🔬 扩展知识
详情
- 【L3】Integer 缓存细节:
Integer.valueOf(int)对 -128~127 范围返回缓存实例(IntegerCache,上界可通过 JVM 参数调大),这也是Integer a = 127; b = 127; a == b为 true 而 128 为 false 的原因——享元机制常以这种面试题形式出现。 - 【L3】内在/外在状态拆分是难点:内在状态必须可共享(不随情景变化),拆错会导致共享对象被污染;共享后对象应尽量不可变,否则并发下多情景互相干扰。
- 【L4】与对象池、缓存的区别:享元按内在状态 key 池化细粒度对象;对象池复用重创建成本的对象(如连接池);两者思想同源但粒度与目的不同。游戏中成千上万个树/粒子共享网格与材质是享元的经典工程案例(推演口径)。
🔀 发散问题
- Q:享元和单例的区别? → 单例保证全局唯一,享元维护一组按 key 缓存的共享实例;享元工厂内部倒是可以用单例管理。
【中等】什么是代理模式?⭐⭐⭐⭐
🎯 目标等级:L2-L3 | ⏱ 建议用时:10 min | 🏷 标签:设计模式 / 结构型模式 / AOP
💎 关键结论
代理模式用于控制对象访问:代理与目标实现同一接口并持有目标引用,在完成延迟初始化、权限控制、日志、缓存等任务后再把请求传给目标。实现分静态代理与动态代理(JDK 基于接口、CGLIB 基于子类),Spring AOP 是最大规模落地。
⚡记忆卡片
- 口诀:同接口、持引用、先办事再透传
- 关键词:控制访问 / JDK vs CGLIB / 自调用失效
- 链路:调用方 → 代理(增强/控制)→ 目标对象
📖 核心知识
代理模式 (Proxy) 用于控制对象访问。
代码骨架

- 服务接口(Service Interface)声明了服务接口。代理必须遵循该接口才能伪装成服务对象。
- 服务(Service)类提供了一些实用的业务逻辑。
- 代理(Proxy)类包含一个指向服务对象的引用成员变量。代理完成其任务(例如延迟初始化、记录日志、访问控制和缓存等)后会将请求传递给服务对象。通常情况下,代理会对其服务对象的整个生命周期进行管理。
- 客户端(Client)能通过同一接口与服务或代理进行交互,所以你可在一切需要服务对象的代码中使用代理。
应用场景
- 远程代理:隐藏对象位于不同地址空间的事实,如 RPC 框架的客户端 Stub、Feign 动态代理。
- 虚拟代理:延迟创建开销大的对象,直到真正需要时(如 Hibernate 懒加载、大图片占位)。
- 保护代理:控制访问权限,校验调用者身份(如 Spring 方法级安全注解)。
- 智能引用:在访问时附加额外操作,如访问计数、日志记录、性能监控。
- 缓存代理:缓存方法结果,减少重复计算(如 Spring @Cacheable)。
- 防火墙代理:保护目标免受恶意访问,控制网络资源。
实现方式
- 静态代理:手动编写代理类,编译前确定。
- 动态代理:运行时生成代理,如 JDK 动态代理(基于接口)和 CGLIB(基于子类)。
🔬 扩展知识
详情
- 【L3】JDK 动态代理 vs CGLIB:
- JDK:
Proxy.newProxyInstance()运行时生成实现目标接口的$Proxy类,调用经InvocationHandler.invoke()转发(反射调用);要求目标必须有接口。 - CGLIB:基于 ASM 生成目标类的子类,用
MethodInterceptor.intercept()拦截;能代理无接口的类,但无法代理 final 类/final 方法;FastClass 机制用索引调用代替反射,性能更好。 - Spring AOP 选择规则:目标有接口默认 JDK 代理,无接口用 CGLIB;Spring Boot 2.x 起默认
proxyTargetClass=true统一用 CGLIB。
- JDK:
- 【L3】经典坑——自调用失效:同类中方法 A 调用方法 B(
this.B())不走代理对象,B 上的@Transactional/@Cacheable失效。解法:注入自身、AopContext.currentProxy()或拆到不同 Bean。 - 【L3】源码实证(精确到类):Spring AOP 入口在
AbstractAutoProxyCreator.postProcessAfterInitialization(),JDK 路径由JdkDynamicAopProxy.invoke()处理(其本身实现InvocationHandler),CGLIB 路径由ObjenesisCglibAopProxy生成子类;AbstractAdvisorAutoProxyCreator负责筛选匹配的切面。MyBatis 的Plugin.wrap()用 JDK 动态代理层层包装Executor实现插件链。 - 【L3】JDK 动态代理为什么要求目标必须有接口?CGLIB 为什么不能代理 final 方法?JDK 动态代理生成的
$Proxy类继承Proxy并实现目标接口,调用通过接口方法分派到InvocationHandler,没有接口就无从实现;CGLIB 通过生成目标类的子类拦截方法,final 类不能继承、final 方法不能被重写,因此无法拦截(private 方法同理,不会被子类覆写)。 - 【L3】
@Transactional失效除了自调用还有哪些常见原因?高频原因:方法不是 public(Spring AOP 默认只代理 public 方法);异常被 catch 吞掉未抛出;抛出的是检查型异常而未配rollbackFor;数据库引擎不支持事务(如 MyISAM);Bean 没被 Spring 管理。排查顺序从“代理是否生效”到“事务传播行为”再到“异常类型”。 - 【L4】代理模式和装饰器结构相同,怎么在代码评审时区分两者的使用是否恰当?看两点:目标对象由谁创建(代理自己创建/管理目标,装饰器由客户端传入)和意图(代理控制访问——客户端本可以不经过代理直接访问目标;装饰器增强功能——客户端主动选择叠加能力)。若一个“代理”允许客户端任意换包装对象并叠加多层,它实际上是装饰器,命名和设计都应按装饰器组织。
- 【L4】滥用反模式:为每个类都包代理做日志/监控,造成栈深与 GC 压力;在热路径上用动态代理做简单委派(反射开销不可忽视,高频场景考虑方法句柄或静态代理);用代理解决本该用职责链/事件解决的问题,导致代理嵌套失控。
- 【L3】实践视角:代理层数过深会增加栈深度与调用开销,高频路径应避免多层代理嵌套;网关鉴权、AOP 横切、RPC Stub 是代理模式的三处典型落地。
- 【L3】场景演练:监控团队要求所有对外 RPC 接口加耗时埋点,有人提议给每个 Service 写静态代理类;另一个提议用
HandlerInterceptor;还有人提议自定义注解 + AOP。静态代理侵入性强,每加一个方法都要同步改代理类,维护成本 O(n),仅适合无框架的裸接口;Interceptor 只能拦截 HTTP 入口,RPC 内部调用覆盖不到;注解 + AOP(@Around记录耗时)侵入最小、可精确到方法级、随业务代码演进自动覆盖,是首选。权衡点:AOP 方案要控制切面匹配范围(pointcut 收窄到注解标注的方法),避免全量拦截带来的反射开销;同时埋点逻辑要异步上报,不能阻塞业务主路径。
🔀 发散问题
- Q:代理和装饰器的区别? → 代理控制访问、自己管理目标生命周期;装饰器增强功能、目标由客户端传入且可叠加,见本文档「装饰器、适配器、代理、桥接这四种设计模式有什么区别?」。
- Q:AOP 是代理模式的落地吗? → 是。Spring AOP 基于 JDK/CGLIB 动态代理实现横切增强,事务、缓存、异步都是其应用。
【中等】装饰器、适配器、代理、桥接这四种设计模式有什么区别?⭐⭐⭐
🎯 目标等级:L2-L3 | ⏱ 建议用时:10 min | 🏷 标签:设计模式 / 结构型模式 / 横向对比
💎 关键结论
四者结构上都是“包装/引用”,区别看意图:装饰器动态增强功能(接口一致、可递归组合),适配器做接口转换让不兼容类协作,代理控制对象访问,桥接分离抽象与实现使二者独立变化。
⚡记忆卡片
- 口诀:装饰增强、适配转换、代理控制、桥接分离
- 关键词:意图区分 / 接口是否改变 / 谁创建目标
- 链路:结构相似 → 看意图 → 定模式归属
📖 核心知识
| 模式 | 核心意图 | 结构特点 | 典型场景 |
|---|---|---|---|
| 装饰器 | 动态增强对象功能 | 包装真实对象,接口一致,递归组合 | IO 流、Collections 包装类 |
| 适配器 | 接口转换,让不兼容的类协同工作 | 包装被适配者,实现目标接口 | 日志门面 SLF4J、第三方库适配 |
| 代理 | 控制对象访问,延迟加载或权限控制 | 代理与目标实现同一接口,代理持有引用 | RPC 客户端、AOP 代理、懒加载 |
| 桥接 | 分离抽象与实现,独立变化 | 抽象持有实现接口的引用,二者可各自扩展 | 跨平台 UI 组件、JDBC 驱动 |
🔬 扩展知识
详情
- 【L3】快速判别三连问:接口是否变了?变了→适配器。目的是增强功能还是控制访问?增强→装饰器,控制→代理。是否把两个变化维度拆开各自演化?是→桥接。
- 【L3】装饰器与代理结构同构,判别看目标对象由谁创建(代理自己创建/管理,装饰器由客户端传入)与是否可叠加(装饰器可多层叠加,代理通常单层)。
- 【L4】面试加分点:能举出同一机制在不同视角呈现不同模式的例子(如 SLF4J 对业务是外观、对日志实现绑定层是适配器),说明模式分类看意图不看结构。
🔀 发散问题
- Q:装饰器的典型场景有哪些? → Java IO 流、
Collections.synchronizedXXX()/unmodifiableXXX(),见本文档「什么是装饰器模式?」。 - Q:桥接和适配器的事前/事后区别? → 见本文档「什么是桥接模式?」。
行为型模式
【中等】什么是模板方法模式?⭐⭐⭐
🎯 目标等级:L2-L3 | ⏱ 建议用时:10 min | 🏷 标签:设计模式 / 行为型模式
💎 关键结论
模板方法模式定义算法骨架,子类实现步骤:抽象类声明步骤方法与依次调用它们的模板方法,子类可重写步骤但不能重写模板方法自身。典型落地:AQS、AbstractApplicationContext.refresh()、HttpServlet.service()。
⚡记忆卡片
- 口诀:父类定骨架,子类填步骤
- 关键词:算法骨架 / 继承 / 钩子方法 / 编译期固定
- 链路:提取公共流程 → 差异步骤留抽象/钩子 → 子类覆写细节
📖 核心知识
模板方法模式 (Template Method) 设置算法骨架,子类实现步骤。
代码骨架

- 抽象类(AbstractClass)会声明作为算法步骤的方法,以及依次调用它们的实际模板方法。算法步骤可以被声明为
抽象类型,也可以提供一些默认实现。 - 具体类(ConcreteClass)可以重写所有步骤,但不能重写模板方法自身。
应用场景
使用示例: 模版方法模式在 Java 框架中很常见。开发者通常使用它来向框架用户提供通过继承实现的、对标准功能进行扩展的简单方式。
这里是一些核心 Java 程序库中模版方法的示例:
java.io.InputStream、java.io.OutputStream、java.io.Reader和java.io.Writer的所有非抽象方法。java.util.AbstractList、java.util.AbstractSet和java.util.AbstractMap的所有非抽象方法。javax.servlet.http.HttpServlet,所有默认发送 HTTP 405 “方法不允许” 错误响应的doXXX()方法。你可随时对其进行重写。
识别方法: 模版方法可以通过行为方法来识别,该方法已有一个在基类中定义的 “默认” 行为。
🔬 扩展知识
详情
- 【L3】源码实证(精确到方法):
AbstractQueuedSynchronizer(AQS):acquire()定义获取锁骨架,子类只需实现tryAcquire()/tryRelease()钩子,ReentrantLock/Semaphore/CountDownLatch全靠它——JDK 并发包最重要的模板方法案例。AbstractApplicationContext.refresh():定义容器启动 12 步骨架,ClassPathXmlApplicationContext等子类只实现getResource()/onRefresh()等钩子。HttpServlet.service()按 method 分发到doGet()/doPost();InputStream.read(byte[], int, int)基于抽象的单字节read()构建批量读逻辑。
- 【L3】模板方法 vs 策略的本质差异:模板方法用继承固定骨架、子类覆写步骤,变化点编译期确定;策略用组合注入算法,运行期可切换。判别:子类只改一两个步骤且骨架稳定→模板方法;整个算法需要动态替换→策略。Java 8+ 也可用函数式参数(回调)替代继承式模板方法(如
JdbcTemplate)。 - 【L3】滥用反模式:继承层次过深导致流程难以理解(模板方法在父类、细节散在多层子类,调试时频繁跳类);父类骨架被频繁修改时所有子类被动受影响(父类变更影响面不可控);子类覆写时调用
super追加行为,说明抽象边界设计错误。 - 【L4】量化视角:流程步骤 ≥ 3 个且 80% 逻辑公共时才值得提取骨架;若子类间差异大于共性,说明抽象错误,应改用组合(策略/管道)而非继承。
- 【L3】AQS 中哪些是模板方法,哪些是钩子方法?
acquire()/release()/acquireShared()是模板方法——定义 CAS + 入队 + park 的固定骨架,不允许子类覆写;tryAcquire()/tryRelease()/tryAcquireShared()是钩子,默认抛UnsupportedOperationException强制子类实现。这样设计把“排队/阻塞的复杂通用逻辑”锁在父类(保证正确性),把“独占/共享语义”留给子类(保证灵活性),是模板方法的教科书级应用。 - 【L4】Java 8 之后的替代方案:函数式回调——把步骤作为
Function/Consumer参数传入骨架方法(JdbcTemplate.query(sql, rowMapper)即如此),或直接用default方法在接口里提供骨架。优势是不占用继承位、可运行期组合;代价是丢失了编译期类型层次的可见性。继承已用满或步骤间需共享状态时仍选模板方法。 - 【L3】Spring 中
JdbcTemplate是模板方法还是模板模式?JdbcTemplate是 GoF 模板方法的回调变体:骨架(开连接/执行/关资源/异常转译)写在JdbcTemplate自身,用户通过RowMapper/StatementCallback回调填充差异步骤,而非继承子类。Spring 把这种用法泛化为 Template 家族(RedisTemplate、RestTemplate),本质都是“固定流程 + 回调填充变化点”。 - 【L3】场景演练:数据同步任务有 MySQL 版和 ES 版:两边代码结构几乎一样(连接源→分批读取→字段转换→写入目标→更新位点),但两份代码各自演化,已出现“改了 MySQL 版的分批逻辑忘了同步 ES 版”的线上 bug。两份代码流程骨架相同、细节不同,是模板方法的典型场景。方案:提取
AbstractSyncJob定义骨架(分批大小、位点提交顺序、失败重试策略统一收在父类),read()/transform()/write()抽象由两个子类实现。边界:若未来出现第三种完全不同的流程(如流式同步),继承体系会僵化,应预留钩子或改用策略组合;骨架一旦发布应遵循开闭原则,修改骨架要评估所有子类的受影响面。
🔀 发散问题
- Q:模板方法和策略怎么选? → 骨架稳定、只改个别步骤用模板方法(继承);整个算法需运行期替换用策略(组合),见本文档「什么是策略模式?」。
- Q:模板方法常和哪些模式组合? → 策略基类内常套模板方法定公共骨架,工厂方法也常作为模板方法的一个步骤,见本文档「实际业务场景中如何选型设计模式?」。
【中等】什么是策略模式?⭐⭐⭐⭐
🎯 目标等级:L2-L3 | ⏱ 建议用时:10 min | 🏷 标签:设计模式 / 行为型模式
💎 关键结论
策略模式使算法可替换:上下文持有策略接口引用,客户端创建具体策略并传入,运行期可切换。它是消灭 if-else 的主力模式:Spring 中把多实现注入为 Map<String, Strategy> 按 key 路由,新增策略只加一个 @Component 实现。
⚡记忆卡片
- 口诀:算法成对象,运行期可换
- 关键词:可替换算法 / Map 路由 / 开闭原则 / 消灭 if-else
- 链路:定义策略接口 → 多实现注册 → 上下文按 key/配置选择 → 运行期切换
📖 核心知识
策略模式 (Strategy) 使得算法可替换。
代码骨架

- 上下文(Context)维护指向具体策略的引用,且仅通过策略接口与该对象进行交流。
- 策略(Strategy)接口是所有具体策略的通用接口,它声明了一个上下文用于执行策略的方法。
- 具体策略(Concrete Strategies)实现了上下文所用算法的各种不同变体。
- 当上下文需要运行算法时,它会在其已连接的策略对象上调用执行方法。上下文不清楚其所涉及的策略类型与算法的执行方式。
- 客户端(Client)会创建一个特定策略对象并将其传递给上下文。上下文则会提供一个设置器以便客户端在运行时替换相关联的策略。
应用场景
- 对
java.util.Comparator#compare()的调用来自Collections#sort(). javax.servlet.http.HttpServlet:service()方法,还有所有接受HttpServletRequest和HttpServletResponse对象作为参数的doXXX()方法。javax.servlet.Filter#doFilter()- Dubbo 中负载均衡算法采用了策略模式,便于切换算法。
🔬 扩展知识
详情
- 【L3】消灭 if-else 的标准套路:Spring 中把策略接口的多个实现注入为
Map<String, Strategy>(key 为 Bean 名),按业务类型路由;新增策略只需加一个@Component实现,符合开闭原则。策略映射还可放配置中心,不发版切换算法(如 Dubbo 负载均衡算法可运行时切换)。 - 【L3】源码实证(精确到类):
Comparator是最纯的策略——Arrays.sort(T[], Comparator)把比较算法作为参数注入;Dubbo 的LoadBalance接口有RandomLoadBalance/RoundRobinLoadBalance/LeastActiveLoadBalance等实现,通过 SPI 按配置加载;Spring MVC 的HandlerMapping是策略族(RequestMappingHandlerMapping、BeanNameUrlHandlerMapping);ThreadPoolExecutor的拒绝策略RejectedExecutionHandler(AbortPolicy/CallerRunsPolicy等)。 - 【L3】Spring 中注入
Map<String, Strategy>的原理:容器启动时DefaultListableBeanFactory.resolveMultipleBeans()发现注入点是 Map 且 value 类型匹配,会收集所有该类型的 Bean,key 默认为 Bean 名。实践中策略类用@Component("ALIPAY")显式命名,或实现getType()方法后在初始化时重建 Map,避免 key 与 Bean 名语义耦合。 - 【L3】选型权衡(策略 vs 状态):两者结构几乎相同,差异在意图与切换主体:策略由客户端选择且各算法互相独立(排序选快排还是堆排);状态由对象自身随生命周期自动流转,状态间常知道彼此(订单从待支付转已支付)。判别口诀:外部选算法用策略,内部随生命周期流转用状态。
- 【L3】策略模式和模板方法经常一起出现,如何分工?策略封装可替换的整体算法(维度间互斥),模板方法封装固定流程中的变化步骤(维度内共享)。典型组合:策略接口的抽象基类用模板方法定义公共骨架(参数校验→执行→埋点),具体策略只覆写核心步骤;Spring 的
AbstractHandlerMapping+ 各HandlerMapping实现即是此结构。 - 【L4】策略类之间有公共逻辑怎么处理?优先提取抽象基类放公共骨架(模板方法),但警惕继承层次过深;若公共逻辑是独立能力(如埋点、限流),优先用装饰器/AOP 横切而非继承。判别:公共部分与策略算法耦合紧密→继承;正交能力→组合。
- 【L4】滥用反模式:每个 if 分支抽一个策略类导致类爆炸(一行逻辑配一个类);策略接口被迫不断加方法以适配新策略,接口腐化;策略间存在隐式依赖(共享静态状态)导致替换不安全。分支只有两三个且稳定不变时,枚举 + switch 更简单。
- 【L3】场景演练:优惠计算模块有 15 个 if-else 分支:满减、折扣、第二件半价、拼团、会员价……,且存在叠加规则(会员价可与满减叠加)。单纯策略模式只能解决“多选一”,但优惠存在叠加/互斥规则,说明需求本质是“规则组合求值”而非“算法替换”。合理方案:每个优惠定义成
PromotionRule(策略),叠加/互斥关系用责任链或规则引擎编排(可叠加的规则链式求值,互斥规则取最优);叠加规则本身配置化,避免硬编码。规模判断:15 个分支且持续新增,重构收益明确;若只有两三个稳定优惠,枚举 + switch 更划算。
🔀 发散问题
- Q:策略和状态的差别到底是什么? → 切换主体不同:策略由客户端选、状态由对象自身流转,见本文档「什么是状态模式?」。
- Q:策略模式怎么消灭 if-else? → 策略 + 注册表路由,见本文档「如何用设计模式消除大量 if-else 硬编码?」。
【中等】什么是观察者模式?⭐⭐⭐
🎯 目标等级:L2-L3 | ⏱ 建议用时:10 min | 🏷 标签:设计模式 / 行为型模式 / 事件驱动
💎 关键结论
观察者模式实现一对多通知依赖者:发布者状态变化时遍历订阅列表逐个通知,订阅者间互不耦合。Spring 事件机制(ApplicationEventPublisher + @EventListener)是现代 Java 的主流落地;默认同步执行是高频坑。
⚡记忆卡片
- 口诀:一发多收,订阅解耦
- 关键词:一对多 / 事件发布 / 同步默认 / 隐式调用链
- 链路:发布者注册订阅者 → 状态变化发事件 → 遍历通知订阅者
📖 核心知识
观察者模式 (Observer) 用于一对多通知依赖者。
代码骨架

- 发布者(Publisher)会向其他对象发送值得关注的事件。事件会在发布者自身状态改变或执行特定行为后发生。发布者中包含一个允许新订阅者加入和当前订阅者离开列表的订阅构架。
- 当新事件发生时,发送者会遍历订阅列表并调用每个订阅者对象的通知方法。该方法是在订阅者接口中声明的。
- 订阅者(Subscriber)接口声明了通知接口。在绝大多数情况下,该接口仅包含一个
update更新方法。该方法可以拥有多个参数,使发布者能在更新时传递事件的详细信息。 - 具体订阅者(Concrete Subscribers)可以执行一些操作来回应发布者的通知。所有具体订阅者类都实现了同样的接口,因此发布者不需要与具体类相耦合。
- 订阅者通常需要一些上下文信息来正确地处理更新。因此,发布者通常会将一些上下文数据作为通知方法的参数进行传递。发布者也可将自身作为参数进行传递,使订阅者直接获取所需的数据。
- 客户端(Client)会分别创建发布者和订阅者对象,然后为订阅者注册发布者更新。
应用场景
java.util.Observer/java.util.Observable(极少在真实世界中使用)java.util.EventListener的所有实现 (几乎广泛存在于 Swing 组件中)javax.servlet.http.HttpSessionBindingListenerjavax.servlet.http.HttpSessionAttributeListenerjavax.faces.event.PhaseListener
🔬 扩展知识
详情
- 【L3】源码实证(精确到类):
- Spring 事件机制:
ApplicationEventPublisher.publishEvent()发布,SimpleApplicationEventMulticaster默认同步逐个调用ApplicationListener,@EventListener注解方法由EventListenerMethodProcessor扫描注册;配@Async才变异步。 - JDK:
java.util.Observer/Observable因不支持接口继承、线程安全设计粗糙,自 JDK 9 起已废弃,取而代之的是PropertyChangeListener/FlowAPI(响应式Flow.Publisher/Flow.Subscriber)。 - Guava
EventBus、Nacos 配置监听、AWT/Swing 的ActionListener都是同模式。
- Spring 事件机制:
- 【L3】滥用反模式(高频考点):
- 流程隐藏:下单后发了事件,谁在监听、执行顺序如何完全靠 IDE 全局搜索才能发现,事件驱动容易演变成“隐式调用链”,排查问题困难;关键主流程慎用。
- 同步监听拖慢主流程:默认同步执行时,监听器里调第三方接口会直接拖垮主接口 RT;需显式改异步(
@Async/MQ)并处理失败补偿。 - 事件风暴:每个业务动作都发事件,监听关系变成网状,系统行为不可推理。
- 【L3】Spring 事件监听是同步还是异步?监听器抛异常会怎样?默认同步(
SimpleApplicationEventMulticaster未配置 Executor 时),在发布者线程逐个调用;监听器抛异常会中断后续监听器并向发布者抛出,若发布在事务内则导致回滚。异步需自定义 multicaster 配置TaskExecutor或对监听器加@Async,但异步后异常不再反馈给发布者,需自行记录日志/补偿。 - 【L4】
@EventListener和实现ApplicationListener接口有什么区别?接口方式是显式强类型,需实现onApplicationEvent(),可配合@Order控制顺序;注解方式由EventListenerMethodProcessor扫描,支持 SpEL 条件过滤(condition属性)和多事件类型,代码更简洁。需要泛型事件精确分发时用接口方式更可控。 - 【L4】选型权衡(观察者 vs 消息队列):进程内观察者适合同步、强一致性要求低的联动;跨服务、需持久化/重试/削峰时升级到 MQ(MQ 本质是分布式观察者 + 持久化)。注意 Spring 事件默认同事务,监听器失败会导致主事务回滚(
@TransactionalEventListener可改为提交后触发)。三个升级信号:监听方变成独立服务(跨进程)、事件需要可靠投递/重试、发布订阅数量增长到需要削峰;升级时保留事件对象结构不变,只换投递通道。 - 【L4】量化视角:联动方 ≥ 2 个且预期会增减时才值得事件化;只有 1 个固定联动方时直接显式调用更清晰。
- 【L3】场景演练:下单成功后需要联动:扣库存、送积分、发短信、通知仓库发货。目前用同步代码依次调用四个服务,任何一个失败整个下单接口就报错,且每次新增联动方都要改下单主流程。先分清哪些联动能接受最终一致:发短信/送积分/通知仓库适合异步事件(失败重试补偿),扣库存必须留在主事务内同步执行(强一致),不能盲目全改事件。方案:库存扣减保留在主流程,其余改为 Spring 事件 +
@TransactionalEventListener(AFTER_COMMIT)或 MQ,发布前需确保订单已落库;同时提醒:事件化后链路可视化变差,需配套事件日志/监控,避免隐式调用链失控。
🔀 发散问题
- Q:观察者模式和中介者模式的区别? → 观察者是一对多广播、订阅者间不感知;中介者是多对多集中协调,见本文档「什么是中介者模式?」。
- Q:进程内事件什么时候该升级为 MQ? → 跨进程、需可靠投递/重试、需要削峰时,事件结构不变只换投递通道。
【中等】什么是状态模式?⭐⭐⭐
🎯 目标等级:L2-L3 | ⏱ 建议用时:10 min | 🏷 标签:设计模式 / 行为型模式 / 状态机
💎 关键结论
状态模式让行为随状态改变:上下文持有当前状态对象并把状态相关行为委派给它,状态转换通过替换状态对象完成。它消灭的是散落在代码各处的 if (status == X) 判断;复杂业务常用状态机框架(如 Spring Statemachine)落地。
⚡记忆卡片
- 口诀:状态成对象,行为随它变
- 关键词:状态对象 / 行为委派 / 状态转移 / 非法转移拦截
- 链路:状态判断散落 → 抽状态对象 → 上下文委派 → 转移时替换状态
📖 核心知识
状态模式 (State):状态改变行为。
代码骨架

- 上下文(Context)保存了对于一个具体状态对象的引用,并会将所有与该状态相关的工作委派给它。上下文通过状态接口与状态对象交互,且会提供一个设置器用于传递新的状态对象。
- 状态(State)接口会声明特定于状态的方法。这些方法应能被其他所有具体状态所理解,因为你不希望某些状态所拥有的方法永远不会被调用。
- 具体状态(Concrete States)会自行实现特定于状态的方法。为了避免多个状态中包含相似代码,你可以提供一个封装有部分通用行为的中间抽象类。
- 状态对象可存储对于上下文对象的反向引用。状态可以通过该引用从上下文处获取所需信息,并且能触发状态转移。
- 上下文和具体状态都可以设置上下文的下个状态,并可通过替换连接到上下文的状态对象来完成实际的状态转换。
应用场景
- Spring 状态机
- 将订单、流程等状态流转抽象为状态机模型,通过配置状态、事件、转换来管理复杂的业务状态。
- 核心要素包括当前状态、触发事件、响应函数、目标状态。
- Netty 网络框架
- 在通道处理器(
ChannelHandler)中存储连接状态(如登录态、协议版本),根据状态决定消息处理逻辑。 - 实现有状态协议(如同步请求-响应)时,通过状态变量控制消息发送时机,避免并发冲突。
- 在通道处理器(
🔬 扩展知识
详情
- 【L3】状态模式消灭的是什么:订单代码里散落的
if (status == 待支付) {...} else if (...)每加一个状态要改所有分支;状态模式把每个状态的行为收进状态类,新增状态只加类。本质是把“状态 × 行为”的二维判断表变成多态分派。 - 【L3】状态 vs 策略:结构几乎相同,差异在意图与切换主体——策略由客户端选择且各算法独立;状态由对象自身随生命周期流转,状态间常知道彼此(可触发下一状态)。判别口诀:外部选算法用策略,内部随生命周期流转用状态。
- 【L3】状态机增强:真实业务的合法转移需显式声明(状态 + 事件 + 目标状态 + guard 条件),非法转移直接拒绝——这是防止“未支付就发货”类 bug 的最硬防线;Spring Statemachine、Cola Statemachine 等框架把转移表配置化。
- 【L3】场景演练:订单状态流转(待支付→已支付→已发货→已完成)用 if-else 写在多个 Service 里,出现“已取消订单被重复支付回调改成已支付”的 bug。正解:状态机集中管理转移表,支付回调先校验“当前状态=待支付”才允许转已支付,非法转移直接丢弃并告警;每个状态类只包含该状态下允许的行为。
🔀 发散问题
- Q:状态模式和策略模式怎么区分? → 看切换主体与状态间是否可互知,见本文档「什么是策略模式?」。
- Q:简单状态流转也要上状态机框架吗? → 状态少于 5 个且转移固定时,枚举 + 状态模式手写即可;状态多、转移需配置化/审计时才上框架,见本文档「实际业务场景中如何选型设计模式?」。
【中等】什么是职责链模式?⭐⭐⭐⭐
🎯 目标等级:L2-L3 | ⏱ 建议用时:10 min | 🏷 标签:设计模式 / 行为型模式
💎 关键结论
职责链模式让请求沿链传递、多处理器依次处理:每个处理者决定是否处理以及是否继续传递。分纯链(找到能处理的就终止,如审批流)与不纯链(所有节点都处理,如 Filter)。典型落地:Servlet Filter、Spring Interceptor、Netty ChannelPipeline。
⚡记忆卡片
- 口诀:链上节点逐个过,能处理就处理、不能就传
- 关键词:纯链 vs 不纯链 / 洋葱模型 / 可插拔 / 按拒绝率排序
- 链路:请求入链 → 节点依次处理/传递 → 处理完成或链尾终止
📖 核心知识
职责链模式 (Chain of Responsibility):请求沿链传递,多处理器
代码骨架

处理者(Handler)声明了所有具体处理者的通用接口。该接口通常仅包含单个方法用于请求处理,但有时其还会包含一个设置链上下个处理者的方法。
基础处理者(Base Handler)是一个可选的类,你可以将所有处理者共用的样本代码放置在其中。
通常情况下,该类中定义了一个保存对于下个处理者引用的成员变量。客户端可通过将处理者传递给上个处理者的构造函数或设定方法来创建链。该类还可以实现默认的处理行为:确定下个处理者存在后再将请求传递给它。
具体处理者(Concrete Handlers)包含处理请求的实际代码。每个处理者接收到请求后,都必须决定是否进行处理,以及是否沿着链传递请求。
处理者通常是独立且不可变的,需要通过构造函数一次性地获得所有必要地数据。
客户端(Client)可根据程序逻辑一次性或者动态地生成链。值得注意的是,请求可发送给链上的任意一个处理者,而非必须是第一个处理者。
应用场景
- Servlet Filter(过滤器链)
- 每个过滤器对请求或响应进行处理,决定是否继续调用下一个过滤器或目标 Servlet。
- 典型应用:日志记录、权限校验、字符编码设置。
- Spring Interceptor(拦截器链)
- 在 Spring MVC 中,拦截器按配置顺序执行
preHandle、postHandle、afterCompletion。 - 任一拦截器返回 false 即可中断请求,常用于登录检查、性能监控。
- 在 Spring MVC 中,拦截器按配置顺序执行
- Netty 的 ChannelPipeline
- 每个 ChannelHandler 处理入站或出站事件,可选择传递给下一个处理器。
- 实现协议编解码、流量整形、业务逻辑的灵活编排。
- Spring Security 过滤器链
- 由多个
SecurityFilter组成(如UsernamePasswordAuthenticationFilter、ExceptionTranslationFilter),依次处理认证和授权。 - 可动态配置过滤器顺序和组合。
- 由多个
- MyBatis 插件(Interceptor)
- 插件实现
Interceptor接口,通过责任链拦截Executor、StatementHandler等核心对象的方法调用。 - 用于分页、性能监控、SQL 重写等扩展。
- 插件实现
🔬 扩展知识
详情
- 【L3】两种链形态:纯职责链(找到一个能处理的就终止,如审批流)与不纯链(所有节点都处理,如 Filter/Interceptor——每个 Filter 处理完主动调用
chain.doFilter()传递,本质是递归调用)。 - 【L3】源码印证:Netty
ChannelPipeline用双向链表存 handler,入站事件从头向尾传播,支持运行时动态增删;MyBatis 插件是多层Plugin.wrap()代理嵌套,形成洋葱模型,外层先进后出。 - 【L3】源码实证补充(精确到类):Servlet 的
ApplicationFilterChain(Tomcat 实现)用数组 + 索引推进doFilter();Spring MVC 的HandlerExecutionChain聚合所有HandlerInterceptor;Spring Security 的FilterChainProxy持有多个SecurityFilterChain,内部VirtualFilterChain逐个推进;Spring 容器侧BeanPostProcessor由PostProcessorRegistrationDelegate按@Order/PriorityOrdered排序后依次执行。 - 【L3】工程细节:链上节点应无状态(单例共享);按“耗时短、拒绝率高”的顺序编排节点(参数校验放最前)让请求尽早失败;链的组装可用建造者/配置化,支持热插拔。
- 【L3】Servlet 过滤器链的执行顺序由什么决定?
chain.doFilter()前后代码分别什么时候执行?顺序由 web.xml 声明顺序或@WebFilter/Spring Boot 中FilterRegistrationBean.setOrder()决定,值小先执行。doFilter()之前的代码在请求进入阶段执行,之后的代码在响应返回阶段执行(递归退栈),这是洋葱模型:最先进入的过滤器最后退出,因此异常处理和响应改写要注意层级。 - 【L4】MyBatis 插件和 Servlet Filter 都是职责链,实现方式有何不同?Filter 是链式推进(持有下一个节点引用,迭代/递归调用);MyBatis 插件是代理嵌套(每个
Interceptor用Plugin.wrap()把目标包一层 JDK 动态代理,多个插件形成洋葱式的代理层),执行顺序与注册顺序相反(后注册的先执行)。两者语义等价但调试时栈结构不同,需区别认识。 - 【L3】什么场景该用职责链而不是策略模式?策略是“多选一”(一个请求只由一个算法处理),职责链是“多对多遍历”(请求可能被多个节点处理/逐级传递直到有人接手)。判别:多个处理者都可能参与同一请求的处理、且需要中断/跳过能力→职责链;处理者互斥只选一个→策略。审批流、风控规则链、Filter 是职责链;负载均衡选节点是策略。
- 【L4】滥用反模式:链上节点间存在隐式依赖(后节点依赖前节点塞入的上下文变量,顺序一调就坏);把本应多选一的逻辑写成链导致每个请求白走完所有节点;链过长且无监控,某个节点变慢拖垮整链 RT——关键链需要节点级耗时埋点。
- 【L3】场景演练:提现风控需要依次校验:黑名单→金额限额→频次限制→实名状态,现在代码是一个方法里四个 if 顺序硬编码,新增规则要改主方法,且不同业务线(提现/转账/兑换)需要的规则子集不同,主方法里已出现大量
if (bizType == 提现 && ...)。这是“规则子集按业务线组合”的场景,单纯一条固定链不够。方案:每个规则实现RiskRule(无状态、声明拒绝率/耗时),链的组装配置化——按 bizType 从配置中选择规则列表并排序(拒绝率高的前置);纯链形态:任一规则拒绝即终止。权衡:规则间若有依赖(如频次规则依赖黑名单结果)需显式声明依赖而非隐式约定顺序;同时提醒新规则上线前用影子模式(只记录不拦截)验证误杀率。
🔀 发散问题
- Q:职责链和策略怎么选? → 多节点可叠加处理/需中断能力用职责链,互斥多选一用策略,见本文档「什么是策略模式?」。
- Q:MyBatis 插件链和 Filter 链实现有何不同? → 代理嵌套(洋葱模型)vs 链式推进,见本文档「什么是代理模式?」。
【中等】什么是命令模式?⭐⭐⭐
🎯 目标等级:L2-L3 | ⏱ 建议用时:10 min | 🏷 标签:设计模式 / 行为型模式
💎 关键结论
命令模式把请求封装为对象:具体命令持有接收者与参数,发送者只负责触发命令而不直接调用接收者,从而实现任务创建与执行分离,支撑操作队列、延迟执行、撤销等能力。Runnable/Callable 是 JDK 中最典型的落地。
⚡记忆卡片
- 口诀:请求成对象,发送接收两不相干
- 关键词:请求对象化 / 队列化 / 可撤销 / 发送者-接收者解耦
- 链路:封装请求为命令对象 → 发送者触发 → 命令委派接收者执行
📖 核心知识
命令模式 (Command):请求封装为对象,支持操作队列
代码骨架

发送者(Sender)——亦称“触发者(Invoker)”——类负责对请求进行初始化,其中必须包含一个成员变量来存储对于命令对象的引用。发送者触发命令,而不向接收者直接发送请求。注意,发送者并不负责创建命令对象:它通常会通过构造函数从客户端处获得预先生成的命令。
命令(Command)接口通常仅声明一个执行命令的方法。
具体命令(Concrete Commands)会实现各种类型的请求。具体命令自身并不完成工作,而是会将调用委派给一个业务逻辑对象。但为了简化代码,这些类可以进行合并。
接收对象执行方法所需的参数可以声明为具体命令的成员变量。你可以将命令对象设为不可变,仅允许通过构造函数对这些成员变量进行初始化。
接收者(Receiver)类包含部分业务逻辑。几乎任何对象都可以作为接收者。绝大部分命令只处理如何将请求传递到接收者的细节,接收者自己会完成实际的工作。
客户端(Client)会创建并配置具体命令对象。客户端必须将包括接收者实体在内的所有请求参数传递给命令的构造函数。此后,生成的命令就可以与一个或多个发送者相关联了。
应用场景
- JDK 并发框架
Runnable和Callable将线程执行的命令封装为对象,提交给Executor或Thread执行,实现任务创建与执行分离。ThreadPoolExecutor内部将任务对象放入阻塞队列,工作线程从队列中取出并执行。
- Spring 框架
JdbcTemplate将数据库操作封装为PreparedStatementCallback或ResultSetExtractor等命令对象,统一管理连接和事务。PlatformTransactionManager将事务的提交、回滚等操作封装为命令,支持编程式和声明式事务。
- Netty 网络框架
ChannelFuture和ChannelPromise将异步 I/O 操作封装为命令,通过回调通知结果。- 编解码器将消息的读取和写入封装为命令,在
ChannelPipeline中传递。
- 消息中间件(RabbitMQ / Kafka 客户端)
- 生产者将消息封装为命令对象(如
RabbitTemplate的send()方法内部构建Message对象),通过网络发送。 - 消费者将接收到的消息封装为命令对象,传递给业务处理器。
- 生产者将消息封装为命令对象(如
🔬 扩展知识
详情
- 【L3】命令对象化带来的三个能力:队列化(命令入队异步执行,
ThreadPoolExecutor的阻塞队列即是)、延迟/定时执行(命令可持久化后重放)、可撤销(命令携带逆操作或状态快照,编辑器的 Ctrl+Z 是经典实现)。 - 【L3】命令与回调的关系:Java 8 之后函数式接口(
Runnable、Consumer)常直接充当轻量命令对象;命令模式的价值在需要保存/传递/排队请求时才超过 lambda,只需执行一次时不必建类。 - 【L4】与消息队列的思想同源:MQ 中“消息即命令”——发送者生产命令、Broker 排队、消费者作为接收者执行;差异在 MQ 提供了跨进程、持久化、重试等基础设施能力。
🔀 发散问题
- Q:命令模式和策略模式的区别? → 命令封装“对谁做什么”(含接收者与参数),策略封装“怎么做”(算法可替换);命令常入队,策略常切换。
- Q:Runnable 算命令模式吗? → 算最典型的落地:任务是命令对象,
Thread/Executor是发送者,见本文档「说说 JDK 和 Spring 源码中用到了哪些设计模式?」。
【简单】什么是迭代器模式?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:5 min | 🏷 标签:设计模式 / 行为型模式
💎 关键结论
迭代器模式提供顺序访问聚合元素的统一方式:具体迭代器跟踪自身遍历进度,使多个迭代器可独立遍历同一集合;客户端面向 Iterator 接口编程,不感知集合内部结构。java.util.Iterator 即该模式。
⚡记忆卡片
- 口诀:集合不暴露,迭代器带路
- 关键词:顺序访问 / hasNext-next / 遍历进度独立
- 链路:集合提供 iterator() → 迭代器跟踪进度 → 客户端统一遍历
📖 核心知识
迭代器模式 (Iterator):顺序访问聚合元素
代码骨架

- 迭代器(Iterator)接口声明了遍历集合所需的操作:获取下一个元素、获取当前位置和重新开始迭代等。
- 具体迭代器(Concrete Iterators)实现遍历集合的一种特定算法。迭代器对象必须跟踪自身遍历的进度。这使得多个迭代器可以相互独立地遍历同一集合。
- 集合(Collection)接口声明一个或多个方法来获取与集合兼容的迭代器。请注意,返回方法的类型必须被声明为迭代器接口,因此具体集合可以返回各种不同种类的迭代器。
- 具体集合(Concrete Collections)会在客户端请求迭代器时返回一个特定的具体迭代器类实体。你可能会琢磨,剩下的集合代码在什么地方呢?不用担心,它也会在同一个类中。只是这些细节对于实际模式来说并不重要,所以我们将其省略了而已。
- 客户端(Client)通过集合和迭代器的接口与两者进行交互。这样一来客户端无需与具体类进行耦合,允许同一客户端代码使用各种不同的集合和迭代器。
- 客户端通常不会自行创建迭代器,而是会从集合中获取。但在特定情况下,客户端可以直接创建一个迭代器(例如当客户端需要自定义特殊迭代器时)。
应用场景
java.util.Iterator的所有实现(还有java.util.Scanner)。java.util.Enumeration的所有实现
🔀 发散问题
- Q:遍历时修改集合会怎样? → 触发 fail-fast:
ArrayList等通过modCount检测结构性修改,抛ConcurrentModificationException;并发场景应使用CopyOnWriteArrayList等弱一致容器。 - Q:迭代器模式为什么感觉“隐形”了? → 因为它已内化为语言级设施(for-each/
Iterable),这正是模式设计的最高境界:使用者无需感知模式存在。
【简单】什么是中介者模式?⭐
🎯 目标等级:L2 | ⏱ 建议用时:5 min | 🏷 标签:设计模式 / 行为型模式
💎 关键结论
中介者模式封装对象间交互,降低耦合:组件间不再直接互相引用,而是通过中介者通信;中介者对组件是黑箱,发送者不知道谁处理、接收者不知道谁发出。Executor、Timer 是 JDK 中的体现。
⚡记忆卡片
- 口诀:同事不串门,有事找中介
- 关键词:集中协调 / 网状变星形 / 黑箱
- 链路:组件间网状交互 → 集中到中介者 → 组件只持中介者引用
📖 核心知识
中介者模式 (Mediator):封装对象间交互,降低耦合
代码骨架

- 组件(Component)是各种包含业务逻辑的类。每个组件都有一个指向中介者的引用,该引用被声明为中介者接口类型。组件不知道中介者实际所属的类,因此你可通过将其连接到不同的中介者以使其能在其他程序中复用。
- 中介者(Mediator)接口声明了与组件交流的方法,但通常仅包括一个通知方法。组件可将任意上下文(包括自己的对象)作为该方法的参数,只有这样接收组件和发送者类之间才不会耦合。
- 具体中介者(Concrete Mediator)封装了多种组件间的关系。具体中介者通常会保存所有组件的引用并对其进行管理,甚至有时会对其生命周期进行管理。
- 组件并不知道其他组件的情况。如果组件内发生了重要事件,它只能通知中介者。中介者收到通知后能轻易地确定发送者,这或许已足以判断接下来需要触发的组件了。
- 对于组件来说,中介者看上去完全就是一个黑箱。发送者不知道最终会由谁来处理自己的请求,接收者也不知道最初是谁发出了请求。
应用场景
java.util.Timer(所有scheduleXXX()方法)java.util.concurrent.Executor#execute()java.util.concurrent.ExecutorService(invokeXXX()和submit()方法)java.util.concurrent.ScheduledExecutorService(所有scheduleXXX()方法)java.lang.reflect.Method#invoke()
🔀 发散问题
- Q:中介者和观察者的区别? → 观察者是一对多广播、订阅者互不感知;中介者集中协调多对多交互、双向通信,见本文档「什么是观察者模式?」。
- Q:中介者有什么风险? → 交互逻辑集中后中介者自身容易膨胀为上帝类,需按职责拆分多个中介者或配合外观分层。
【困难】什么是访问者模式?⭐⭐
🎯 目标等级:L3-L4 | ⏱ 建议用时:15 min | 🏷 标签:设计模式 / 行为型模式
💎 关键结论
访问者模式在不改变元素类的前提下增加新操作:元素提供 accept(Visitor) 方法,访问者按元素类型重载处理逻辑,靠双重分派把“操作”从“数据结构”中解耦。代价是新增元素类型要改所有访问者,因此只适合元素类型稳定的结构(如 AST)。
⚡记忆卡片
- 口诀:元素不变操作变,accept 迎客双重派
- 关键词:双重分派 / accept-visit / AST 遍历
- 链路:元素 accept 访问者 → 访问者回调匹配自身类型的方法 → 执行新操作
📖 核心知识
访问者模式 (Visitor):在不改变元素类前提下增加新操作
代码骨架

- 访问者(Visitor)接口声明了一系列以对象结构的具体元素为参数的访问者方法。如果编程语言支持重载,这些方法的名称可以是相同的,但是其参数一定是不同的。
- 具体访问者(Concrete Visitor)会为不同的具体元素类实现相同行为的几个不同版本。
- 元素(Element)接口声明了一个方法来“接收”访问者。该方法必须有一个参数被声明为访问者接口类型。
- 具体元素(Concrete Element)必须实现接收方法。该方法的目的是根据当前元素类将其调用重定向到相应访问者的方法。请注意,即使元素基类实现了该方法,所有子类都必须对其进行重写并调用访问者对象中的合适方法。
- 客户端(Client)通常会作为集合或其他复杂对象(例如一个 组合 树)的代表。客户端通常不知晓所有的具体元素类,因为它们会通过抽象接口与集合中的对象进行交互。
应用场景
- JDK NIO 的
FileVisitor接口让文件树遍历与具体操作(查找、删除)分离。 - Apache Calcite 的
SqlVisitor在不修改 AST 节点的情况下执行类型检查、优化等操作。 - Apache Commons Lang 的
LockingVisitors将锁管理与对受保护对象的读写操作解耦。 - Lombok 在编译期作为访问者遍历 AST,根据注解动态添加方法或字段。
- Java 编译器 API 的
ElementVisitor在注解处理时遍历源代码元素(类、方法等)并执行自定义逻辑。
🔬 扩展知识
详情
- 【L3】双重分派原理:Java 方法重载是静态绑定(编译期按声明类型选),访问者用“元素 accept → 访问者 visit(this)”两次动态分派,让运行时类型决定执行哪个 visit 版本——这是访问者模式的核心机制,也是面试深挖点。
- 【L3】适用边界:操作频繁新增、元素类型稳定的结构(编译器 AST、文档树、表达式树);反之元素类型频繁新增时,每加一种元素要改所有访问者,维护成本爆炸。
- 【L4】与组合模式的配合:访问者常在组合树上遍历(如文件系统树、语法树),组合管结构、访问者管操作,职责互补。
- 【L4】为什么它是最不常用的 GoF 模式?双重分派理解门槛高、破坏封装(访问者常需元素的细节接口)、Java 无原生多方法支持;能用多态或策略解决的场景都不应优先选访问者。
🔀 发散问题
- Q:访问者模式和迭代器模式的区别? → 迭代器只管“怎么遍历”,访问者管“遍历到每种元素做什么”,两者常配合使用,见本文档「什么是迭代器模式?」。
- Q:编译器/解释器为什么大量用访问者? → AST 节点类型编译后稳定,而对 AST 的操作(类型检查、优化、代码生成)持续新增,见本文档「什么是解释器模式?」。
【中等】什么是备忘录模式?⭐
🎯 目标等级:L2-L3 | ⏱ 建议用时:8 min | 🏷 标签:设计模式 / 行为型模式
💎 关键结论
备忘录模式保存和恢复对象状态:原发器生成自身状态的不可变快照(备忘录),负责人只管理快照栈(何时存、何时恢复)而不接触状态细节。三种实现按封装严格度递增:嵌套类、中间接口、独立备忘录类。
⚡记忆卡片
- 口诀:原发器拍照、负责人存档、需要时回放
- 关键词:状态快照 / 不可变 / 封装保护 / Ctrl+Z
- 链路:原发器创建备忘录 → 负责人压栈保存 → 恢复时出栈回传
📖 核心知识
备忘录模式 (Memento):保存和恢复对象状态
代码骨架
基于嵌套类的实现

原发器(Originator)类可以生成自身状态的快照,也可以在需要时通过快照恢复自身状态。
备忘录(Memento)是原发器状态快照的值对象(value object)。通常做法是将备忘录设为不可变的,并通过构造函数一次性传递数据。
负责人(Caretaker)仅知道“何时”和“为何”捕捉原发器的状态,以及何时恢复状态。
负责人通过保存备忘录栈来记录原发器的历史状态。当原发器需要回溯历史状态时,负责人将从栈中获取最顶部的备忘录,并将其传递给原发器的恢复(restoration)方法。
在该实现方法中,备忘录类将被嵌套在原发器中。这样原发器就可访问备忘录的成员变量和方法,即使这些方法被声明为私有。另一方面,负责人对于备忘录的成员变量和方法的访问权限非常有限:它们只能在栈中保存备忘录,而不能修改其状态。
基于中间接口的实现
另外一种实现方法适用于不支持嵌套类的编程语言(没错,我说的就是 PHP)。

- 在没有嵌套类的情况下,你可以规定负责人仅可通过明确声明的中间接口与备忘录互动,该接口仅声明与备忘录元数据相关的方法,限制其对备忘录成员变量的直接访问权限。
- 另一方面,原发器可以直接与备忘录对象进行交互,访问备忘录类中声明的成员变量和方法。这种方式的缺点在于你需要将备忘录的所有成员变量声明为公有。
封装更加严格的实现
如果你不想让其他类有任何机会通过备忘录来访问原发器的状态,那么还有另一种可用的实现方式。

- 这种实现方式允许存在多种不同类型的原发器和备忘录。每种原发器都和其相应的备忘录类进行交互。原发器和备忘录都不会将其状态暴露给其他类。
- 负责人此时被明确禁止修改存储在备忘录中的状态。但负责人类将独立于原发器,因为此时恢复方法被定义在了备忘录类中。
- 每个备忘录将与创建了自身的原发器连接。原发器会将自己及状态传递给备忘录的构造函数。由于这些类之间的紧密联系,只要原发器定义了合适的设置器(setter),备忘录就能恢复其状态。
应用场景
- JDK 标准库
java.io.Serializable通过序列化将对象状态保存为字节流,需要时反序列化恢复,是广义的备忘录实现。java.util.Date的clone()方法可创建对象副本,本质上是备忘录的简化形式。
- Spring 框架
- Spring Web Flow 在用户导航过程中保存每个页面的状态,支持回退到之前的状态,体现备忘录的保存与恢复概念。
- 声明式事务管理事务开始时保存数据库状态,失败时回滚到事务前状态,实现状态恢复机制。
- Bean 状态管理
BeanFactory和ApplicationContext允许定义、保存和恢复 Bean 的配置状态。
🔬 扩展知识
详情
- 【L3】封装与访问的矛盾是设计核心:负责人需要持有快照但不能看/改内容,三种实现按封装严格度递增——嵌套类(Java 首选,利用嵌套类访问权限)、中间接口(限制负责人只读元数据)、独立备忘录类(状态完全不暴露,恢复方法在备忘录内)。选型看对封装的要求程度。
- 【L3】备忘录与原型的关系:快照也可用克隆实现(
clone()是备忘录的简化形式);差异在于备忘录强调封装与历史栈管理,原型强调复制本身,见本文档「什么是原型模式?」。 - 【L4】落地注意:状态对象大时备忘录占内存(可用增量/差异快照);恢复的语义需明确(是否连带恢复关联对象);编辑器的多级撤销(Ctrl+Z)、IDE 本地历史、数据库事务回滚都是同思想落地。
🔀 发散问题
- Q:备忘录和命令模式怎么配合实现撤销? → 命令执行前保存原发器状态的备忘录,撤销时恢复;命令管操作、备忘录管状态。
【中等】什么是解释器模式?⭐⭐
🎯 目标等级:L2-L3 | ⏱ 建议用时:8 min | 🏷 标签:设计模式 / 行为型模式
💎 关键结论
解释器模式给定一个语言,定义它的文法表示,并构建解释器解析句子:文法规则用类表达,递归解释求值。优点是扩展新规则只需新增类(开闭),缺点是复杂文法类爆炸、递归性能差,因此只适用于文法简单的领域。
⚡记忆卡片
- 口诀:文法成类,递归解释
- 关键词:终结符 / 非终结符 / AST / 正则与 SpEL
- 链路:定义文法 → 构建表达式树 → 递归 interpret 求值
📖 核心知识
解释器模式 (Interpreter):给定一个语言,定义它的文法表示,并构建解释器来解析该语言中的句子。
核心结构
- 抽象表达式:声明
interpret()解释接口。 - 终结符表达式:文法中最小不可再分的符号(如变量、常量),直接解释求值。
- 非终结符表达式:对应文法规则(如加减运算),持有子表达式引用,递归解释。
- 上下文/环境:存储解释过程需要的全局数据(如变量取值表)。
经典应用
- 正则表达式:
Pattern将正则字符串解析为语法树后逐节点解释执行。 - Spring EL:
SpelExpressionParser将表达式解析为 AST 后解释求值,注解中#{...}即用此机制。 - SQL 解析引擎:Calcite、Druid SQLParser 将 SQL 词法/语法分析为 AST 后做校验、改写、优化。
- 规则引擎:Aviator、QLExpress、Drools(Rete 算法)解释执行规则表达式。
- MyBatis:
SqlNode树解释<if>、<foreach>等动态 SQL 标签。
优缺点
- 优点:文法规则用类表达,扩展新规则只需新增类(开闭原则)。
- 缺点:复杂文法导致类爆炸、递归解释性能差——因此它只适用于文法简单的领域,复杂场景通常用专业解析器生成工具(ANTLR)。
🔬 扩展知识
详情
- 【L3】识别特征:只要代码里出现“把字符串解析成树再递归求值”(表达式、DSL、模板标签),背后就有解释器思想;MyBatis 动态 SQL 是最贴近业务的案例:XML 标签 →
SqlNode树 →apply()递归拼接 SQL。 - 【L4】适用边界与替代:文法超过十几个规则就不应手写解释器——ANTLR 等解析器生成器把词法/语法分析交给工具,手写类只保留语义处理;这也是该模式在 GoF 中冷门的原因。
- 【L3】与访问者的配合:AST 建好后,类型检查、优化、代码生成等后续操作常以访问者遍历实现,见本文档「什么是访问者模式?」。
🔀 发散问题
- Q:SpEL、Aviator 这类表达式引擎算解释器模式吗? → 算同思想落地,只是工业级实现用了解析器生成与字节码编译优化,而非手写表达式类。
综合
【困难】说说 JDK 和 Spring 源码中用到了哪些设计模式?⭐⭐⭐⭐
🎯 目标等级:L3-L4 | ⏱ 建议用时:15 min | 🏷 标签:设计模式 / JDK 源码 / Spring 源码
💎 关键结论
答题关键是能定位到具体类并说清为什么用它:JDK 侧高频是单例(Runtime)、工厂(Collection.iterator())、装饰器(IO 流)、策略(Comparator)、命令(Runnable);Spring 侧是工厂(BeanFactory)、代理(AOP)、模板方法(refresh())、观察者(事件机制)、职责链(BeanPostProcessor)。
⚡记忆卡片
- 口诀:JDK 看集合 IO,Spring 看容器 AOP
- 关键词:定位到类 / 为什么用 / 不用会怎样
- 链路:报模式 → 报类名 → 讲解决的扩展性问题 → 串联启动链路
📖 核心知识
这是考察“模式是否真正理解”的综合题,答题关键是能定位到具体类并说清为什么用它。
JDK 中的设计模式
| 模式 | 源码体现 |
|---|---|
| 单例 | Runtime.getRuntime() |
| 工厂方法 | Calendar.getInstance()、Collection.iterator() |
| 建造者 | StringBuilder.append() 链式构建 |
| 原型 | Object.clone() + Cloneable |
| 适配器 | Arrays.asList()、InputStreamReader(字节流适配为字符流) |
| 装饰器 | IO 流嵌套包装(BufferedInputStream 包 FileInputStream) |
| 代理 | java.lang.reflect.Proxy 动态代理 |
| 模板方法 | AbstractList、InputStream 的非抽象方法 |
| 策略 | Comparator 注入 Collections.sort() |
| 观察者 | java.util.EventListener 体系 |
| 命令 | Runnable、Callable |
| 迭代器 | java.util.Iterator |
Spring 中的设计模式
| 模式 | 源码体现 |
|---|---|
| 工厂 | BeanFactory/ApplicationContext 创建并管理 Bean |
| 单例 | Bean 默认 singleton 作用域,三级缓存存储 |
| 代理 | AOP 基于 JDK 动态代理/CGLIB 实现事务、缓存、异步 |
| 模板方法 | JdbcTemplate、AbstractApplicationContext.refresh() 定义启动骨架 |
| 观察者 | ApplicationEventPublisher + @EventListener 事件机制 |
| 适配器 | HandlerAdapter 适配不同类型的 Controller |
| 职责链 | BeanPostProcessor、HandlerInterceptor、Filter 链 |
| 策略 | HandlerMapping、InstantiationStrategy、Resource 多实现 |
| 装饰器 | BeanWrapper、各种 XXXWrapper |
L4 答题建议:不要只背清单,要能讲出三层——是什么(哪个类)、为什么(框架为什么选这个模式,解决什么扩展性问题)、不用会怎样(如 AOP 不用代理就得在每个业务方法里重复写事务代码)。能结合自己读过的源码细节(如 refresh() 十二步、三级缓存解决循环依赖中的提前暴露)是明显加分项。
🔬 扩展知识
详情
- 【L3】高频追问点补充(精确到类名):
- 代理在 Spring 中的完整链路:
AbstractAutoProxyCreator.postProcessAfterInitialization()判断是否需要代理 →JdkDynamicAopProxy/ObjenesisCglibAopProxy生成代理;事务由TransactionInterceptor拦截实现。 - 观察者:
ApplicationEventMulticaster维护监听器注册表,@TransactionalEventListener支持事务提交后触发。 - JDK 补充:
AbstractList的indexOf()/contains()基于抽象的get()构建(模板方法);TreeMap/PriorityQueue的排序行为通过Comparator注入(策略)。
- 代理在 Spring 中的完整链路:
- 【L3】答题反模式:只报模式名不报类名会被直接降级(“代理模式——Spring AOP 用了”这种回答无信息量);把不确定的类名报错比含糊回答更扣分——拿不准就说“大概是某个 AbstractXXXTemplate 系列”。
- 【L4】串联视角:能讲出模式间的组合更有说服力,如 Spring 启动链路:
refresh()(模板方法)→BeanPostProcessor链(职责链)→ 事件发布(观察者)→ AOP 代理生成(代理),一条链路串四种模式。 - 【L3】Spring 为什么用三级缓存而不是二级解决循环依赖?三级缓存(singletonFactories)存的是 ObjectFactory,关键是延迟决定是否生成 AOP 代理:若 Bean A 依赖 B、B 又依赖 A,B 创建时通过 ObjectFactory 提前拿到 A 的引用(若 A 需代理则此时提前生成代理对象),保证 B 持有的是代理而非原始对象。三级缓存的核心作用是解决循环依赖中的提前暴露;若只用二级缓存提前暴露,无法判断是否需提前代理,会破坏代理一致性。单例 + 构造器注入的循环依赖无法解决(可用
@Lazy或改 setter 注入)。 - 【L3】
refresh()方法里哪几步最关键?关键步:invokeBeanFactoryPostProcessors()(执行 BeanDefinition 级别扩展,@Configuration类的 CGLIB 增强在此发生)、registerBeanPostProcessors()(注册 Bean 级扩展,职责链的组装)、onRefresh()/finishBeanFactoryInitialization()(实例化所有非懒加载单例,循环依赖在此暴露)、finishRefresh()(发布 ContextRefreshedEvent,观察者模式)。 - 【L4】JDK 中有没有“模式的反面教材”(设计被公认不佳的案例)?经典案例:
java.util.Observable是类而非接口且方法带 synchronized,JDK 9 正式废弃;Properties继承Hashtable违反 LSP;Calendar可变且线程不安全、API 设计遭诟病后被java.time替代。能主动提这些反面案例,说明是真读过源码而不是背清单。
🏭 实战场景
详情
- @Transactional 事务链路追问(典型面试场景口径):面试官追问:“你说 Spring AOP 用了代理,那从调用
@Transactional方法到事务真正开启,中间经过哪些类?”链路应为:调用方持有的是代理对象(JdkDynamicAopProxy或 CGLIB 子类)→invoke()/intercept()进入 →ReflectiveMethodInvocation.proceed()依次执行拦截器链(责任链)→ 命中TransactionInterceptor.invoke()→ 委托PlatformTransactionManager(策略,DataSourceTransactionManager等实现)开启事务。能讲到proceed()的递归推进和TransactionInterceptor即为 L3 水准;说不清则暴露只背了结论。 - 生产案例口径:网关鉴权(保护代理)、接口耗时埋点(AOP 代理 + 注解)、下单后发短信送积分(Spring 事件异步化,主接口 RT 从串联四服务降为只含主流程,推演口径)——同一套模式在真实项目中的三处落地。
⚠️ 常见误区
详情
常见误区:
- ❌ “三级缓存是用来创建单例 Bean 的” → 三级缓存存的是单例 Bean 及其提前暴露的引用,核心作用是解决循环依赖中的提前暴露(延迟决定是否提前生成 AOP 代理),不是“创建”单例。
- ❌ “Spring AOP 就是 JDK 动态代理” → 有接口默认 JDK,无接口用 CGLIB,Spring Boot 2.x 起默认统一 CGLIB;且自调用不走代理,这是最高频失效原因。
- ❌ “报模式名就算答对了” → 只说“代理模式——Spring AOP 用了”无信息量,必须定位到类(
AbstractAutoProxyCreator、JdkDynamicAopProxy)并说清为什么用它。
🔀 发散问题
- Q:Spring 启动链路能串起哪几种模式? →
refresh()(模板方法)→BeanPostProcessor(职责链)→ 事件发布(观察者)→ AOP 代理(代理),一条链串四种。 - Q:业务代码里怎么落地这些模式? → 见本文档「实际业务场景中如何选型设计模式?」与「如何用设计模式消除大量 if-else 硬编码?」。
【中等】实际业务场景中如何选型设计模式?⭐⭐⭐⭐⭐
🎯 目标等级:L3-L4 | ⏱ 建议用时:15 min | 🏷 标签:设计模式 / 选型决策
💎 关键结论
考察重点不是背诵模式定义,而是识别场景特征、匹配模式意图:先看变化点在哪(算法→策略、状态→状态、流程步骤→模板方法、处理节点→职责链、对象创建→工厂/建造者),再看扩展方式是新增还是替换,最后权衡复杂度——分支少且稳定时 if-else 也是合理选择。
⚡记忆卡片
- 口诀:先找变化点,再配模式,最后算成本
- 关键词:场景特征 / 模式意图 / Rule of Three / 量化门槛
- 链路:识别变化维度 → 匹配模式意图 → 评估引入成本 vs 不引入成本 → 决策
📖 核心知识
高频场景速查表:
| 业务场景 | 推荐模式 | 选型理由 |
|---|---|---|
| 订单状态流转(待支付→已支付→发货→完成) | 状态模式/状态机 | 行为随状态变化,避免状态判断散落各处 |
| 支付渠道/促销规则/风控策略多选一 | 策略模式 + 工厂 | 算法可替换可扩展,新增渠道不改存量 |
| 风控校验、审批流、Filter 链 | 职责链模式 | 多处理器顺序执行,可动态编排/中断 |
| 下单后联动发短信、送积分、通知仓库 | 观察者/事件驱动 | 主流程与联动逻辑解耦,新增订阅者不改主流程 |
| 复杂对象组装(构建复杂查询条件、导出配置) | 建造者模式 | 分步构建 + 链式调用,参数多时避免构造器爆炸 |
| 调用第三方接口适配/老系统改造 | 适配器/防腐层 | 隔离外部模型变化 |
| 事务、日志、缓存等横切增强 | 代理模式(AOP) | 不侵入业务代码动态增强 |
| 流程固定但步骤有差异(导入导出、支付回调处理) | 模板方法模式 | 骨架固定,子类只实现差异步骤 |
| 延迟加载、权限控制、远程调用封装 | 代理模式 | 控制访问、对调用方透明 |
选型心法:先看变化点在哪——变化的是算法(策略)、状态(状态)、流程步骤(模板方法)、处理节点(职责链)、对象创建(工厂/建造者);再看扩展方式是新增还是替换;最后权衡复杂度,分支少且稳定时 if-else 也是合理选择。
🔬 扩展知识
详情
- 【L3】量化决策门槛(经验值):分支 ≤ 3 且一年内不会新增 → if-else/switch;分支 4~8 且持续新增 → 策略 + 注册表;处理步骤 ≥ 3 且需动态编排 → 职责链;联动方 ≥ 2 且会增减 → 事件驱动;构造参数 ≥ 5 且可选组合多 → 建造者。数字不是铁律,但面试中能给出门槛说明真做过选型。
- 【L3】选型顺序:先评估“不引入模式的成本”(修改频率 × 影响面),再评估引入成本(类数量、学习曲线、调试难度);多数业务代码的正确答案是“先简单写,第三次重复时再抽象”(Rule of Three)。
- 【L4】滥用反模式:为炫技在 CRUD 上套工厂 + 策略 + 观察者,30 行逻辑拆成 8 个类,新人两周看不懂;模式应跟随问题生长,而不是预设问题。能讲一个“最终没用模式”的决策更有说服力——如评估后认为某分支两年只加过一次,保持 if-else 并加注释说明扩展方式,避免过度设计。
- 【L3】同一个需求(如“多渠道支付”)策略、职责链、状态都说得通,怎么破局?回到三个问题:多个处理者是互斥还是叠加(互斥→策略,叠加→职责链)?行为随对象生命周期变化吗(是→状态)?选择由调用方决定还是对象自身决定(调用方→策略,自身→状态)?支付渠道是互斥多选一、由调用方指定,答案是策略;若追问“支付过程中重试/降级切换渠道”,才引入状态维度。
- 【L4】什么时候应该拒绝“上模式”的重构提案?三个信号:分支数量少且两年内无新增记录(git 可验证)、引入模式后类数增长超过 5 倍而逻辑不变、团队成员无人熟悉该模式且无时间培训。模式是负债不是资产,每个新增类都有维护成本;拒绝时要给出数据(修改频率统计),而不是只说“没必要”。
- 【L4】遗留系统改造时,模式引入的正确节奏是什么?绞杀者模式(Strangler):不推倒重写,在边界处引入新模式——先定义抽象接口包住旧逻辑(适配器/防腐层),新需求走新结构,旧分支逐步迁移。每一步可回滚、可灰度;避免一次性重构引入大爆炸风险。这也是选型能力的一部分:选对模式不如选对节奏。
🏭 实战场景
详情
- 订单履约流程选型(典型架构评审场景口径):团队在讨论“订单履约流程”的重构:订单创建后需依次执行风控、拆单、库存锁定、支付、发货,不同商品类型(实物/虚拟/跨境)流程步骤不同,跨境还要加清关步骤。有人建议策略模式,有人建议职责链,有人建议状态机。三个候选要逐个排除:状态机描述的是订单生命周期状态流转(待支付→已支付→已发货),解决的是“状态 + 动作”合法性问题,不解决流程编排;策略是互斥多选一,但这里是步骤叠加而非替换。正解是职责链/管道:每个步骤是无状态处理器,按商品类型组装不同的链(跨境链插入清关节点);再结合状态机校验每步前后的状态合法性。同时提醒:链的组装配置化,新商品类型上线不改代码只加配置。
⚠️ 常见误区
详情
常见误区:
- ❌ “模式用得越多代码越好” → 模式是负债不是资产,每个新增类都有维护成本;选型先看修改频率与影响面,多数 CRUD 不需要模式。
- ❌ “策略和职责链都能处理多分支,随便选一个” → 互斥多选一用策略,叠加/逐级处理用职责链,选错会导致请求白走完所有节点或漏处理。
- ❌ “重构就该一步到位换成模式” → 遗留系统应用绞杀者节奏:先包抽象、新需求走新结构、旧分支逐步迁移,每步可回滚;大爆炸式重写风险最高。
🔀 发散问题
- Q:选型和消灭 if-else 是一回事吗? → 高度相关:if-else 的分类(行为分支/串行校验/固定流程/状态嵌套)直接决定选哪种模式,见本文档「如何用设计模式消除大量 if-else 硬编码?」。
【中等】如何用设计模式消除大量 if-else 硬编码?⭐⭐⭐⭐⭐
🎯 目标等级:L3-L4 | ⏱ 建议用时:15 min | 🏷 标签:设计模式 / 重构实战
💎 关键结论
关键是分清 if-else 的类型再对症下药:行为分支→策略+工厂,串行校验→职责链,固定流程→模板方法,状态嵌套→状态机,规则频变→规则引擎/配置化。消灭 if-else 的目标是隔离变化,不是消灭分支本身。
⚡记忆卡片
- 口诀:先分类、再开药,目标是隔离变化
- 关键词:分支分类学 / 策略注册表 / 枚举 key / 双跑比对
- 链路:识别分支类型 → 匹配模式 → 注册表路由 → 验证新旧一致
📖 核心知识
这是模式落地最高频的实战题,答题关键是分清 if-else 的类型再对症下药:
1. 根据参数选择不同逻辑(行为分支)→ 策略 + 工厂
- 定义策略接口(如
PayStrategy.pay()),每个分支一个实现类;Spring 中把所有实现注入为Map<String, Strategy>,按业务类型 key 路由。 - 新增分支只需加一个
@Component实现类,存量代码零修改(开闭原则)。
2. 多个条件依次校验(串行判断)→ 职责链
- 如风控规则链、参数校验链,每个节点独立可插拔,按“拒绝率高、耗时短”的顺序编排。
3. 流程固定、细节不同 → 模板方法
- 抽象类定骨架,差异步骤抽象由子类实现,消除多个分支中重复的前后置逻辑。
4. 状态判断嵌套行为 → 状态模式/状态机
- 订单状态流转中“当前状态 + 动作”的组合判断,用状态机把非法转移直接挡掉。
5. 规则频繁变化 → 规则引擎/配置化
- 策略路由表、折扣规则放配置中心或规则引擎(QLExpress、Drools),不发版调整。
改造前后对比
改造前:
if ("ALIPAY".equals(channel)) {
// 50 行支付宝逻辑
} else if ("WECHAT".equals(channel)) {
// 50 行微信逻辑
} // 新增渠道继续加 else if...改造后:payStrategyMap.get(channel).pay(order),新增渠道只需新增一个策略实现。
注意边界:分支只有两三个且永不变时,过度设计反而增加类数量与阅读成本;消灭 if-else 的目标是隔离变化,不是消灭分支本身。
🔬 扩展知识
详情
- 【L3】if-else 分类学的补充维度:除了按“行为分支/串行校验/固定流程/状态嵌套”分类,还要看分支条件的性质:按类型分发的用多态(对象自身行为);按业务参数路由的用策略注册表;按数据特征判断的(如
amount > 1000)往往不该用模式,规则引擎或表达式(SpEL、Aviator)更合适。 - 【L3】源码级案例:Spring
BeanDefinitionParser体系把 XML 标签解析的 switch 改为解析器注册表;HandlerMapping链把 URL 匹配的 if-else 改为策略族遍历;MyBatis 动态 SQL 把条件分支交给SqlNode树(解释器)。读源码时留意框架“消灭 switch”的手法是加分项。 - 【L4】落地风险与量化:重构后路由 key 拼写错误会从编译期错误变成运行期 NPE(
map.get(key)返回 null)——需用枚举作 key 或注册时校验;改造前先统计分支数量与新增频率(git log),分支 > 5 且近一年新增 ≥ 2 次才值得动手,否则成本大于收益。 - 【L3】策略注册表方案中,如何避免“找不到策略时返回 null 引发 NPE”?三层防线:key 用枚举而非字符串(编译期防拼写错误);注册表初始化时校验所有枚举值都有实现(启动快速失败);查找失败抛带上下文的业务异常而非返回 null。也可提供默认策略(Default 实现)兼容未知渠道,但要显式决策而非偶然行为。
- 【L4】Java 17 的 switch 模式匹配/密封类能不能替代策略模式?部分可以:sealed 类 + switch 模式匹配让编译器穷举检查分支(漏分支直接编译错误),分支少且稳定时比策略模式更轻量、无类爆炸。但策略模式的优势在于开闭(新增不改存量)与跨模块扩展(新 jar 加实现);分支频繁新增或实现在不同模块时仍选策略。
- 【L4】重构后如何验证新旧逻辑一致?标准手法:双跑比对(新旧实现同时执行、比对结果、只以旧结果为准)、特性开关灰度切流、针对原 if-else 每个分支补齐参数化单测(重构前先补测试是安全前提)。不要直接上线新实现再靠线上反馈发现问题。
🏭 实战场景
详情
- 渠道分发重构(典型案例口径):订单服务
createOrder方法有 12 个 if-else 分支按渠道(APP/H5/小程序/直播/企业购……)分发,每个分支内部结构相似:参数适配→优惠计算→风控校验→下单,但优惠计算逻辑各渠道完全不同。新人每次加渠道要改 400 行的老方法,已两次改出回归 bug。先拆维度:分支内部“参数适配→优惠→风控→下单”是固定流程,渠道差异主要在优惠计算——这是模板方法 + 策略的复合:抽象AbstractChannelOrderCreator定骨架,优惠计算作为策略注入或抽象步骤;路由用Map<ChannelEnum, OrderCreator>注册表(枚举 key 防拼写错误)。落地节奏:先补全现有 12 分支的单测→重构→双跑比对→灰度。同时强调:400 行方法本身就是坏味道,即使不上模式也应先做 Extract Method,重构和模式化可分两步走降低风险。
⚠️ 常见误区
详情
常见误区:
- ❌ “消灭 if-else 就是消灭所有分支” → 目标是隔离变化,分支少且永不变时 if-else 更简单;为消灭而消灭是过度设计。
- ❌ “策略注册表用字符串 key 更灵活” → 字符串 key 拼错从编译期错误变成运行期 NPE,应用枚举 key + 启动期校验 + 查找失败抛业务异常三层防线。
- ❌ “重构完直接上线新实现” → 必须先补参数化单测,再双跑比对 + 灰度切流,只以旧结果为准;直接上线靠线上反馈发现问题是事故温床。
🔀 发散问题
- Q:行为分支和串行校验分别用什么模式? → 策略 + 工厂(多选一)与职责链(逐节点校验),见本文档「什么是策略模式?」「什么是职责链模式?」。
- Q:状态嵌套的 if-else 怎么消灭? → 状态机把“当前状态 + 动作”的非法转移直接挡掉,见本文档「什么是状态模式?」。