[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site-settings":3,"\u002Farticles\u002Fdesign-pattern-30":26,"\u002Farticles\u002Fdesign-pattern-30\u002Frelated":43,"\u002Farticles\u002Fdesign-pattern-30\u002Fadjacent":72,"\u002Fcategories":83,"render-design-pattern-30":109},{"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},"2113935138768293890","design-pattern-30","设计模式修炼之路 ㉚ 前端控制器模式——所有请求，先过我这道门","横切关注点写一次全站无死角、抄漏一处校验就是一个洞、门神必须薄、路由分散声明集中执行、与 MVC 的 C 对账","2113897628365230081",1,2755,5825,15,"2025-05-12T18:40:38","> J2EE 模式第五站。\n\n㉙ 的 DAO 把\"怎么存\"收走了，业务层终于干净。可 Web 应用的麻烦不止在存储——请求进门的第一个问题是：**谁接的活？**\n\nServlet 时代的默认答案：一个 URL 一个 Servlet，各自为政。于是\"进门三件套\"——设置编码、登录校验、访问日志——只能靠复制粘贴在每个 Servlet 里传播：老员工抄得齐，新来的不知道有模板，抄漏一处校验，就是一个安全洞。想要\"全局加个限流\"？挨个文件改一遍，改漏一个就白干。\n\n**用一个控制器收口所有请求：进门的事集中办，业务的事分发办**——前端控制器模式，J2EE 八式里的门神。\n\n## 一、前端控制器模式是什么\n\nJ2EE 蓝皮书的定义大意：前端控制器（Front Controller）为 Web 应用提供一个**集中的请求处理入口**，所有请求先到这里，统一的认证、授权、编码、日志在此完成，再由它分发给具体的处理器。\n\n四个角色：\n\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>角色\u003C\u002Fth>\u003Cth>干什么\u003C\u002Fth>\u003Cth>本文对应\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\u003Ctr>\u003Ctd>Front Controller（前端控制器）\u003C\u002Ftd>\u003Ctd>全应用唯一入口：横切关注点 + 分发\u003C\u002Ftd>\u003Ctd>FrontController\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Dispatcher（分发器）\u003C\u002Ftd>\u003Ctd>维护 URL 到处理器的映射，负责路由\u003C\u002Ftd>\u003Ctd>本文并入 FrontController 的 routes\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Handler \u002F Controller（业务控制器）\u003C\u002Ftd>\u003Ctd>一个 URL 一家，只管业务\u003C\u002Ftd>\u003Ctd>OrderController \u002F UserController\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>View（视图）\u003C\u002Ftd>\u003Ctd>渲染响应\u003C\u002Ftd>\u003Ctd>本文 println 代劳\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\n\n一句话理解分工：**前端控制器说\"所有请求先过我这道门\"，业务控制器说\"这个请求的业务我管\"。**\n\n它同样不是 GoF 二十三式，是 J2EE 蓝皮书成员——诞生动机就是上面那个复制粘贴惨案：横切关注点（所有请求都要做、且做法一样的事）散落在 N 个 Servlet 里，纪律靠自觉，漏洞靠运气。\n\n## 二、优点与缺点\n\n**优点：**\n\n1. **横切关注点写一次，全站无死角**：编码、认证、日志、限流——写在门神身上，任何 URL 都逃不掉，新同事想漏都漏不了。\n2. **集中分发，路由一目了然**：URL 到哪个处理器，一张路由表看全——\"这个接口谁在管\"从考古问题变成查表问题。\n3. **统一错误处理**：404、401、500 在一个地方兜底，不会有的页面报错白屏、有的页面报错误码。\n4. **安全审计有唯一卡口**：想拦截所有请求做审计、限流、灰度——只需在门口加，不用满楼跑。\n\n**缺点：**\n\n1. **唯一入口 = 唯一故障点**：门神挂了全站挂——所以现实里它必须薄（只做进门三件事加分发），重活不许干。\n2. **所有请求串一遍相同检查**：静态资源请求也走登录校验，部分检查对部分路径是浪费——解药是 ㉛ 按路径配过滤器。\n3. **路由表会膨胀**：大应用几百条路由堆在一个类里——现实框架用注解把路由拆回各个控制器（第五节见）。\n\n## 三、什么时候用\n\n判断标准：**有多条入口 URL + 存在对所有请求一视同仁的横切关注点**——就用前端控制器，这几乎是 Web 应用的出厂设置。\n\n- **典型场景**：Web 应用、HTTP API——认证、日志、编码、限流，哪个应用躲得开？\n- **微服务 API 网关**：跨服务版前端控制器——认证、限流、路由，在集群门口统一办\n- **反面对照**：桌面应用、单文件脚本——没有\"请求进门\"这回事，硬套是给自己立门神；纯内部工具只有一个入口时，前端控制器退化成一个 main 方法，模式已经在了，只是不值得命名\n\n## 四、实现方式：谁能看个人资料\n\n场景：一个迷你 Web 应用，两个页面——订单列表 `\u002Forder\u002Flist`、个人资料 `\u002Fuser\u002Fprofile`。进门三件套：编码、日志、登录校验。看两套世界。\n\n### 4.1 反面教材：每个 Servlet 各自为政\n\n老员工写的 OrderServlet，三件套抄得整整齐齐：\n\n```java\npublic class BadOrderServlet {\n\n    public void service(Request request) {\n        System.out.println(\"   [OrderServlet] 设置编码 UTF-8\");\n        System.out.println(\"   [OrderServlet] 检查登录：\" + request.getUserId() + \" 已登录\");\n        System.out.println(\"   [OrderServlet] 记访问日志：\" + request.getUserId() + \" -> \" + request.getPath());\n        System.out.println(\"   [OrderServlet] 业务：拉取订单列表\");\n    }\n}\n```\n\n新同事写的 UserServlet——不知道有模板，只写了业务，**编译照样过，功能照样跑**：\n\n```java\npublic class BadUserServlet {\n\n    public void service(Request request) {\n        System.out.println(\"   [UserServlet] 业务：拉取个人资料\");\n    }\n}\n```\n\n运行输出：\n\n```\n========== 反面教材：每个 Servlet 各自为政 ==========\n-- 请求 \u002Forder\u002Flist（u001 已登录，OrderServlet 接活） --\n   [OrderServlet] 设置编码 UTF-8\n   [OrderServlet] 检查登录：u001 已登录\n   [OrderServlet] 记访问日志：u001 -> \u002Forder\u002Flist\n   [OrderServlet] 业务：拉取订单列表\n\n-- 请求 \u002Fuser\u002Fprofile（u001 已登录，UserServlet 接活） --\n   [UserServlet] 业务：拉取个人资料\n   UserServlet 少了三件套——三件套是靠复制粘贴传播的，抄没抄全靠自觉\n\n-- 请求 \u002Fuser\u002Fprofile（匿名用户，UserServlet 接活） --\n   [UserServlet] 业务：拉取个人资料\n   安全事故：未登录直接拉到个人资料——抄漏一处校验，就是一个洞\n```\n\n三个病灶全齐：**纪律靠自觉**（三件套的正确性不靠结构保证，靠每个写代码的人记得抄）；**抄漏一处就是一个洞**（UserServlet 没拦住匿名请求——复制粘贴传播的校验，传播不到的地方就是裸奔）；**全局改动 N 处**（哪天校验规则要改、要加限流，挨个 Servlet 翻，改漏一个就前功尽弃）。注意 OrderServlet 本身没写错——反面教材从来不是谁的错，是**结构把正确的希望寄托在自觉上**。\n\n### 4.2 正解：前端控制器\n\n**业务控制器干干净净**，只有业务：\n\n```java\npublic class UserController implements Controller {\n\n    @Override\n    public void handle(Request request) {\n        System.out.println(\"   [UserController] 业务：拉取个人资料\");\n    }\n}\n```\n\n**门神收口一切**——三件套写一次，路由一张表：\n\n```java\npublic class FrontController {\n\n    private final Map\u003CString, Controller> routes = new HashMap\u003C>();\n\n    public FrontController() {\n        routes.put(\"\u002Forder\u002Flist\", new OrderController());\n        routes.put(\"\u002Fuser\u002Fprofile\", new UserController());\n    }\n\n    \u002F** 所有请求都从这里进 *\u002F\n    public void handle(Request request) {\n        System.out.println(\"   [前端控制器] 统一设置编码 UTF-8\");\n        System.out.println(\"   [前端控制器] 统一访问日志：\" + request.displayUser() + \" -> \" + request.getPath());\n\n        if (!request.isLoggedIn()) {\n            System.out.println(\"   [前端控制器] 统一登录检查：未登录，拒绝：401 请先登录\");\n            return;\n        }\n        System.out.println(\"   [前端控制器] 统一登录检查：\" + request.getUserId() + \" 已登录\");\n\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}\n```\n\n运行输出：\n\n```\n========== 正解：前端控制器统一入口 ==========\n-- 请求 \u002Forder\u002Flist（u001 已登录） --\n   [前端控制器] 统一设置编码 UTF-8\n   [前端控制器] 统一访问日志：u001 -> \u002Forder\u002Flist\n   [前端控制器] 统一登录检查：u001 已登录\n   [OrderController] 业务：拉取订单列表\n\n-- 请求 \u002Fuser\u002Fprofile（u001 已登录） --\n   [前端控制器] 统一设置编码 UTF-8\n   [前端控制器] 统一访问日志：u001 -> \u002Fuser\u002Fprofile\n   [前端控制器] 统一登录检查：u001 已登录\n   [UserController] 业务：拉取个人资料\n\n-- 请求 \u002Fuser\u002Fprofile（匿名用户再试） --\n   [前端控制器] 统一设置编码 UTF-8\n   [前端控制器] 统一访问日志：匿名 -> \u002Fuser\u002Fprofile\n   [前端控制器] 统一登录检查：未登录，拒绝：401 请先登录\n\n-- 请求 \u002Fno\u002Fsuch\u002Fpage（乱敲的 URL） --\n   [前端控制器] 统一设置编码 UTF-8\n   [前端控制器] 统一访问日志：u001 -> \u002Fno\u002Fsuch\u002Fpage\n   [前端控制器] 统一登录检查：u001 已登录\n   [前端控制器] 404：没有这个页面 \u002Fno\u002Fsuch\u002Fpage\n```\n\n三处对照读出含金量：UserController 里**一行校验都没有**，但匿名请求进不来——正确性从\"每个人记得抄\"变成\"结构保证\"；被拒绝的请求也**先记了日志**（匿名 -> \u002Fuser\u002Fprofile）——审计无死角，攻击尝试留痕；乱敲的 URL 统一 404——错误处理有了兜底的地方。反面教材那个安全洞，在这里**结构上就不可能存在**。\n\n### 4.3 三处细节\n\n**细节一：门神必须薄。** 前端控制器只干两件事：进门三件套 + 分发。业务逻辑一旦写进门神，它就从\"全站唯一入口\"堕落成\"全站唯一上帝类\"——缺点一的故障点风险直接翻倍。判断标准：门神里的代码应该对**所有**请求成立，\"只对订单成立\"的代码立刻请去 OrderController。\n\n**细节二：分发的形态会进化。** 本文路由表是构造器里硬编码的 `Map.put`——够用，但加页面要改门神。下一步进化是命令模式（⑱）：请求参数直接映射处理器类；再进一步是注解扫描：Spring 的 `@RequestMapping(\"\u002Fuser\u002Fprofile\")` 把路由声明放回各个控制器，框架启动时自动汇总成路由表——**路由分散声明、集中执行**，膨胀问题就这么解的。\n\n**细节三：三件套会长大成三十件套。** 编码、认证、日志只是开胃菜：限流、审计、灰度、租户识别、链路追踪……全往 handle() 方法里塞，门神眼看撑爆。而且它们各有各的讲究：编码必须最前、权限校验得在认证之后、限流只想挂部分路径、顺序还得可配置。**横切关注点\"可插拔、可排序\"的需求，就是 ㉛ 拦截过滤器模式的出生证明**——下一篇见分晓。\n\n## 五、在框架里找它\n\n1. **Spring MVC 的 DispatcherServlet**：名字都写在脸上了——Front **Dispatcher** Servlet。所有请求先进它，HandlerMapping 是路由表，@Controller 是业务控制器，@RequestMapping 是路由声明。Interceptor（拦截器）则是它给横切关注点留的插槽。本文 FrontController 的工业化完整版。\n\n2. **Struts 的 ActionServlet**：老一代答案——ActionServlet 收口一切，按配置把请求分发给各个 Action。前端控制器模式教会了整整一代 Java 框架\"怎么当门神\"。\n\n3. **Django \u002F Flask \u002F FastAPI**：Python 世界同款思想——WSGI\u002FASGI 统一入口 + URL 路由表分发到视图函数；中间件（middleware）承担三件套职责。框架换了语言，门神的岗位职责全球统一。\n\n4. **API 网关（Kong、Spring Cloud Gateway）**：微服务时代的前端控制器——几十个服务前面立一扇大门，认证、限流、灰度、路由到下游服务，全都门口办。**从\"一个应用的门神\"放大到\"一个集群的门神\"**，模式没变，编制升了。\n\n## 六、两个\"控制器\"对账——㉖ MVC 的 C vs ㉚ 前端控制器\n\n㉖ 讲 MVC 时留过一个尾巴：MVC 的 Controller 和前端控制器是什么关系？摊开对账：\n\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>\u003C\u002Fth>\u003Cth>MVC 的 Controller（㉖）\u003C\u002Fth>\u003Cth>Front Controller（㉚）\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\u003Ctr>\u003Ctd>数量\u003C\u002Ftd>\u003Ctd>一个用例一个：LoginController、OrderController\u003C\u002Ftd>\u003Ctd>全应用唯一\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>管什么\u003C\u002Ftd>\u003Ctd>这个请求的**业务编排**：调模型、选视图\u003C\u002Ftd>\u003Ctd>所有请求的**共同前奏**：认证、日志、编码，加分发\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>谁写\u003C\u002Ftd>\u003Ctd>你写\u003C\u002Ftd>\u003Ctd>框架写（DispatcherServlet、ActionServlet）\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>视角\u003C\u002Ftd>\u003Ctd>业务视角\u003C\u002Ftd>\u003Ctd>基建视角\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>一句话\u003C\u002Ftd>\u003Ctd>接到活以后怎么干\u003C\u002Ftd>\u003Ctd>活怎么统一地接到\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\n\nSpring MVC 里两者同框：**DispatcherServlet 是前端控制器，@Controller 是 MVC 的 C**——你的每个 @Controller 方法干的是 ㉖ 的活，而把它们串起来的那扇门是 ㉚。所以别再问\"MVC 的 C 是不是前端控制器\"——狭义 MVC 只说\"控制器接收输入、调用模型\"；落到 Web 上，这活被拆成两半：门口的统一前奏（㉚，框架的）和屋里的一单一办（㉖，你的）。\n\n## 写在最后\n\n- 前端控制器 = 全应用唯一入口：横切关注点写一次，分发路由一张表\n- 疗效：校验无死角、审计有卡口、错误有兜底——正确性从靠自觉变成靠结构\n- 铁律：门神必须薄——对所有请求成立的代码才配进门神\n- 分发的进化：硬编码路由表 → 命令映射 → 注解扫描，路由分散声明、集中执行\n- 当代转世：DispatcherServlet、Django 中间件、API 网关——门神从应用门口站到集群门口\n- 对账：㉖ 的 C 是\"接到活以后怎么干\"，㉚ 是\"活怎么统一地接到\"——Spring MVC 里两者同框\n\n前端控制器把进门三件套写死在 handle() 里，顺序固定、全体适用。可现实很快翻脸：编码必须在最前、权限校验得排在认证后、限流只想挂 API 路径、静态资源什么检查都不想要——横切关注点越长越多，还各有各的顺序和地盘，门神的 handle() 方法眼看撑爆。谁让这些横切逻辑\"**可插拔、可排序、可挑路径**\"？——拦截过滤器模式，下一篇见。\n","\u003Cblockquote>\n \u003Cp>J2EE 模式第五站。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>㉙ 的 DAO 把\"怎么存\"收走了，业务层终于干净。可 Web 应用的麻烦不止在存储——请求进门的第一个问题是：\u003Cstrong>谁接的活？\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>Servlet 时代的默认答案：一个 URL 一个 Servlet，各自为政。于是\"进门三件套\"——设置编码、登录校验、访问日志——只能靠复制粘贴在每个 Servlet 里传播：老员工抄得齐，新来的不知道有模板，抄漏一处校验，就是一个安全洞。想要\"全局加个限流\"？挨个文件改一遍，改漏一个就白干。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>用一个控制器收口所有请求：进门的事集中办，业务的事分发办\u003C\u002Fstrong>——前端控制器模式，J2EE 八式里的门神。\u003C\u002Fp>\n\u003Ch2>一、前端控制器模式是什么\u003C\u002Fh2>\n\u003Cp>J2EE 蓝皮书的定义大意：前端控制器（Front Controller）为 Web 应用提供一个\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>Front Controller（前端控制器）\u003C\u002Ftd>\n   \u003Ctd>全应用唯一入口：横切关注点 + 分发\u003C\u002Ftd>\n   \u003Ctd>FrontController\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Dispatcher（分发器）\u003C\u002Ftd>\n   \u003Ctd>维护 URL 到处理器的映射，负责路由\u003C\u002Ftd>\n   \u003Ctd>本文并入 FrontController 的 routes\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Handler \u002F Controller（业务控制器）\u003C\u002Ftd>\n   \u003Ctd>一个 URL 一家，只管业务\u003C\u002Ftd>\n   \u003Ctd>OrderController \u002F UserController\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>View（视图）\u003C\u002Ftd>\n   \u003Ctd>渲染响应\u003C\u002Ftd>\n   \u003Ctd>本文 println 代劳\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>一句话理解分工：\u003Cstrong>前端控制器说\"所有请求先过我这道门\"，业务控制器说\"这个请求的业务我管\"。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>它同样不是 GoF 二十三式，是 J2EE 蓝皮书成员——诞生动机就是上面那个复制粘贴惨案：横切关注点（所有请求都要做、且做法一样的事）散落在 N 个 Servlet 里，纪律靠自觉，漏洞靠运气。\u003C\u002Fp>\n\u003Ch2>二、优点与缺点\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>优点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>横切关注点写一次，全站无死角\u003C\u002Fstrong>：编码、认证、日志、限流——写在门神身上，任何 URL 都逃不掉，新同事想漏都漏不了。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>集中分发，路由一目了然\u003C\u002Fstrong>：URL 到哪个处理器，一张路由表看全——\"这个接口谁在管\"从考古问题变成查表问题。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>统一错误处理\u003C\u002Fstrong>：404、401、500 在一个地方兜底，不会有的页面报错白屏、有的页面报错误码。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>安全审计有唯一卡口\u003C\u002Fstrong>：想拦截所有请求做审计、限流、灰度——只需在门口加，不用满楼跑。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>\u003Cstrong>缺点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>唯一入口 = 唯一故障点\u003C\u002Fstrong>：门神挂了全站挂——所以现实里它必须薄（只做进门三件事加分发），重活不许干。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>所有请求串一遍相同检查\u003C\u002Fstrong>：静态资源请求也走登录校验，部分检查对部分路径是浪费——解药是 ㉛ 按路径配过滤器。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>路由表会膨胀\u003C\u002Fstrong>：大应用几百条路由堆在一个类里——现实框架用注解把路由拆回各个控制器（第五节见）。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>三、什么时候用\u003C\u002Fh2>\n\u003Cp>判断标准：\u003Cstrong>有多条入口 URL + 存在对所有请求一视同仁的横切关注点\u003C\u002Fstrong>——就用前端控制器，这几乎是 Web 应用的出厂设置。\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>典型场景\u003C\u002Fstrong>：Web 应用、HTTP API——认证、日志、编码、限流，哪个应用躲得开？\u003C\u002Fli>\n \u003Cli>\u003Cstrong>微服务 API 网关\u003C\u002Fstrong>：跨服务版前端控制器——认证、限流、路由，在集群门口统一办\u003C\u002Fli>\n \u003Cli>\u003Cstrong>反面对照\u003C\u002Fstrong>：桌面应用、单文件脚本——没有\"请求进门\"这回事，硬套是给自己立门神；纯内部工具只有一个入口时，前端控制器退化成一个 main 方法，模式已经在了，只是不值得命名\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2>四、实现方式：谁能看个人资料\u003C\u002Fh2>\n\u003Cp>场景：一个迷你 Web 应用，两个页面——订单列表 \u003Ccode>\u002Forder\u002Flist\u003C\u002Fcode>、个人资料 \u003Ccode>\u002Fuser\u002Fprofile\u003C\u002Fcode>。进门三件套：编码、日志、登录校验。看两套世界。\u003C\u002Fp>\n\u003Ch3>4.1 反面教材：每个 Servlet 各自为政\u003C\u002Fh3>\n\u003Cp>老员工写的 OrderServlet，三件套抄得整整齐齐：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class BadOrderServlet {\n\n    public void service(Request request) {\n        System.out.println(\"   [OrderServlet] 设置编码 UTF-8\");\n        System.out.println(\"   [OrderServlet] 检查登录：\" + request.getUserId() + \" 已登录\");\n        System.out.println(\"   [OrderServlet] 记访问日志：\" + request.getUserId() + \" -&gt; \" + request.getPath());\n        System.out.println(\"   [OrderServlet] 业务：拉取订单列表\");\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>新同事写的 UserServlet——不知道有模板，只写了业务，\u003Cstrong>编译照样过，功能照样跑\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class BadUserServlet {\n\n    public void service(Request request) {\n        System.out.println(\"   [UserServlet] 业务：拉取个人资料\");\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行输出：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>========== 反面教材：每个 Servlet 各自为政 ==========\n-- 请求 \u002Forder\u002Flist（u001 已登录，OrderServlet 接活） --\n   [OrderServlet] 设置编码 UTF-8\n   [OrderServlet] 检查登录：u001 已登录\n   [OrderServlet] 记访问日志：u001 -&gt; \u002Forder\u002Flist\n   [OrderServlet] 业务：拉取订单列表\n\n-- 请求 \u002Fuser\u002Fprofile（u001 已登录，UserServlet 接活） --\n   [UserServlet] 业务：拉取个人资料\n   UserServlet 少了三件套——三件套是靠复制粘贴传播的，抄没抄全靠自觉\n\n-- 请求 \u002Fuser\u002Fprofile（匿名用户，UserServlet 接活） --\n   [UserServlet] 业务：拉取个人资料\n   安全事故：未登录直接拉到个人资料——抄漏一处校验，就是一个洞\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>三个病灶全齐：\u003Cstrong>纪律靠自觉\u003C\u002Fstrong>（三件套的正确性不靠结构保证，靠每个写代码的人记得抄）；\u003Cstrong>抄漏一处就是一个洞\u003C\u002Fstrong>（UserServlet 没拦住匿名请求——复制粘贴传播的校验，传播不到的地方就是裸奔）；\u003Cstrong>全局改动 N 处\u003C\u002Fstrong>（哪天校验规则要改、要加限流，挨个 Servlet 翻，改漏一个就前功尽弃）。注意 OrderServlet 本身没写错——反面教材从来不是谁的错，是\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 class UserController implements Controller {\n\n    @Override\n    public void handle(Request request) {\n        System.out.println(\"   [UserController] 业务：拉取个人资料\");\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>门神收口一切\u003C\u002Fstrong>——三件套写一次，路由一张表：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class FrontController {\n\n    private final Map&lt;String, Controller&gt; routes = new HashMap&lt;&gt;();\n\n    public FrontController() {\n        routes.put(\"\u002Forder\u002Flist\", new OrderController());\n        routes.put(\"\u002Fuser\u002Fprofile\", new UserController());\n    }\n\n    \u002F** 所有请求都从这里进 *\u002F\n    public void handle(Request request) {\n        System.out.println(\"   [前端控制器] 统一设置编码 UTF-8\");\n        System.out.println(\"   [前端控制器] 统一访问日志：\" + request.displayUser() + \" -&gt; \" + request.getPath());\n\n        if (!request.isLoggedIn()) {\n            System.out.println(\"   [前端控制器] 统一登录检查：未登录，拒绝：401 请先登录\");\n            return;\n        }\n        System.out.println(\"   [前端控制器] 统一登录检查：\" + request.getUserId() + \" 已登录\");\n\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}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行输出：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>========== 正解：前端控制器统一入口 ==========\n-- 请求 \u002Forder\u002Flist（u001 已登录） --\n   [前端控制器] 统一设置编码 UTF-8\n   [前端控制器] 统一访问日志：u001 -&gt; \u002Forder\u002Flist\n   [前端控制器] 统一登录检查：u001 已登录\n   [OrderController] 业务：拉取订单列表\n\n-- 请求 \u002Fuser\u002Fprofile（u001 已登录） --\n   [前端控制器] 统一设置编码 UTF-8\n   [前端控制器] 统一访问日志：u001 -&gt; \u002Fuser\u002Fprofile\n   [前端控制器] 统一登录检查：u001 已登录\n   [UserController] 业务：拉取个人资料\n\n-- 请求 \u002Fuser\u002Fprofile（匿名用户再试） --\n   [前端控制器] 统一设置编码 UTF-8\n   [前端控制器] 统一访问日志：匿名 -&gt; \u002Fuser\u002Fprofile\n   [前端控制器] 统一登录检查：未登录，拒绝：401 请先登录\n\n-- 请求 \u002Fno\u002Fsuch\u002Fpage（乱敲的 URL） --\n   [前端控制器] 统一设置编码 UTF-8\n   [前端控制器] 统一访问日志：u001 -&gt; \u002Fno\u002Fsuch\u002Fpage\n   [前端控制器] 统一登录检查：u001 已登录\n   [前端控制器] 404：没有这个页面 \u002Fno\u002Fsuch\u002Fpage\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>三处对照读出含金量：UserController 里\u003Cstrong>一行校验都没有\u003C\u002Fstrong>，但匿名请求进不来——正确性从\"每个人记得抄\"变成\"结构保证\"；被拒绝的请求也\u003Cstrong>先记了日志\u003C\u002Fstrong>（匿名 -&gt; \u002Fuser\u002Fprofile）——审计无死角，攻击尝试留痕；乱敲的 URL 统一 404——错误处理有了兜底的地方。反面教材那个安全洞，在这里\u003Cstrong>结构上就不可能存在\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Ch3>4.3 三处细节\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>细节一：门神必须薄。\u003C\u002Fstrong> 前端控制器只干两件事：进门三件套 + 分发。业务逻辑一旦写进门神，它就从\"全站唯一入口\"堕落成\"全站唯一上帝类\"——缺点一的故障点风险直接翻倍。判断标准：门神里的代码应该对\u003Cstrong>所有\u003C\u002Fstrong>请求成立，\"只对订单成立\"的代码立刻请去 OrderController。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节二：分发的形态会进化。\u003C\u002Fstrong> 本文路由表是构造器里硬编码的 \u003Ccode>Map.put\u003C\u002Fcode>——够用，但加页面要改门神。下一步进化是命令模式（⑱）：请求参数直接映射处理器类；再进一步是注解扫描：Spring 的 \u003Ccode>@RequestMapping(\"\u002Fuser\u002Fprofile\")\u003C\u002Fcode> 把路由声明放回各个控制器，框架启动时自动汇总成路由表——\u003Cstrong>路由分散声明、集中执行\u003C\u002Fstrong>，膨胀问题就这么解的。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节三：三件套会长大成三十件套。\u003C\u002Fstrong> 编码、认证、日志只是开胃菜：限流、审计、灰度、租户识别、链路追踪……全往 handle() 方法里塞，门神眼看撑爆。而且它们各有各的讲究：编码必须最前、权限校验得在认证之后、限流只想挂部分路径、顺序还得可配置。\u003Cstrong>横切关注点\"可插拔、可排序\"的需求，就是 ㉛ 拦截过滤器模式的出生证明\u003C\u002Fstrong>——下一篇见分晓。\u003C\u002Fp>\n\u003Ch2>五、在框架里找它\u003C\u002Fh2>\n\u003Col>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Spring MVC 的 DispatcherServlet\u003C\u002Fstrong>：名字都写在脸上了——Front \u003Cstrong>Dispatcher\u003C\u002Fstrong> Servlet。所有请求先进它，HandlerMapping 是路由表，@Controller 是业务控制器，@RequestMapping 是路由声明。Interceptor（拦截器）则是它给横切关注点留的插槽。本文 FrontController 的工业化完整版。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Struts 的 ActionServlet\u003C\u002Fstrong>：老一代答案——ActionServlet 收口一切，按配置把请求分发给各个 Action。前端控制器模式教会了整整一代 Java 框架\"怎么当门神\"。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Django \u002F Flask \u002F FastAPI\u003C\u002Fstrong>：Python 世界同款思想——WSGI\u002FASGI 统一入口 + URL 路由表分发到视图函数；中间件（middleware）承担三件套职责。框架换了语言，门神的岗位职责全球统一。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>API 网关（Kong、Spring Cloud Gateway）\u003C\u002Fstrong>：微服务时代的前端控制器——几十个服务前面立一扇大门，认证、限流、灰度、路由到下游服务，全都门口办。\u003Cstrong>从\"一个应用的门神\"放大到\"一个集群的门神\"\u003C\u002Fstrong>，模式没变，编制升了。\u003C\u002Fp>\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>六、两个\"控制器\"对账——㉖ MVC 的 C vs ㉚ 前端控制器\u003C\u002Fh2>\n\u003Cp>㉖ 讲 MVC 时留过一个尾巴：MVC 的 Controller 和前端控制器是什么关系？摊开对账：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>\u003C\u002Fth>\n   \u003Cth>MVC 的 Controller（㉖）\u003C\u002Fth>\n   \u003Cth>Front Controller（㉚）\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>数量\u003C\u002Ftd>\n   \u003Ctd>一个用例一个：LoginController、OrderController\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>你写\u003C\u002Ftd>\n   \u003Ctd>框架写（DispatcherServlet、ActionServlet）\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>接到活以后怎么干\u003C\u002Ftd>\n   \u003Ctd>活怎么统一地接到\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>Spring MVC 里两者同框：\u003Cstrong>DispatcherServlet 是前端控制器，@Controller 是 MVC 的 C\u003C\u002Fstrong>——你的每个 @Controller 方法干的是 ㉖ 的活，而把它们串起来的那扇门是 ㉚。所以别再问\"MVC 的 C 是不是前端控制器\"——狭义 MVC 只说\"控制器接收输入、调用模型\"；落到 Web 上，这活被拆成两半：门口的统一前奏（㉚，框架的）和屋里的一单一办（㉖，你的）。\u003C\u002Fp>\n\u003Ch2>写在最后\u003C\u002Fh2>\n\u003Cul>\n \u003Cli>前端控制器 = 全应用唯一入口：横切关注点写一次，分发路由一张表\u003C\u002Fli>\n \u003Cli>疗效：校验无死角、审计有卡口、错误有兜底——正确性从靠自觉变成靠结构\u003C\u002Fli>\n \u003Cli>铁律：门神必须薄——对所有请求成立的代码才配进门神\u003C\u002Fli>\n \u003Cli>分发的进化：硬编码路由表 → 命令映射 → 注解扫描，路由分散声明、集中执行\u003C\u002Fli>\n \u003Cli>当代转世：DispatcherServlet、Django 中间件、API 网关——门神从应用门口站到集群门口\u003C\u002Fli>\n \u003Cli>对账：㉖ 的 C 是\"接到活以后怎么干\"，㉚ 是\"活怎么统一地接到\"——Spring MVC 里两者同框\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>前端控制器把进门三件套写死在 handle() 里，顺序固定、全体适用。可现实很快翻脸：编码必须在最前、权限校验得排在认证后、限流只想挂 API 路径、静态资源什么检查都不想要——横切关注点越长越多，还各有各的顺序和地盘，门神的 handle() 方法眼看撑爆。谁让这些横切逻辑\"\u003Cstrong>可插拔、可排序、可挑路径\u003C\u002Fstrong>\"？——拦截过滤器模式，下一篇见。\u003C\u002Fp>",[],"index,follow",null,{"code":4,"message":5,"data":44},[45,54,63],{"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},"2113935201573801985","design-pattern-31","设计模式修炼之路 ㉛ 拦截过滤器模式——一个过滤器，只管一件事","一个过滤器一个横切关注点、return 走不出的审计盲区、契约两句话放行就 next、后置代码包住整条链、顺序即范围、与责任链对账",1038,7452,19,"2025-05-27T20:35:21",{"id":55,"slug":56,"title":57,"summary":58,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":59,"likeCount":4,"wordCount":60,"readingMinutes":61,"publishedAt":62},"2113935519934058498","design-pattern-26","设计模式修炼之路 ㉖ MVC 模式——改界面不动业务，改业务不动界面","比 GoF 早十五年、依赖方向定生死、业务逻辑永远住 Model、换视图零成本、MVC→MVP→MVVM 进化树",2596,8085,21,"2025-02-16T16:19:17",{"id":64,"slug":65,"title":66,"summary":67,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":68,"likeCount":4,"wordCount":69,"readingMinutes":70,"publishedAt":71},"2113935261715927041","design-pattern-32","设计模式修炼之路 ㉜ 服务定位器模式——服务只找一次，之后全靠缓存","查找与缓存一处收口、注册中心只被打扰 N 次、缓存的是服务不是结果、种子从 ㉗ 长成 static 注册台、字符串名字没有编译器保护、与业务代表对账",1926,7172,18,"2025-06-20T10:10:19",{"code":4,"message":5,"data":73},{"prev":74,"next":82},{"id":75,"slug":76,"title":77,"summary":78,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":79,"likeCount":4,"wordCount":80,"readingMinutes":70,"publishedAt":81},"2113935074440253441","design-pattern-29","设计模式修炼之路 ㉙ 数据访问对象模式——业务对象不该懂数据库","业务对象不该懂数据库、接口只说业务语言、换实现不换业务、单测不连库、Spring Data 工业化 DAO、DAO 与 Repository 对账",3913,6821,"2025-04-15T11:27:29",{"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},{"code":4,"message":5,"data":84},[85,91,96,102],{"id":86,"name":87,"slug":88,"description":89,"sortOrder":4,"articleCount":90},"1946000000000000001","antlr4","tech","编程与技术笔记","13",{"id":32,"name":92,"slug":93,"description":94,"sortOrder":4,"articleCount":95},"设计模式","design-pattern","设计模式系列","34",{"id":97,"name":98,"slug":99,"description":100,"sortOrder":33,"articleCount":101},"1946000000000000002","随笔","essay","生活与思考","0",{"id":103,"name":104,"slug":105,"description":106,"sortOrder":107,"articleCount":108},"2105200093764698113","工具","tools","好用的工具推荐",2,"1",{"html":110,"toc":111},"\u003Cblockquote>\n \u003Cp>J2EE 模式第五站。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>㉙ 的 DAO 把\"怎么存\"收走了，业务层终于干净。可 Web 应用的麻烦不止在存储——请求进门的第一个问题是：\u003Cstrong>谁接的活？\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>Servlet 时代的默认答案：一个 URL 一个 Servlet，各自为政。于是\"进门三件套\"——设置编码、登录校验、访问日志——只能靠复制粘贴在每个 Servlet 里传播：老员工抄得齐，新来的不知道有模板，抄漏一处校验，就是一个安全洞。想要\"全局加个限流\"？挨个文件改一遍，改漏一个就白干。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>用一个控制器收口所有请求：进门的事集中办，业务的事分发办\u003C\u002Fstrong>——前端控制器模式，J2EE 八式里的门神。\u003C\u002Fp>\n\u003Ch2 id=\"toc-1\">一、前端控制器模式是什么\u003C\u002Fh2>\n\u003Cp>J2EE 蓝皮书的定义大意：前端控制器（Front Controller）为 Web 应用提供一个\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>Front Controller（前端控制器）\u003C\u002Ftd>\n   \u003Ctd>全应用唯一入口：横切关注点 + 分发\u003C\u002Ftd>\n   \u003Ctd>FrontController\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Dispatcher（分发器）\u003C\u002Ftd>\n   \u003Ctd>维护 URL 到处理器的映射，负责路由\u003C\u002Ftd>\n   \u003Ctd>本文并入 FrontController 的 routes\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Handler \u002F Controller（业务控制器）\u003C\u002Ftd>\n   \u003Ctd>一个 URL 一家，只管业务\u003C\u002Ftd>\n   \u003Ctd>OrderController \u002F UserController\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>View（视图）\u003C\u002Ftd>\n   \u003Ctd>渲染响应\u003C\u002Ftd>\n   \u003Ctd>本文 println 代劳\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>一句话理解分工：\u003Cstrong>前端控制器说\"所有请求先过我这道门\"，业务控制器说\"这个请求的业务我管\"。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>它同样不是 GoF 二十三式，是 J2EE 蓝皮书成员——诞生动机就是上面那个复制粘贴惨案：横切关注点（所有请求都要做、且做法一样的事）散落在 N 个 Servlet 里，纪律靠自觉，漏洞靠运气。\u003C\u002Fp>\n\u003Ch2 id=\"toc-2\">二、优点与缺点\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>优点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>横切关注点写一次，全站无死角\u003C\u002Fstrong>：编码、认证、日志、限流——写在门神身上，任何 URL 都逃不掉，新同事想漏都漏不了。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>集中分发，路由一目了然\u003C\u002Fstrong>：URL 到哪个处理器，一张路由表看全——\"这个接口谁在管\"从考古问题变成查表问题。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>统一错误处理\u003C\u002Fstrong>：404、401、500 在一个地方兜底，不会有的页面报错白屏、有的页面报错误码。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>安全审计有唯一卡口\u003C\u002Fstrong>：想拦截所有请求做审计、限流、灰度——只需在门口加，不用满楼跑。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>\u003Cstrong>缺点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>唯一入口 = 唯一故障点\u003C\u002Fstrong>：门神挂了全站挂——所以现实里它必须薄（只做进门三件事加分发），重活不许干。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>所有请求串一遍相同检查\u003C\u002Fstrong>：静态资源请求也走登录校验，部分检查对部分路径是浪费——解药是 ㉛ 按路径配过滤器。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>路由表会膨胀\u003C\u002Fstrong>：大应用几百条路由堆在一个类里——现实框架用注解把路由拆回各个控制器（第五节见）。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"toc-3\">三、什么时候用\u003C\u002Fh2>\n\u003Cp>判断标准：\u003Cstrong>有多条入口 URL + 存在对所有请求一视同仁的横切关注点\u003C\u002Fstrong>——就用前端控制器，这几乎是 Web 应用的出厂设置。\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>典型场景\u003C\u002Fstrong>：Web 应用、HTTP API——认证、日志、编码、限流，哪个应用躲得开？\u003C\u002Fli>\n \u003Cli>\u003Cstrong>微服务 API 网关\u003C\u002Fstrong>：跨服务版前端控制器——认证、限流、路由，在集群门口统一办\u003C\u002Fli>\n \u003Cli>\u003Cstrong>反面对照\u003C\u002Fstrong>：桌面应用、单文件脚本——没有\"请求进门\"这回事，硬套是给自己立门神；纯内部工具只有一个入口时，前端控制器退化成一个 main 方法，模式已经在了，只是不值得命名\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"toc-4\">四、实现方式：谁能看个人资料\u003C\u002Fh2>\n\u003Cp>场景：一个迷你 Web 应用，两个页面——订单列表 \u003Ccode>\u002Forder\u002Flist\u003C\u002Fcode>、个人资料 \u003Ccode>\u002Fuser\u002Fprofile\u003C\u002Fcode>。进门三件套：编码、日志、登录校验。看两套世界。\u003C\u002Fp>\n\u003Ch3 id=\"toc-5\">4.1 反面教材：每个 Servlet 各自为政\u003C\u002Fh3>\n\u003Cp>老员工写的 OrderServlet，三件套抄得整整齐齐：\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\"> BadOrderServlet\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\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> service\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\">) {\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\">\"   [OrderServlet] 设置编码 UTF-8\"\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\">\"   [OrderServlet] 检查登录：\"\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\">        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\">\"   [OrderServlet] 记访问日志：\"\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:#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\">        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\">\"   [OrderServlet] 业务：拉取订单列表\"\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>新同事写的 UserServlet——不知道有模板，只写了业务，\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\"> BadUserServlet\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\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> service\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\">) {\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\">\"   [UserServlet] 业务：拉取个人资料\"\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>运行输出：\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>========== 反面教材：每个 Servlet 各自为政 ==========\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- 请求 \u002Forder\u002Flist（u001 已登录，OrderServlet 接活） --\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [OrderServlet] 设置编码 UTF-8\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [OrderServlet] 检查登录：u001 已登录\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [OrderServlet] 记访问日志：u001 -> \u002Forder\u002Flist\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [OrderServlet] 业务：拉取订单列表\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- 请求 \u002Fuser\u002Fprofile（u001 已登录，UserServlet 接活） --\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [UserServlet] 业务：拉取个人资料\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   UserServlet 少了三件套——三件套是靠复制粘贴传播的，抄没抄全靠自觉\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- 请求 \u002Fuser\u002Fprofile（匿名用户，UserServlet 接活） --\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [UserServlet] 业务：拉取个人资料\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   安全事故：未登录直接拉到个人资料——抄漏一处校验，就是一个洞\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>三个病灶全齐：\u003Cstrong>纪律靠自觉\u003C\u002Fstrong>（三件套的正确性不靠结构保证，靠每个写代码的人记得抄）；\u003Cstrong>抄漏一处就是一个洞\u003C\u002Fstrong>（UserServlet 没拦住匿名请求——复制粘贴传播的校验，传播不到的地方就是裸奔）；\u003Cstrong>全局改动 N 处\u003C\u002Fstrong>（哪天校验规则要改、要加限流，挨个 Servlet 翻，改漏一个就前功尽弃）。注意 OrderServlet 本身没写错——反面教材从来不是谁的错，是\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\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> UserController\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> implements\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Controller\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\"> handle\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\">) {\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\">\"   [UserController] 业务：拉取个人资料\"\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>门神收口一切\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\"> FrontController\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\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Map&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">String\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Controller\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> routes \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> HashMap&#x3C;>();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> FrontController\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        routes.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">put\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\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\">        routes.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">put\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\">\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:#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 \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">request\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\">\"   [前端控制器] 统一设置编码 UTF-8\"\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\">\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\">\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\">\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>-- 请求 \u002Forder\u002Flist（u001 已登录） --\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [前端控制器] 统一设置编码 UTF-8\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [前端控制器] 统一访问日志：u001 -> \u002Forder\u002Flist\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [前端控制器] 统一登录检查：u001 已登录\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [OrderController] 业务：拉取订单列表\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- 请求 \u002Fuser\u002Fprofile（u001 已登录） --\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 已登录\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [UserController] 业务：拉取个人资料\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- 请求 \u002Fuser\u002Fprofile（匿名用户再试） --\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [前端控制器] 统一设置编码 UTF-8\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [前端控制器] 统一访问日志：匿名 -> \u002Fuser\u002Fprofile\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [前端控制器] 统一登录检查：未登录，拒绝：401 请先登录\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- 请求 \u002Fno\u002Fsuch\u002Fpage（乱敲的 URL） --\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 已登录\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [前端控制器] 404：没有这个页面 \u002Fno\u002Fsuch\u002Fpage\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>三处对照读出含金量：UserController 里\u003Cstrong>一行校验都没有\u003C\u002Fstrong>，但匿名请求进不来——正确性从\"每个人记得抄\"变成\"结构保证\"；被拒绝的请求也\u003Cstrong>先记了日志\u003C\u002Fstrong>（匿名 -&gt; \u002Fuser\u002Fprofile）——审计无死角，攻击尝试留痕；乱敲的 URL 统一 404——错误处理有了兜底的地方。反面教材那个安全洞，在这里\u003Cstrong>结构上就不可能存在\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Ch3 id=\"toc-7\">4.3 三处细节\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>细节一：门神必须薄。\u003C\u002Fstrong> 前端控制器只干两件事：进门三件套 + 分发。业务逻辑一旦写进门神，它就从\"全站唯一入口\"堕落成\"全站唯一上帝类\"——缺点一的故障点风险直接翻倍。判断标准：门神里的代码应该对\u003Cstrong>所有\u003C\u002Fstrong>请求成立，\"只对订单成立\"的代码立刻请去 OrderController。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节二：分发的形态会进化。\u003C\u002Fstrong> 本文路由表是构造器里硬编码的 \u003Ccode>Map.put\u003C\u002Fcode>——够用，但加页面要改门神。下一步进化是命令模式（⑱）：请求参数直接映射处理器类；再进一步是注解扫描：Spring 的 \u003Ccode>@RequestMapping(\"\u002Fuser\u002Fprofile\")\u003C\u002Fcode> 把路由声明放回各个控制器，框架启动时自动汇总成路由表——\u003Cstrong>路由分散声明、集中执行\u003C\u002Fstrong>，膨胀问题就这么解的。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节三：三件套会长大成三十件套。\u003C\u002Fstrong> 编码、认证、日志只是开胃菜：限流、审计、灰度、租户识别、链路追踪……全往 handle() 方法里塞，门神眼看撑爆。而且它们各有各的讲究：编码必须最前、权限校验得在认证之后、限流只想挂部分路径、顺序还得可配置。\u003Cstrong>横切关注点\"可插拔、可排序\"的需求，就是 ㉛ 拦截过滤器模式的出生证明\u003C\u002Fstrong>——下一篇见分晓。\u003C\u002Fp>\n\u003Ch2 id=\"toc-8\">五、在框架里找它\u003C\u002Fh2>\n\u003Col>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Spring MVC 的 DispatcherServlet\u003C\u002Fstrong>：名字都写在脸上了——Front \u003Cstrong>Dispatcher\u003C\u002Fstrong> Servlet。所有请求先进它，HandlerMapping 是路由表，@Controller 是业务控制器，@RequestMapping 是路由声明。Interceptor（拦截器）则是它给横切关注点留的插槽。本文 FrontController 的工业化完整版。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Struts 的 ActionServlet\u003C\u002Fstrong>：老一代答案——ActionServlet 收口一切，按配置把请求分发给各个 Action。前端控制器模式教会了整整一代 Java 框架\"怎么当门神\"。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Django \u002F Flask \u002F FastAPI\u003C\u002Fstrong>：Python 世界同款思想——WSGI\u002FASGI 统一入口 + URL 路由表分发到视图函数；中间件（middleware）承担三件套职责。框架换了语言，门神的岗位职责全球统一。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>API 网关（Kong、Spring Cloud Gateway）\u003C\u002Fstrong>：微服务时代的前端控制器——几十个服务前面立一扇大门，认证、限流、灰度、路由到下游服务，全都门口办。\u003Cstrong>从\"一个应用的门神\"放大到\"一个集群的门神\"\u003C\u002Fstrong>，模式没变，编制升了。\u003C\u002Fp>\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"toc-9\">六、两个\"控制器\"对账——㉖ MVC 的 C vs ㉚ 前端控制器\u003C\u002Fh2>\n\u003Cp>㉖ 讲 MVC 时留过一个尾巴：MVC 的 Controller 和前端控制器是什么关系？摊开对账：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>\u003C\u002Fth>\n   \u003Cth>MVC 的 Controller（㉖）\u003C\u002Fth>\n   \u003Cth>Front Controller（㉚）\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>数量\u003C\u002Ftd>\n   \u003Ctd>一个用例一个：LoginController、OrderController\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>你写\u003C\u002Ftd>\n   \u003Ctd>框架写（DispatcherServlet、ActionServlet）\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>接到活以后怎么干\u003C\u002Ftd>\n   \u003Ctd>活怎么统一地接到\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>Spring MVC 里两者同框：\u003Cstrong>DispatcherServlet 是前端控制器，@Controller 是 MVC 的 C\u003C\u002Fstrong>——你的每个 @Controller 方法干的是 ㉖ 的活，而把它们串起来的那扇门是 ㉚。所以别再问\"MVC 的 C 是不是前端控制器\"——狭义 MVC 只说\"控制器接收输入、调用模型\"；落到 Web 上，这活被拆成两半：门口的统一前奏（㉚，框架的）和屋里的一单一办（㉖，你的）。\u003C\u002Fp>\n\u003Ch2 id=\"toc-10\">写在最后\u003C\u002Fh2>\n\u003Cul>\n \u003Cli>前端控制器 = 全应用唯一入口：横切关注点写一次，分发路由一张表\u003C\u002Fli>\n \u003Cli>疗效：校验无死角、审计有卡口、错误有兜底——正确性从靠自觉变成靠结构\u003C\u002Fli>\n \u003Cli>铁律：门神必须薄——对所有请求成立的代码才配进门神\u003C\u002Fli>\n \u003Cli>分发的进化：硬编码路由表 → 命令映射 → 注解扫描，路由分散声明、集中执行\u003C\u002Fli>\n \u003Cli>当代转世：DispatcherServlet、Django 中间件、API 网关——门神从应用门口站到集群门口\u003C\u002Fli>\n \u003Cli>对账：㉖ 的 C 是\"接到活以后怎么干\"，㉚ 是\"活怎么统一地接到\"——Spring MVC 里两者同框\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>前端控制器把进门三件套写死在 handle() 里，顺序固定、全体适用。可现实很快翻脸：编码必须在最前、权限校验得排在认证后、限流只想挂 API 路径、静态资源什么检查都不想要——横切关注点越长越多，还各有各的顺序和地盘，门神的 handle() 方法眼看撑爆。谁让这些横切逻辑\"\u003Cstrong>可插拔、可排序、可挑路径\u003C\u002Fstrong>\"？——拦截过滤器模式，下一篇见。\u003C\u002Fp>",[112,115,118,121,124,128,131,134,137,140],{"id":113,"text":114,"level":107},"toc-1","一、前端控制器模式是什么",{"id":116,"text":117,"level":107},"toc-2","二、优点与缺点",{"id":119,"text":120,"level":107},"toc-3","三、什么时候用",{"id":122,"text":123,"level":107},"toc-4","四、实现方式：谁能看个人资料",{"id":125,"text":126,"level":127},"toc-5","4.1 反面教材：每个 Servlet 各自为政",3,{"id":129,"text":130,"level":127},"toc-6","4.2 正解：前端控制器",{"id":132,"text":133,"level":127},"toc-7","4.3 三处细节",{"id":135,"text":136,"level":107},"toc-8","五、在框架里找它",{"id":138,"text":139,"level":107},"toc-9","六、两个\"控制器\"对账——㉖ MVC 的 C vs ㉚ 前端控制器",{"id":141,"text":142,"level":107},"toc-10","写在最后"]