秋雨De blog

  • 首页
  • 留言板
  • 关于
  • rss
秋雨De blog
一个技术小白的个人博客
  1. 首页
  2. 未分类
  3. 正文

装饰器模式-如何封装一个好用的HttpUtils

2026年6月11日 182点热度 0人点赞 0条评论

实际开发中经常需要对接多个第三方平台,每个平台的 token 校验规则和返回值格式都不一样。有的平台用 HTTP 状态码表示成功失败,有的永远返回 200 但在 body 里塞自己的错误码,有的 token 过期返回特定错误字段,有的直接抛 401。

最直接的做法是在 HttpUtils 里对每种情况做判断:

public class HttpUtils {
    public static String post(String url, String body) { ... }

    public static String postWithAuthA(String url, String body) {
        // 平台A的 token 逻辑
        // 平台A的返回值判断
    }

    public static String postWithAuthB(String url, String body) {
        // 平台B的 token 逻辑
        // 平台B的返回值判断
    }

    public static String postWithAuthAAndRetry(String url, String body) {
        // 平台A + 重试
    }
    // ...
}

每接入一个新平台就多一批方法,平台规则一变就要翻出对应的方法逐一修改。这么写的时候就觉得不对劲,但当时没想到更好的办法。

后来在工作中持续学习,逐渐了解到设计模式,接触到装饰器链这个思路,才意识到当时的问题可以用这种方式重新组织:把每个平台的差异描述成一个独立的"能力",调用方按需组合,HttpUtils 本身不感知任何平台细节。


先看效果

重新设计之后,调用方式变成这样:

// 针对不同平台预置不同的链
public class HttpUtils {
    public static final HttpRequestChain simple   = new HttpRequestChain();
    public static final HttpRequestChain platformA = simple.add(HttpFilters.authA()); // 平台A的鉴权
    public static final HttpRequestChain platformB = simple.add(HttpFilters.authB()); // 平台B的鉴权
}

// 创建订单:平台A,带鉴权
HttpUtils.platformA.post("/order/create", orderJson);

// 查询订单:平台A,带鉴权 + 重试
HttpUtils.platformA.add(HttpFilters.retry()).post("/order/query", queryJson);

// 查询订单:平台B,带鉴权
HttpUtils.platformB.post("/order/query", queryJson);

// 普通请求,不带任何鉴权
HttpUtils.simple.get("/health");

每个平台的鉴权差异封装在各自的 HttpFilters.authX() 里,调用方只需要选对链,不需要知道平台规则的细节。

需要临时加一个自定义能力,一行搞定,不需要改任何现有代码:

// 每个请求加一个唯一的 X-Request-Id,方便在日志中追踪完整的请求链路
HttpUtils.platformA
    .add(exec -> request -> {
        request.header("X-Request-Id", UUID.randomUUID().toString());
        return exec.execute(request);
    })
    .post("/order/create", orderJson);

调用方不需要知道内部怎么实现的,用哪些能力自己组合,用完就走。


核心设计

整个设计只有两个关键点。

在展开之前,先建立一个直觉。Java 里的 lambda 不只是"简写的匿名类",它本身是一个值,可以被传递、被存储、被当作参数:

// 一个"把字符串变大写"的函数,赋值给变量
Function<String, String> toUpper = s -> s.toUpperCase();

// 一个"在字符串前后加括号"的函数
Function<String, String> wrap = s -> "(" + s + ")";

// 把两个函数组合起来:先大写,再加括号
Function<String, String> combined = toUpper.andThen(wrap);

combined.apply("hello"); // 结果:(HELLO)

函数可以组合,组合之后还是一个函数。这个思路放到 HTTP 请求里:每个"能力"(加 token、重试)就是一个函数,把它们组合起来,得到一个增强版的"执行请求"函数。

前置:HttpRequest 的结构

HttpRequest 是对一次请求的简单封装,支持链式设置参数:

public class HttpRequest {
    public String method;
    public String url;
    public Map<String, String> headers = new HashMap<>();
    public String body;

    public HttpRequest method(String method) { this.method = method; return this; }
    public HttpRequest url(String url) { this.url = url; return this; }
    public HttpRequest header(String key, String value) { this.headers.put(key, value); return this; }
    public HttpRequest body(String body) { this.body = body; return this; }
}

装饰器通过 header()、body() 等方法在请求发出前修改它,HttpUtils::doExecute 拿到最终的 HttpRequest 对象后真正发起请求。

另外还有一个 HttpResult,封装了原始的响应字节,提供多种取值方式:

