【设计模式】装饰模式(四):框架源码实战——从 Java I/O 到 Spring 到 MyBatis

【设计模式】装饰模式(一)实战:从计费系统的"动态加功能"说起

​​​​​​【设计模式】装饰模式(二):认识装饰模式

【设计模式】装饰模式(三):从装饰模式看 Java 与面向对象设计(原理篇)-CSDN博客

引言

装饰模式不是教科书玩具,我们天天跟它打交道------读文件时 new BufferedInputStream(new FileInputStream("a.txt")),写 Web 时 Filter 里 new TrimParameterRequestWrapper(request),用 MyBatis 时二级缓存自动生效------但我从未把它们跟"装饰模式"这四个字联系起来。在这篇文章我们将深入我们熟知的框架中学习装饰模式,层层拆解,学习优秀框架如何"穿搭"。


一、Java I/O:教科书级的装饰模式现场

小明去东北,自己决定穿几件衣服------想套就套,想脱就脱,每件各管各的保暖。

Java I/O 是装饰模式在 JDK 里最经典的现场。

1.1 类图 + 角色对照

装饰模式角色 Java I/O 里的对应
抽象组件 InputStream
具体组件("人") FileInputStreamByteArrayInputStream
抽象装饰器("衣架") FilterInputStream
具体装饰器("衣服") BufferedInputStreamDataInputStream

1.2 FilterInputStream 是空壳吗?

看源码,FilterInputStream 里几乎所有方法都是一行 return in.xxx()。它自己不干正事,那存在的意义是什么?

工程层面的复用,不是语法层面的必须。

它干了三件事:

  1. 统一声明 protected InputStream in 字段------所有具体装饰器不用各自重复声明
  2. 提供默认委托实现------子类只重写需要改的方法,其余直接继承默认行为
  3. 给所有装饰器一个共同父类------方便类型判断和统一管理

如果你把 FilterInputStream 删掉,让 BufferedInputStream 直接继承 InputStream------透明化照样成立 ,只是每个装饰器都要自己写 in 字段和构造器,样板代码翻倍。

1.3 调用链

java 复制代码
InputStream in = new DataInputStream(
                    new BufferedInputStream(
                        new FileInputStream("data.txt")));
in.read();

实际执行顺序(从外到内):

java 复制代码
DataInputStream.read()
  → 需要读 4 个字节拼 int
  → BufferedInputStream.read()
    → 查 buf,有数据直接返回
    → buf 空了,调 FileInputStream.read(byte[])
      → 操作系统真正读磁盘

每一层只干自己的事:DataInputStream 管类型解析,BufferedInputStream 管批量缓冲,FileInputStream 管真正读文件。调用方只管 read(),不知道里面套了几层。

1.4 小结

Java I/O 是装饰模式的标准标杆:抽象装饰器复用结构,具体装饰器各管各的附加职责,调用方只认抽象组件类型。


二、Spring HttpServletRequestWrapper:半透明的妥协

小明戴了顶帽子,平时别人跟他说话正常聊(透明),但有人想摸帽子上的装饰就得扒开认出"哦你戴了帽"(强转/半透明)。

2.1 场景引入

产品经理说:所有接口提交过来的参数,自动去掉前后空格,Controller 不用自己 trim。

你第一反应可能是 AOP?但 AOP 拿不到 HttpServletRequest 里的原始参数。直接改 Tomcat 源码?别闹。

装饰模式:写一个 HttpServletRequestWrapper 子类,重写 getParameter(),在里面 trim。

2.2 从零写一个 Wrapper

java 复制代码
public class TrimParameterRequestWrapper extends HttpServletRequestWrapper {

    public TrimParameterRequestWrapper(HttpServletRequest request) {
        super(request); // 把原始 request 传给父类,存在 this.request 字段里
    }

    @Override
    public String getParameter(String name) {
        String value = super.getParameter(name); // 先拿原始值
        return value != null ? value.trim() : null; // 再 trim
    }
}

2.3 Filter 里套上去

java 复制代码
@Component
public class TrimFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                        FilterChain chain) throws IOException, ServletException {
        HttpServletRequest wrappedRequest =
            new TrimParameterRequestWrapper((HttpServletRequest) request);
        chain.doFilter(wrappedRequest, response); // 传的是装饰后的对象
    }
}

chain.doFilter(wrappedRequest, ...) 把装饰后的 request 沿 Filter 链往后传,最终到达 Controller。

Controller 里:

java 复制代码
@PostMapping("/user")
public String createUser(HttpServletRequest request) {
    String name = request.getParameter("name");
    // 用户提交 " 张三 "
    // request 实际类型是 TrimParameterRequestWrapper
    // getParameter() 被重写了,自动 trim
    // name 直接就是 "张三"
    return "hello " + name;
}

**Controller 一行没改,但行为变了。**​ 这就是透明化------跟 Java I/O 一模一样的道理。

2.4 半透明

现在想加个批量方法,一次性返回所有参数(已 trim 的):

java 复制代码
public Map<String, String> getAllParamsTrimmed() {
    Map<String, String> result = new HashMap<>();
    Enumeration<String> names = getParameterNames();
    while (names.hasMoreElements()) {
        String name = names.nextElement();
        result.put(name, getParameter(name));
    }
    return result;
}

Controller 里想用:

