[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$f29ju76mtn6wrf":3},{"_id":4,"slug":5,"title":6,"subtitle":7,"kind":8,"cards":9,"tags":29,"categories":30,"source":31,"lang":34,"author":35,"audioState":38,"stats":39,"publishedAt":42,"renderer":43},"6abb462eca21c797c7e9c0db","post-584f54de","「一切皆插件」不是口号：手写一个敢上生产的治理管道过滤器","上一篇把接口治理做成了「一个切面 + 两条过滤器链」，开头立了个 flag：一切皆插件。实现一个 PreFilter \u002F PostFilter 注册成 Bean 就自动入链。 先交代一个背景：这个 Starter 已经接进我自己维护的一个真实项目里跑了，下面这些\"关\"不是理论推演，是照着它踩出来的。…","news",[10,14,19,24],{"headline":6,"body":11,"imageUrl":12,"images":13},"上一篇把接口治理做成了「一个切面 + 两条过滤器链」，开头立了个 flag：一切皆插件。实现一个 PreFilter \u002F PostFilter 注册成 Bean 就自动入链。 先交代一个背景：这个 Starter 已经接进我自己维护的一个真实项目里跑了，下面这些\"关\"不是理论推演，是照着它踩出来的。 但「能写出一个能跑的过滤器」和「敢上生产的过滤器」之间差着好几道关。这篇把我自己踩过的地方整理成一份实战记录：先五分钟入门，再讲 FilterContext 到底给了你什么，最后写一个两个过滤器配合的生产级例子。 需求：所有接口的参数不能为 null（真实需求比这复杂，先简化）： return false 就是短路：后续前置过滤器不跑，业务方法不执行，调用方收到 400。就这么简单——但简单也意味着，契约全靠自觉。下面把它变生产级。 二、FilterContext 能力清单：管道里流通的唯一数据载体 自定义过滤器能做什么、不能做什么，全看 FilterContext 给了什么。它按功能分成六组： 这张表背后有个设计决策值得注意：上下文不区分读写角色。前置过滤器能读 args，后置过滤器也能读；没有「前置只写、后置只读」的类型约束，靠的是 order 区间约定。这是取舍——类型安全换来的是简单，我选简单，因为管道总共就两条链。 Filter 接口继承了 Spring 的 Ordered，内置过滤器占了这些号段，自定义过滤器要对号入座： 选错号段是真实的 bug 来源：我见过把参数校验写在 @Order(50) 的——它跑在流量统计之前，被它拒绝的请求永远进不了统计，监控上的 QPS 看起来比实际低。校验类放 300~399，别越界。 另外两个 Filter 接口自带的方法，写生产级插件时都用得上： getName()：默认返回类名，会出现在过滤器链调试日志和管理接口的 \u002Ffilters 端点里，给它一个人类可读的名字； isEnabled()：运行时开关，默认 true。需要灰度\u002F秒级熔断自定义过滤器时，从配置中心读一个开关返回即可，不用改代码发版。","\u002Fapi\u002Fmedia\u002Fposts\u002Fpost-584f54de\u002F0.webp",{"local":12},{"headline":15,"body":16,"imageUrl":17,"images":18},"这是整个管道最容易被误解的行为，我第一次也没答对：前置过滤器短路拒绝后，后置链不会执行。 设计理由：后置链的职责是「处理业务结果」（耗时统计、日志记录），被拒绝…","这是整个管道最容易被误解的行为，我第一次也没答对：前置过滤器短路拒绝后，后置链不会执行。 设计理由：后置链的职责是「处理业务结果」（耗时统计、日志记录），被拒绝的请求没有业务结果，让它跑一遍没有意义，还省了一次无谓的开销。上一篇说的「拒绝请求不污染耗时指标」，根源就在这里。 别把「无论成败都要执行」的逻辑放进 PostFilter——被拒绝的请求走不到那里。这类清理逻辑要么放业务层 finally，要么想想它是不是真的属于这条管道； PostFilter.doFilter 返回 void，你没有拒绝权。业务已经执行完了，此时说什么都晚了——想要「对返回值不满意就拒绝」的能力，那是前置过滤器的活（提前校验），不是后置的。 内置链对过滤器抛出的异常有兜底：前置过滤器抛异常按 500 处理、error 日志记录过滤器名和 API 标识，不会把异常原样炸给业务。但「不炸给业务」不等于「没发生」——你的过滤器每次请求抛异常，等于全站 500。 所以两条纪律：过滤器里不做慢 IO（通知器这类慢操作自行异步化）；依赖的数据源挂了要降级——比如你的黑名单在 Redis 里，Redis 超时时应该放行并记 warn，而不是把异常抛上去把 503 扫射全站。","\u002Fapi\u002Fmedia\u002Fposts\u002Fpost-584f54de\u002F1.webp",{"local":17},{"headline":20,"body":21,"imageUrl":22,"images":23},"还有一个埋在 FilterContext 里的陷阱要专门点名：getJoinPoint() 是公开的，但你永远不要在过滤器里调…","还有一个埋在 FilterContext 里的陷阱要专门点名：getJoinPoint() 是公开的，但你永远不要在过滤器里调 proceed()。整个管道的契约建立在「proceed 有且仅有一次，由切面调用」上，过滤器碰了它，执行模型直接崩塌。切面作者把它暴露给你是给你读元数据的，不是给你调的。 getArgs() 给你的是真实入参的引用。这意味着两个红线： 不要修改数组元素。改了，业务方法收到的就是改过的——你不知不觉从「观察者」变成了「参与者」，这类 bug 排查起来极其痛苦； 不要把 args 原样打日志。手机号、身份证、token 就在入参里。治理框架默认不打印入参（log-request-params 默认 false）就是这个原因——你的自定义过滤器继承同样的责任。 说到这儿，正好引出这篇的正餐：一个和「入参里有敏感数据」正面交手的例子。 需求：等保审计要求记录每个请求的完整入参，但日志里不能出现手机号和身份证号。 为什么不能一个过滤器搞定：脱敏不能改 args（第三道关的红线 1），审计日志又必须在业务执行后输出（不然没有完整的请求上下文）。所以拆成一对，用 attributes 传数据： 过滤器 A（PreFilter，order=350）：业务执行前，对入参做一份深拷贝并脱敏的快照，放进 attributes——不动原始入参一根汗毛： 过滤器 B（PostFilter，order=600）：业务执行后，从 attributes 取快照，连同结果元数据写成审计日志： 拆成两个的意义，恰恰是上一篇讲的管道语义在起作用： A 在（350）短路时，B 自动不执行——被参数校验拒绝的请求没有业务结果，本来就不该出现在审计里，管道结构替你保证了这件事，一行代码没写； A 和 B 之间没有代码耦合——它们唯一的约定是一个 attribute 键名。哪天审计要换存储（写 Kafka），只换 B；哪天脱敏规则要更新（多一类证件号），只换 A； clientIp 打进审计日志没问题，但别拿它做安全决策——它可能被 X-Forwarded-For","\u002Fapi\u002Fmedia\u002Fposts\u002Fpost-584f54de\u002F2.webp",{"local":22},{"headline":25,"body":26,"imageUrl":27,"images":28},"伪造，框架文档里也这么提醒。 三个注解、两个类、零框架改动，审计需求就接进来了——这就是「一切皆插件」的全部含义。…","伪造，框架文档里也这么提醒。 三个注解、两个类、零框架改动，审计需求就接进来了——这就是「一切皆插件」的全部含义。 把四道关压缩成一张自查清单，写完过滤器过一遍再上生产： order 对号入座了吗？（校验 300~399，日志类 500+） 有没有在 PostFilter 里放「短路时也要执行」的逻辑？ 慢 IO 异步化了吗？数据源挂了是降级还是抛异常？ 有没有碰 proceed()？有没有改 getArgs() 里的元素？日志里有没有裸奔的敏感字段？ 评论区聊聊：你的项目里有哪些逻辑现在长在切面里、其实更适合做成管道插件？下一篇打算拆限流键解析器（RateLimitKeyResolver）——按用户、按 IP、按租户限流的那套玩法，关注不迷路。","\u002Fapi\u002Fmedia\u002Fposts\u002Fpost-584f54de\u002F3.webp",{"local":27},[],[],{"name":32,"url":33},"Juejin","https:\u002F\u002Fjuejin.cn\u002Fpost\u002F7690460679959035947","zh",{"handle":36,"displayName":37},"spots","Spots",null,{"views":40,"likes":41,"saves":41,"shares":41,"completions":41,"opens":41,"skips":41,"depthSum":41},3,0,"2026-09-29T05:01:34.644Z","local"]