惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
W
WeLiveSecurity
O
OpenAI News
N
News and Events Feed by Topic
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Webroot Blog
Webroot Blog
Google Online Security Blog
Google Online Security Blog
云风的 BLOG
云风的 BLOG
N
News | PayPal Newsroom
H
Hacker News: Front Page
博客园_首页
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
The Last Watchdog
The Last Watchdog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
H
Heimdal Security Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
S
Schneier on Security
宝玉的分享
宝玉的分享
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Y
Y Combinator Blog
Cyberwarzone
Cyberwarzone
Microsoft Security Blog
Microsoft Security Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
GbyAI
GbyAI
Cloudbric
Cloudbric
TaoSecurity Blog
TaoSecurity Blog
人人都是产品经理
人人都是产品经理
P
Palo Alto Networks Blog
M
MIT News - Artificial intelligence
G
GRAHAM CLULEY
C
Check Point Blog
Apple Machine Learning Research
Apple Machine Learning Research
Last Week in AI
Last Week in AI
T
Troy Hunt's Blog
L
Lohrmann on Cybersecurity
www.infosecurity-magazine.com
www.infosecurity-magazine.com
P
Proofpoint News Feed
Blog — PlanetScale
Blog — PlanetScale
量子位
博客园 - 聂微东
S
Securelist
博客园 - 三生石上(FineUI控件)
F
Full Disclosure
G
Google Developers Blog
L
LINUX DO - 热门话题
P
Proofpoint News Feed
AI
AI
PCI Perspectives
PCI Perspectives

BlogFinder

日常漫步 Vol.24 之漫步前山河 - 雅余 周报 #1-聊聊本周的收获 - Edwin's Blog 我的OpenCode必装插件与Skill Write Something 掌中之物未必在掌握之中 · CRIVU PiliNara,一个更顺手的 PiliPlus 分支 「NekoEcho」:做一个必有回响的猫娘主题博客 2026-05 书影音总结 简化博客主题 - 安迪 我第一次发布 npm 包 拾花小记#45:中考前的二三事 – 小改学习志 黛西花园5月游 #18 枇杷又熟了的五月月报 一些奇奇怪怪的需求?word仿方正书版的几个小操作 - Xiobb's Blog 0419 御温泉之旅 修复了一些bug,网站基本上趋于稳定了 - 新锐博客 又回到四十年前 如何定义成功 迷鹿屋2026已重新上线 科技冰火两重天+一周回顾 ${title} 热度退了,我反而用得更深了-咕咚同学 我到底该不该换个域名? 随身WIFI折腾记 - 安迪 博客撰写体验提升——hexo pro插件 为什么不用相机把屏幕上的接关密码拍下来? 国清寺与天台山 – Ouroboros ★★★★☆《挽救计划》——久违的经济上行感 - Davidの3号基地 删除右键“打开方式”里多余选项 第三周刊_No.53|一切都会被支付两次 安卓APP通话记录与录音上传踩坑记录 - 子舒的博客 天量下跌 inBox 笔记 2.3.8,把工具栏交给了你-咕咚同学 我把小龙虾搬到了微信-咕咚同学 安好 - 响石潭 Compound Engineering Plugin:让每个工程单元都比上一个更容易 MOSS-TTS Family:开源高质量语音与声音生成模型家族深度解析 Crawl4AI:专为 LLM 设计的开源 Web 爬虫与数据抓取工具 Build Your Own X:从零实现你最喜欢的技术——程序员进阶的终极资源清单 Anthropic Skills:用文件夹教 Claude 专业技能的开源框架 1年的去月球(下) - 梅之夏 欢迎回来。 简单讲讲 ASN.1 与 OID DTV - 直播聚合客户端 5.22-5.27 – 不兴江 还没去过鸭川 – 不兴江 张晶晶同学三刷林志颖 关于我 – 不兴江 爱与嫉妒 – 不兴江 港股被持续做空 备案码花了四百块-咕咚同学 一句话生成封面:我给公众号做了4种风格的AI封面生成技能 「官」方認證 再谈费曼学习法 2026-05-28T00:34:11+08:00 2026-05-28T00:28:45+08:00 离谱的英语学习指南:基于AI的英语进阶系统方法论 iii:零集成架构的后端统一运行时 Claude Code Harness:让 Claude Code 工作有迹可循的工程化框架 Heretic:全自动移除大语言模型审查机制的开源工具 MarkItDown:微软开源的万能文档转 Markdown 利器 Harness:让 Claude Code 秒变多智能体协作工厂 这段时间尽折腾AI Agent了,确实极大地提高了效率 近期动态:两个新站点正式上线啦 误判解除!zhouayuan.com 腾讯安全申诉成功 - 周阿源|玩具设计・插画日常・生活随笔 Ralph:让 AI 编码工具自主循环跑完所有 PRD 任务的量产神器 全都违法 – 个人工作记录 关于zhouayuan.com被误判 “含违规信息” 的说明与申诉记录 - 周阿源|玩具设计・插画日常・生活随笔 小米 MiMo v2.5 Pro 白嫖 最大的人间清醒,兜里有钱,但是不花。 夜晚靓歌(12):于文文现场solo - 王志勇的Blog 今日插画:风扬起的倔强 - 周阿源|玩具设计・插画日常・生活随笔 回门习俗 独立网卡 - 忘记了回忆 500亿入股人工智能企业 从命令行到桌面智能体-咕咚同学 第一性原理读书笔记 行者微评论223-加班の守株待兔-博客|政治与时事-风雨行者 ZOZO开源物理接触求解器:GPU加速的可扩展仿真引擎 OpenStock:开源股票市场交易平台技术深度解析 MoneyPrinterTurbo:基于AI的全自动短视频生成工具深度解析 Claude-Mem:为 Claude Code 构建的持久化记忆压缩系统 Twenty:可代码化定制的企业级开源 CRM 平台技术深度解析 2026-05-26T22:59:17+08:00 企业级开源大模型部署平台 GPUStack 实战教程 1年的去月球(上) - 梅之夏 Sevalla - 静态网站托管服务 不用翻墙、不用注册、不用月费,普通人也能用上 Claude Code 装修灯具要注意⚠️ 黄梅天先锋 - 游子微博 公安备案顺利办结,站点备案全部完成 - 周阿源|玩具设计・插画日常・生活随笔 第三次兑换天猫超市卡了宗宗酱-三维狐少儿编程 Don't think, feel. - Rolen's Blog 人这一辈子,到底图个什么 博客迁移 - Edwin's Blog 情感赛道写作模板 再现本轮行情的典型特征 裁员与平常心-咕咚同学 别让“偷懒”,成为隐私泄露的破绽 片刻 - Jdeal | Life is like a Design.
装饰器模式-如何封装一个好用的HttpUtils
秋雨 De blog · 2026-06-11 · via BlogFinder

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