设计模式面试题
创建型:单例模式(懒汉/饿汉/双重校验/静态内部类)、工厂模式、建造者模式
面试官您好!创建型设计模式的核心是 "封装对象创建过程,将对象创建与使用分离",解决的是 "如何优雅地 new 对象" 的问题。下面我逐一为您讲解这几个高频考点 👇
单例模式 👑
核心思想:保证一个类在整个应用中只有一个实例,并提供一个全局访问点。
1. 四种经典实现对比表
| 实现方式 | 线程安全 | 懒加载 | 性能 | 推荐指数 | 核心问题 |
|---|---|---|---|---|---|
| 饿汉式 | ✅ 是 | ❌ 否 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 类加载时就初始化,浪费内存 |
| 懒汉式 (非同步) | ❌ 否 | ✅ 是 | ⭐⭐⭐⭐⭐ | ❌ 不推荐 | 多线程下会创建多个实例 |
| 双重校验锁 (DCL) | ✅ 是 | ✅ 是 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | volatile 关键字防止指令重排 |
| 静态内部类 | ✅ 是 | ✅ 是 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 利用 JVM 类加载机制保证线程安全 |
1.1 饿汉式 🍚 “不管饿不饿,先做饭”
类加载的时候就 new 好实例,天生线程安全,但可能浪费内存。
public class HungrySingleton {
private static final HungrySingleton INSTANCE = new HungrySingleton();
private HungrySingleton() {}
public static HungrySingleton getInstance() {
return INSTANCE;
}
}- ✅ 简单粗暴
- ❌ 类一加载就占资源,如果一直没用就亏了
2. 关键实现代码与坑点
🔹 双重校验锁 (DCL) - 面试必写
public class Singleton {
// 必须加volatile!防止指令重排导致空指针
private static volatile Singleton instance;
private Singleton() {} // 私有构造器,防止外部new
public static Singleton getInstance() {
if (instance == null) { // 第一次校验:避免不必要的加锁
synchronized (Singleton.class) { // 加锁
if (instance == null) { // 第二次校验:防止多线程同时进入第一个if
instance = new Singleton();
}
}
}
return instance;
}
}💡 面试官常问坑点:为什么必须加volatile?
new Singleton()不是原子操作,分为三步:分配内存→初始化对象→引用指向内存- 不加 volatile 可能发生指令重排:分配内存→引用指向内存→初始化对象
- 此时其他线程拿到的是一个未初始化的空对象,导致 NPE
🔹 静态内部类 - 最优实现
public class Singleton {
private Singleton() {}
// 只有调用getInstance()时,才会加载内部类
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}✅ 优点:JVM 在加载类时是单线程的,天然保证线程安全;同时实现了懒加载,性能最优。
枚举单例 🛡️ “反射、反序列化都攻不破”
public enum EnumSingleton {
INSTANCE;
public void doSomething() {}
}✨ 有效防止反射创建新实例、反序列化重创对象,最安全的单例写法。
3. 单例模式的破坏与防御
- 反射破坏:通过
Class.getDeclaredConstructor().newInstance()可以调用私有构造器 - 序列化破坏:序列化再反序列化会创建新对象
- 防御手段:在构造器中抛出异常防止反射;实现
readResolve()方法返回单例对象
工厂模式 🏭
核心思想:定义一个创建对象的接口,让子类决定实例化哪个类,将对象创建延迟到子类。
1. 三种工厂模式演进
┌── 简单工厂(一个工厂生产所有产品) 🔧 参数判断
工厂模式 ──┼── 工厂方法(一种产品一个工厂) 🏭 每个工厂只干一件事
└── 抽象工厂(一族产品的超级工厂)🏗️ 创建相关产品家族2. 适用场景与优缺点
| 模式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 简单工厂 | 产品种类少,变化不频繁 | 结构简单,调用方便 | 违反开闭原则,工厂类职责过重 |
| 工厂方法 | 产品种类多,经常扩展 | 符合开闭原则,每个产品对应一个工厂 | 类数量爆炸,增加系统复杂度 |
| 抽象工厂 | 需要创建一系列相关产品 | 保证产品族的一致性 | 扩展产品族困难,违反开闭原则 |
2.1 简单工厂(Static Factory)
不是 23 种设计模式之一,但常用。
用一个工厂类根据参数 new 不同子类,违反开闭原则,加新产品得改工厂代码。
public class SimpleFactory {
public static Product create(String type) {
if ("A".equals(type)) return new ProductA();
else return new ProductB();
}
}2.2 工厂方法(Factory Method)
定义一个创建对象的接口,让子类决定实例化哪个类。一个产品类对应一个工厂类。
interface Factory { Product create(); }
class FactoryA implements Factory {
public Product create() { return new ProductA(); }
}✅ 符合开闭原则,加新产品只需加新工厂类。
2.3 抽象工厂(Abstract Factory)
创建 一系列 相关或依赖的对象,比如不同操作系统的 UI 组件工厂(按钮、文本框一起搞)。
interface UIFactory {
Button createButton();
TextField createTextField();
}
class WindowsFactory implements UIFactory { ... }
class MacFactory implements UIFactory { ... }3. 代码示例(工厂方法)
// 产品接口
public interface Phone {
void make();
}
// 具体产品
public class IPhone implements Phone {
@Override
public void make() {
System.out.println("生产苹果手机");
}
}
public class Xiaomi implements Phone {
@Override
public void make() {
System.out.println("生产小米手机");
}
}
// 工厂接口
public interface PhoneFactory {
Phone createPhone();
}
// 具体工厂
public class IPhoneFactory implements PhoneFactory {
@Override
public Phone createPhone() {
return new IPhone();
}
}
public class XiaomiFactory implements PhoneFactory {
@Override
public Phone createPhone() {
return new Xiaomi();
}
}建造者模式 🔨
核心思想:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
典型:StringBuilder、Lombok 的 @Builder。
一句话:工厂关心“产哪个品牌的车”,建造者关心“这辆车的配置怎么搭”(cpu、内存、硬盘全自定义)。🚗
1. 适用场景
- 对象构造参数非常多(超过 4 个),且很多参数是可选的
- 需要创建不可变对象(没有 setter 方法)
- 构建过程需要分步骤完成,不同步骤顺序可以产生不同结果
2. 经典实现(链式调用版)
public class User {
// 必选参数
private final String username;
private final String password;
// 可选参数
private final int age;
private final String phone;
private final String address;
// 私有构造器,只能通过Builder构建
private User(UserBuilder builder) {
this.username = builder.username;
this.password = builder.password;
this.age = builder.age;
this.phone = builder.phone;
this.address = builder.address;
}
// 静态内部Builder类
public static class UserBuilder {
// 必选参数
private final String username;
private final String password;
// 可选参数 - 初始化默认值
private int age = 0;
private String phone = "";
private String address = "";
// 必选参数通过构造器传入
public UserBuilder(String username, String password) {
this.username = username;
this.password = password;
}
// 可选参数通过链式方法设置
public UserBuilder age(int age) {
this.age = age;
return this;
}
public UserBuilder phone(String phone) {
this.phone = phone;
return this;
}
public UserBuilder address(String address) {
this.address = address;
return this;
}
// 构建最终对象
public User build() {
return new User(this);
}
}
}
// 使用方式
public class Main {
public static void main(String[] args) {
User user = new User.UserBuilder("zhangsan", "123456")
.age(20)
.phone("13800138000")
.address("北京市朝阳区")
.build();
}
}3. 建造者模式 vs 构造器 vs setter
| 方式 | 优点 | 缺点 |
|---|---|---|
| 重叠构造器 | 简单直接 | 参数多时代码可读性差,容易传错参数 |
| setter 方法 | 灵活,可读性好 | 对象状态可变,无法保证线程安全 |
| 建造者模式 | 可读性好,对象不可变,参数校验方便 | 代码量稍大,需要额外编写 Builder 类 |
面试总结 📝
- 单例模式:重点掌握双重校验锁和静态内部类的实现,以及 volatile 关键字的作用和单例破坏问题
- 工厂模式:理解三种工厂模式的演进过程和各自的适用场景,开闭原则是核心
- 建造者模式:记住链式调用的实现方式,当构造参数超过 4 个且有很多可选参数时优先使用
结构型:代理模式(JDK 动态代理 vs CGLIB)、适配器模式、装饰器模式
面试官您好!结构型设计模式的核心是 "如何将类或对象组合成更大的结构,同时保持结构的灵活性和高效性"。下面我从面试最常考的三个模式逐一为您解答。
代理模式 🔒
1. 核心定义
为其他对象提供一个代理对象,以控制对这个对象的访问。代理对象在客户端和目标对象之间起到中介作用,可以在不修改目标对象的前提下,增加额外功能(如日志、事务、权限控制)。
2. 核心角色
- 抽象主题(Subject):定义真实主题和代理主题的共同接口
- 真实主题(RealSubject):被代理的对象,执行业务逻辑
- 代理(Proxy):持有真实主题的引用,实现与真实主题相同的接口,在调用前后增加额外操作
3. 代理模式类图
3.1 静态代理
代理类在编译期就存在,需要手动编写。
缺点很明显:一个接口就得写一个代理类,类爆炸。
// 接口
public interface UserService {
void save();
}
// 目标对象
public class UserServiceImpl implements UserService {
public void save() { System.out.println("保存用户"); }
}
// 代理类
public class UserServiceProxy implements UserService {
private UserService target;
public UserServiceProxy(UserService target) { this.target = target; }
public void save() {
System.out.println("开启事务");
target.save();
System.out.println("提交事务");
}
}3.2 JDK 动态代理
利用 java.lang.reflect.Proxy 和 InvocationHandler 在运行时生成代理类。
前提条件:目标对象必须实现接口,代理类会实现相同的接口。
UserService target = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
(proxyObj, method, args) -> {
System.out.println("开启事务");
Object result = method.invoke(target, args);
System.out.println("提交事务");
return result;
});
proxy.save();Proxy.newProxyInstance 会动态生成一个实现了指定接口的类($Proxy0),其内部通过反射调用 InvocationHandler.invoke 方法。
局限:只能代理接口,不能代理没有接口的类。
3.3 CGLIB 动态代理
CGLIB(Code Generation Library)基于 ASM 字节码技术,通过生成目标类的子类来实现代理。
因此目标类不能是 final 的,代理方法也不能是 final。
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserServiceImpl.class); // 直接设置父类
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
System.out.println("开启事务");
Object result = proxy.invokeSuper(obj, args);
System.out.println("提交事务");
return result;
});
UserServiceImpl proxy = (UserServiceImpl) enhancer.create();
proxy.save();CGLIB 会生成一个 UserServiceImpl$$EnhancerByCGLIB$$xxxx 的子类,重写非 final 方法,在方法内部调用拦截器。
4. 面试核心:JDK 动态代理 vs CGLIB 动态代理 ✨
这是 Spring AOP 的底层实现,也是面试必考点!
| 对比维度 | JDK 动态代理 | CGLIB 动态代理 |
|---|---|---|
| 底层实现 | 基于 Java 反射机制,生成实现了目标接口的匿名类 | 基于 ASM 字节码框架,生成目标类的子类 |
| 要求 | 目标类必须实现至少一个接口 | 目标类不能被 final 修饰,方法不能是 final/private |
| 性能 | JDK8 + 后性能大幅提升,与 CGLIB 基本持平 | 生成字节码耗时较长,但方法调用更快 |
| Spring AOP 默认 | 当目标类实现接口时使用 | 当目标类没有实现接口时使用 |
| 代理对象创建 | 通过Proxy.newProxyInstance() | 通过Enhancer.create() |
| 方法拦截 | 实现InvocationHandler接口的invoke()方法 | 实现MethodInterceptor接口的intercept()方法 |
🔹 Spring AOP 内部实现了这两种方式的自动切换:如果目标对象实现了接口,则采用 JDK 动态代理;否则使用 CGLIB。也可以通过 proxy-target-class=true 强制使用 CGLIB。
5. 适用场景
- 远程代理:如 RMI、Dubbo 的服务调用
- 虚拟代理:如图片懒加载
- 保护代理:如权限控制
- 智能引用代理:如统计方法调用次数、日志记录
- Spring AOP 的核心实现
适配器模式 🔌
1. 核心定义
将一个类的接口转换成客户端期望的另一个接口,使得原本因接口不兼容而无法一起工作的类能够协同工作。
好比去欧洲旅行,插座是圆孔的,你带了个国标扁头充电器,就得用一个转接头。
2. 核心角色
- 目标接口(Target):客户端期望的接口
- 适配者(Adaptee):需要被适配的现有接口
- 适配器(Adapter):实现目标接口,持有适配者的引用,将目标接口的调用转换为适配者接口的调用
3. 适配器模式类图(对象适配器,最常用)
// 目标接口(客户期望的)
interface MediaPlayer {
void play(String audioType, String fileName);
}
// 被适配者(高级播放器)
class AdvancedMediaPlayer {
void playMp4(String fileName) { System.out.println("播放 mp4: " + fileName); }
}
// 适配器
class MediaAdapter implements MediaPlayer {
private AdvancedMediaPlayer advancedPlayer;
public MediaAdapter() { this.advancedPlayer = new AdvancedMediaPlayer(); }
@Override
public void play(String audioType, String fileName) {
if ("mp4".equalsIgnoreCase(audioType)) {
advancedPlayer.playMp4(fileName); // 转接
}
}
}4. 两种适配器对比
- 对象适配器:通过组合适配者对象实现,灵活度高,推荐使用
- 类适配器:通过继承适配者类实现,Java 不支持多继承,限制较多
5. 适用场景
- 集成第三方库,接口不匹配时
- 统一多个不同接口的调用方式
- 兼容旧系统的接口
- 典型例子:Java 的
InputStreamReader和OutputStreamWriter
5.1 🌰 经典应用:Spring MVC 的 HandlerAdapter
Spring 中有多种处理器(Controller、HttpRequestHandler、Servlet 等),DispatcherServlet 不能直接调用它们,于是用了适配器模式。
public interface HandlerAdapter {
boolean supports(Object handler);
ModelAndView handle(HttpServletRequest req, HttpServletResponse res, Object handler);
}
// 比如适配 Controller 接口的适配器
public class SimpleControllerHandlerAdapter implements HandlerAdapter {
public boolean supports(Object handler) { return handler instanceof Controller; }
public ModelAndView handle(...) { return ((Controller)handler).handleRequest(req, res); }
}无论处理器是什么类型,通过适配器统一了调用方式 ✨。
装饰器模式 🎨
1. 核心定义
动态地给一个对象添加额外的职责,就增加功能来说,装饰器模式比生成子类更加灵活。它遵循 "组合优于继承" 的原则。
像你玩 RPG 游戏,角色可以不断穿戴装备:基础角色→拿剑→穿铠甲→带戒指,每一层装饰都增强功能。
2. 核心角色
- 抽象组件(Component):定义被装饰对象和装饰器的共同接口
- 具体组件(ConcreteComponent):被装饰的原始对象
- 抽象装饰器(Decorator):继承抽象组件,持有抽象组件的引用
- 具体装饰器(ConcreteDecorator):实现具体的装饰功能
// 抽象构件
interface Coffee {
double cost();
String desc();
}
// 具体构件
class SimpleCoffee implements Coffee {
public double cost() { return 10; }
public String desc() { return "咖啡"; }
}
// 抽象装饰器
abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) { this.coffee = coffee; }
}
// 具体装饰器:加奶
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) { super(coffee); }
public double cost() { return coffee.cost() + 2; }
public String desc() { return coffee.desc() + "+牛奶"; }
}
// 使用
Coffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
System.out.println(coffee.desc() + " 价格:" + coffee.cost()); // 咖啡+牛奶 价格:123. 装饰器模式类图
4. 4. 适用场景
- 需要动态地给对象添加功能,且这些功能可以动态撤销
- 需要为大量兄弟类添加功能,避免类爆炸
- 典型例子:Java IO 流体系(
BufferedReader装饰FileReader)
易混淆模式对比 🧐
这是面试官最爱问的区分题!
| 模式 | 核心目的 | 关系 | 典型应用 |
|---|---|---|---|
| 代理模式 | 控制对对象的访问 | 代理与真实对象实现相同接口 | Spring AOP |
| 适配器模式 | 转换接口,解决不兼容问题 | 适配器与适配者接口不同 | InputStreamReader |
| 装饰器模式 | 动态增强对象功能 | 装饰器与被装饰对象实现相同接口 | Java IO 流 |
面试加分回答 💯
- 代理模式:Spring AOP 中,如果目标类实现了接口,默认使用 JDK 动态代理;如果没有实现接口,则使用 CGLIB。可以通过
proxy-target-class=true强制使用 CGLIB。 - 适配器模式:Spring MVC 中的
HandlerAdapter就是适配器模式的典型应用,它将不同类型的处理器(如Controller、HttpRequestHandler)适配成统一的处理器接口。 - 装饰器模式:MyBatis 中的
Cache接口及其实现类使用了装饰器模式,通过不同的装饰器为缓存添加了序列化、日志、定时清理等功能。
行为型:策略模式、模板方法模式、观察者模式、责任链模式
面试官您好!接下来我会从核心思想、角色结构、适用场景、代码实现、优缺点、实际应用这几个维度,为您逐一讲解这四个高频行为型设计模式。
策略模式 (Strategy Pattern) 🎨
核心思想
定义一系列算法,将每个算法封装成独立的类,并使它们可以互相替换。让算法的变化独立于使用算法的客户端。
UML 类图
💡 重点:上下文持有策略接口引用,具体算法实现完全解耦。
核心角色
- Context (上下文):持有策略对象的引用,负责调用策略
- Strategy (抽象策略):定义所有算法的公共接口
- ConcreteStrategy (具体策略):实现抽象策略接口,提供具体算法
适用场景 ✅
- 一个类有多种行为,且需要在运行时动态切换
- 算法需要独立于客户端变化
- 避免使用多重条件判断 (if-else/switch)
Java 关键实现
// 抽象策略
public interface PaymentStrategy {
void pay(double amount);
}
// 具体策略
public class AlipayStrategy implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("支付宝支付:" + amount + "元");
}
}
// 上下文
public class Order {
private PaymentStrategy paymentStrategy;
// 运行时动态设置策略
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void checkout(double amount) {
paymentStrategy.pay(amount);
}
}实际应用
- JDK 的
Comparator接口 (排序策略) - Spring 的
Resource接口 (资源访问策略) - 电商支付方式 (支付宝 / 微信 / 银行卡)
优缺点
| 优点 | 缺点 |
|---|---|
| 算法自由切换 | 策略类数量增多 |
| 避免多重条件判断 | 客户端需要了解所有策略 |
| 符合开闭原则 | 增加系统复杂度 |
面试高频坑点 ⚠️
- 策略模式和简单工厂模式的区别:工厂是创建对象,策略是封装行为
- 策略模式和状态模式的区别:状态模式的状态之间有依赖关系,策略模式的策略之间完全独立
模板方法模式 (Template Method Pattern) 📋
核心思想
定义一个操作中的算法骨架,而将一些步骤延迟到子类中实现。让子类可以在不改变算法结构的情况下,重新定义算法的某些特定步骤。
UML 类图
💡 重点:基类用final保护骨架,对外暴露钩子方法brew()/addCondiments()。
核心角色
- AbstractClass (抽象类):定义模板方法和算法骨架
- ConcreteClass (具体子类):实现抽象类中的抽象步骤
适用场景 ✅
- 多个子类有公共的方法,且逻辑基本相同
- 算法的整体结构固定,但个别步骤需要变化
- 需要控制子类的扩展
Java 关键实现
// 抽象类
public abstract class CoffeeMaker {
// 模板方法,final防止子类重写整个算法
public final void makeCoffee() {
boilWater();
brewCoffee();
pourInCup();
addCondiments(); // 钩子方法
}
// 公共步骤
private void boilWater() {
System.out.println("烧开水");
}
private void pourInCup() {
System.out.println("倒入杯中");
}
// 抽象步骤,由子类实现
protected abstract void brewCoffee();
// 钩子方法,子类可选实现
protected void addCondiments() {}
}
// 具体子类
public class AmericanoMaker extends CoffeeMaker {
@Override
protected void brewCoffee() {
System.out.println("冲泡美式咖啡");
}
}实际应用
- JDK 的
AbstractList、AbstractMap等抽象类 - Spring 的
JdbcTemplate、HibernateTemplate - Servlet 的
HttpServlet(doGet/doPost 方法)
优缺点
| 优点 | 缺点 |
|---|---|
| 代码复用性高 | 每个不同实现都需要一个子类 |
| 算法结构统一 | 继承关系限制了灵活性 |
| 便于维护 | 父类添加新步骤,所有子类都要修改 |
面试高频坑点 ⚠️
- 模板方法一定要用
final修饰,防止子类重写整个算法 - 钩子方法 (Hook Method) 是模板方法的重要扩展点
- 模板方法模式是继承的典型应用,而策略模式是组合的典型应用
观察者模式 (Observer Pattern) 👀
核心思想
定义了对象之间的一对多依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都会收到通知并自动更新。
UML 类图
💡 重点:主题持有观察者集合,状态变化时遍历通知,支持动态增删观察者。
核心角色
- Subject (主题):被观察的对象,维护观察者列表
- Observer (观察者):定义更新接口
- ConcreteSubject (具体主题):实现主题接口,状态变化时通知观察者
- ConcreteObserver (具体观察者):实现观察者接口,处理更新
适用场景 ✅
- 一个对象的变化需要通知其他对象
- 事件驱动系统
- 消息订阅 - 发布系统
Java 关键实现
// 主题接口
public interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}
// 观察者接口
public interface Observer {
void update(String message);
}
// 具体主题
public class NewsAgency implements Subject {
private List<Observer> observers = new ArrayList<>();
private String news;
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(news);
}
}
public void setNews(String news) {
this.news = news;
notifyObservers();
}
}实际应用
- JDK 的
java.util.Observer和java.util.Observable(已过时) - Spring 的事件机制 (
ApplicationEvent和ApplicationListener) - GUI 编程中的事件监听器
- 消息队列的发布 - 订阅模式
优缺点
| 优点 | 缺点 |
|---|---|
| 松耦合 | 观察者过多时通知效率低 |
| 支持广播通信 | 可能导致循环依赖 |
| 符合开闭原则 | 无法知道观察者的执行顺序 |
面试高频坑点 ⚠️
- JDK 自带的
Observable是类而不是接口,有单继承限制 - Spring 事件机制是观察者模式的典型应用,支持异步事件
- 观察者模式和发布 - 订阅模式的区别:发布 - 订阅模式多了一个消息中间件,解耦更彻底
责任链模式 (Chain of Responsibility Pattern) 🔗
核心思想
将请求的发送者和接收者解耦,让多个对象都有机会处理请求。将这些对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止。
UML 类图
💡 重点:每个处理器持有下一个处理器引用,可灵活增加、调序或断开链。
核心角色
- Handler (抽象处理者):定义处理请求的接口,持有下一个处理者的引用
- ConcreteHandler (具体处理者):实现处理请求的方法,决定是否处理请求,否则传递给下一个处理者
适用场景 ✅
- 多个对象可以处理同一个请求,且处理者不确定
- 请求的处理需要按顺序执行
- 需要动态添加或删除处理者
Java 关键实现
// 抽象处理者
public abstract class LeaveHandler {
protected LeaveHandler nextHandler;
public void setNextHandler(LeaveHandler nextHandler) {
this.nextHandler = nextHandler;
}
public abstract void handleRequest(int days);
}
// 具体处理者
public class TeamLeader extends LeaveHandler {
@Override
public void handleRequest(int days) {
if (days <= 3) {
System.out.println("组长批准请假" + days + "天");
} else if (nextHandler != null) {
nextHandler.handleRequest(days);
}
}
}
public class Manager extends LeaveHandler {
@Override
public void handleRequest(int days) {
if (days <= 7) {
System.out.println("经理批准请假" + days + "天");
} else if (nextHandler != null) {
nextHandler.handleRequest(days);
}
}
}实际应用
- Servlet 的
Filter链 - Spring MVC 的
HandlerInterceptor拦截器链 - Netty 的
ChannelPipeline和ChannelHandler - 日志框架的级别处理 (DEBUG/INFO/WARN/ERROR)
优缺点
| 优点 | 缺点 |
|---|---|
| 请求发送者和接收者解耦 | 请求可能得不到处理 |
| 可以动态调整处理顺序 | 链过长时性能下降 |
| 符合单一职责原则 | 调试困难 |
面试高频坑点 ⚠️
- 责任链可以是纯责任链 (必须有一个处理者处理) 或不纯责任链 (可以都不处理)
- Spring 的拦截器链是责任链模式的典型应用
- 责任链模式和装饰器模式的区别:装饰器模式是增强对象功能,责任链模式是处理请求
四个模式对比总结 📊
| 模式 | 核心思想 | 解决的问题 | 关键技术 | 典型应用 |
|---|---|---|---|---|
| 策略模式 | 封装算法,可互换 | 多重条件判断 | 组合 | 支付方式、排序算法 |
| 模板方法 | 定义骨架,延迟实现 | 代码重复 | 继承 | JdbcTemplate、HttpServlet |
| 观察者模式 | 一对多通知 | 状态变化通知 | 注册 - 通知 | Spring 事件、消息订阅 |
| 责任链模式 | 链式处理请求 | 请求处理者不确定 | 链式引用 | Filter、拦截器 |
🚀 结合实战,多说一句加分项
- 策略+责任链可以组合:营销规则引擎里,先由责任链初步过滤,匹配的再调用具体策略计算优惠。
- 观察者常配合异步消息,进一步解耦(如在
@EventListener里发MQ)。 - 模板方法中结合钩子方法,让子类控制流程的可选部分。
Spring 中用到了哪些设计模式?
面试官您好😊!Spring 框架之所以能成为 Java 生态的绝对霸主,核心原因之一就是它把经典设计模式运用到了极致,实现了低耦合、高扩展、可插拔的架构特性。我梳理了面试中最常考、Spring 最核心的 7 种设计模式,结合具体源码场景给您说明。
🧩 一图胜千言:Spring 核心模块中的设计模式分布
核心设计模式总览表
| 设计模式 | 核心作用 | Spring 典型应用场景 | 表情包 |
|---|---|---|---|
| 工厂模式(简单 / 工厂方法 / 抽象工厂) | 统一对象创建,隔离业务逻辑与实例化过程 | BeanFactory、ApplicationContext、FactoryBean | 🏭 |
| 单例模式 | 保证容器中一个 Bean 只有一个实例 | 默认单例 Bean、单例注册表 | 🔒 |
| 代理模式 | 不修改原代码实现功能增强 | AOP 动态代理(JDK/CGLIB) | 🎭 |
| 观察者模式 | 实现事件驱动,解耦事件发布与订阅 | Spring 事件机制(ApplicationEvent/Listener) | 👀 |
| 模板方法模式 | 封装不变流程,扩展可变部分 | JdbcTemplate、RedisTemplate、TransactionTemplate | 📋 |
| 策略模式 | 同一接口多种实现,动态切换 | Resource 资源加载、InstantiationStrategy 实例化策略 | 🎯 |
| 装饰器模式 | 动态给对象添加额外功能 | BeanWrapperImpl、各种 Wrapper 包装类 | 🎨 |
高频考点深度解析(面试必答)
1. 🏭 工厂模式(Spring 的基石)
- 简单工厂:BeanFactory 的
getBean()方法,根据 Bean 名称 / 类型创建对象,是最简单的工厂实现 - 工厂方法:每个复杂对象对应一个 FactoryBean,比如
SqlSessionFactoryBean,把对象创建逻辑与业务代码完全隔离 - 抽象工厂:ApplicationContext,同时管理 Bean 的创建、生命周期、依赖注入等全流程
- 面试加分:BeanFactory 是 Spring 的通用 Bean 工厂;FactoryBean 是特殊 Bean,专门用来创建特定类型的复杂对象
2. 🔒 单例模式(Spring 默认 Bean 作用域)
- 实现方式:采用单例注册表,用
ConcurrentHashMap存储单例 Bean 实例 - 核心源码:
DefaultSingletonBeanRegistry中的singletonObjects一级缓存 - 设计优势:避免频繁创建销毁对象的性能开销,统一管理 Bean 的生命周期
// 容器就是一个大工厂,返回给我们单例 Bean
ApplicationContext ctx = ...;
UserService userService = ctx.getBean(UserService.class); // 每次拿到的都是同一个对象3. 🎭 代理模式(AOP 的灵魂)
- 核心原理:运行时生成代理对象,在目标方法执行前后插入增强逻辑
- 两种实现:
- JDK 动态代理:基于接口实现,要求目标类必须实现接口
- CGLIB 动态代理:基于继承实现,目标类可以不实现接口
- Spring 默认策略:目标类实现接口用 JDK 代理,否则用 CGLIB 代理
- 面试加分:Spring AOP 是运行时动态代理,AspectJ 是编译时 / 类加载时织入
@Service
public class OrderService {
@Transactional // 背后是事务代理对象在干活
public void placeOrder(Order order) { ... }
}4. 👀 观察者模式(事件驱动模型)
Spring 内置事件驱动模型,用于模块间松耦合通信。
- 三个核心角色:
- 事件源:ApplicationContext,负责发布事件
- 事件:ApplicationEvent,比如
ContextRefreshedEvent、ContextClosedEvent - 监听器:ApplicationListener,负责处理对应事件
- 设计优势:完全解耦事件发布者和订阅者,新增监听器无需修改原有代码
@Component
public class OrderListener {
@EventListener
public void handleOrderPlaced(OrderPlacedEvent event) {
// 发送短信、记录日志等,完全不侵入订单主流程
}
}5. 📋 模板方法模式(各种 Template 的底层)
Spring 提供了大量 *Template,它们把固定的流程(获取连接→执行→释放资源)写好,只留出可变部分让你填充。
核心思想:父类封装固定不变的流程,子类实现可变的业务步骤
典型例子:JdbcTemplate 封装了获取连接、执行 SQL、关闭连接的固定流程,用户只需编写 SQL 和处理结果集
设计优势:极大减少重复代码,统一流程规范
JdbcTemplate:执行 SQL 的骨架已定,你只需传入 SQL 和参数。RestTemplate:发起 HTTP 请求的标准流程。RedisTemplate、RabbitTemplate同理。
jdbcTemplate.query("SELECT * FROM user WHERE id = ?",
new Object[]{1},
(rs, rowNum) -> new User(rs.getLong("id"), rs.getString("name")));
// 我不需要关心连接、Statement、ResultSet 的关闭 —— 模板方法全包了6. 🎯 策略模式(灵活切换实现)
- 核心思想:定义一系列算法,每个算法独立封装,可互相替换
- 典型例子:Resource 接口有
ClassPathResource、FileSystemResource、UrlResource等实现,Spring 根据资源路径自动选择加载策略 - 设计优势:扩展性极强,新增算法无需修改原有逻辑
7. 🎨 装饰器模式(动态增强对象)
- 核心思想:通过包装对象动态添加功能,比继承更灵活
- 典型例子:BeanWrapperImpl 包装 Bean 对象,提供属性访问、类型转换等功能;
HttpServletRequestWrapper可动态增强请求对象
8. 🔌 适配器模式(Spring MVC 的灵魂)
DispatcherServlet 拿到 Handler 之后,并不知道是普通的 @Controller、还是 HttpRequestHandler 或 Servlet。
于是它通过 HandlerAdapter 来适配:
RequestMappingHandlerAdapter→ 适配@RequestMapping方法HttpRequestHandlerAdapter→ 适配HttpRequestHandlerSimpleServletHandlerAdapter→ 适配原生Servlet
各种 Handler 就像不同形状的插头,HandlerAdapter 就是万能转接头🔌。
9. ⛓️ 责任链模式(请求过滤与拦截)
- Servlet Filter 链:
Filter一个个传递request/response。 - Spring Interceptor 链:
preHandle中任一返回false即可中断链条。 - AOP 通知链:多个切面按照优先级构成一条调用链,逐个执行。
// 拦截器链:日志拦截器 -> 权限拦截器 -> 真正 Controller
public class LogInterceptor implements HandlerInterceptor { ... }Spring 核心流程设计模式协同图
面试加分项(体现技术深度)
除了以上核心模式,Spring 还大量运用了其他经典设计模式:
- 适配器模式:HandlerAdapter 适配不同类型的 Controller(@RequestMapping、@Controller)
- 责任链模式:Spring MVC 拦截器链、Spring Security 过滤器链
- 迭代器模式:BeanDefinition 遍历、集合类型依赖注入
- 桥接模式:Spring 事务抽象,分离事务管理与具体实现(JDBC、JPA、Hibernate)
- 🎀 装饰器:
BeanDefinitionDecorator、Session 管理的SessionRepositoryRequestWrapper;不改变接口,动态增强功能 - 🛠️ 建造者:
RestTemplateBuilder、ResponseEntity.ok().body(...)的链式构建;一步步构造复杂对象 - 📑 原型:
@Scope("prototype");每次获取新实例
结尾总结(给面试官留好印象)
Spring 不是生硬地套模式,而是用模式解决了实际痛点:
- 工厂 + 单例 → 管好对象的生命周期
- 代理 + 责任链 → 无侵入地加功能
- 模板方法 → 让开发者只关注业务逻辑
- 适配器 → 让框架兼容各种组件
- 观察者 → 实现组件间松耦合
总的来说,Spring 的设计哲学就是 "面向接口编程,而不是面向实现编程",所有设计模式的运用都是为了实现这个目标。通过合理运用设计模式,Spring 才能做到如此灵活和可扩展,成为 Java 开发的事实标准。
