实际开发中经常需要对接多个第三方平台,每个平台的 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()) 各自独立 |
文章评论