java 复制代码
// request 声明类型是 HttpServletRequest
// getAllParamsTrimmed() 是 Wrapper 特有方法,接口里没有
// request.getAllParamsTrimmed(); // 编译报错

// 必须强转:
TrimParameterRequestWrapper wrapper = (TrimParameterRequestWrapper) request;
wrapper.getAllParamsTrimmed();

透明性破了。 ​ Controller 原本只认 HttpServletRequest,现在必须知道"这是个 TrimParameterRequestWrapper"才能调特有方法。这就是半透明------大部分方法透明用,特有方法必须强转。

2.5 为什么工程上能接受

方案 透明性 代价
纯装饰(只重写接口方法) ✅ 完全透明 不能加新功能
半透明(加特有方法) ⚠️ 部分透明 少数地方需要强转
直接改核心类 ❌ 破坏开闭原则 改容器代码,升级就炸

HttpServletRequest 是 Servlet 规范定义的接口,你改不了。半透明是三者中最务实的选择------大部分场景享受透明化的好处,少数场景付出强转的代价,两害相权取其轻。

2.6 小结

Spring 的 HttpServletRequestWrapper 展示了装饰模式在真实 Web 框架里的样子:**大部分时候完全透明,偶尔为了扩展特有功能破一下透明,这是工程实践里可接受的代价。**​ 它验证了第二篇说的------透明化不是绝对的,当你需要新增接口之外的方法时,半透明是唯一的出路。


三、MyBatis 缓存:装饰模式的工业化生产

小明上台唱戏,戏服按角色要求一层层穿,顺序不能乱------盔头戴错位了上台就是笑话。

3.1 类图

3.2 特点速览

特点 说明
无抽象装饰器 每个装饰器直接实现 Cache 接口,自己声明 delegate 字段。抽象装饰器是工程最佳实践,不是语法必须
每层只干一件事 日志、同步、LRU、定时清理、序列化------各管各的,互不耦合
框架自动组装 根据配置决定套几层、什么顺序,不需要手动 new
顺序敏感 序列化须在最外层(先序列化再存),LRU 须紧贴 PerpetualCache(直接管 HashMap 大小),反了功能就废

3.3 小结

MyBatis 缓存是装饰模式的"工业化生产"版本------没有抽象装饰器,每个装饰器只干一件事,框架根据配置自动组装,调用方只管 getput,完全不知道里面套了几层。它证明了装饰模式不仅能手动用,还能被框架自动化地玩出花来。

四、三框架横向对比

对比维度 Java I/O Spring Wrapper MyBatis 缓存
抽象装饰器 无,直接实现接口
组装方式 手动 new Filter 里手动 new 框架按配置自动组装
透明性 完全透明 半透明(特有方法需强转) 完全透明
装饰器数量 不固定,按需叠加 通常 1~2 层,也可更多 0~6+ 层,配置驱动
顺序是否敏感 部分敏感(DataInputStream 须在外层) 一般不敏感(Wrapper 间通常互不影响) 敏感(序列化最外、LRU 紧贴核心)

五、什么时候该想到装饰模式

5.1 3 秒识别法

看到代码里有这些信号,就该想到装饰模式:

  • 构造器接收一个同类对象
  • 类内部持有同类型引用
  • 方法体里干了自己的事,同时委托给内部对象(顺序可前可后可两头)
  • 调用方拿到的类型跟传进去的不一样,但用起来没感觉

5.2 该用的信号

当脑子里有这种想法的时候:

  • "我不想改这个类的源码,但想给它加点功能" → 装饰器零侵入
  • "这个功能不是所有人都需要" → 按需挂载
  • "可能要叠加,而且顺序可能有影响" → 一层层套
  • "我想运行时动态决定加不加" → 运行时组合,不是编译时继承

5.3 警惕点

警惕点 说明
调用栈变深 套了 5 层 Wrapper,debug 时栈会很长,心里要有数
半透明蔓延 特有方法越来越多、到处强转时,停下来重新设计,别硬塞
内存泄漏 Wrapper 被缓存/异步持有 → 原始对象连带整个请求上下文释放不了
equals / hashCode 陷阱 Wrapper 没重写,当 Map key 时行为不符合预期

5.4 结尾

装饰模式是"不改源码就能加功能"的最优解,但不是免费的------每一层装饰都是一层间接、一层栈、一层心智负担。用它的判断标准很简单:这个增强是"可选的、可叠加的、调用方无感知的"吗?如果是,套上;如果不是,别硬套。

相关推荐
nvvas1 小时前
Spring AI 2.0正式GA:Token砍掉64%,Java的AI反击战打响了
java·人工智能
程序员阿鹏2 小时前
双亲委派机制
java·jvm·数据结构·后端
步行cgn2 小时前
Spring Bean 的生命周期详解
java·后端·spring
ps酷教程2 小时前
数据权限DataPersmission
java·学习
驭渊的小故事2 小时前
Java 项目-jckson序列化工具
java·开发语言
君顾12 小时前
上海24小时自助健身房系统开发实战指南:从架构设计到功能实现
java·开发语言·健身房
用户3721574261352 小时前
Java 拆分 Word 文档教程:按页、分页符和分节符拆分
java
liyinchi19883 小时前
微信小程序支付遇到“由于小程序违规,支付功能暂时无法使用” 解决办法
java·微信小程序·go