public class HttpResult {
    private final byte[] raw;

    public HttpResult(byte[] raw) {
        this.raw = raw;
    }

    public byte[] getByte() {
        return raw;
    }

    public String getString() {
        return new String(raw, StandardCharsets.UTF_8);
    }

    public JSONObject getJson() {
        return JSON.parseObject(getString());
    }
}

1. 把"能力"抽象成 executor 的变换

先定义一个函数式接口,表示"执行一次 HTTP 请求":

@FunctionalInterface
public interface RequestExecutor {
    HttpResult execute(HttpRequest request) throws IOException;
}

真正发请求的方法是 HttpUtils::doExecute,它就是最原始的 RequestExecutor。

每个装饰器的本质是:接收一个 executor,返回一个增强版的 executor。用 Java 类型表达就是:

Function<RequestExecutor, RequestExecutor>

比如 authA 装饰器(平台A的鉴权):

// 接收原始 executor,返回一个"先加 token,再调原始 executor"的新 executor
exec -> request -> {
    request.header("Authorization", "Bearer " + getTokenA()); // 获取平台A的token
    return exec.execute(request);
}

比如 retry 装饰器:

exec -> request -> RetryHelper.doWithRetry(request, exec)

RetryHelper 的逻辑是:先正常执行,遇到异常就判断是否需要重试。由于每个平台的错误格式不一样,重试的判断条件也做成可配置的,由调用方传入:

public class RetryHelper {
    // 默认判断:包含 "401" 就重试
    public static HttpResult doWithRetry(HttpRequest request, RequestExecutor executor) throws IOException {
        return doWithRetry(request, executor, e -> e.getMessage().contains("401"));
    }

    // 可自定义重试条件
    public static HttpResult doWithRetry(HttpRequest request, RequestExecutor executor,
                                     Predicate<IOException> shouldRetry) throws IOException {
        try {
            return executor.execute(request);
        } catch (IOException e) {
            if (shouldRetry.test(e)) {
                // token 已过期,此处可加入刷新 token 的逻辑
                return executor.execute(request);  // 重试一次
            }
            throw e;
        }
    }
}

比如平台B的错误码是自定义格式,可以传入自己的判断逻辑:

// 平台B:json 里 code 为 401 才重试
HttpUtils.platformB
    .add(exec -> request -> RetryHelper.doWithRetry(request, exec,
        e -> e.getMessage().contains("平台B token 过期")))
    .post("/order/create", orderJson);

HttpFilters.retry() 走默认逻辑,特殊平台自己传条件,两种场景都覆盖到了。

2. 链式收集,最后一次性折叠执行

HttpRequestChain 只做一件事:收集装饰器,发请求时折叠执行。它不知道任何业务逻辑:

public class HttpRequestChain {
    private final List<Function<RequestExecutor, RequestExecutor>> decorators = new ArrayList<>();

    public HttpRequestChain add(Function<RequestExecutor, RequestExecutor> decorator) {
        HttpRequestChain next = new HttpRequestChain();
        next.decorators.addAll(this.decorators);
        next.decorators.add(decorator);
        return next;  // 返回新链,原链不变
    }

    public HttpResult post(String url, String json) throws IOException {
        return execute(new HttpRequest().method("POST").url(url).body(json));
    }

    public HttpResult get(String url) throws IOException {
        return execute(new HttpRequest().method("GET").url(url));
    }

    private HttpResult execute(HttpRequest req) throws IOException {
        RequestExecutor executor = HttpUtils::doExecute;  // 从最原始的 executor 开始
        for (Function<RequestExecutor, RequestExecutor> deco : decorators) {
            executor = deco.apply(executor);  // 逐层包装
        }
        return executor.execute(req);  // 最终执行
    }
}

具体的能力统一放在 HttpFilters 里:

public class HttpFilters {
    // 平台A:token 放 Header,用 HTTP 状态码判断成功失败
    // 注:getTokenA() 为占位方法,实际项目中替换为真实的 token 获取逻辑
    public static Function<RequestExecutor, RequestExecutor> authA() {
        return exec -> request -> {
            // 平台A通过 HTTP 状态码表示结果,异常情况由 doExecute 直接抛出 IOException
            request.header("Authorization", "Bearer " + getTokenA());
            return exec.execute(request);
        };
    }

