结构型设计模式深度解析
面试官:代理模式和装饰器模式有什么区别?
你:表面上它们很像,都是包装对象增强功能。但本质目的不同:代理模式是控制访问,装饰器模式是增强功能。
面试官:能具体说说 Spring AOP 用的是哪种模式吗?为什么?
这道题考察的是对模式本质的理解。能说出”目的不同”并给出具体应用场景,才算真正掌握。
链式追问一:代理模式
Section titled “链式追问一:代理模式”Q1:JDK 动态代理和 CGLIB 代理的区别?Spring AOP 如何选择?必考
Section titled “Q1:JDK 动态代理和 CGLIB 代理的区别?Spring AOP 如何选择?”核心差异对比:
| 对比维度 | JDK 动态代理 | CGLIB 代理 |
|---|---|---|
| 实现原理 | 实现接口,Proxy.newProxyInstance() | 继承目标类,生成子类 |
| 使用要求 | 目标类必须实现接口 | 目标类不能是 final,方法不能是 final |
| 性能(生成代理) | 快(直接生成) | 慢(ASM 字节码生成) |
| 性能(方法调用) | 较慢(反射调用) | 快(直接调用) |
| Spring 默认 | 有接口时(Spring 5 之前) | Spring Boot 2.x 默认全用 CGLIB |
JDK 动态代理实现:
// 目标接口public interface UserService { User findById(Long id); void save(User user);}
// 目标实现public class UserServiceImpl implements UserService { @Override public User findById(Long id) { return userRepository.findById(id); }}
// 代理处理器public class LoggingProxy implements InvocationHandler { private final Object target;
public LoggingProxy(Object target) { this.target = target; }
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 前置增强 System.out.println("[LOG] 调用方法:" + method.getName());
// 调用目标方法 Object result = method.invoke(target, args);
// 后置增强 System.out.println("[LOG] 方法返回:" + result);
return result; }}
// 创建代理对象UserService proxy = (UserService) Proxy.newProxyInstance( UserService.class.getClassLoader(), // 类加载器 new Class[]{UserService.class}, // 代理的接口 new LoggingProxy(new UserServiceImpl()) // 调用处理器);
proxy.findById(1L); // 调用代理方法CGLIB 代理实现:
// 目标类(不需要接口)public class UserService { public User findById(Long id) { return userRepository.findById(id); }}
// CGLIB 拦截器public class CglibProxy implements MethodInterceptor { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { // 前置增强 System.out.println("[CGLIB] 调用方法:" + method.getName());
// 调用目标方法(通过代理,避免递归) Object result = proxy.invokeSuper(obj, args);
// 后置增强 System.out.println("[CGLIB] 方法返回:" + result);
return result; }}
// 创建代理对象Enhancer enhancer = new Enhancer();enhancer.setSuperclass(UserService.class); // 设置父类enhancer.setCallback(new CglibProxy()); // 设置拦截器
UserService proxy = (UserService) enhancer.create();proxy.findById(1L);代理生成原理图:
┌────────────────────────────────────────────────┐│ JDK 动态代理 │├────────────────────────────────────────────────┤│ ││ 目标类 代理类 ││ ┌─────────┐ ┌────────────────────┐ ││ │UserService│ │$Proxy0 │ ││ │ │ implements│UserService │ ││ │findById()│ ◄──────┤ │ ││ └─────────┘ │ - target │ ││ │ - invoke() │ ││ │ findById() { │ ││ │ invoke(target, │ ││ │ findById, args)│ ││ │ } │ ││ └────────────────────┘ ││ │ ││ ▼ ││ InvocationHandler.invoke() ││ │└────────────────────────────────────────────────┘
┌────────────────────────────────────────────────┐│ CGLIB 代理 │├────────────────────────────────────────────────┤│ ││ 目标类 代理类(子类) ││ ┌─────────┐ ┌────────────────────┐ ││ │UserService│ │UserService$$Enhancer│ ││ │ │ extends│byCGLIB │ ││ │findById()│ ◄──────┤ │ ││ └─────────┘ │ - CGLIB$findById$0│ ││ │ (原方法) │ ││ │ - findById() { │ ││ │ interceptor. │ ││ │ intercept() │ ││ │ } │ ││ └────────────────────┘ ││ │ ││ ▼ ││ MethodInterceptor.intercept() ││ │└────────────────────────────────────────────────┘Spring AOP 的选择策略:
// Spring Boot 2.x 默认配置@Configuration@EnableAspectJAutoProxy(proxyTargetClass = true) // true: 强制用 CGLIBpublic class AopConfig { }
// 选择逻辑:// 1. proxyTargetClass = true → 强制 CGLIB// 2. 目标类实现接口 → JDK 动态代理(Spring 5 之前默认)// 3. 目标类无接口 → CGLIB// 4. Spring Boot 2.x 默认 proxyTargetClass = trueQ2:代理模式和装饰器模式的区别?必考
Section titled “Q2:代理模式和装饰器模式的区别?”本质区别:
| 对比维度 | 代理模式 | 装饰器模式 |
|---|---|---|
| 设计目的 | 控制对对象的访问 | 动态增强对象的功能 |
| 客户端感知 | 客户端不知道代理存在 | 客户端主动创建装饰器 |
| 对象关系 | 代理内部固定持有目标对象 | 装饰器可以动态叠加多层 |
| 关注点 | 对象是否可以访问 | 对象功能如何增强 |
| 典型应用 | Spring AOP、RPC 远程代理 | Java IO 流 |
代码对比:
// ==================== 代理模式 ====================// 目的:控制访问(权限、缓存、远程)public interface UserService { User findById(Long id);}
public class UserServiceProxy implements UserService { private UserService target; // 固定持有目标对象 private Cache cache;
@Override public User findById(Long id) { // 1. 控制访问:先查缓存 User cached = cache.get("user:" + id); if (cached != null) { return cached; // 缓存命中,不访问目标 }
// 2. 缓存未命中,访问目标 User user = target.findById(id); cache.put("user:" + id, user); return user; }}// 客户端不知道代理存在UserService service = context.getBean(UserService.class);
// ==================== 装饰器模式 ====================// 目的:动态增强功能public interface Coffee { double cost(); String description();}
public class SimpleCoffee implements Coffee { @Override public double cost() { return 10; } @Override public String description() { return "简单咖啡"; }}
// 装饰器基类public abstract class CoffeeDecorator implements Coffee { protected Coffee coffee; // 可以动态替换
public CoffeeDecorator(Coffee coffee) { this.coffee = coffee; }}
// 具体装饰器:牛奶public class MilkDecorator extends CoffeeDecorator { public MilkDecorator(Coffee coffee) { super(coffee); }
@Override public double cost() { return coffee.cost() + 2; // 增强功能 }
@Override public String description() { return coffee.description() + " + 牛奶"; }}
// 具体装饰器:糖public class SugarDecorator extends CoffeeDecorator { public SugarDecorator(Coffee coffee) { super(coffee); }
@Override public double cost() { return coffee.cost() + 1; }
@Override public String description() { return coffee.description() + " + 糖"; }}
// 客户端主动创建装饰器,可以叠加多层Coffee coffee = new SimpleCoffee(); // 简单咖啡:10 元coffee = new MilkDecorator(coffee); // + 牛奶:12 元coffee = new SugarDecorator(coffee); // + 糖:13 元System.out.println(coffee.description() + " = " + coffee.cost());// 输出:简单咖啡 + 牛奶 + 糖 = 13.0装饰器模式:动态叠加:
┌────────────────────────────────────────────────┐│ 装饰器的动态叠加 │├────────────────────────────────────────────────┤│ ││ SimpleCoffee ││ │ ││ ▼ ││ MilkDecorator(SimpleCoffee) ││ │ ││ ▼ ││ SugarDecorator(MilkDecorator) ││ │ ││ ▼ ││ 客户端调用 cost() ││ │ ││ ▼ ││ SugarDecorator.cost() ││ │ ││ ├── MilkDecorator.cost() ││ │ │ ││ │ └── SimpleCoffee.cost() ││ │ │ ││ │ └── return 10 ││ │ ││ └── return 10 + 2 + 1 = 13 ││ │└────────────────────────────────────────────────┘Java IO 流的装饰器模式:
// 经典的装饰器模式应用InputStream is = new FileInputStream("file.txt"); // 基础组件is = new BufferedInputStream(is); // 装饰器1:缓冲is = new GZIPInputStream(is); // 装饰器2:解压is = new CipherInputStream(is, cipher); // 装饰器3:解密
// 可以任意组合装饰器,增强功能链式追问二:适配器模式
Section titled “链式追问二:适配器模式”Q3:适配器模式是什么?Spring MVC 如何用它统一处理器?高频
Section titled “Q3:适配器模式是什么?Spring MVC 如何用它统一处理器?”定义:将一个类的接口转换成客户端期望的另一个接口,使原本不兼容的类可以一起工作。
三种实现方式:
// 目标接口public interface Target { void request();}
// 被适配者(已有类,接口不兼容)public class Adaptee { public void specificRequest() { System.out.println("被适配者的特定请求"); }}
// ==================== 方式1:类适配器(继承) ====================public class ClassAdapter extends Adaptee implements Target { @Override public void request() { super.specificRequest(); // 调用父类方法 }}
// ==================== 方式2:对象适配器(组合,推荐) ====================public class ObjectAdapter implements Target { private Adaptee adaptee; // 组合
public ObjectAdapter(Adaptee adaptee) { this.adaptee = adaptee; }
@Override public void request() { adaptee.specificRequest(); // 委托给被适配者 }}
// ==================== 方式3:接口适配器(缺省适配) ====================// 目标接口有很多方法,但客户端只需要其中几个public interface TargetWithMultipleMethods { void method1(); void method2(); void method3(); void method4();}
// 抽象类提供空实现(适配器)public abstract class AbstractAdapter implements TargetWithMultipleMethods { @Override public void method1() { }
@Override public void method2() { }
@Override public void method3() { }
@Override public void method4() { }}
// 客户端只需重写需要的方法public class ConcreteAdapter extends AbstractAdapter { @Override public void method1() { // 只实现需要的方法 }}类适配器 vs 对象适配器:
| 对比维度 | 类适配器(继承) | 对象适配器(组合) |
|---|---|---|
| 灵活性 | 低(只能适配一个类) | 高(可以适配多个类) |
| 耦合度 | 高(继承) | 低(组合) |
| 重用性 | 低 | 高 |
| 推荐度 | ⭐⭐ | ⭐⭐⭐⭐⭐ |
Spring MVC 的适配器应用:
// Spring MVC 的 HandlerAdapter:统一不同类型的处理器
// 目标接口:处理器适配器public interface HandlerAdapter { boolean supports(Object handler); // 是否支持该处理器 ModelAndView handle(HttpServletRequest req, HttpServletResponse resp, Object handler);}
// 被适配者1:Controller 接口public interface Controller { ModelAndView handleRequest(HttpServletRequest req, HttpServletResponse resp);}
// 适配器1:Controller 适配器public class SimpleControllerHandlerAdapter implements HandlerAdapter { @Override public boolean supports(Object handler) { return handler instanceof Controller; }
@Override public ModelAndView handle(HttpServletRequest req, HttpServletResponse resp, Object handler) { return ((Controller) handler).handleRequest(req, resp); }}
// 被适配者2:HttpRequestHandler 接口public interface HttpRequestHandler { void handleRequest(HttpServletRequest req, HttpServletResponse resp);}
// 适配器2:HttpRequestHandler 适配器public class HttpRequestHandlerAdapter implements HandlerAdapter { @Override public boolean supports(Object handler) { return handler instanceof HttpRequestHandler; }
@Override public ModelAndView handle(HttpServletRequest req, HttpServletResponse resp, Object handler) { ((HttpRequestHandler) handler).handleRequest(req, resp); return null; }}
// DispatcherServlet 统一处理public class DispatcherServlet { private List<HandlerAdapter> handlerAdapters;
protected void doDispatch(HttpServletRequest req, HttpServletResponse resp) { Object handler = getHandler(req); // 获取处理器
// 找到支持的适配器 HandlerAdapter adapter = getHandlerAdapter(handler);
// 通过适配器统一调用 ModelAndView mv = adapter.handle(req, resp, handler); }}Spring MVC 适配器架构:
┌────────────────────────────────────────────────┐│ Spring MVC 的处理器适配 │├────────────────────────────────────────────────┤│ ││ DispatcherServlet ││ │ ││ ├── HandlerAdapter(统一接口) ││ │ │ ││ │ ├── SimpleControllerHandlerAdapter││ │ │ └── adapts Controller ││ │ │ ││ │ ├── HttpRequestHandlerAdapter ││ │ │ └── adapts HttpRequestHandler││ │ │ ││ │ ├── RequestMappingHandlerAdapter ││ │ │ └── adapts @RequestMapping││ │ │ ││ │ └── ServletHandlerAdapter ││ │ └── adapts HttpServlet ││ │ ││ └── 所有处理器都统一通过适配器调用 ││ │└────────────────────────────────────────────────┘链式追问三:外观模式
Section titled “链式追问三:外观模式”Q4:外观模式是什么?与迪米特法则的关系?中频
Section titled “Q4:外观模式是什么?与迪米特法则的关系?”定义:为子系统中的一组接口提供一个统一的入口,外观模式定义了一个高层接口,这个接口使得子系统更容易使用。
应用场景:简化复杂子系统的使用。
// 子系统:多个复杂的服务public class InventoryService { public boolean checkStock(String productId, int quantity) { // 复杂的库存检查逻辑 return true; }}
public class PaymentService { public boolean charge(String orderId, BigDecimal amount) { // 复杂的支付逻辑 return true; }}
public class ShippingService { public void ship(String orderId, String address) { // 复杂的物流逻辑 }}
public class NotificationService { public void sendEmail(String email, String message) { // 发送邮件 }
public void sendSms(String phone, String message) { // 发送短信 }}
// ✅ 外观模式:提供简化的统一入口public class OrderFacade { private InventoryService inventoryService; private PaymentService paymentService; private ShippingService shippingService; private NotificationService notificationService;
// 简化的下单流程 public boolean placeOrder(Order order) { // 1. 检查库存 if (!inventoryService.checkStock(order.getProductId(), order.getQuantity())) { return false; }
// 2. 扣款 if (!paymentService.charge(order.getId(), order.getTotal())) { return false; }
// 3. 发货 shippingService.ship(order.getId(), order.getAddress());
// 4. 发送通知 notificationService.sendEmail(order.getEmail(), "订单已发货"); notificationService.sendSms(order.getPhone(), "订单已发货");
return true; }}
// 客户端使用:非常简单OrderFacade facade = new OrderFacade();facade.placeOrder(order); // 一行代码完成下单外观模式 vs 适配器模式:
| 对比维度 | 外观模式 | 适配器模式 |
|---|---|---|
| 目的 | 简化接口 | 转换接口 |
| 接口数量 | 多个接口 → 一个接口 | 一个接口 → 另一个接口 |
| 使用场景 | 子系统复杂,需要简化 | 接口不兼容,需要转换 |
| 是否新增功能 | 否(只是封装) | 否(只是转换) |
与迪米特法则的关系:外观模式是迪米特法则的最佳实践。客户端只需要与外观交互,不需要了解子系统的内部结构。
链式追问四:享元模式
Section titled “链式追问四:享元模式”Q5:享元模式是什么?Integer.valueOf() 如何体现?高频
Section titled “Q5:享元模式是什么?Integer.valueOf() 如何体现?”定义:运用共享技术有效地支持大量细粒度对象的复用。通过共享对象减少内存占用。
核心概念:
- 内部状态(Intrinsic):可以共享的状态
- 外部状态(Extrinsic):不可共享的状态,由客户端传入
Integer 的享元模式:
// Integer.valueOf() 的实现public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) { return IntegerCache.cache[i + (-IntegerCache.low)]; // 返回缓存对象 } return new Integer(i); // 超出缓存范围,新建对象}
// Integer 内部缓存private static class IntegerCache { static final int low = -128; static final int high = 127; // 默认缓存 -128 ~ 127 static final Integer cache[];
static { cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) { cache[k] = new Integer(j++); // 预先创建缓存对象 } }}
// 测试Integer a = Integer.valueOf(100);Integer b = Integer.valueOf(100);System.out.println(a == b); // true:同一个缓存对象
Integer c = Integer.valueOf(200);Integer d = Integer.valueOf(200);System.out.println(c == d); // false:超出缓存范围,新建对象享元模式结构:
┌────────────────────────────────────────────────┐│ Integer 的享元缓存 │├────────────────────────────────────────────────┤│ ││ IntegerCache ││ │ ││ ├── cache[](内部状态,可共享) ││ │ ├── cache[0] → Integer(-128) ││ │ ├── cache[1] → Integer(-127) ││ │ ├── ... ││ │ └── cache[255] → Integer(127) ││ │ ││ └── valueOf(int i) ││ ├── -128 ≤ i ≤ 127 → 返回缓存 ││ └── 其他 → 新建 Integer ││ ││ 客户端代码 ││ ├── Integer.valueOf(100) → 缓存命中 ││ └── Integer.valueOf(200) → 新建对象 ││ │└────────────────────────────────────────────────┘享元模式的应用场景:
| 应用 | 内部状态(共享) | 外部状态(不共享) |
|---|---|---|
| Integer | 数值 | 无 |
| String | 字符串内容 | 无(String.intern()) |
| 线程池 | 线程对象 | 任务(Runnable) |
| 连接池 | 连接对象 | SQL 语句 |
性能对比:
// 不用享元:每次新建对象long start = System.currentTimeMillis();for (int i = 0; i < 10000000; i++) { Integer num = new Integer(100); // 每次新建}long end = System.currentTimeMillis();System.out.println("新建对象耗时:" + (end - start) + "ms"); // 约 500ms
// 使用享元:复用缓存对象start = System.currentTimeMillis();for (int i = 0; i < 10000000; i++) { Integer num = Integer.valueOf(100); // 从缓存获取}end = System.currentTimeMillis();System.out.println("享元模式耗时:" + (end - start) + "ms"); // 约 50ms
// 性能提升约 10 倍链式追问五:组合模式
Section titled “链式追问五:组合模式”Q6:组合模式是什么?Java 集合框架如何应用?中频
Section titled “Q6:组合模式是什么?Java 集合框架如何应用?”定义:将对象组合成树形结构以表示”部分-整体”的层次结构,使客户端对单个对象和组合对象的使用具有一致性。
应用场景:树形结构(文件系统、组织架构、菜单)。
// 抽象组件public interface FileSystemNode { void display(); // 统一接口 int getSize();}
// 叶子节点(文件)public class File implements FileSystemNode { private String name; private int size;
public File(String name, int size) { this.name = name; this.size = size; }
@Override public void display() { System.out.println("文件:" + name); }
@Override public int getSize() { return size; }}
// 组合节点(文件夹)public class Directory implements FileSystemNode { private String name; private List<FileSystemNode> children = new ArrayList<>();
public Directory(String name) { this.name = name; }
public void add(FileSystemNode node) { children.add(node); }
@Override public void display() { System.out.println("文件夹:" + name); for (FileSystemNode child : children) { child.display(); // 递归显示子节点 } }
@Override public int getSize() { int total = 0; for (FileSystemNode child : children) { total += child.getSize(); // 递归计算大小 } return total; }}
// 客户端:统一处理文件和文件夹FileSystemNode root = new Directory("根目录");FileSystemNode folder1 = new Directory("文件夹1");FileSystemNode folder2 = new Directory("文件夹2");
folder1.add(new File("文件1.txt", 100));folder1.add(new File("文件2.txt", 200));folder2.add(new File("文件3.txt", 300));
root.add(folder1);root.add(folder2);root.add(new File("文件4.txt", 400));
root.display(); // 递归显示所有文件和文件夹System.out.println("总大小:" + root.getSize()); // 1000组合模式结构图:
┌────────────────────────────────────────────────┐│ 组合模式:树形结构 │├────────────────────────────────────────────────┤│ ││ FileSystemNode(抽象组件) ││ ├── display() ││ └── getSize() ││ │ ││ ├── File(叶子节点) ││ │ ├── display() ││ │ └── getSize() ││ │ ││ └── Directory(组合节点) ││ ├── List<FileSystemNode> ││ ├── add() ││ ├── display() ││ │ └── 递归调用子节点 ││ └── getSize() ││ └── 递归累加子节点大小 ││ ││ 客户端:无需区分 File 和 Directory ││ └── 统一调用 display() 和 getSize() ││ │└────────────────────────────────────────────────┘Java 集合框架的组合模式:
// Java 集合的组合模式Collection<String> single = Collections.singleton("单个元素"); // 叶子节点Collection<String> list = new ArrayList<>(); // 组合节点list.add("元素1");list.add("元素2");
// 客户端统一处理void process(Collection<String> collection) { for (String item : collection) { System.out.println(item); } // 无论单个元素还是集合,统一遍历}
process(single); // 单个元素process(list); // 集合结构型模式总结
Section titled “结构型模式总结”六种模式对比:
| 模式 | 核心问题 | 关键特征 | 典型应用 |
|---|---|---|---|
| 代理 | 控制访问 | 包装对象,控制访问权限 | Spring AOP、RPC |
| 装饰器 | 动态增强功能 | 可叠加的包装器 | Java IO 流 |
| 适配器 | 接口不兼容 | 转换接口 | Spring MVC HandlerAdapter |
| 外观 | 子系统复杂 | 统一入口,简化接口 | JdbcTemplate |
| 享元 | 对象数量多 | 共享内部状态 | Integer 缓存、连接池 |
| 组合 | 树形结构 | 统一处理叶子和组合 | 文件系统、菜单 |
选择决策树:
┌────────────────────────────────────────────────┐│ 结构型模式选择 │├────────────────────────────────────────────────┤│ ││ 需要控制对象访问? ││ └── 是 → 代理模式 ││ ││ 需要动态增强功能? ││ └── 是 → 装饰器模式 ││ ││ 接口不兼容需要转换? ││ └── 是 → 适配器模式 ││ ││ 需要简化复杂子系统? ││ └── 是 → 外观模式 ││ ││ 大量相似对象,内存占用高? ││ └── 是 → 享元模式 ││ ││ 需要处理树形结构? ││ └── 是 → 组合模式 ││ │└────────────────────────────────────────────────┘高频面试题总结
Section titled “高频面试题总结”常见追问:
-
Q:代理模式会增加系统复杂度,值得吗? A:值得。代理可以实现 AOP、缓存、权限控制、远程调用等横切关注点,将这些逻辑与业务逻辑分离,提高代码可维护性。
-
Q:装饰器模式为什么比继承更灵活? A:继承是静态的,装饰器是动态的。装饰器可以在运行时任意组合,而继承在编译时就确定了。
-
Q:享元模式会不会导致线程安全问题? A:享元对象必须是不可变的,只包含内部状态。外部状态由客户端传入,不存在线程安全问题。
-
Q:外观模式和中介者模式有什么区别? A:外观模式是单向的(客户端调用外观),中介者模式是双向的(同事类互相通信)。外观模式简化接口,中介者模式解耦同事类。