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

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

简单工厂模式通常是定义一个工厂类,这个类可以根据不同变量返回不同类的产品实例。
简单工厂模式是一种对象创建型模式。但是简单工厂模式不属于 23 种 Gof 设计模式之一。
【中等】什么是工厂方法模式?⭐⭐⭐⭐
工厂方法模式 (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/L4 深挖要点
- SPI 与工厂的关系:JDK
ServiceLoader、Dubbo 的扩展点加载本质上都是“工厂方法 + 配置文件”,把“创建哪个实现”的决策从代码硬编码转移到配置,符合开闭原则。 - Spring 中的体现:
FactoryBean是工厂方法模式的典型落地——getObject()由子类决定返回什么对象(如SqlSessionFactoryBean生产 SqlSessionFactory)。 - 何时不用:只有一种实现且未来不太可能扩展时,直接 new 更简单,避免过度设计;产品族创建逻辑频繁变化时才值得引入。
- 滥用反模式:为每个只有一个实现的产品都建一个工厂类,造成类数翻倍却没有任何扩展收益;更隐蔽的误用是工厂方法里塞入业务逻辑(如创建后顺手做初始化/注册),使工厂演变为上帝类。
- 量化视角:创建逻辑只有
new XXX()一行且产品类型 ≤ 3 个时,简单工厂(甚至直接 new)更合适;产品超过 5 个、创建过程有配置/缓存/校验逻辑时,工厂方法的扩展收益才开始体现。
拓展追问
- 追问 1:工厂方法模式和抽象工厂怎么选?什么信号说明该升级到抽象工厂?
工厂方法面向单一产品,通过子类继承决定创建谁;抽象工厂面向产品族,通过组合保证一组产品配套。升级信号:出现“产品 A 和产品 B 必须配套”的约束(如 MySQL 的 Connection 不能配 Oracle 的 Statement),此时单产品工厂会产生非法组合,需要抽象工厂保证族内一致性。 - 追问 2:
Collection.iterator()为什么算工厂方法模式?
工厂方法的本质是把对象创建委派给子类/实现类决定:ArrayList.iterator()返回内部类Itr,LinkedList返回自己的迭代器实现,客户端面向Iterator接口编程而不感知具体实现。注意它与简单工厂的差别:工厂方法是多态创建(每个集合自己决定),简单工厂是集中式创建(一个工厂类 switch)。 - 追问 3:Spring 中
FactoryBean和普通 Bean 有什么区别?什么场景必须用 FactoryBean?
普通 Bean 由容器通过反射直接实例化;FactoryBean是“生产 Bean 的 Bean”,getObject()的返回值才是目标对象(getBean("x")返回产品,getBean("&x")返回工厂自身)。必须用的场景:目标对象的创建过程无法用构造器表达,如 MyBatis 的SqlSessionFactoryBean生产SqlSessionFactory、JdbcTemplate类代理对象的生成。
场景题
导出模块支持 Excel/CSV 三种格式,代码里
ExportService用 if-else 判断 format 参数后直接new ExcelExporter()/new CsvExporter(),且创建后还要各自设置不同的默认配置、注册到导出记录表。新增 PDF 格式时,除了新增类还要改ExportService。
分析要点:这里有两个层次的问题:创建逻辑(new + 默认配置 + 注册)与路由逻辑(if-else)混在业务类里,违反 OCP。拆法上先把“创建 + 初始化”收敛到工厂(每种格式一个工厂方法,或简单工厂 + 注册表),再把路由改为 Map<String, ExporterFactory> 查表;同时评估规模——若格式长期只有三种且不再扩展,简单工厂已够用,不必升级到每个格式一个工厂类的工厂方法体系,避免过度设计。
【中等】什么是抽象工厂模式?⭐⭐
抽象工厂模式 (Abstract Factory) 用于创建相关或依赖的产品族。
代码骨架

- 抽象产品 (Abstract Product) 为构成系列产品的一组不同但相关的产品声明接口。
- 具体产品 (Concrete Product) 是抽象产品的多种不同类型实现。 所有变体 (维多利亚/现代) 都必须实现相应的抽象产品 (椅子/沙发)。
- 抽象工厂 (Abstract Factory) 接口声明了一组创建各种抽象产品的方法。
- 具体工厂 (Concrete Factory) 实现抽象工厂的构建方法。 每个具体工厂都对应特定产品变体, 且仅创建此种产品变体。
- 尽管具体工厂会对具体产品进行初始化, 其构建方法签名必须返回相应的抽象产品。 这样, 使用工厂类的客户端代码就不会与工厂创建的特定产品变体耦合。 客户端 (Client) 只需通过抽象接口调用工厂和产品对象, 就能与任何具体工厂/产品变体交互。
应用场景
- 多数据库支持(DAO 层):为 MySQL、Oracle 等数据库分别实现用户、订单等 DAO 对象,切换数据库时只需更换工厂,保证各操作类兼容。
- 框架集成:Spring 的
BeanFactory可视为抽象工厂变体,按环境(开发/生产)创建配置相关的 Bean 组。
【中等】工厂模式和抽象工厂模式有什么区别?⭐⭐⭐
- 工厂方法:一个工厂生产一个产品,通过继承创建。
- 抽象工厂:一个工厂生产一簇产品,通过组合创建,强调产品之间的配套约束。
【中等】什么是建造者模式?⭐⭐⭐
建造者模式 (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的所有实现
与工厂模式区别
- 工厂:一次性创建,侧重产品类型
- 建造者:分步创建,侧重产品内部结构
【中等】什么是原型模式?⭐⭐
原型模式 (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等方法来识别。
结构型模式
【中等】什么是适配器模式?⭐⭐⭐
适配器模式 (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对象)
【简单】什么是桥接模式?⭐⭐
桥接模式 (Bridge) 用于抽象与实现分离,独立变化。
代码骨架

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

【简单】什么是组合模式?⭐⭐
组合模式 (Composite) 用于树形结构表示整体-部分。
代码骨架

- 组件 (Component) 接口描述了树中简单项目和复杂项目所共有的操作。
- 叶节点 (Leaf) 是树的基本结构, 它不包含子项目。一般情况下, 叶节点最终会完成大部分的实际工作, 因为它们无法将工作指派给其他部分。
- 容器 (Container)——又名 “组合 (Composite)”——是包含叶节点或其他容器等子项目的单位。 容器不知道其子项目所属的具体类, 它只通过通用的组件接口与其子项目交互。容器接收到请求后会将工作分配给自己的子项目, 处理中间结果, 然后将最终结果返回给客户端。
- 客户端 (Client) 通过组件接口与所有项目交互。 因此, 客户端能以相同方式与树状结构中的简单或复杂项目交互。
应用场景
- 文件系统:目录(文件夹)包含文件或子目录,统一提供获取大小、删除等操作,客户端无差别对待单个文件或整个目录。
- 缓存组合:如 Spring Cache 的
CompositeCacheManager,组合多个缓存管理器,统一处理缓存操作。
【中等】什么是装饰器模式?⭐⭐⭐
装饰模式 (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给缓存装饰事务感知能力,CachedIntrospectionResults、MyBatis 的Cache装饰器链(SynchronizedCache包LruCache包PerpetualCache)也是经典嵌套。 - 选型权衡(装饰器 vs 代理):两者结构几乎同构,差异在意图与生命周期:装饰器增强功能、由客户端显式传入被包装对象、可叠加;代理控制访问、代理自己创建/管理目标对象、客户端无感知。结构一样时看“谁创建目标、目的是增强还是控制”。
- 滥用反模式:装饰层次过深导致
new A(new B(new C(stream)))可读性差、调试栈冗长;每层都持有引用增加内存开销。工程上用建造者/工厂封装组装过程(如 IO 工具类统一构建),把嵌套藏在边界内。 - 量化视角:需要自由组合的能力超过 3 种时才值得引入;若组合固定只有一种,直接继承/子类化更简单(但要警惕子类组合爆炸:n 个能力用继承需要 2ⁿ 个子类)。
拓展追问
- 追问 1:装饰器和继承都能“增强功能”,为什么优先选装饰器?
继承是编译期静态增强,能力组合靠类爆炸(缓冲 + 压缩 + 加密需要三个组合就需 7 个子类);装饰器是运行期动态组合,能力正交、自由叠加、可随时拆除。代价是多层包装增加调用链长度和调试复杂度,因此能力维度 ≥ 2 且需自由组合时才值得。 - 追问 2:Java IO 流为什么选装饰器而不是每个组合一个类?
IO 能力维度(缓冲/压缩/加密/对象化)与数据源维度(文件/网络/字节数组)是笛卡尔积关系,硬编码组合会造成类爆炸;装饰器把能力正交化:数据源提供基础流,能力装饰器自由叠加(new ObjectInputStream(new BufferedInputStream(new FileInputStream(...)))),新增能力只需加一个装饰器类。 - 追问 3:Servlet 中用
HttpServletRequestWrapper装饰请求的典型场景是什么?
最典型是请求体重复读取:包装类在构造时把 body 缓存为字节数组,重写getInputStream()每次返回新的ByteArrayInputStream,解决日志过滤器读掉 body 后 Controller 读不到的问题;ContentCachingRequestWrapper(Spring 提供)同理。
场景题
报表导出接口需要按租户动态叠加:压缩、加密、水印。现有代码用继承实现了 6 个子类,新需求“部分租户只要压缩+水印”上线后发现没有对应子类,只能临时又加了一个类,子类已膨胀到 9 个。
分析要点:这是典型的“能力组合爆炸”:压缩/加密/水印三个能力用继承表达需要穷举 2³=8 种组合。重构方案:定义 ReportWriter 接口,基础实现输出原始内容,压缩/加密/水印各做一个装饰器,运行时按租户配置动态叠加;组装逻辑放在工厂/Builder 中,避免业务代码里出现多层 new 嵌套。同时提醒:装饰顺序有语义(先加密再压缩 ≠ 先压缩再加密),需在组装处显式约定并写清注释。
【中等】什么是外观模式?⭐⭐
外观模式 (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()屏蔽绑定发现与具体实现差异。 - 选型权衡(外观 vs 适配器 vs 中介者):外观简化子系统对外的入口(多对一、单向简化);适配器让不兼容的接口协作(转换已有接口);中介者集中多个同事对象间的网状交互(双向协调)。判别口诀:简化入口用外观,转换接口用适配器,协调交互用中介者。
- 滥用反模式:外观类越加越多变成新的上帝类(“万能 Service”);外观屏蔽过度导致高级用户无法绕过,只能再开旁路接口。工程实践:外观只做编排不做业务逻辑,复杂逻辑仍下沉到子系统;同时保留子系统接口的可见性,让特殊场景可以绕过外观。
- 量化视角:子系统对象 ≥ 3 个、调用方需理解调用顺序/初始化约束时才值得封装;只有一两个对象的“门面”纯属多一层间接。
拓展追问
- 追问 1:外观模式和聚合服务(BFF/应用服务层)是什么关系?
微服务架构中的 BFF(Backend For Frontend)层本质就是外观:把订单、库存、用户等多个下游服务的调用编排成一个面向前端的接口,屏蔽下游的调用顺序、容错、数据组装细节。差别在于外观是类级别,BFF 是服务级别,设计约束一致:只编排不下沉业务规则。 - 追问 2:SLF4J 为什么叫日志门面?它和适配器是什么关系?
SLF4J API 对业务代码是外观——统一入口、屏蔽 Logback/Log4j 差异;而对具体日志实现则通过StaticLoggerBinder/SPI 绑定,绑定层本质是适配器(如log4j-over-slf4j把 Log4j API 适配到 SLF4J)。同一套机制在不同视角下呈现两种模式,说明模式分类看意图不看结构。 - 追问 3:什么时候应该拒绝“加一个门面统一入口”的提案?
当门面只是把调用原样转发、没有任何简化或编排价值时,它只增加跳转成本;当子系统接口本身已经足够简单稳定(如 JDK 集合 API),再包一层只会造成双重维护。外观的价值在“简化复杂度”,复杂度没被简化就是伪外观。
场景题
业务方接入短信服务需要:选供应商(阿里云/腾讯云)、模板报备、签名管理、频控、失败重试、降级切换,现在每个业务方都自己拼这套流程,已有 6 个业务方各写了一份,且有一家漏了频控被供应商封了号。
分析要点:这是外观模式的教科书场景——子系统对象多(供应商 SDK、模板服务、频控组件、重试组件)、调用有顺序约束(先报备再发送)、出错后果重。方案:建 SmsFacade.send(scene, params) 统一入口,内部编排模板选择→频控校验→供应商路由→重试降级;同时保留直连供应商 SDK 的能力作为旁路。要强调取舍:门面要收敛“必须经过的逻辑”(频控/审计),可选项(如自定义模板)留接口参数,避免门面膨胀成上帝类。
【中等】什么是享元模式?⭐⭐
享元模式 (Flyweight) 共享细粒度对象,节省内存。
代码骨架

- 享元模式只是一种优化。 在应用该模式之前, 你要确定程序中存在与大量类似对象同时占用内存相关的内存消耗问题, 并且确保该问题无法使用其他更好的方式来解决。
- 享元 (Flyweight) 类包含原始对象中部分能在多个对象中共享的状态。 同一享元对象可在许多不同情景中使用。 享元中存储的状态被称为 “内在状态”。 传递给享元方法的状态被称为 “外在状态”。
- 情景 (Context) 类包含原始对象中各不相同的外在状态。 情景与享元对象组合在一起就能表示原始对象的全部状态。
- 通常情况下, 原始对象的行为会保留在享元类中。 因此调用享元方法必须提供部分外在状态作为参数。 但你也可将行为移动到情景类中, 然后将连入的享元作为单纯的数据对象。
- 客户端 (Client) 负责计算或存储享元的外在状态。 在客户端看来, 享元是一种可在运行时进行配置的模板对象, 具体的配置方式为向其方法中传入一些情景数据参数。
- 享元工厂 (Flyweight Factory) 会对已有享元的缓存池进行管理。 有了工厂后, 客户端就无需直接创建享元, 它们只需调用工厂并向其传递目标享元的一些内在状态即可。 工厂会根据参数在之前已创建的享元中进行查找, 如果找到满足条件的享元就将其返回; 如果没有找到就根据参数新建享元。
应用场景
使用示例: 享元模式只有一个目的: 将内存消耗最小化。 如果你的程序没有遇到内存容量不足的问题, 则可以暂时忽略该模式。
享元模式在核心 Java 程序库中的示例:
识别方法: 享元可以通过构建方法来识别, 它会返回缓存对象而不是创建新的对象。
【中等】什么是代理模式?⭐⭐⭐⭐
代理模式 (Proxy) 用于控制对象访问。
代码骨架

- 服务接口 (Service Interface) 声明了服务接口。 代理必须遵循该接口才能伪装成服务对象。
- 服务 (Service) 类提供了一些实用的业务逻辑。
- 代理 (Proxy) 类包含一个指向服务对象的引用成员变量。 代理完成其任务 (例如延迟初始化、 记录日志、 访问控制和缓存等) 后会将请求传递给服务对象。 通常情况下, 代理会对其服务对象的整个生命周期进行管理。
- 客户端 (Client) 能通过同一接口与服务或代理进行交互, 所以你可在一切需要服务对象的代码中使用代理。
应用场景
- 远程代理:隐藏对象位于不同地址空间的事实,如 RPC 框架的客户端 Stub、Feign 动态代理。
- 虚拟代理:延迟创建开销大的对象,直到真正需要时(如 Hibernate 懒加载、大图片占位)。
- 保护代理:控制访问权限,校验调用者身份(如 Spring 方法级安全注解)。
- 智能引用:在访问时附加额外操作,如访问计数、日志记录、性能监控。
- 缓存代理:缓存方法结果,减少重复计算(如 Spring @Cacheable)。
- 防火墙代理:保护目标免受恶意访问,控制网络资源。
实现方式
- 静态代理:手动编写代理类,编译前确定。
- 动态代理:运行时生成代理,如 JDK 动态代理(基于接口)和 CGLIB(基于子类)。
L3/L4 深挖要点(高频追问)
- 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:
- 经典坑——自调用失效:同类中方法 A 调用方法 B(
this.B())不走代理对象,B 上的@Transactional/@Cacheable失效。解法:注入自身、AopContext.currentProxy()或拆到不同 Bean。 - 实践视角:代理层数过深会增加栈深度与调用开销,高频路径应避免多层代理嵌套;网关鉴权、AOP 横切、RPC Stub 是我们项目中代理模式的三处落地。
- 源码实证补充:Spring AOP 入口在
AbstractAutoProxyCreator.postProcessAfterInitialization(),JDK 路径由JdkDynamicAopProxy.invoke()处理(其本身实现InvocationHandler),CGLIB 路径由ObjenesisCglibAopProxy生成子类;AbstractAdvisorAutoProxyCreator负责筛选匹配的切面。MyBatis 的Plugin.wrap()用 JDK 动态代理层层包装Executor实现插件链。 - 滥用反模式:为每个类都包代理做日志/监控,造成栈深与 GC 压力;在热路径上用动态代理做简单委派(反射开销不可忽视,高频场景考虑方法句柄或静态代理);用代理解决本该用职责链/事件解决的问题,导致代理嵌套失控。
拓展追问
- 追问 1:JDK 动态代理为什么要求目标必须有接口?CGLIB 为什么不能代理 final 方法?
JDK 动态代理生成的$Proxy类继承Proxy并实现目标接口,调用通过接口方法分派到InvocationHandler,没有接口就无从实现;CGLIB 通过生成目标类的子类拦截方法,final 类不能继承、final 方法不能被重写,因此无法拦截(private 方法同理,不会被子类覆写)。 - 追问 2:
@Transactional失效除了自调用还有哪些常见原因?
高频原因:方法不是 public(Spring AOP 默认只代理 public 方法);异常被 catch 吞掉未抛出;抛出的是检查型异常而未配rollbackFor;数据库引擎不支持事务(如 MyISAM);Bean 没被 Spring 管理。排查顺序从“代理是否生效”到“事务传播行为”再到“异常类型”。 - 追问 3:代理模式和装饰器结构相同,怎么在代码评审时区分两者的使用是否恰当?
看两点:目标对象由谁创建(代理自己创建/管理目标,装饰器由客户端传入)和意图(代理控制访问——客户端本可以不经过代理直接访问目标;装饰器增强功能——客户端主动选择叠加能力)。若一个“代理”允许客户端任意换包装对象并叠加多层,它实际上是装饰器,命名和设计都应按装饰器组织。
场景题
监控团队要求所有对外 RPC 接口加耗时埋点,有人提议给每个 Service 写静态代理类;另一个提议用
HandlerInterceptor;还有人提议自定义注解 + AOP。三种方案摆在面前,怎么选?
分析要点:静态代理侵入性强,每加一个方法都要同步改代理类,维护成本 O(n),仅适合无框架的裸接口;Interceptor 只能拦截 HTTP 入口,RPC 内部调用覆盖不到;注解 + AOP(@Around 记录耗时)侵入最小、可精确到方法级、随业务代码演进自动覆盖,是首选。权衡点:AOP 方案要控制切面匹配范围(pointcut 收窄到注解标注的方法),避免全量拦截带来的反射开销;同时埋点逻辑要异步上报,不能阻塞业务主路径。
【中等】装饰器、适配器、代理、桥接这四种设计模式有什么区别?⭐⭐⭐
| 模式 | 核心意图 | 结构特点 | 典型场景 |
|---|---|---|---|
| 装饰器 | 动态增强对象功能 | 包装真实对象,接口一致,递归组合 | IO 流、Spring 事务注解 |
| 适配器 | 接口转换,让不兼容的类协同工作 | 包装被适配者,实现目标接口 | 日志门面 SLF4J、第三方库适配 |
| 代理 | 控制对象访问,延迟加载或权限控制 | 代理与目标实现同一接口,代理持有引用 | RPC 客户端、AOP 代理、懒加载 |
| 桥接 | 分离抽象与实现,独立变化 | 抽象持有实现接口的引用,二者可各自扩展 | 跨平台 UI 组件、JDBC 驱动 |
行为型模式
【中等】什么是模板方法模式?⭐⭐⭐
模板方法模式 (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()构建批量读逻辑。
- 模板方法 vs 策略的本质差异:模板方法用继承固定骨架、子类覆写步骤,变化点编译期确定;策略用组合注入算法,运行期可切换。判别:子类只改一两个步骤且骨架稳定→模板方法;整个算法需要动态替换→策略。Java 8+ 也可用函数式参数(回调)替代继承式模板方法(如
JdbcTemplate)。 - 滥用反模式:继承层次过深导致流程难以理解(模板方法在父类、细节散在多层子类,调试时频繁跳类);父类骨架被频繁修改时所有子类被动受影响(父类变更影响面不可控);子类覆写时调用
super追加行为,说明抽象边界设计错误。 - 量化视角:流程步骤 ≥ 3 个且 80% 逻辑公共时才值得提取骨架;若子类间差异大于共性,说明抽象错误,应改用组合(策略/管道)而非继承。
拓展追问
- 追问 1:AQS 中哪些是模板方法,哪些是钩子方法?为什么这样设计?
acquire()/release()/acquireShared()是模板方法——定义 CAS + 入队 + park 的固定骨架,不允许子类覆写;tryAcquire()/tryRelease()/tryAcquireShared()是钩子,默认抛UnsupportedOperationException强制子类实现。这样设计把“排队/阻塞的复杂通用逻辑”锁在父类(保证正确性),把“独占/共享语义”留给子类(保证灵活性),是模板方法的教科书级应用。 - 追问 2:模板方法用继承,Java 8 之后有什么替代方案?
函数式回调:把步骤作为Function/Consumer参数传入骨架方法(JdbcTemplate.query(sql, rowMapper)即如此),或直接用default方法在接口里提供骨架。优势是不占用继承位、可运行期组合;代价是丢失了编译期类型层次的可见性。继承已用满或步骤间需共享状态时仍选模板方法。 - 追问 3:Spring 中
JdbcTemplate是模板方法还是模板模式?两者什么关系?JdbcTemplate是 GoF 模板方法的回调变体:骨架(开连接/执行/关资源/异常转译)写在JdbcTemplate自身,用户通过RowMapper/StatementCallback回调填充差异步骤,而非继承子类。Spring 把这种用法泛化为 Template 家族(RedisTemplate、RestTemplate),本质都是“固定流程 + 回调填充变化点”。
场景题
数据同步任务有 MySQL 版和 ES 版:两边代码结构几乎一样(连接源→分批读取→字段转换→写入目标→更新位点),但两份代码各自演化,已出现“改了 MySQL 版的分批逻辑忘了同步 ES 版”的线上 bug。
分析要点:两份代码流程骨架相同、细节不同,是模板方法的典型场景。方案:提取 AbstractSyncJob 定义骨架(分批大小、位点提交顺序、失败重试策略统一收在父类),read()/transform()/write() 抽象由两个子类实现。同时说明边界:若未来出现第三种完全不同的流程(如流式同步),继承体系会僵化,应预留钩子或改用策略组合;骨架一旦发布应遵循开闭原则,修改骨架要评估所有子类的受影响面。
【中等】什么是策略模式?⭐⭐⭐⭐
策略模式 (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/L4 实战要点
- 消灭 if-else 的标准套路:Spring 中把策略接口的多个实现注入为
Map<String, Strategy>(key 为 Bean 名),按业务类型路由;新增策略只需加一个@Component实现,符合开闭原则。 - 策略路由配置化:策略映射放配置中心,可不发版切换算法(如 Dubbo 负载均衡算法可运行时切换)。
- 与其他模式组合:策略 + 工厂(工厂负责选策略)、策略 + 模板方法(策略内部骨架固定)。
- 避免过度设计:分支只有两三个且稳定不变时,枚举 + switch 更简单;硬套策略会造成类爆炸。
- 源码实证(精确到类):
Comparator是最纯的策略——Arrays.sort(T[], Comparator)把比较算法作为参数注入;Dubbo 的LoadBalance接口有RandomLoadBalance/RoundRobinLoadBalance/LeastActiveLoadBalance等实现,通过 SPI 按配置加载;Spring MVC 的HandlerMapping是策略族(RequestMappingHandlerMapping、BeanNameUrlHandlerMapping);ThreadPoolExecutor的拒绝策略RejectedExecutionHandler(AbortPolicy/CallerRunsPolicy等)。 - 选型权衡(策略 vs 状态):两者结构几乎相同,差异在意图与切换主体:策略由客户端选择且各算法互相独立(排序选快排还是堆排);状态由对象自身随生命周期自动流转,状态间常知道彼此(订单从待支付转已支付)。判别口诀:外部选算法用策略,内部随生命周期流转用状态。
- 滥用反模式:每个 if 分支抽一个策略类导致类爆炸(一行逻辑配一个类);策略接口被迫不断加方法以适配新策略,接口腐化;策略间存在隐式依赖(共享静态状态)导致替换不安全。
拓展追问
- 追问 1:Spring 中注入
Map<String, Strategy>的原理是什么?key 从哪来?
容器启动时DefaultListableBeanFactory.resolveMultipleBeans()发现注入点是 Map 且 value 类型匹配,会收集所有该类型的 Bean,key 默认为 Bean 名。实践中策略类用@Component("ALIPAY")显式命名,或实现getType()方法后在初始化时重建 Map,避免 key 与 Bean 名语义耦合。 - 追问 2:策略模式和模板方法经常一起出现,如何分工?
策略封装可替换的整体算法(维度间互斥),模板方法封装固定流程中的变化步骤(维度内共享)。典型组合:策略接口的抽象基类用模板方法定义公共骨架(参数校验→执行→埋点),具体策略只覆写核心步骤;Spring 的AbstractHandlerMapping+ 各HandlerMapping实现即是此结构。 - 追问 3:策略类之间有公共逻辑怎么处理?重复写三遍还是提取父类?
优先提取抽象基类放公共骨架(模板方法),但警惕继承层次过深;若公共逻辑是独立能力(如埋点、限流),优先用装饰器/AOP 横切而非继承。判别:公共部分与策略算法耦合紧密→继承;正交能力→组合。
场景题
优惠计算模块有 15 个 if-else 分支:满减、折扣、第二件半价、拼团、会员价……,且存在叠加规则(会员价可与满减叠加)。有人建议每个优惠抽一个策略类,用策略模式重构。
分析要点:单纯策略模式只能解决“多选一”,但优惠存在叠加/互斥规则,说明需求本质是“规则组合求值”而非“算法替换”。合理方案:每个优惠定义成 PromotionRule(策略),叠加/互斥关系用责任链或规则引擎编排(可叠加的规则链式求值,互斥规则取最优);叠加规则本身配置化,避免硬编码。同时给出规模判断:15 个分支且持续新增,重构收益明确;若只有两三个稳定优惠,枚举 + switch 更划算。
【中等】什么是观察者模式?⭐⭐⭐
观察者模式 (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 事件机制:
- 滥用反模式(高频考点):
- 流程隐藏:下单后发了事件,谁在监听、执行顺序如何完全靠 IDE 全局搜索才能发现,事件驱动容易演变成“隐式调用链”,排查问题困难;关键主流程慎用。
- 同步监听拖慢主流程:默认同步执行时,监听器里调第三方接口会直接拖垮主接口 RT;需显式改异步(
@Async/MQ)并处理失败补偿。 - 事件风暴:每个业务动作都发事件,监听关系变成网状,系统行为不可推理。
- 选型权衡(观察者 vs 消息队列):进程内观察者适合同步、强一致性要求低的联动;跨服务、需持久化/重试/削峰时升级到 MQ(MQ 本质是分布式观察者 + 持久化)。注意 Spring 事件默认同事务,监听器失败会导致主事务回滚(
@TransactionalEventListener可改为提交后触发)。 - 量化视角:联动方 ≥ 2 个且预期会增减时才值得事件化;只有 1 个固定联动方时直接显式调用更清晰。
拓展追问
- 追问 1:Spring 事件监听是同步还是异步?监听器抛异常会怎样?
默认同步(SimpleApplicationEventMulticaster未配置 Executor 时),在发布者线程逐个调用;监听器抛异常会中断后续监听器并向发布者抛出,若发布在事务内则导致回滚。异步需自定义 multicaster 配置TaskExecutor或对监听器加@Async,但异步后异常不再反馈给发布者,需自行记录日志/补偿。 - 追问 2:
@EventListener和实现ApplicationListener接口有什么区别?
接口方式是显式强类型,需实现onApplicationEvent(),可配合@Order控制顺序;注解方式由EventListenerMethodProcessor扫描,支持 SpEL 条件过滤(condition属性)和多事件类型,代码更简洁。需要泛型事件(ApplicationEvent子类)精确分发时用接口方式更可控。 - 追问 3:什么情况下应该把进程内事件升级为消息队列?
三个信号:监听方变成独立服务(跨进程)、事件需要可靠投递/重试(进程内事件随进程崩溃丢失)、发布订阅数量增长到需要削峰。升级时保留事件对象结构不变,只换投递通道,这也是先做进程内事件设计的收益——迁移成本低。
场景题
下单成功后需要联动:扣库存、送积分、发短信、通知仓库发货。目前用同步代码依次调用四个服务,任何一个失败整个下单接口就报错,且每次新增联动方都要改下单主流程。有人建议改成
publishEvent(OrderCreatedEvent)。
分析要点:先分清哪些联动能接受最终一致:发短信/送积分/通知仓库适合异步事件(失败重试补偿),扣库存必须留在主事务内同步执行(强一致),不能盲目全改事件。方案:库存扣减保留在主流程,其余改为 Spring 事件 + @TransactionalEventListener(AFTER_COMMIT) 或 MQ,发布前需确保订单已落库;同时提醒:事件化后链路可视化变差,需配套事件日志/监控,避免隐式调用链失控。
【中等】什么是状态模式?⭐⭐⭐
状态模式 (State):状态改变行为。
代码骨架

- 上下文 (Context) 保存了对于一个具体状态对象的引用, 并会将所有与该状态相关的工作委派给它。 上下文通过状态接口与状态对象交互, 且会提供一个设置器用于传递新的状态对象。
- 状态 (State) 接口会声明特定于状态的方法。 这些方法应能被其他所有具体状态所理解, 因为你不希望某些状态所拥有的方法永远不会被调用。
- 具体状态 (Concrete States) 会自行实现特定于状态的方法。 为了避免多个状态中包含相似代码, 你可以提供一个封装有部分通用行为的中间抽象类。
- 状态对象可存储对于上下文对象的反向引用。 状态可以通过该引用从上下文处获取所需信息, 并且能触发状态转移。
- 上下文和具体状态都可以设置上下文的下个状态, 并可通过替换连接到上下文的状态对象来完成实际的状态转换。
应用场景
- Spring 状态机
- 将订单、流程等状态流转抽象为状态机模型,通过配置状态、事件、转换来管理复杂的业务状态。
- 核心要素包括当前状态、触发事件、响应函数、目标状态。
- Netty 网络框架
- 在通道处理器(
ChannelHandler)中存储连接状态(如登录态、协议版本),根据状态决定消息处理逻辑。 - 实现有状态协议(如同步请求-响应)时,通过状态变量控制消息发送时机,避免并发冲突。
- 在通道处理器(
【中等】什么是职责链模式?⭐⭐⭐⭐
职责链模式 (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/L4 实战要点
- 两种链形态:纯职责链(找到一个能处理的就终止,如审批流)与不纯链(所有节点都处理,如 Filter/Interceptor——每个 Filter 处理完主动调用
chain.doFilter()传递,本质是递归调用)。 - 源码印证:Netty
ChannelPipeline用双向链表存 handler,入站事件从头向尾传播,支持运行时动态增删;MyBatis 插件是多层Plugin.wrap()代理嵌套,形成洋葱模型,外层先进后出。 - 工程细节:链上节点应无状态(单例共享);按“耗时短、拒绝率高”的顺序编排节点(参数校验放最前)让请求尽早失败;链的组装可用建造者/配置化,支持热插拔。
- 源码实证补充(精确到类):Servlet 的
ApplicationFilterChain(Tomcat 实现)用数组 + 索引推进doFilter();Spring MVC 的HandlerExecutionChain聚合所有HandlerInterceptor;Spring Security 的FilterChainProxy持有多个SecurityFilterChain,内部VirtualFilterChain逐个推进;Spring 容器侧BeanPostProcessor由PostProcessorRegistrationDelegate按@Order/PriorityOrdered排序后依次执行。 - 滥用反模式:链上节点间存在隐式依赖(后节点依赖前节点塞入的上下文变量,顺序一调就坏);把本应多选一的逻辑写成链导致每个请求白走完所有节点;链过长且无监控,某个节点变慢拖垮整链 RT——关键链需要节点级耗时埋点。
拓展追问
- 追问 1:Servlet 过滤器链的执行顺序由什么决定?
chain.doFilter()前后代码分别什么时候执行?
顺序由 web.xml 声明顺序或@WebFilter/Spring Boot 中FilterRegistrationBean.setOrder()决定,值小先执行。doFilter()之前的代码在请求进入阶段执行,之后的代码在响应返回阶段执行(递归退栈),这是洋葱模型:最先进入的过滤器最后退出,因此异常处理和响应改写要注意层级。 - 追问 2:MyBatis 插件和 Servlet Filter 都是职责链,实现方式有何不同?
Filter 是链式推进(持有下一个节点引用,迭代/递归调用);MyBatis 插件是代理嵌套(每个Interceptor用Plugin.wrap()把目标包一层 JDK 动态代理,多个插件形成洋葱式的代理层),执行顺序与注册顺序相反(后注册的先执行)。两者语义等价但调试时栈结构不同,需区别认识。 - 追问 3:什么场景该用职责链而不是策略模式?
策略是“多选一”(一个请求只由一个算法处理),职责链是“多对多遍历”(请求可能被多个节点处理/逐级传递直到有人接手)。判别:多个处理者都可能参与同一请求的处理、且需要中断/跳过能力→职责链;处理者互斥只选一个→策略。审批流、风控规则链、Filter 是职责链;负载均衡选节点是策略。
场景题
提现风控需要依次校验:黑名单→金额限额→频次限制→实名状态,现在代码是一个方法里四个 if 顺序硬编码,新增规则要改主方法,且不同业务线(提现/转账/兑换)需要的规则子集不同,主方法里已出现大量
if (bizType == 提现 && ...)。
分析要点:这是“规则子集按业务线组合”的场景,单纯一条固定链不够。方案:每个规则实现 RiskRule(无状态、声明拒绝率/耗时),链的组装配置化——按 bizType 从配置中选择规则列表并排序(拒绝率高的前置);纯链形态:任一规则拒绝即终止。权衡:规则间若有依赖(如频次规则依赖黑名单结果)需显式声明依赖而非隐式约定顺序;同时提醒新规则上线前用影子模式(只记录不拦截)验证误杀率。
【中等】什么是命令模式?⭐⭐⭐
命令模式 (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对象),通过网络发送。 - 消费者将接收到的消息封装为命令对象,传递给业务处理器。
- 生产者将消息封装为命令对象(如
【简单】什么是迭代器模式?⭐⭐
迭代器模式 (Iterator):顺序访问聚合元素
代码骨架

- 迭代器 (Iterator) 接口声明了遍历集合所需的操作: 获取下一个元素、 获取当前位置和重新开始迭代等。
- 具体迭代器 (Concrete Iterators) 实现遍历集合的一种特定算法。 迭代器对象必须跟踪自身遍历的进度。 这使得多个迭代器可以相互独立地遍历同一集合。
- 集合 (Collection) 接口声明一个或多个方法来获取与集合兼容的迭代器。 请注意, 返回方法的类型必须被声明为迭代器接口, 因此具体集合可以返回各种不同种类的迭代器。
- 具体集合 (Concrete Collections) 会在客户端请求迭代器时返回一个特定的具体迭代器类实体。 你可能会琢磨, 剩下的集合代码在什么地方呢? 不用担心, 它也会在同一个类中。 只是这些细节对于实际模式来说并不重要, 所以我们将其省略了而已。
- 客户端 (Client) 通过集合和迭代器的接口与两者进行交互。 这样一来客户端无需与具体类进行耦合, 允许同一客户端代码使用各种不同的集合和迭代器。
- 客户端通常不会自行创建迭代器, 而是会从集合中获取。 但在特定情况下, 客户端可以直接创建一个迭代器 (例如当客户端需要自定义特殊迭代器时)。
应用场景
java.util.Iterator的所有实现 (还有java.util.Scanner)。java.util.Enumeration的所有实现
【简单】什么是中介者模式?⭐
中介者模式 (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()
【困难】什么是访问者模式?⭐⭐
访问者模式 (Visitor):在不改变元素类前提下增加新操作
代码骨架

- 访问者 (Visitor) 接口声明了一系列以对象结构的具体元素为参数的访问者方法。 如果编程语言支持重载, 这些方法的名称可以是相同的, 但是其参数一定是不同的。
- 具体访问者 (Concrete Visitor) 会为不同的具体元素类实现相同行为的几个不同版本。
- 元素 (Element) 接口声明了一个方法来 “接收” 访问者。 该方法必须有一个参数被声明为访问者接口类型。
- 具体元素 (Concrete Element) 必须实现接收方法。 该方法的目的是根据当前元素类将其调用重定向到相应访问者的方法。 请注意, 即使元素基类实现了该方法, 所有子类都必须对其进行重写并调用访问者对象中的合适方法。
- 客户端 (Client) 通常会作为集合或其他复杂对象 (例如一个 组合 树) 的代表。 客户端通常不知晓所有的具体元素类, 因为它们会通过抽象接口与集合中的对象进行交互。
应用场景
- JDK NIO 的
FileVisitor接口让文件树遍历与具体操作(查找、删除)分离。 - Apache Calcite 的
SqlVisitor在不修改 AST 节点的情况下执行类型检查、优化等操作。 - Apache Commons Lang 的
LockingVisitors将锁管理与对受保护对象的读写操作解耦。 - Lombok 在编译期作为访问者遍历 AST,根据注解动态添加方法或字段。
- Java 编译器 API 的
ElementVisitor在注解处理时遍历源代码元素(类、方法等)并执行自定义逻辑。
【中等】什么是备忘录模式?⭐
备忘录模式 (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 的配置状态。
【中等】什么是解释器模式?⭐⭐
解释器模式 (Interpreter):给定一个语言,定义它的文法表示,并构建解释器来解析该语言中的句子。
核心结构
- 抽象表达式:声明
interpret()解释接口。 - 终结符表达式:文法中最小不可再分的符号(如变量、常量),直接解释求值。
- 非终结符表达式:对应文法规则(如加减运算),持有子表达式引用,递归解释。
- 上下文/环境:存储解释过程需要的全局数据(如变量取值表)。
经典应用
- 正则表达式:
Pattern将正则字符串解析为语法树后逐节点解释执行。 - Spring EL:
SpelExpressionParser将表达式解析为 AST 后解释求值,注解中#{...}即用此机制。 - SQL 解析引擎:Calcite、Druid SQLParser 将 SQL 词法/语法分析为 AST 后做校验、改写、优化。
- 规则引擎:Aviator、QLExpress、Drools(Rete 算法)解释执行规则表达式。
- MyBatis:
SqlNode树解释<if>、<foreach>等动态 SQL 标签。
优缺点
- 优点:文法规则用类表达,扩展新规则只需新增类(开闭原则)。
- 缺点:复杂文法导致类爆炸、递归解释性能差——因此它只适用于文法简单的领域,复杂场景通常用专业解析器生成工具(ANTLR)。
综合
【困难】说说 JDK 和 Spring 源码中用到了哪些设计模式?⭐⭐⭐⭐
这是考察“模式是否真正理解”的综合题,答题关键是能定位到具体类并说清为什么用它。
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 中的完整链路:
- 答题反模式:只报模式名不报类名会被直接降级(“代理模式——Spring AOP 用了”这种回答无信息量);把不确定的类名报错比含糊回答更扣分——拿不准就说“大概是某个 AbstractXXXTemplate 系列”。
- 串联视角:能讲出模式间的组合更有说服力,如 Spring 启动链路:
refresh()(模板方法)→BeanPostProcessor链(职责链)→ 事件发布(观察者)→ AOP 代理生成(代理),一条链路串四种模式。
拓展追问
- 追问 1:Spring 为什么用三级缓存而不是二级解决循环依赖?
三级缓存(singletonFactories)存的是 ObjectFactory,关键是延迟决定是否生成 AOP 代理:若 Bean A 依赖 B、B 又依赖 A,B 创建时通过 ObjectFactory 提前拿到 A 的引用(若 A 需代理则此时提前生成代理对象),保证 B 持有的是代理而非原始对象。若只用二级缓存提前暴露,无法判断是否需提前代理,会破坏代理一致性。单例 + 构造器注入的循环依赖无法解决(可用@Lazy或改 setter 注入)。 - 追问 2:
refresh()方法里哪几步最关键?能背出几个?
关键步:invokeBeanFactoryPostProcessors()(执行 BeanDefinition 级别扩展,@Configuration类的 CGLIB 增强在此发生)、registerBeanPostProcessors()(注册 Bean 级扩展,职责链的组装)、onRefresh()/finishBeanFactoryInitialization()(实例化所有非懒加载单例,循环依赖在此暴露)、finishRefresh()(发布 ContextRefreshedEvent,观察者模式)。 - 追问 3:JDK 中有没有“模式的反面教材”(设计被公认不佳的案例)?
经典案例:java.util.Observable是类而非接口且方法带 synchronized,JDK 9 正式废弃;Properties继承Hashtable违反 LSP;Calendar可变且线程不安全、API 设计遭诟病后被java.time替代。能主动提这些反面案例,说明是真读过源码而不是背清单。
场景题
面试官追问:“你说 Spring AOP 用了代理,那从调用
@Transactional方法到事务真正开启,中间经过哪些类?”请尽可能精确地描述。
分析要点:链路应为:调用方持有的是代理对象(JdkDynamicAopProxy 或 CGLIB 子类)→ invoke()/intercept() 进入 → ReflectiveMethodInvocation.proceed() 依次执行拦截器链(责任链)→ 命中 TransactionInterceptor.invoke() → 委托 PlatformTransactionManager(策略,DataSourceTransactionManager 等实现)开启事务。能讲到 proceed() 的递归推进和 TransactionInterceptor 即为 L3 水准;说不清则暴露只背了结论。
【中等】实际业务场景中如何选型设计模式?⭐⭐⭐⭐⭐
考察重点不是背诵模式定义,而是识别场景特征、匹配模式意图。高频场景速查表:
| 业务场景 | 推荐模式 | 选型理由 |
|---|---|---|
| 订单状态流转(待支付→已支付→发货→完成) | 状态模式/状态机 | 行为随状态变化,避免状态判断散落各处 |
| 支付渠道/促销规则/风控策略多选一 | 策略模式 + 工厂 | 算法可替换可扩展,新增渠道不改存量 |
| 风控校验、审批流、Filter 链 | 职责链模式 | 多处理器顺序执行,可动态编排/中断 |
| 下单后联动发短信、送积分、通知仓库 | 观察者/事件驱动 | 主流程与联动逻辑解耦,新增订阅者不改主流程 |
| 复杂对象组装(构建复杂查询条件、导出配置) | 建造者模式 | 分步构建 + 链式调用,参数多时避免构造器爆炸 |
| 调用第三方接口适配/老系统改造 | 适配器/防腐层 | 隔离外部模型变化 |
| 事务、日志、缓存等横切增强 | 代理模式(AOP) | 不侵入业务代码动态增强 |
| 流程固定但步骤有差异(导入导出、支付回调处理) | 模板方法模式 | 骨架固定,子类只实现差异步骤 |
| 延迟加载、权限控制、远程调用封装 | 代理模式 | 控制访问、对调用方透明 |
选型心法:先看变化点在哪——变化的是算法(策略)、状态(状态)、流程步骤(模板方法)、处理节点(职责链)、对象创建(工厂/建造者);再看扩展方式是新增还是替换;最后权衡复杂度,分支少且稳定时 if-else 也是合理选择。
L3 深挖要点(真实项目视角)
- 量化决策门槛(经验值):分支 ≤ 3 且一年内不会新增 → if-else/switch;分支 4~8 且持续新增 → 策略 + 注册表;处理步骤 ≥ 3 且需动态编排 → 职责链;联动方 ≥ 2 且会增减 → 事件驱动;构造参数 ≥ 5 且可选组合多 → 建造者。数字不是铁律,但面试中能给出门槛说明真做过选型。
- 选型顺序:先评估“不引入模式的成本”(修改频率 × 影响面),再评估引入成本(类数量、学习曲线、调试难度);多数业务代码的正确答案是“先简单写,第三次重复时再抽象”(Rule of Three)。
- 滥用反模式:为炫技在 CRUD 上套工厂 + 策略 + 观察者,30 行逻辑拆成 8 个类,新人两周看不懂;模式应跟随问题生长,而不是预设问题。
- 真实案例口径:能讲一个“最终没用模式”的决策更有说服力——如评估后认为某分支两年只加过一次,保持 if-else 并加注释说明扩展方式,避免过度设计。
拓展追问
- 追问 1:同一个需求(如“多渠道支付”)策略、职责链、状态都说得通,怎么破局?
回到三个问题:多个处理者是互斥还是叠加(互斥→策略,叠加→职责链)?行为随对象生命周期变化吗(是→状态)?选择由调用方决定还是对象自身决定(调用方→策略,自身→状态)?支付渠道是互斥多选一、由调用方指定,答案是策略;若追问“支付过程中重试/降级切换渠道”,才引入状态维度。 - 追问 2:什么时候应该拒绝“上模式”的重构提案?
三个信号:分支数量少且两年内无新增记录(git 可验证)、引入模式后类数增长超过 5 倍而逻辑不变、团队成员无人熟悉该模式且无时间培训。模式是负债不是资产,每个新增类都有维护成本;拒绝时要给出数据(修改频率统计),而不是只说“没必要”。 - 追问 3:遗留系统改造时,模式引入的正确节奏是什么?
绞杀者模式(Strangler):不推倒重写,在边界处引入新模式——先定义抽象接口包住旧逻辑(适配器/防腐层),新需求走新结构,旧分支逐步迁移。每一步可回滚、可灰度;避免一次性重构引入大爆炸风险。这也是选型能力的一部分:选对模式不如选对节奏。
场景题
团队在讨论“订单履约流程”的重构:订单创建后需依次执行风控、拆单、库存锁定、支付、发货,不同商品类型(实物/虚拟/跨境)流程步骤不同,跨境还要加清关步骤。有人建议策略模式,有人建议职责链,有人建议状态机。
分析要点:三个候选要逐个排除:状态机描述的是订单生命周期状态流转(待支付→已支付→已发货),解决的是“状态 + 动作”合法性问题,不解决流程编排;策略是互斥多选一,但这里是步骤叠加而非替换。正解是职责链/管道:每个步骤是无状态处理器,按商品类型组装不同的链(跨境链插入清关节点);再结合状态机校验每步前后的状态合法性。同时提醒:链的组装配置化,新商品类型上线不改代码只加配置。
【中等】如何用设计模式消除大量 if-else 硬编码?⭐⭐⭐⭐⭐
这是模式落地最高频的实战题,答题关键是分清 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)更合适。 - 源码级案例:Spring
BeanDefinitionParser体系把 XML 标签解析的 switch 改为解析器注册表;HandlerMapping链把 URL 匹配的 if-else 改为策略族遍历;MyBatis 动态 SQL 把条件分支交给SqlNode树(解释器)。读源码时留意框架“消灭 switch”的手法是加分项。 - 落地风险与量化:重构后路由 key 拼写错误会从编译期错误变成运行期 NPE(
map.get(key)返回 null)——需用枚举作 key 或注册时校验;改造前先统计分支数量与新增频率(git log),分支 > 5 且近一年新增 ≥ 2 次才值得动手,否则成本大于收益。
拓展追问
- 追问 1:策略注册表方案中,如何避免“找不到策略时返回 null 引发 NPE”?
三层防线:key 用枚举而非字符串(编译期防拼写错误);注册表初始化时校验所有枚举值都有实现(启动快速失败);查找失败抛带上下文的业务异常而非返回 null。也可提供默认策略(Default 实现)兼容未知渠道,但要显式决策而非偶然行为。 - 追问 2:Java 17 的 switch 模式匹配/密封类能不能替代策略模式?
部分可以:sealed 类 + switch 模式匹配让编译器穷举检查分支(漏分支直接编译错误),分支少且稳定时比策略模式更轻量、无类爆炸。但策略模式的优势在于开闭(新增不改存量)与跨模块扩展(新 jar 加实现);分支频繁新增或实现在不同模块时仍选策略。 - 追问 3:重构后如何验证新旧逻辑一致?
标准手法:双跑比对(新旧实现同时执行、比对结果、只以旧结果为准)、特性开关灰度切流、针对原 if-else 每个分支补齐参数化单测(重构前先补测试是安全前提)。不要直接上线新实现再靠线上反馈发现问题。
场景题
订单服务
createOrder方法有 12 个 if-else 分支按渠道(APP/H5/小程序/直播/企业购……)分发,每个分支内部结构相似:参数适配→优惠计算→风控校验→下单,但优惠计算逻辑各渠道完全不同。新人每次加渠道要改 400 行的老方法,已两次改出回归 bug。
分析要点:先拆维度:分支内部“参数适配→优惠→风控→下单”是固定流程,渠道差异主要在优惠计算——这是模板方法 + 策略的复合:抽象 AbstractChannelOrderCreator 定骨架,优惠计算作为策略注入或抽象步骤;路由用 Map<ChannelEnum, OrderCreator> 注册表(枚举 key 防拼写错误)。落地节奏:先补全现有 12 分支的单测→重构→双跑比对→灰度。同时强调:400 行方法本身就是坏味道,即使不上模式也应先做 Extract Method,重构和模式化可分两步走降低风险。