[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site-settings":3,"\u002Farticles\u002Fdesign-pattern-31":26,"\u002Farticles\u002Fdesign-pattern-31\u002Frelated":43,"\u002Farticles\u002Fdesign-pattern-31\u002Fadjacent":70,"\u002Fcategories":82,"render-design-pattern-31":108},{"code":4,"message":5,"data":6},0,"ok",{"author_city":7,"author_years":8,"site_tagline":9,"site_description":10,"skill_one_desc":11,"author_status":12,"skill_two_title":13,"about_hero_image":9,"skill_one_title":14,"icp":15,"author_role":16,"og_default_image":9,"closing_quote_date":17,"toolkit":18,"skill_two_desc":19,"author_bio":9,"site_footer":9,"site_url":20,"author_tagline":21,"weekly_thought":9,"tagline_line_one":22,"author_bio_two":9,"closing_quote":23,"site_name":24,"tagline_line_two":25,"home_hero_image":9},"西安","10","","花开空白 - 个人博客","熟悉Java、Python、Go等后端语言。","努力工作中","前端开发","后端开发","陕ICP备2021009682号-1","开发者","2022年·秋","Java,JavaScript,TypeScript,Python,Go","熟悉Js、Ts语言以及Vue、uniapp等框架。","https:\u002F\u002Firule.cn","很高兴遇见你。","把复杂的事，","把每次一发布，都当成一次和陌生人的握手。","花开空白","写成简单的故事。",{"code":4,"message":5,"data":27},{"id":28,"slug":29,"title":30,"summary":31,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":34,"likeCount":4,"wordCount":35,"readingMinutes":36,"publishedAt":37,"contentMd":38,"contentHtml":39,"tags":40,"metaTitle":9,"metaDescription":9,"keywords":9,"canonicalUrl":9,"robots":41,"ogImage":9,"faq":42,"aiSummary":42},"2113935201573801985","design-pattern-31","设计模式修炼之路 ㉛ 拦截过滤器模式——一个过滤器，只管一件事","一个过滤器一个横切关注点、return 走不出的审计盲区、契约两句话放行就 next、后置代码包住整条链、顺序即范围、与责任链对账","2113897628365230081",1,1038,7452,19,"2025-05-27T20:35:21","> J2EE 模式第六站。\n\n㉚ 结尾埋了个雷：三件套会长大成三十件套。本篇就让它长——横切需求从三件涨到五件：编码、审计（还得\"开始\u002F结束\"各记一笔）、访问日志、登录校验、限流（只挂 \u002Fapi 路径）。全塞进门神的 handle() 里会怎样？\n\n一个方法，六年需求，年年往里塞。**把横切关注点拆成一个个独立的过滤器，串成链，让请求依次穿过**——拦截过滤器模式，J2EE 八式里的安检门。\n\n## 一、拦截过滤器模式是什么\n\nJ2EE 蓝皮书的定义大意：拦截过滤器（Intercepting Filter）在请求到达目标处理器**之前和之后**，通过一串可插拔的过滤器对请求做检查与加工；过滤器彼此独立，由过滤器链负责串联，新增、删除、调整横切逻辑都不必动处理器一根毛。\n\n四个角色：\n\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>角色\u003C\u002Fth>\u003Cth>干什么\u003C\u002Fth>\u003Cth>本文对应\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\u003Ctr>\u003Ctd>Filter（过滤器）\u003C\u002Ftd>\u003Ctd>一个横切关注点一个类；放行还是拦截自己说了算\u003C\u002Ftd>\u003Ctd>Encoding \u002F Audit \u002F Logging \u002F Authentication \u002F RateLimitFilter\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Filter Chain（过滤器链）\u003C\u002Ftd>\u003Ctd>串过滤器 + 管\"传给下一个\"；链尾是目标\u003C\u002Ftd>\u003Ctd>FilterChain\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Target（目标资源）\u003C\u002Ftd>\u003Ctd>链尾真正的业务处理\u003C\u002Ftd>\u003Ctd>Router + OrderController \u002F UserController\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Filter Manager（过滤器管理器）\u003C\u002Ftd>\u003Ctd>装配链、定顺序\u003C\u002Ftd>\u003Ctd>本文并入测试类的装配代码\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\n\n一句话理解分工：**过滤器说\"我这关过不过\"，链说\"下一关是谁\"，目标说\"都过了再来找我\"。**\n\n它同样不是 GoF 二十三式，是 J2EE 蓝皮书成员——和 ㉚ 是亲兄弟：前端控制器是**门**（全应用唯一入口），拦截过滤器是**门上的安检流程**（可插拔的关卡序列）。㉚ 解决\"请求从哪儿进\"，㉛ 解决\"进门路上过哪几道检查\"——现实框架里两个几乎总是同框。\n\n## 二、优点与缺点\n\n**优点：**\n\n1. **一个过滤器一个横切关注点**：新增限流 = 写一个 RateLimitFilter + 装配处加一行，谁也不碰谁——三件套长成三十件套也不怕，每个需求都有了自己的形状。\n2. **可排序**：链就是 List，顺序即声明顺序——编码必须最前？放第一个。想审计包住一切？也放第一个（细节三见）。\n3. **前置\u002F后置天然表达**：chain.next 之前的代码在\"去程\"执行，之后的代码在\"回程\"执行——审计的\"结束\"不再依赖请求一路走运，这是胖门神结构上做不到的。\n4. **可挑路径**：限流只想挂 \u002Fapi？过滤器自己看 path 决定干不干活，不用任何人配合，没有一个 if 长在别人家。\n\n**缺点：**\n\n1. **顺序没有编译器保护**：把授权排在认证前面，授权拿到的是没认证的请求——链的正确性全靠装配的人想明白，排错了就是安全事故，编译器一声不吭。\n2. **不调 next = 静默拦截**：新人写过滤器忘了 chain.next，全站请求无声消失在某一关——契约简单到没有任何东西监督它。\n3. **链长了就是迷宫**：十几二十个过滤器，\"这个请求到底过了哪几关\"变成考古题，只能靠日志倒推。\n\n## 三、什么时候用\n\n判断标准：**横切关注点不止三两个、各有顺序讲究、还要挑路径**——塞进一个方法里已经排不开队了。\n\n- **典型场景**：Web 请求处理——编码、认证、审计、限流，条条都要、顺序敏感、各管各的地盘\n- **框架基建**：Spring Security 整个安全体系就是一条过滤器链（第五节见）\n- **反面对照**：只有一两个横切关注点且顺序无讲究——直接写在前端控制器里更直观。三件套时代用 ㉚ 写死就挺好，**模式是横切关注点长大以后才请出来的**——为一个编码设置立一条链，是杀鸡动了机场安检\n\n## 四、实现方式：安检门怎么搭\n\n场景承接 ㉚：同一个迷你应用，横切需求长到五件——编码、审计（开始\u002F结束各记一笔）、访问日志、登录校验、限流（只挂 \u002Fapi）。看两套世界。\n\n### 4.1 反面教材：门神被六年需求喂胖\n\n每年来一个新需求，每个都往 handle() 里塞一段：\n\n```java\n\u002F** 所有需求都往这一个方法里塞 *\u002F\npublic void handle(Request request) {\n    \u002F\u002F 第 1 年上线时：编码\n    System.out.println(\"   [胖门神] 设置编码 UTF-8\");\n\n    \u002F\u002F 第 2 年新需求：审计要\"开始\u002F结束\"各记一笔\n    System.out.println(\"   [胖门神] 审计 >>> \" + request.displayUser() + \" -> \" + request.getPath());\n\n    \u002F\u002F 第 3 年新需求：登录检查\n    if (!request.isLoggedIn()) {\n        System.out.println(\"   [胖门神] 未登录，拒绝：401 请先登录\");\n        return;\n    }\n\n    \u002F\u002F 第 4 年新需求：访问日志\n    System.out.println(\"   [胖门神] 访问日志：\" + request.displayUser() + \" -> \" + request.getPath());\n\n    \u002F\u002F 第 5 年新需求：API 限流——只挂 \u002Fapi 路径\n    if (request.getPath().startsWith(\"\u002Fapi\")) {\n        System.out.println(\"   [胖门神] \" + request.getPath() + \" 是 API：限流检查（每秒 100 次）\");\n    }\n\n    \u002F\u002F 分发本职，已经被挤到方法尾部\n    Controller controller = routes.get(request.getPath());\n    if (controller == null) {\n        System.out.println(\"   [胖门神] 404：没有这个页面 \" + request.getPath());\n        return;\n    }\n    controller.handle(request);\n\n    \u002F\u002F 审计的\"结束\"记在这里——只有全程顺利的请求才配拥有\n    System.out.println(\"   [胖门神] 审计 \u003C\u003C\u003C 请求处理结束\");\n}\n```\n\n运行输出：\n\n```\n========== 反面教材：门神被需求喂胖 ==========\n-- 请求 \u002Fapi\u002Forder\u002Flist（u001 已登录，全程顺利） --\n   [胖门神] 设置编码 UTF-8\n   [胖门神] 审计 >>> u001 -> \u002Fapi\u002Forder\u002Flist\n   [胖门神] 访问日志：u001 -> \u002Fapi\u002Forder\u002Flist\n   [胖门神] \u002Fapi\u002Forder\u002Flist 是 API：限流检查（每秒 100 次）\n   [OrderController] 业务：拉取订单列表\n   [胖门神] 审计 \u003C\u003C\u003C 请求处理结束\n\n-- 请求 \u002Fapi\u002Forder\u002Flist（匿名用户，401 拒绝） --\n   [胖门神] 设置编码 UTF-8\n   [胖门神] 审计 >>> 匿名 -> \u002Fapi\u002Forder\u002Flist\n   [胖门神] 未登录，拒绝：401 请先登录\n   审计只有 >>> 没有 \u003C\u003C\u003C：return 一走，后置代码全部跳过\n\n-- 请求 \u002Fno\u002Fsuch\u002Fpage（u001，404） --\n   [胖门神] 设置编码 UTF-8\n   [胖门神] 审计 >>> u001 -> \u002Fno\u002Fsuch\u002Fpage\n   [胖门神] 访问日志：u001 -> \u002Fno\u002Fsuch\u002Fpage\n   [胖门神] 404：没有这个页面 \u002Fno\u002Fsuch\u002Fpage\n   又丢一笔：每多一个 return，审计盲区就大一块\n```\n\n三个病灶全齐：**return 走不出审计盲区**——审计的\"结束\"写在方法尾部，可方法里每个横切关注点都有提前 return 的权利：401 一个 return、404 一个 return，每多一个 return，盲区就大一块；想补救就在每个 return 前补一行审计，可 return 还会越加越多，每个都是新的漏点。**挑路径靠 if 越积越多**——限流只挂 \u002Fapi 是一个 if，哪天\"静态资源免检\"又是一个 if，每个需求留下自己的形状，handle() 长成一碗面条。**需求没有形状**——六年的需求挤在一个方法里，靠注释分块、靠人脑维持秩序。注意：每一关都干了活、功能一切正常——反面教材从来不是功能错误，是结构错误。\n\n病根一句话：**顺序型收尾**（\"结束\"写在最后，人人可以跳过它）和**短路型控制流**（不过关就走人）在同一个方法里天生打架。\n\n### 4.2 正解：拆成过滤器，串成链\n\n**过滤器接口，契约只有两句话**：\n\n```java\npublic interface Filter {\n\n    void doFilter(Request request, FilterChain chain);\n}\n```\n\n放行就调 chain.next，拦截就不调——链的一切行为都从这两句推出。\n\n**链管\"传给下一个\"，游标走到头就是目标**：\n\n```java\npublic void next(Request request) {\n    if (index \u003C filters.size()) {\n        filters.get(index++).doFilter(request, this);\n    } else {\n        endpoint.accept(request);   \u002F\u002F 链尾：分发器（Router）兜 404\n    }\n}\n```\n\n**每个过滤器只管一件事**——登录校验，拦截就是简简单单不调 next：\n\n```java\npublic class AuthenticationFilter implements Filter {\n\n    @Override\n    public void doFilter(Request request, FilterChain chain) {\n        if (!request.isLoggedIn()) {\n            System.out.println(\"   [登录过滤器] 未登录，拦截：401 请先登录\");\n            return;\n        }\n        System.out.println(\"   [登录过滤器] \" + request.getUserId() + \" 已登录，放行\");\n        chain.next(request);\n    }\n}\n```\n\n**审计过滤器——后置代码包住整条链的关键**：\n\n```java\npublic class AuditFilter implements Filter {\n\n    @Override\n    public void doFilter(Request request, FilterChain chain) {\n        System.out.println(\"   [审计过滤器] >>> 收到 \" + request.displayUser() + \" -> \" + request.getPath());\n        chain.next(request);\n        System.out.println(\"   [审计过滤器] \u003C\u003C\u003C 请求处理结束\");\n    }\n}\n```\n\n**装配——顺序即声明顺序，想调就调 List**：\n\n```java\nRouter router = new Router()\n        .add(\"\u002Fapi\u002Forder\u002Flist\", new OrderController())\n        .add(\"\u002Fuser\u002Fprofile\", new UserController());\n\nFilterChain chain = new FilterChain(\n        List.of(new EncodingFilter(),\n                new AuditFilter(),\n                new LoggingFilter(),\n                new AuthenticationFilter(),\n                new RateLimitFilter()),\n        router::route);\n```\n\n运行输出：\n\n```\n========== 正解：横切关注点拆成过滤器串成链 ==========\n-- 请求 \u002Fapi\u002Forder\u002Flist（u001 已登录，走完全链） --\n   [编码过滤器] 统一设置编码 UTF-8\n   [审计过滤器] >>> 收到 u001 -> \u002Fapi\u002Forder\u002Flist\n   [日志过滤器] 访问日志：u001 -> \u002Fapi\u002Forder\u002Flist\n   [登录过滤器] u001 已登录，放行\n   [限流过滤器] \u002Fapi\u002Forder\u002Flist 是 API：限流检查（每秒 100 次）\n   [分发器] \u002Fapi\u002Forder\u002Flist -> OrderController\n   [OrderController] 业务：拉取订单列表\n   [审计过滤器] \u003C\u003C\u003C 请求处理结束\n\n-- 请求 \u002Fapi\u002Forder\u002Flist（匿名用户，被登录过滤器拦截） --\n   [编码过滤器] 统一设置编码 UTF-8\n   [审计过滤器] >>> 收到 匿名 -> \u002Fapi\u002Forder\u002Flist\n   [日志过滤器] 访问日志：匿名 -> \u002Fapi\u002Forder\u002Flist\n   [登录过滤器] 未登录，拦截：401 请先登录\n   [审计过滤器] \u003C\u003C\u003C 请求处理结束\n   拦截归拦截，审计 \u003C\u003C\u003C 照样记——后置代码包住了后面的整条链\n\n-- 请求 \u002Fno\u002Fsuch\u002Fpage（u001，链尾 404） --\n   [编码过滤器] 统一设置编码 UTF-8\n   [审计过滤器] >>> 收到 u001 -> \u002Fno\u002Fsuch\u002Fpage\n   [日志过滤器] 访问日志：u001 -> \u002Fno\u002Fsuch\u002Fpage\n   [登录过滤器] u001 已登录，放行\n   [分发器] 404：没有这个页面 \u002Fno\u002Fsuch\u002Fpage\n   [审计过滤器] \u003C\u003C\u003C 请求处理结束\n   404 也逃不掉审计 \u003C\u003C\u003C\n\n-- 请求 \u002Fuser\u002Fprofile（u001，非 API 路径） --\n   [编码过滤器] 统一设置编码 UTF-8\n   [审计过滤器] >>> 收到 u001 -> \u002Fuser\u002Fprofile\n   [日志过滤器] 访问日志：u001 -> \u002Fuser\u002Fprofile\n   [登录过滤器] u001 已登录，放行\n   [分发器] \u002Fuser\u002Fprofile -> UserController\n   [UserController] 业务：返回个人资料\n   [审计过滤器] \u003C\u003C\u003C 请求处理结束\n   限流过滤器只认 \u002Fapi：不是自己的地盘，直接放行\n```\n\n四处对照读出含金量：匿名请求被登录过滤器拦下，**审计的 \u003C\u003C\u003C 照样记**——反面教材里 return 走不出的盲区，在链上结构上就不存在；404 发生在链尾，**同样逃不掉审计**——后置代码包住的是\"后面的整条链\"，链上发生什么都拦不住它执行；`\u002Fuser\u002Fprofile` 全程没出现限流的影子——RateLimitFilter 自己看路径，不是自己的地盘直接放行；㉚ 门神身上的横切关注点被整链收走后，剩下的本职瘦成了链尾的分发器——**门与安检流程，就此分家**。\n\n### 4.3 三处细节\n\n**细节一：契约只有两句话。** 放行就调 chain.next，拦截就不调。所有行为从这两句推出：不调 next，请求到此为止，后面的过滤器和目标全不执行；next 之后的代码要等回程才执行。这套契约没有任何东西监督（缺点二），写过滤器第一件事是把它焊在脑子里。\n\n**细节二：后置代码包住的是\"后面的整条链\"。** next 之前的代码在去程执行，之后的代码在回程执行——AuditFilter 的 >>> 和 \u003C\u003C\u003C 像一双筷子，夹住了它后面的一切：后面的过滤器、拦截、404、目标业务，全在筷子中间，一个都溜不掉。胖门神的病根是**顺序型收尾**（写在方法尾，谁 return 都能跳过它），链的解法是**对称包裹**（写在 next 两侧，回程必经）——本质是 try\u002Ffinally 的亲戚，但表达力强得多：链上每一层都能这么包，一层套一层，洋葱圈一样。\n\n**细节三：顺序即范围。** 过滤器越靠前，它包住的范围越大：AuditFilter 放第一个，连编码都在它筷子里；放最后，只包住目标。\"编码必须最前\"\"权限必须在认证后\"翻译成链语言就是：**用声明顺序表达依赖关系**。这也是缺点一的来源——顺序即语义，语义错了编译器不吭声，装配的人必须想明白。\n\n## 五、在框架里找它\n\n1. **Servlet Filter**：javax.servlet.Filter 的 `doFilter(request, response, chain)` + FilterChain——本文 Filter 接口的亲爹，连\"不调 chain.doFilter 就是拦截\"这条契约都一字不差；@WebFilter 的 urlPatterns 就是\"挑路径\"。你跑的每个 Web 应用，最底下都趴着这条链。\n\n2. **Spring MVC 的 HandlerInterceptor**：preHandle \u002F postHandle \u002F afterCompletion 三段式——本文\"去程\u002F回程\"的工业化版本：preHandle 返回 false 相当于不调 next，afterCompletion 连异常场景都兜住，比本文的裸回程更周全。\n\n3. **Spring Security**：一整个框架就是一条 SecurityFilterChain——UsernamePasswordAuthenticationFilter 认证、授权过滤器放后面，十几个过滤器各就各位，**顺序就是安全语义本身**。想自定义登录？往链上插一个过滤器，别人的一个都不用动——\"可插拔\"三个字的工业级兑现。\n\n4. **Express \u002F Koa 的 middleware**：JavaScript 世界同款——app.use(fn) 挂上去，fn 里调 next() 放行；Django 中间件、Gin 的 handler 链，全球框架抄的是同一份作业。\n\n5. **Netty 的 ChannelPipeline**：入站\u002F出站事件在 handler 链上双向穿过，编码器、解码器、业务 handler 排队站——过滤器链思想搬到了网络层，一条 pipeline 就是安检门的传送带版。\n\n## 六、两条链对账——⑰ 责任链 vs ㉛ 拦截过滤器\n\n⑰ 讲责任链时留过一个口子：第三节第 4 条场景\"请求预处理管线\"。两条链长得实在太像——都是\"传给下一个\"，都靠节点自觉。摊开对账：\n\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>\u003C\u002Fth>\u003Cth>责任链（⑰）\u003C\u002Fth>\u003Cth>拦截过滤器（㉛）\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\u003Ctr>\u003Ctd>链上的人\u003C\u002Ftd>\u003Ctd>候选处理者：谁有权限谁签\u003C\u002Ftd>\u003Ctd>关卡：每个都要过一遍\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>谁终结链\u003C\u002Ftd>\u003Ctd>处理者命中即终结（组长签了就不劳经理）\u003C\u002Ftd>\u003Ctd>拦截者，或链尾目标\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>链尾\u003C\u002Ftd>\u003Ctd>开放的——可能没人处理（⑰ 缺点一：200 万报销单落空）\u003C\u002Ftd>\u003Ctd>一定有目标等着（Router 兜 404）\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>链的语义\u003C\u002Ftd>\u003Ctd>这个请求**谁来处理**\u003C\u002Ftd>\u003Ctd>这个请求**怎么穿过**\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>典型\u003C\u002Ftd>\u003Ctd>报销审批、异常处理 catch\u003C\u002Ftd>\u003Ctd>编码、认证、审计、限流\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\n\n一句话：**责任链是\"谁认领这个活\"，拦截过滤器是\"这个活要过哪几道关\"**。Java 的异常处理是责任链——catch 命中一个就结束，后面再合适也不看；Servlet Filter 是拦截过滤器——每个请求所有过滤器依次穿过，谁都不能跳过。顺带一笔：⑩ 装饰器也\"包住\"，但装饰器包的是**能力**（同一个接口的增强），过滤器包的是**请求**（横切关注点的加工）——包的东西不同，人生就不同。\n\n## 写在最后\n\n- 拦截过滤器 = 横切关注点一人一格排成链：可插拔、可排序、可挑路径\n- 契约只有两句话：放行就 chain.next，拦截就不调——链的一切行为由此推出\n- 后置代码写在 next 两侧，包住后面的整条链——return 走不出的审计盲区，链上结构上不存在\n- 顺序即范围：越靠前包得越宽；用声明顺序表达依赖，但顺序没有编译器保护\n- 当代转世：Servlet Filter、HandlerInterceptor、Spring Security、Express middleware、Netty Pipeline——安检门全球连锁\n- 对账：⑰ 责任链挑人（谁认领），㉛ 拦截过滤器过卡（怎么穿过）；⑩ 装饰器包能力，㉛ 包请求\n\n过滤器链把横切关注点安排得明明白白——可凑近看装配代码：五个过滤器、两个控制器，全是 new 出来摆好的。真实应用何止这几个角色：DAO、缓存、消息队列、支付网关……谁需要什么服务，要么构造函数一路传到底，要么满地 new 各造一份。有没有一个\"服务注册处\"，服务登记一次、全应用按名取用？——服务定位器模式，下一篇见。\n","\u003Cblockquote>\n \u003Cp>J2EE 模式第六站。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>㉚ 结尾埋了个雷：三件套会长大成三十件套。本篇就让它长——横切需求从三件涨到五件：编码、审计（还得\"开始\u002F结束\"各记一笔）、访问日志、登录校验、限流（只挂 \u002Fapi 路径）。全塞进门神的 handle() 里会怎样？\u003C\u002Fp>\n\u003Cp>一个方法，六年需求，年年往里塞。\u003Cstrong>把横切关注点拆成一个个独立的过滤器，串成链，让请求依次穿过\u003C\u002Fstrong>——拦截过滤器模式，J2EE 八式里的安检门。\u003C\u002Fp>\n\u003Ch2>一、拦截过滤器模式是什么\u003C\u002Fh2>\n\u003Cp>J2EE 蓝皮书的定义大意：拦截过滤器（Intercepting Filter）在请求到达目标处理器\u003Cstrong>之前和之后\u003C\u002Fstrong>，通过一串可插拔的过滤器对请求做检查与加工；过滤器彼此独立，由过滤器链负责串联，新增、删除、调整横切逻辑都不必动处理器一根毛。\u003C\u002Fp>\n\u003Cp>四个角色：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>角色\u003C\u002Fth>\n   \u003Cth>干什么\u003C\u002Fth>\n   \u003Cth>本文对应\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>Filter（过滤器）\u003C\u002Ftd>\n   \u003Ctd>一个横切关注点一个类；放行还是拦截自己说了算\u003C\u002Ftd>\n   \u003Ctd>Encoding \u002F Audit \u002F Logging \u002F Authentication \u002F RateLimitFilter\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Filter Chain（过滤器链）\u003C\u002Ftd>\n   \u003Ctd>串过滤器 + 管\"传给下一个\"；链尾是目标\u003C\u002Ftd>\n   \u003Ctd>FilterChain\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Target（目标资源）\u003C\u002Ftd>\n   \u003Ctd>链尾真正的业务处理\u003C\u002Ftd>\n   \u003Ctd>Router + OrderController \u002F UserController\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Filter Manager（过滤器管理器）\u003C\u002Ftd>\n   \u003Ctd>装配链、定顺序\u003C\u002Ftd>\n   \u003Ctd>本文并入测试类的装配代码\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>一句话理解分工：\u003Cstrong>过滤器说\"我这关过不过\"，链说\"下一关是谁\"，目标说\"都过了再来找我\"。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>它同样不是 GoF 二十三式，是 J2EE 蓝皮书成员——和 ㉚ 是亲兄弟：前端控制器是\u003Cstrong>门\u003C\u002Fstrong>（全应用唯一入口），拦截过滤器是\u003Cstrong>门上的安检流程\u003C\u002Fstrong>（可插拔的关卡序列）。㉚ 解决\"请求从哪儿进\"，㉛ 解决\"进门路上过哪几道检查\"——现实框架里两个几乎总是同框。\u003C\u002Fp>\n\u003Ch2>二、优点与缺点\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>优点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>一个过滤器一个横切关注点\u003C\u002Fstrong>：新增限流 = 写一个 RateLimitFilter + 装配处加一行，谁也不碰谁——三件套长成三十件套也不怕，每个需求都有了自己的形状。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>可排序\u003C\u002Fstrong>：链就是 List，顺序即声明顺序——编码必须最前？放第一个。想审计包住一切？也放第一个（细节三见）。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>前置\u002F后置天然表达\u003C\u002Fstrong>：chain.next 之前的代码在\"去程\"执行，之后的代码在\"回程\"执行——审计的\"结束\"不再依赖请求一路走运，这是胖门神结构上做不到的。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>可挑路径\u003C\u002Fstrong>：限流只想挂 \u002Fapi？过滤器自己看 path 决定干不干活，不用任何人配合，没有一个 if 长在别人家。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>\u003Cstrong>缺点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>顺序没有编译器保护\u003C\u002Fstrong>：把授权排在认证前面，授权拿到的是没认证的请求——链的正确性全靠装配的人想明白，排错了就是安全事故，编译器一声不吭。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>不调 next = 静默拦截\u003C\u002Fstrong>：新人写过滤器忘了 chain.next，全站请求无声消失在某一关——契约简单到没有任何东西监督它。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>链长了就是迷宫\u003C\u002Fstrong>：十几二十个过滤器，\"这个请求到底过了哪几关\"变成考古题，只能靠日志倒推。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>三、什么时候用\u003C\u002Fh2>\n\u003Cp>判断标准：\u003Cstrong>横切关注点不止三两个、各有顺序讲究、还要挑路径\u003C\u002Fstrong>——塞进一个方法里已经排不开队了。\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>典型场景\u003C\u002Fstrong>：Web 请求处理——编码、认证、审计、限流，条条都要、顺序敏感、各管各的地盘\u003C\u002Fli>\n \u003Cli>\u003Cstrong>框架基建\u003C\u002Fstrong>：Spring Security 整个安全体系就是一条过滤器链（第五节见）\u003C\u002Fli>\n \u003Cli>\u003Cstrong>反面对照\u003C\u002Fstrong>：只有一两个横切关注点且顺序无讲究——直接写在前端控制器里更直观。三件套时代用 ㉚ 写死就挺好，\u003Cstrong>模式是横切关注点长大以后才请出来的\u003C\u002Fstrong>——为一个编码设置立一条链，是杀鸡动了机场安检\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2>四、实现方式：安检门怎么搭\u003C\u002Fh2>\n\u003Cp>场景承接 ㉚：同一个迷你应用，横切需求长到五件——编码、审计（开始\u002F结束各记一笔）、访问日志、登录校验、限流（只挂 \u002Fapi）。看两套世界。\u003C\u002Fp>\n\u003Ch3>4.1 反面教材：门神被六年需求喂胖\u003C\u002Fh3>\n\u003Cp>每年来一个新需求，每个都往 handle() 里塞一段：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">\u002F** 所有需求都往这一个方法里塞 *\u002F\npublic void handle(Request request) {\n    \u002F\u002F 第 1 年上线时：编码\n    System.out.println(\"   [胖门神] 设置编码 UTF-8\");\n\n    \u002F\u002F 第 2 年新需求：审计要\"开始\u002F结束\"各记一笔\n    System.out.println(\"   [胖门神] 审计 &gt;&gt;&gt; \" + request.displayUser() + \" -&gt; \" + request.getPath());\n\n    \u002F\u002F 第 3 年新需求：登录检查\n    if (!request.isLoggedIn()) {\n        System.out.println(\"   [胖门神] 未登录，拒绝：401 请先登录\");\n        return;\n    }\n\n    \u002F\u002F 第 4 年新需求：访问日志\n    System.out.println(\"   [胖门神] 访问日志：\" + request.displayUser() + \" -&gt; \" + request.getPath());\n\n    \u002F\u002F 第 5 年新需求：API 限流——只挂 \u002Fapi 路径\n    if (request.getPath().startsWith(\"\u002Fapi\")) {\n        System.out.println(\"   [胖门神] \" + request.getPath() + \" 是 API：限流检查（每秒 100 次）\");\n    }\n\n    \u002F\u002F 分发本职，已经被挤到方法尾部\n    Controller controller = routes.get(request.getPath());\n    if (controller == null) {\n        System.out.println(\"   [胖门神] 404：没有这个页面 \" + request.getPath());\n        return;\n    }\n    controller.handle(request);\n\n    \u002F\u002F 审计的\"结束\"记在这里——只有全程顺利的请求才配拥有\n    System.out.println(\"   [胖门神] 审计 &lt;&lt;&lt; 请求处理结束\");\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行输出：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>========== 反面教材：门神被需求喂胖 ==========\n-- 请求 \u002Fapi\u002Forder\u002Flist（u001 已登录，全程顺利） --\n   [胖门神] 设置编码 UTF-8\n   [胖门神] 审计 &gt;&gt;&gt; u001 -&gt; \u002Fapi\u002Forder\u002Flist\n   [胖门神] 访问日志：u001 -&gt; \u002Fapi\u002Forder\u002Flist\n   [胖门神] \u002Fapi\u002Forder\u002Flist 是 API：限流检查（每秒 100 次）\n   [OrderController] 业务：拉取订单列表\n   [胖门神] 审计 &lt;&lt;&lt; 请求处理结束\n\n-- 请求 \u002Fapi\u002Forder\u002Flist（匿名用户，401 拒绝） --\n   [胖门神] 设置编码 UTF-8\n   [胖门神] 审计 &gt;&gt;&gt; 匿名 -&gt; \u002Fapi\u002Forder\u002Flist\n   [胖门神] 未登录，拒绝：401 请先登录\n   审计只有 &gt;&gt;&gt; 没有 &lt;&lt;&lt;：return 一走，后置代码全部跳过\n\n-- 请求 \u002Fno\u002Fsuch\u002Fpage（u001，404） --\n   [胖门神] 设置编码 UTF-8\n   [胖门神] 审计 &gt;&gt;&gt; u001 -&gt; \u002Fno\u002Fsuch\u002Fpage\n   [胖门神] 访问日志：u001 -&gt; \u002Fno\u002Fsuch\u002Fpage\n   [胖门神] 404：没有这个页面 \u002Fno\u002Fsuch\u002Fpage\n   又丢一笔：每多一个 return，审计盲区就大一块\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>三个病灶全齐：\u003Cstrong>return 走不出审计盲区\u003C\u002Fstrong>——审计的\"结束\"写在方法尾部，可方法里每个横切关注点都有提前 return 的权利：401 一个 return、404 一个 return，每多一个 return，盲区就大一块；想补救就在每个 return 前补一行审计，可 return 还会越加越多，每个都是新的漏点。\u003Cstrong>挑路径靠 if 越积越多\u003C\u002Fstrong>——限流只挂 \u002Fapi 是一个 if，哪天\"静态资源免检\"又是一个 if，每个需求留下自己的形状，handle() 长成一碗面条。\u003Cstrong>需求没有形状\u003C\u002Fstrong>——六年的需求挤在一个方法里，靠注释分块、靠人脑维持秩序。注意：每一关都干了活、功能一切正常——反面教材从来不是功能错误，是结构错误。\u003C\u002Fp>\n\u003Cp>病根一句话：\u003Cstrong>顺序型收尾\u003C\u002Fstrong>（\"结束\"写在最后，人人可以跳过它）和\u003Cstrong>短路型控制流\u003C\u002Fstrong>（不过关就走人）在同一个方法里天生打架。\u003C\u002Fp>\n\u003Ch3>4.2 正解：拆成过滤器，串成链\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>过滤器接口，契约只有两句话\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public interface Filter {\n\n    void doFilter(Request request, FilterChain chain);\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>放行就调 chain.next，拦截就不调——链的一切行为都从这两句推出。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>链管\"传给下一个\"，游标走到头就是目标\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public void next(Request request) {\n    if (index &lt; filters.size()) {\n        filters.get(index++).doFilter(request, this);\n    } else {\n        endpoint.accept(request);   \u002F\u002F 链尾：分发器（Router）兜 404\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>每个过滤器只管一件事\u003C\u002Fstrong>——登录校验，拦截就是简简单单不调 next：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class AuthenticationFilter implements Filter {\n\n    @Override\n    public void doFilter(Request request, FilterChain chain) {\n        if (!request.isLoggedIn()) {\n            System.out.println(\"   [登录过滤器] 未登录，拦截：401 请先登录\");\n            return;\n        }\n        System.out.println(\"   [登录过滤器] \" + request.getUserId() + \" 已登录，放行\");\n        chain.next(request);\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>审计过滤器——后置代码包住整条链的关键\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class AuditFilter implements Filter {\n\n    @Override\n    public void doFilter(Request request, FilterChain chain) {\n        System.out.println(\"   [审计过滤器] &gt;&gt;&gt; 收到 \" + request.displayUser() + \" -&gt; \" + request.getPath());\n        chain.next(request);\n        System.out.println(\"   [审计过滤器] &lt;&lt;&lt; 请求处理结束\");\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>装配——顺序即声明顺序，想调就调 List\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">Router router = new Router()\n        .add(\"\u002Fapi\u002Forder\u002Flist\", new OrderController())\n        .add(\"\u002Fuser\u002Fprofile\", new UserController());\n\nFilterChain chain = new FilterChain(\n        List.of(new EncodingFilter(),\n                new AuditFilter(),\n                new LoggingFilter(),\n                new AuthenticationFilter(),\n                new RateLimitFilter()),\n        router::route);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行输出：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>========== 正解：横切关注点拆成过滤器串成链 ==========\n-- 请求 \u002Fapi\u002Forder\u002Flist（u001 已登录，走完全链） --\n   [编码过滤器] 统一设置编码 UTF-8\n   [审计过滤器] &gt;&gt;&gt; 收到 u001 -&gt; \u002Fapi\u002Forder\u002Flist\n   [日志过滤器] 访问日志：u001 -&gt; \u002Fapi\u002Forder\u002Flist\n   [登录过滤器] u001 已登录，放行\n   [限流过滤器] \u002Fapi\u002Forder\u002Flist 是 API：限流检查（每秒 100 次）\n   [分发器] \u002Fapi\u002Forder\u002Flist -&gt; OrderController\n   [OrderController] 业务：拉取订单列表\n   [审计过滤器] &lt;&lt;&lt; 请求处理结束\n\n-- 请求 \u002Fapi\u002Forder\u002Flist（匿名用户，被登录过滤器拦截） --\n   [编码过滤器] 统一设置编码 UTF-8\n   [审计过滤器] &gt;&gt;&gt; 收到 匿名 -&gt; \u002Fapi\u002Forder\u002Flist\n   [日志过滤器] 访问日志：匿名 -&gt; \u002Fapi\u002Forder\u002Flist\n   [登录过滤器] 未登录，拦截：401 请先登录\n   [审计过滤器] &lt;&lt;&lt; 请求处理结束\n   拦截归拦截，审计 &lt;&lt;&lt; 照样记——后置代码包住了后面的整条链\n\n-- 请求 \u002Fno\u002Fsuch\u002Fpage（u001，链尾 404） --\n   [编码过滤器] 统一设置编码 UTF-8\n   [审计过滤器] &gt;&gt;&gt; 收到 u001 -&gt; \u002Fno\u002Fsuch\u002Fpage\n   [日志过滤器] 访问日志：u001 -&gt; \u002Fno\u002Fsuch\u002Fpage\n   [登录过滤器] u001 已登录，放行\n   [分发器] 404：没有这个页面 \u002Fno\u002Fsuch\u002Fpage\n   [审计过滤器] &lt;&lt;&lt; 请求处理结束\n   404 也逃不掉审计 &lt;&lt;&lt;\n\n-- 请求 \u002Fuser\u002Fprofile（u001，非 API 路径） --\n   [编码过滤器] 统一设置编码 UTF-8\n   [审计过滤器] &gt;&gt;&gt; 收到 u001 -&gt; \u002Fuser\u002Fprofile\n   [日志过滤器] 访问日志：u001 -&gt; \u002Fuser\u002Fprofile\n   [登录过滤器] u001 已登录，放行\n   [分发器] \u002Fuser\u002Fprofile -&gt; UserController\n   [UserController] 业务：返回个人资料\n   [审计过滤器] &lt;&lt;&lt; 请求处理结束\n   限流过滤器只认 \u002Fapi：不是自己的地盘，直接放行\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>四处对照读出含金量：匿名请求被登录过滤器拦下，\u003Cstrong>审计的 &lt;&lt;&lt; 照样记\u003C\u002Fstrong>——反面教材里 return 走不出的盲区，在链上结构上就不存在；404 发生在链尾，\u003Cstrong>同样逃不掉审计\u003C\u002Fstrong>——后置代码包住的是\"后面的整条链\"，链上发生什么都拦不住它执行；\u003Ccode>\u002Fuser\u002Fprofile\u003C\u002Fcode> 全程没出现限流的影子——RateLimitFilter 自己看路径，不是自己的地盘直接放行；㉚ 门神身上的横切关注点被整链收走后，剩下的本职瘦成了链尾的分发器——\u003Cstrong>门与安检流程，就此分家\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Ch3>4.3 三处细节\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>细节一：契约只有两句话。\u003C\u002Fstrong> 放行就调 chain.next，拦截就不调。所有行为从这两句推出：不调 next，请求到此为止，后面的过滤器和目标全不执行；next 之后的代码要等回程才执行。这套契约没有任何东西监督（缺点二），写过滤器第一件事是把它焊在脑子里。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节二：后置代码包住的是\"后面的整条链\"。\u003C\u002Fstrong> next 之前的代码在去程执行，之后的代码在回程执行——AuditFilter 的 &gt;&gt;&gt; 和 &lt;&lt;&lt; 像一双筷子，夹住了它后面的一切：后面的过滤器、拦截、404、目标业务，全在筷子中间，一个都溜不掉。胖门神的病根是\u003Cstrong>顺序型收尾\u003C\u002Fstrong>（写在方法尾，谁 return 都能跳过它），链的解法是\u003Cstrong>对称包裹\u003C\u002Fstrong>（写在 next 两侧，回程必经）——本质是 try\u002Ffinally 的亲戚，但表达力强得多：链上每一层都能这么包，一层套一层，洋葱圈一样。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节三：顺序即范围。\u003C\u002Fstrong> 过滤器越靠前，它包住的范围越大：AuditFilter 放第一个，连编码都在它筷子里；放最后，只包住目标。\"编码必须最前\"\"权限必须在认证后\"翻译成链语言就是：\u003Cstrong>用声明顺序表达依赖关系\u003C\u002Fstrong>。这也是缺点一的来源——顺序即语义，语义错了编译器不吭声，装配的人必须想明白。\u003C\u002Fp>\n\u003Ch2>五、在框架里找它\u003C\u002Fh2>\n\u003Col>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Servlet Filter\u003C\u002Fstrong>：javax.servlet.Filter 的 \u003Ccode>doFilter(request, response, chain)\u003C\u002Fcode> + FilterChain——本文 Filter 接口的亲爹，连\"不调 chain.doFilter 就是拦截\"这条契约都一字不差；@WebFilter 的 urlPatterns 就是\"挑路径\"。你跑的每个 Web 应用，最底下都趴着这条链。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Spring MVC 的 HandlerInterceptor\u003C\u002Fstrong>：preHandle \u002F postHandle \u002F afterCompletion 三段式——本文\"去程\u002F回程\"的工业化版本：preHandle 返回 false 相当于不调 next，afterCompletion 连异常场景都兜住，比本文的裸回程更周全。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Spring Security\u003C\u002Fstrong>：一整个框架就是一条 SecurityFilterChain——UsernamePasswordAuthenticationFilter 认证、授权过滤器放后面，十几个过滤器各就各位，\u003Cstrong>顺序就是安全语义本身\u003C\u002Fstrong>。想自定义登录？往链上插一个过滤器，别人的一个都不用动——\"可插拔\"三个字的工业级兑现。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Express \u002F Koa 的 middleware\u003C\u002Fstrong>：JavaScript 世界同款——app.use(fn) 挂上去，fn 里调 next() 放行；Django 中间件、Gin 的 handler 链，全球框架抄的是同一份作业。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Netty 的 ChannelPipeline\u003C\u002Fstrong>：入站\u002F出站事件在 handler 链上双向穿过，编码器、解码器、业务 handler 排队站——过滤器链思想搬到了网络层，一条 pipeline 就是安检门的传送带版。\u003C\u002Fp>\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>六、两条链对账——⑰ 责任链 vs ㉛ 拦截过滤器\u003C\u002Fh2>\n\u003Cp>⑰ 讲责任链时留过一个口子：第三节第 4 条场景\"请求预处理管线\"。两条链长得实在太像——都是\"传给下一个\"，都靠节点自觉。摊开对账：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>\u003C\u002Fth>\n   \u003Cth>责任链（⑰）\u003C\u002Fth>\n   \u003Cth>拦截过滤器（㉛）\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>链上的人\u003C\u002Ftd>\n   \u003Ctd>候选处理者：谁有权限谁签\u003C\u002Ftd>\n   \u003Ctd>关卡：每个都要过一遍\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>谁终结链\u003C\u002Ftd>\n   \u003Ctd>处理者命中即终结（组长签了就不劳经理）\u003C\u002Ftd>\n   \u003Ctd>拦截者，或链尾目标\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>链尾\u003C\u002Ftd>\n   \u003Ctd>开放的——可能没人处理（⑰ 缺点一：200 万报销单落空）\u003C\u002Ftd>\n   \u003Ctd>一定有目标等着（Router 兜 404）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>链的语义\u003C\u002Ftd>\n   \u003Ctd>这个请求**谁来处理**\u003C\u002Ftd>\n   \u003Ctd>这个请求**怎么穿过**\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>典型\u003C\u002Ftd>\n   \u003Ctd>报销审批、异常处理 catch\u003C\u002Ftd>\n   \u003Ctd>编码、认证、审计、限流\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>一句话：\u003Cstrong>责任链是\"谁认领这个活\"，拦截过滤器是\"这个活要过哪几道关\"\u003C\u002Fstrong>。Java 的异常处理是责任链——catch 命中一个就结束，后面再合适也不看；Servlet Filter 是拦截过滤器——每个请求所有过滤器依次穿过，谁都不能跳过。顺带一笔：⑩ 装饰器也\"包住\"，但装饰器包的是\u003Cstrong>能力\u003C\u002Fstrong>（同一个接口的增强），过滤器包的是\u003Cstrong>请求\u003C\u002Fstrong>（横切关注点的加工）——包的东西不同，人生就不同。\u003C\u002Fp>\n\u003Ch2>写在最后\u003C\u002Fh2>\n\u003Cul>\n \u003Cli>拦截过滤器 = 横切关注点一人一格排成链：可插拔、可排序、可挑路径\u003C\u002Fli>\n \u003Cli>契约只有两句话：放行就 chain.next，拦截就不调——链的一切行为由此推出\u003C\u002Fli>\n \u003Cli>后置代码写在 next 两侧，包住后面的整条链——return 走不出的审计盲区，链上结构上不存在\u003C\u002Fli>\n \u003Cli>顺序即范围：越靠前包得越宽；用声明顺序表达依赖，但顺序没有编译器保护\u003C\u002Fli>\n \u003Cli>当代转世：Servlet Filter、HandlerInterceptor、Spring Security、Express middleware、Netty Pipeline——安检门全球连锁\u003C\u002Fli>\n \u003Cli>对账：⑰ 责任链挑人（谁认领），㉛ 拦截过滤器过卡（怎么穿过）；⑩ 装饰器包能力，㉛ 包请求\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>过滤器链把横切关注点安排得明明白白——可凑近看装配代码：五个过滤器、两个控制器，全是 new 出来摆好的。真实应用何止这几个角色：DAO、缓存、消息队列、支付网关……谁需要什么服务，要么构造函数一路传到底，要么满地 new 各造一份。有没有一个\"服务注册处\"，服务登记一次、全应用按名取用？——服务定位器模式，下一篇见。\u003C\u002Fp>",[],"index,follow",null,{"code":4,"message":5,"data":44},[45,54,62],{"id":46,"slug":47,"title":48,"summary":49,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":50,"likeCount":4,"wordCount":51,"readingMinutes":52,"publishedAt":53},"2113935138768293890","design-pattern-30","设计模式修炼之路 ㉚ 前端控制器模式——所有请求，先过我这道门","横切关注点写一次全站无死角、抄漏一处校验就是一个洞、门神必须薄、路由分散声明集中执行、与 MVC 的 C 对账",2755,5825,15,"2025-05-12T18:40:38",{"id":55,"slug":56,"title":57,"summary":58,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":59,"likeCount":4,"wordCount":60,"readingMinutes":52,"publishedAt":61},"2113932208883372034","design-pattern-17","设计模式修炼之路 ⑰ 责任链模式——请求沿链条传递","客户端只认识链头、节点自治三段式、纯链与管线式、从 Servlet Filter 到 Netty Pipeline",3708,5950,"2024-07-23T20:24:49",{"id":63,"slug":64,"title":65,"summary":66,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":67,"likeCount":4,"wordCount":68,"readingMinutes":36,"publishedAt":69},"2113931511261564929","design-pattern-08","设计模式修炼之路 ⑧ 过滤器模式——与或非条件当积木","非 GoF、与或非条件当积木、Predicate 是 JDK 收编版",3801,7442,"2024-01-08T20:51:45",{"code":4,"message":5,"data":71},{"prev":72,"next":73},{"id":46,"slug":47,"title":48,"summary":49,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":50,"likeCount":4,"wordCount":51,"readingMinutes":52,"publishedAt":53},{"id":74,"slug":75,"title":76,"summary":77,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":78,"likeCount":4,"wordCount":79,"readingMinutes":80,"publishedAt":81},"2113935261715927041","design-pattern-32","设计模式修炼之路 ㉜ 服务定位器模式——服务只找一次，之后全靠缓存","查找与缓存一处收口、注册中心只被打扰 N 次、缓存的是服务不是结果、种子从 ㉗ 长成 static 注册台、字符串名字没有编译器保护、与业务代表对账",1926,7172,18,"2025-06-20T10:10:19",{"code":4,"message":5,"data":83},[84,90,95,101],{"id":85,"name":86,"slug":87,"description":88,"sortOrder":4,"articleCount":89},"1946000000000000001","antlr4","tech","编程与技术笔记","13",{"id":32,"name":91,"slug":92,"description":93,"sortOrder":4,"articleCount":94},"设计模式","design-pattern","设计模式系列","34",{"id":96,"name":97,"slug":98,"description":99,"sortOrder":33,"articleCount":100},"1946000000000000002","随笔","essay","生活与思考","0",{"id":102,"name":103,"slug":104,"description":105,"sortOrder":106,"articleCount":107},"2105200093764698113","工具","tools","好用的工具推荐",2,"1",{"html":109,"toc":110},"\u003Cblockquote>\n \u003Cp>J2EE 模式第六站。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>㉚ 结尾埋了个雷：三件套会长大成三十件套。本篇就让它长——横切需求从三件涨到五件：编码、审计（还得\"开始\u002F结束\"各记一笔）、访问日志、登录校验、限流（只挂 \u002Fapi 路径）。全塞进门神的 handle() 里会怎样？\u003C\u002Fp>\n\u003Cp>一个方法，六年需求，年年往里塞。\u003Cstrong>把横切关注点拆成一个个独立的过滤器，串成链，让请求依次穿过\u003C\u002Fstrong>——拦截过滤器模式，J2EE 八式里的安检门。\u003C\u002Fp>\n\u003Ch2 id=\"toc-1\">一、拦截过滤器模式是什么\u003C\u002Fh2>\n\u003Cp>J2EE 蓝皮书的定义大意：拦截过滤器（Intercepting Filter）在请求到达目标处理器\u003Cstrong>之前和之后\u003C\u002Fstrong>，通过一串可插拔的过滤器对请求做检查与加工；过滤器彼此独立，由过滤器链负责串联，新增、删除、调整横切逻辑都不必动处理器一根毛。\u003C\u002Fp>\n\u003Cp>四个角色：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>角色\u003C\u002Fth>\n   \u003Cth>干什么\u003C\u002Fth>\n   \u003Cth>本文对应\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>Filter（过滤器）\u003C\u002Ftd>\n   \u003Ctd>一个横切关注点一个类；放行还是拦截自己说了算\u003C\u002Ftd>\n   \u003Ctd>Encoding \u002F Audit \u002F Logging \u002F Authentication \u002F RateLimitFilter\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Filter Chain（过滤器链）\u003C\u002Ftd>\n   \u003Ctd>串过滤器 + 管\"传给下一个\"；链尾是目标\u003C\u002Ftd>\n   \u003Ctd>FilterChain\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Target（目标资源）\u003C\u002Ftd>\n   \u003Ctd>链尾真正的业务处理\u003C\u002Ftd>\n   \u003Ctd>Router + OrderController \u002F UserController\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Filter Manager（过滤器管理器）\u003C\u002Ftd>\n   \u003Ctd>装配链、定顺序\u003C\u002Ftd>\n   \u003Ctd>本文并入测试类的装配代码\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>一句话理解分工：\u003Cstrong>过滤器说\"我这关过不过\"，链说\"下一关是谁\"，目标说\"都过了再来找我\"。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>它同样不是 GoF 二十三式，是 J2EE 蓝皮书成员——和 ㉚ 是亲兄弟：前端控制器是\u003Cstrong>门\u003C\u002Fstrong>（全应用唯一入口），拦截过滤器是\u003Cstrong>门上的安检流程\u003C\u002Fstrong>（可插拔的关卡序列）。㉚ 解决\"请求从哪儿进\"，㉛ 解决\"进门路上过哪几道检查\"——现实框架里两个几乎总是同框。\u003C\u002Fp>\n\u003Ch2 id=\"toc-2\">二、优点与缺点\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>优点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>一个过滤器一个横切关注点\u003C\u002Fstrong>：新增限流 = 写一个 RateLimitFilter + 装配处加一行，谁也不碰谁——三件套长成三十件套也不怕，每个需求都有了自己的形状。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>可排序\u003C\u002Fstrong>：链就是 List，顺序即声明顺序——编码必须最前？放第一个。想审计包住一切？也放第一个（细节三见）。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>前置\u002F后置天然表达\u003C\u002Fstrong>：chain.next 之前的代码在\"去程\"执行，之后的代码在\"回程\"执行——审计的\"结束\"不再依赖请求一路走运，这是胖门神结构上做不到的。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>可挑路径\u003C\u002Fstrong>：限流只想挂 \u002Fapi？过滤器自己看 path 决定干不干活，不用任何人配合，没有一个 if 长在别人家。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>\u003Cstrong>缺点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>顺序没有编译器保护\u003C\u002Fstrong>：把授权排在认证前面，授权拿到的是没认证的请求——链的正确性全靠装配的人想明白，排错了就是安全事故，编译器一声不吭。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>不调 next = 静默拦截\u003C\u002Fstrong>：新人写过滤器忘了 chain.next，全站请求无声消失在某一关——契约简单到没有任何东西监督它。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>链长了就是迷宫\u003C\u002Fstrong>：十几二十个过滤器，\"这个请求到底过了哪几关\"变成考古题，只能靠日志倒推。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"toc-3\">三、什么时候用\u003C\u002Fh2>\n\u003Cp>判断标准：\u003Cstrong>横切关注点不止三两个、各有顺序讲究、还要挑路径\u003C\u002Fstrong>——塞进一个方法里已经排不开队了。\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>典型场景\u003C\u002Fstrong>：Web 请求处理——编码、认证、审计、限流，条条都要、顺序敏感、各管各的地盘\u003C\u002Fli>\n \u003Cli>\u003Cstrong>框架基建\u003C\u002Fstrong>：Spring Security 整个安全体系就是一条过滤器链（第五节见）\u003C\u002Fli>\n \u003Cli>\u003Cstrong>反面对照\u003C\u002Fstrong>：只有一两个横切关注点且顺序无讲究——直接写在前端控制器里更直观。三件套时代用 ㉚ 写死就挺好，\u003Cstrong>模式是横切关注点长大以后才请出来的\u003C\u002Fstrong>——为一个编码设置立一条链，是杀鸡动了机场安检\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"toc-4\">四、实现方式：安检门怎么搭\u003C\u002Fh2>\n\u003Cp>场景承接 ㉚：同一个迷你应用，横切需求长到五件——编码、审计（开始\u002F结束各记一笔）、访问日志、登录校验、限流（只挂 \u002Fapi）。看两套世界。\u003C\u002Fp>\n\u003Ch3 id=\"toc-5\">4.1 反面教材：门神被六年需求喂胖\u003C\u002Fh3>\n\u003Cp>每年来一个新需求，每个都往 handle() 里塞一段：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F** 所有需求都往这一个方法里塞 *\u002F\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> handle\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(Request request) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F 第 1 年上线时：编码\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"   [胖门神] 设置编码 UTF-8\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F 第 2 年新需求：审计要\"开始\u002F结束\"各记一笔\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"   [胖门神] 审计 >>> \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> request.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">displayUser\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" -> \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> request.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getPath\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F 第 3 年新需求：登录检查\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">!\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">request.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">isLoggedIn\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">()) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"   [胖门神] 未登录，拒绝：401 请先登录\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F 第 4 年新需求：访问日志\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"   [胖门神] 访问日志：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> request.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">displayUser\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" -> \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> request.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getPath\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F 第 5 年新需求：API 限流——只挂 \u002Fapi 路径\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (request.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getPath\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">().\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">startsWith\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u002Fapi\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"   [胖门神] \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> request.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getPath\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" 是 API：限流检查（每秒 100 次）\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F 分发本职，已经被挤到方法尾部\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    Controller controller \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> routes.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">get\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(request.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getPath\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (controller \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">==\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> null\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"   [胖门神] 404：没有这个页面 \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> request.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getPath\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    controller.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">handle\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(request);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F 审计的\"结束\"记在这里——只有全程顺利的请求才配拥有\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"   [胖门神] 审计 &#x3C;&#x3C;&#x3C; 请求处理结束\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行输出：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>========== 反面教材：门神被需求喂胖 ==========\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- 请求 \u002Fapi\u002Forder\u002Flist（u001 已登录，全程顺利） --\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [胖门神] 设置编码 UTF-8\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [胖门神] 审计 >>> u001 -> \u002Fapi\u002Forder\u002Flist\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [胖门神] 访问日志：u001 -> \u002Fapi\u002Forder\u002Flist\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [胖门神] \u002Fapi\u002Forder\u002Flist 是 API：限流检查（每秒 100 次）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [OrderController] 业务：拉取订单列表\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [胖门神] 审计 &#x3C;&#x3C;&#x3C; 请求处理结束\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- 请求 \u002Fapi\u002Forder\u002Flist（匿名用户，401 拒绝） --\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [胖门神] 设置编码 UTF-8\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [胖门神] 审计 >>> 匿名 -> \u002Fapi\u002Forder\u002Flist\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [胖门神] 未登录，拒绝：401 请先登录\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   审计只有 >>> 没有 &#x3C;&#x3C;&#x3C;：return 一走，后置代码全部跳过\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- 请求 \u002Fno\u002Fsuch\u002Fpage（u001，404） --\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [胖门神] 设置编码 UTF-8\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [胖门神] 审计 >>> u001 -> \u002Fno\u002Fsuch\u002Fpage\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [胖门神] 访问日志：u001 -> \u002Fno\u002Fsuch\u002Fpage\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [胖门神] 404：没有这个页面 \u002Fno\u002Fsuch\u002Fpage\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   又丢一笔：每多一个 return，审计盲区就大一块\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>三个病灶全齐：\u003Cstrong>return 走不出审计盲区\u003C\u002Fstrong>——审计的\"结束\"写在方法尾部，可方法里每个横切关注点都有提前 return 的权利：401 一个 return、404 一个 return，每多一个 return，盲区就大一块；想补救就在每个 return 前补一行审计，可 return 还会越加越多，每个都是新的漏点。\u003Cstrong>挑路径靠 if 越积越多\u003C\u002Fstrong>——限流只挂 \u002Fapi 是一个 if，哪天\"静态资源免检\"又是一个 if，每个需求留下自己的形状，handle() 长成一碗面条。\u003Cstrong>需求没有形状\u003C\u002Fstrong>——六年的需求挤在一个方法里，靠注释分块、靠人脑维持秩序。注意：每一关都干了活、功能一切正常——反面教材从来不是功能错误，是结构错误。\u003C\u002Fp>\n\u003Cp>病根一句话：\u003Cstrong>顺序型收尾\u003C\u002Fstrong>（\"结束\"写在最后，人人可以跳过它）和\u003Cstrong>短路型控制流\u003C\u002Fstrong>（不过关就走人）在同一个方法里天生打架。\u003C\u002Fp>\n\u003Ch3 id=\"toc-6\">4.2 正解：拆成过滤器，串成链\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>过滤器接口，契约只有两句话\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> interface\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Filter\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> doFilter\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(Request \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">request\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, FilterChain \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">chain\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>放行就调 chain.next，拦截就不调——链的一切行为都从这两句推出。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>链管\"传给下一个\"，游标走到头就是目标\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> next\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(Request request) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (index \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> filters.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">size\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">()) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        filters.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">get\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(index\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">++\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">).\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">doFilter\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(request, \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">this\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    } \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">else\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        endpoint.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">accept\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(request);   \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 链尾：分发器（Router）兜 404\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>每个过滤器只管一件事\u003C\u002Fstrong>——登录校验，拦截就是简简单单不调 next：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> AuthenticationFilter\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> implements\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Filter\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> doFilter\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(Request \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">request\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, FilterChain \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">chain\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">!\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">request.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">isLoggedIn\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">()) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"   [登录过滤器] 未登录，拦截：401 请先登录\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"   [登录过滤器] \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> request.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getUserId\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" 已登录，放行\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        chain.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">next\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(request);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>审计过滤器——后置代码包住整条链的关键\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> AuditFilter\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> implements\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Filter\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> doFilter\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(Request \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">request\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, FilterChain \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">chain\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"   [审计过滤器] >>> 收到 \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> request.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">displayUser\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" -> \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> request.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getPath\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        chain.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">next\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(request);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"   [审计过滤器] &#x3C;&#x3C;&#x3C; 请求处理结束\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>装配——顺序即声明顺序，想调就调 List\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">Router router \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Router\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">()\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        .\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">add\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u002Fapi\u002Forder\u002Flist\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> OrderController\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">())\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        .\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">add\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u002Fuser\u002Fprofile\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> UserController\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">FilterChain chain \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> FilterChain\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        List.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">of\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> EncodingFilter\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(),\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">                new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> AuditFilter\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(),\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">                new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> LoggingFilter\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(),\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">                new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> AuthenticationFilter\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(),\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">                new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> RateLimitFilter\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">()),\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        router\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">::\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">route);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行输出：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>========== 正解：横切关注点拆成过滤器串成链 ==========\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- 请求 \u002Fapi\u002Forder\u002Flist（u001 已登录，走完全链） --\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [编码过滤器] 统一设置编码 UTF-8\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [审计过滤器] >>> 收到 u001 -> \u002Fapi\u002Forder\u002Flist\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [日志过滤器] 访问日志：u001 -> \u002Fapi\u002Forder\u002Flist\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [登录过滤器] u001 已登录，放行\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [限流过滤器] \u002Fapi\u002Forder\u002Flist 是 API：限流检查（每秒 100 次）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [分发器] \u002Fapi\u002Forder\u002Flist -> OrderController\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [OrderController] 业务：拉取订单列表\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [审计过滤器] &#x3C;&#x3C;&#x3C; 请求处理结束\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- 请求 \u002Fapi\u002Forder\u002Flist（匿名用户，被登录过滤器拦截） --\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [编码过滤器] 统一设置编码 UTF-8\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [审计过滤器] >>> 收到 匿名 -> \u002Fapi\u002Forder\u002Flist\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [日志过滤器] 访问日志：匿名 -> \u002Fapi\u002Forder\u002Flist\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [登录过滤器] 未登录，拦截：401 请先登录\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [审计过滤器] &#x3C;&#x3C;&#x3C; 请求处理结束\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   拦截归拦截，审计 &#x3C;&#x3C;&#x3C; 照样记——后置代码包住了后面的整条链\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- 请求 \u002Fno\u002Fsuch\u002Fpage（u001，链尾 404） --\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [编码过滤器] 统一设置编码 UTF-8\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [审计过滤器] >>> 收到 u001 -> \u002Fno\u002Fsuch\u002Fpage\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [日志过滤器] 访问日志：u001 -> \u002Fno\u002Fsuch\u002Fpage\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [登录过滤器] u001 已登录，放行\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [分发器] 404：没有这个页面 \u002Fno\u002Fsuch\u002Fpage\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [审计过滤器] &#x3C;&#x3C;&#x3C; 请求处理结束\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   404 也逃不掉审计 &#x3C;&#x3C;&#x3C;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- 请求 \u002Fuser\u002Fprofile（u001，非 API 路径） --\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [编码过滤器] 统一设置编码 UTF-8\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [审计过滤器] >>> 收到 u001 -> \u002Fuser\u002Fprofile\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [日志过滤器] 访问日志：u001 -> \u002Fuser\u002Fprofile\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [登录过滤器] u001 已登录，放行\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [分发器] \u002Fuser\u002Fprofile -> UserController\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [UserController] 业务：返回个人资料\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [审计过滤器] &#x3C;&#x3C;&#x3C; 请求处理结束\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   限流过滤器只认 \u002Fapi：不是自己的地盘，直接放行\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>四处对照读出含金量：匿名请求被登录过滤器拦下，\u003Cstrong>审计的 &lt;&lt;&lt; 照样记\u003C\u002Fstrong>——反面教材里 return 走不出的盲区，在链上结构上就不存在；404 发生在链尾，\u003Cstrong>同样逃不掉审计\u003C\u002Fstrong>——后置代码包住的是\"后面的整条链\"，链上发生什么都拦不住它执行；\u003Ccode>\u002Fuser\u002Fprofile\u003C\u002Fcode> 全程没出现限流的影子——RateLimitFilter 自己看路径，不是自己的地盘直接放行；㉚ 门神身上的横切关注点被整链收走后，剩下的本职瘦成了链尾的分发器——\u003Cstrong>门与安检流程，就此分家\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Ch3 id=\"toc-7\">4.3 三处细节\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>细节一：契约只有两句话。\u003C\u002Fstrong> 放行就调 chain.next，拦截就不调。所有行为从这两句推出：不调 next，请求到此为止，后面的过滤器和目标全不执行；next 之后的代码要等回程才执行。这套契约没有任何东西监督（缺点二），写过滤器第一件事是把它焊在脑子里。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节二：后置代码包住的是\"后面的整条链\"。\u003C\u002Fstrong> next 之前的代码在去程执行，之后的代码在回程执行——AuditFilter 的 &gt;&gt;&gt; 和 &lt;&lt;&lt; 像一双筷子，夹住了它后面的一切：后面的过滤器、拦截、404、目标业务，全在筷子中间，一个都溜不掉。胖门神的病根是\u003Cstrong>顺序型收尾\u003C\u002Fstrong>（写在方法尾，谁 return 都能跳过它），链的解法是\u003Cstrong>对称包裹\u003C\u002Fstrong>（写在 next 两侧，回程必经）——本质是 try\u002Ffinally 的亲戚，但表达力强得多：链上每一层都能这么包，一层套一层，洋葱圈一样。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节三：顺序即范围。\u003C\u002Fstrong> 过滤器越靠前，它包住的范围越大：AuditFilter 放第一个，连编码都在它筷子里；放最后，只包住目标。\"编码必须最前\"\"权限必须在认证后\"翻译成链语言就是：\u003Cstrong>用声明顺序表达依赖关系\u003C\u002Fstrong>。这也是缺点一的来源——顺序即语义，语义错了编译器不吭声，装配的人必须想明白。\u003C\u002Fp>\n\u003Ch2 id=\"toc-8\">五、在框架里找它\u003C\u002Fh2>\n\u003Col>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Servlet Filter\u003C\u002Fstrong>：javax.servlet.Filter 的 \u003Ccode>doFilter(request, response, chain)\u003C\u002Fcode> + FilterChain——本文 Filter 接口的亲爹，连\"不调 chain.doFilter 就是拦截\"这条契约都一字不差；@WebFilter 的 urlPatterns 就是\"挑路径\"。你跑的每个 Web 应用，最底下都趴着这条链。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Spring MVC 的 HandlerInterceptor\u003C\u002Fstrong>：preHandle \u002F postHandle \u002F afterCompletion 三段式——本文\"去程\u002F回程\"的工业化版本：preHandle 返回 false 相当于不调 next，afterCompletion 连异常场景都兜住，比本文的裸回程更周全。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Spring Security\u003C\u002Fstrong>：一整个框架就是一条 SecurityFilterChain——UsernamePasswordAuthenticationFilter 认证、授权过滤器放后面，十几个过滤器各就各位，\u003Cstrong>顺序就是安全语义本身\u003C\u002Fstrong>。想自定义登录？往链上插一个过滤器，别人的一个都不用动——\"可插拔\"三个字的工业级兑现。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Express \u002F Koa 的 middleware\u003C\u002Fstrong>：JavaScript 世界同款——app.use(fn) 挂上去，fn 里调 next() 放行；Django 中间件、Gin 的 handler 链，全球框架抄的是同一份作业。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Netty 的 ChannelPipeline\u003C\u002Fstrong>：入站\u002F出站事件在 handler 链上双向穿过，编码器、解码器、业务 handler 排队站——过滤器链思想搬到了网络层，一条 pipeline 就是安检门的传送带版。\u003C\u002Fp>\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"toc-9\">六、两条链对账——⑰ 责任链 vs ㉛ 拦截过滤器\u003C\u002Fh2>\n\u003Cp>⑰ 讲责任链时留过一个口子：第三节第 4 条场景\"请求预处理管线\"。两条链长得实在太像——都是\"传给下一个\"，都靠节点自觉。摊开对账：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>\u003C\u002Fth>\n   \u003Cth>责任链（⑰）\u003C\u002Fth>\n   \u003Cth>拦截过滤器（㉛）\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>链上的人\u003C\u002Ftd>\n   \u003Ctd>候选处理者：谁有权限谁签\u003C\u002Ftd>\n   \u003Ctd>关卡：每个都要过一遍\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>谁终结链\u003C\u002Ftd>\n   \u003Ctd>处理者命中即终结（组长签了就不劳经理）\u003C\u002Ftd>\n   \u003Ctd>拦截者，或链尾目标\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>链尾\u003C\u002Ftd>\n   \u003Ctd>开放的——可能没人处理（⑰ 缺点一：200 万报销单落空）\u003C\u002Ftd>\n   \u003Ctd>一定有目标等着（Router 兜 404）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>链的语义\u003C\u002Ftd>\n   \u003Ctd>这个请求**谁来处理**\u003C\u002Ftd>\n   \u003Ctd>这个请求**怎么穿过**\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>典型\u003C\u002Ftd>\n   \u003Ctd>报销审批、异常处理 catch\u003C\u002Ftd>\n   \u003Ctd>编码、认证、审计、限流\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>一句话：\u003Cstrong>责任链是\"谁认领这个活\"，拦截过滤器是\"这个活要过哪几道关\"\u003C\u002Fstrong>。Java 的异常处理是责任链——catch 命中一个就结束，后面再合适也不看；Servlet Filter 是拦截过滤器——每个请求所有过滤器依次穿过，谁都不能跳过。顺带一笔：⑩ 装饰器也\"包住\"，但装饰器包的是\u003Cstrong>能力\u003C\u002Fstrong>（同一个接口的增强），过滤器包的是\u003Cstrong>请求\u003C\u002Fstrong>（横切关注点的加工）——包的东西不同，人生就不同。\u003C\u002Fp>\n\u003Ch2 id=\"toc-10\">写在最后\u003C\u002Fh2>\n\u003Cul>\n \u003Cli>拦截过滤器 = 横切关注点一人一格排成链：可插拔、可排序、可挑路径\u003C\u002Fli>\n \u003Cli>契约只有两句话：放行就 chain.next，拦截就不调——链的一切行为由此推出\u003C\u002Fli>\n \u003Cli>后置代码写在 next 两侧，包住后面的整条链——return 走不出的审计盲区，链上结构上不存在\u003C\u002Fli>\n \u003Cli>顺序即范围：越靠前包得越宽；用声明顺序表达依赖，但顺序没有编译器保护\u003C\u002Fli>\n \u003Cli>当代转世：Servlet Filter、HandlerInterceptor、Spring Security、Express middleware、Netty Pipeline——安检门全球连锁\u003C\u002Fli>\n \u003Cli>对账：⑰ 责任链挑人（谁认领），㉛ 拦截过滤器过卡（怎么穿过）；⑩ 装饰器包能力，㉛ 包请求\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>过滤器链把横切关注点安排得明明白白——可凑近看装配代码：五个过滤器、两个控制器，全是 new 出来摆好的。真实应用何止这几个角色：DAO、缓存、消息队列、支付网关……谁需要什么服务，要么构造函数一路传到底，要么满地 new 各造一份。有没有一个\"服务注册处\"，服务登记一次、全应用按名取用？——服务定位器模式，下一篇见。\u003C\u002Fp>",[111,114,117,120,123,127,130,133,136,139],{"id":112,"text":113,"level":106},"toc-1","一、拦截过滤器模式是什么",{"id":115,"text":116,"level":106},"toc-2","二、优点与缺点",{"id":118,"text":119,"level":106},"toc-3","三、什么时候用",{"id":121,"text":122,"level":106},"toc-4","四、实现方式：安检门怎么搭",{"id":124,"text":125,"level":126},"toc-5","4.1 反面教材：门神被六年需求喂胖",3,{"id":128,"text":129,"level":126},"toc-6","4.2 正解：拆成过滤器，串成链",{"id":131,"text":132,"level":126},"toc-7","4.3 三处细节",{"id":134,"text":135,"level":106},"toc-8","五、在框架里找它",{"id":137,"text":138,"level":106},"toc-9","六、两条链对账——⑰ 责任链 vs ㉛ 拦截过滤器",{"id":140,"text":141,"level":106},"toc-10","写在最后"]