    // 平台B:token 放自定义 Header,返回值用 JSONObject 解析错误码
    // 注:getTokenB() 为占位方法,实际项目中替换为真实的 token 获取逻辑
    public static Function<RequestExecutor, RequestExecutor> authB() {
        return exec -> request -> {
            request.header("X-Token", getTokenB()); // 平台B:token 放自定义 Header
            HttpResult result = exec.execute(request);
            JSONObject json = result.getJson();
            if (json.getIntValue("code") == 401) {
                throw new IOException("平台B token 过期");
            }
            return result;
        };
    }

    public static Function<RequestExecutor, RequestExecutor> retry() {
        return exec -> request -> RetryHelper.doWithRetry(request, exec);
    }
}

这样职责是清晰的:HttpRequestChain 是通用的管道容器,HttpFilters 收敛所有业务能力。新增一个能力只加 HttpFilters,容器本身不需要动。

另外,add 每次返回的是新链,原链不会被修改:

HttpRequestChain base = HttpUtils.platformA;

// 两个请求各自组合,互不影响
base.add(HttpFilters.retry()).post("/order/create", orderJson);
base.post("/order/query", queryJson);  // 这条请求不带 retry,base 没有被修改

这意味着 HttpUtils.platformA、HttpUtils.platformB 这些预置链可以安全地在多处复用,不会互相干扰。


跟传统装饰器写法的对比

传统做法是用类套类:

public interface HttpExecutor {
    String execute(String url, String body); // 简化示意,实际对应 HttpRequest
}

public class AuthDecorator implements HttpExecutor {
    private final HttpExecutor wrapped;
    public AuthDecorator(HttpExecutor wrapped) { this.wrapped = wrapped; }

    @Override
    public String execute(String url, String body) {
        // 加 token
        return wrapped.execute(url, body);
    }
}

// 使用:手动套娃
new AuthDecorator(new RetryDecorator(new SimpleExecutor())).execute(url, body);

每加一个能力就要新建一个类,组合靠手动嵌套,用起来很笨重。

用 Function<RequestExecutor, RequestExecutor> 替代"新建类"这一步,每个装饰器就是一个 lambda,add 收集起来最后折叠,本质上是同一个模式,但轻量很多。


一个需要注意的细节:执行顺序

decorators 是顺序折叠的,但实际执行顺序是反的。

以 .add(authA).add(retry) 为例,decorators 里是 [authA, retry],折叠过程是:

executor = doExecute
executor = authA.apply(doExecute)         // authA 包了一层
executor = retry.apply(authA(doExecute))  // retry 又包了一层

最终 retry 在最外层,执行顺序是 retry → authA,跟调用顺序相反。

大多数场景下这不是问题,但如果顺序有要求——比如日志装饰器必须在最外层才能记录完整耗时——就需要注意调用顺序是反过来写的。

如果想让调用顺序和执行顺序保持一致,折叠时倒序遍历就行:

java

for (int i = decorators.size() - 1; i >= 0; i--) {
    executor = decorators.get(i).apply(executor);
}

另一个需要注意的细节:调用链爆炸

这个问题是跟一位大佬聊技术的时候聊到的。链式调用很灵活,但如果不加约束,可能会出现这种代码:

java

HttpUtils.simple
    .add(HttpFilters.authA())
    .add(HttpFilters.retry())
    .add(HttpFilters.trace())  // trace/log 为示意,实现方式同 authA/retry
    .add(HttpFilters.log())
    .post(url, body);

这有两个问题。

一是性能:每次 add 都会 new 一个新的 HttpRequestChain 并复制整个 decorators 列表,链越长复制开销越大。不过实际场景里装饰器数量很少超过5个,这个开销通常可以忽略。

二是维护:链太长意味着调用方在手动管理"我需要哪些能力"。一旦多个地方都需要同一个长链,就会出现复制粘贴,或者靠口口相传"记得加这几个filter"。

解法是对高频组合做收敛,在 HttpUtils 里预置业务场景对应的链,调用方直接用,不需要自己拼:

java

public class HttpUtils {
    public static final HttpRequestChain simple      = new HttpRequestChain();
    public static final HttpRequestChain platformA   = simple.add(HttpFilters.authA());
    public static final HttpRequestChain platformARetry = platformA.add(HttpFilters.retry());  // 高频组合直接预置
}

// 调用方不需要关心链怎么拼
HttpUtils.platformARetry.post("/order/create", orderJson);

自定义链只留给真正有特殊需求的场景,常规请求走预置链。链式调用的灵活性和使用的简单性就都保住了。


这背后是什么思想

写完这个实现之后可以想一个问题:它跟传统OOP装饰器的本质区别是什么?

传统装饰器靠的是对象包对象——每个能力是一个类,组合靠嵌套实例化,结构是静态的。这个实现靠的是函数包函数——每个能力是一个 Function,组合靠折叠,结构是动态组装的。

这个思路在函数式编程里有个名字叫函数组合(Function Composition),核心就是 f(g(h(x))) 这种形式。整个折叠过程本质上就是:

retry( authA( doExecute ) )

每一层都是一个函数包着另一个函数,最后得到一个新函数,不需要建任何类。

在C++里,用函数对象(functor)和模板做类似的事情很自然——能力可以直接用函数表达,不依赖类的继承层级。Java的传统OOP思维会让人下意识地去建类、实现接口,但lambda出现之后,Java也可以用同样的方式写:把能力描述成一个值,传递它、存储它、组合它。

这个模式在很多地方都有同样的影子:Koa.js 和 Express 的中间件管道、OkHttp 的 Interceptor 链、Haskell 里的 Kleisli composition,本质上都是同一套东西。Java没有原生的函数组合操作符,所以这里用 for 循环手动折叠,但思想是一致的。


什么时候适合用这个设计

说实话,直接用这个形态的项目不多。

大多数项目会直接用现成的 HTTP 客户端——OkHttp、Apache HttpClient、Feign,这些本身就内置了拦截器机制,相当于把这套装饰器链的基础设施做好了,不需要自己造。业务代码里更常见的做法是 AOP,加 token、加日志、加重试,套一个切面就解决了。

但它在几个场景下是真实有用的:

对接多个第三方平台,每个平台行为差异大——这正是本文的背景。平台A和平台B的 token 逻辑、返回值格式完全不同,用 AOP 切面很难针对每个平台定制,用这套链反而清晰。

封装给其他团队用的 HTTP 工具库——调用方不知道内部细节,用链式 API 暴露扩展点,调用方按需组合,不需要改工具库本身。

需要在运行时动态组合能力——比如根据配置决定要不要加重试、要不要加鉴权,链式组合比 if/else 堆方法更容易维护。

所以这个设计不是替代 OkHttp 或 AOP,而是在需要对多个平台差异化定制请求行为时,提供一种更清晰的组织方式。


小结

这个设计解决的核心问题是:能力和调用方式解耦。

调用方只需要声明"我要哪些能力",不需要知道这些能力怎么实现,也不需要关心它们之间怎么组合。新增一个能力只加 HttpFilters,不动容器,不动调用方。

传统写法链式装饰器
新增能力改工具类,加新方法或新类HttpFilters 加一个方法,其他不动
能力组合每种组合一个方法或手动套娃调用方自由 add 组合
调用方使用记住用哪个方法链式声明,语义清晰
复用基础链不支持base.add(HttpFilters.retry())、base.add(HttpFilters.authA()) 各自独立

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: 暂无
最后更新:2026年6月11日

fallrain

种一棵树最好的时间是十年前,其次是现在。

点赞
< 上一篇

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

fallrain

种一棵树最好的时间是十年前,其次是现在。

文章目录
  • 先看效果
  • 核心设计
    • 前置:HttpRequest 的结构
    • 1. 把"能力"抽象成 executor 的变换
    • 2. 链式收集,最后一次性折叠执行
  • 跟传统装饰器写法的对比
  • 一个需要注意的细节:执行顺序
  • 另一个需要注意的细节:调用链爆炸
  • 这背后是什么思想
  • 什么时候适合用这个设计
  • 小结
友情连接
猫饭范文泉博客迎風别葉CODING手艺人ScarSu博友圈
归档
  • 2026 年 6 月
  • 2026 年 3 月
  • 2025 年 11 月
  • 2025 年 5 月
  • 2025 年 4 月
  • 2025 年 3 月
  • 2024 年 12 月
  • 2024 年 10 月
  • 2024 年 5 月
  • 2023 年 2 月
  • 2022 年 11 月
  • 2022 年 3 月
  • 2021 年 12 月
  • 2021 年 8 月
  • 2021 年 5 月
  • 2021 年 4 月
  • 2021 年 3 月
  • 2020 年 12 月
  • 2020 年 11 月
  • 2020 年 8 月
  • 2020 年 5 月
  • 2019 年 12 月
  • 2019 年 3 月

吉ICP备18007356号

吉公网安备22020302000184号

Theme Kratos Made By Seaton Jiang

COPYRIGHT © 2026 秋雨De blog ALL RIGHTS RESERVED