前端控制器模式
本章是 Spring MVC 教程的架构思想层。上一章"Spring MVC 是什么"回答了"为什么需要 Spring MVC",本章深入回答"为什么 Spring MVC 选择 DispatcherServlet 作为唯一入口"。理解前端控制器模式这一设计思想,才能真正明白 DispatcherServlet 存在的合理性,以及它与传统 Servlet 开发的本质区别。
定义与作用
前端控制器模式(Front Controller Pattern)是一种经典的 Web 架构设计模式,其核心思想是:用一个中央控制器接收所有进入应用的请求,再由这个中央控制器根据请求特征分发给具体的业务处理器。这个中央控制器本身不处理业务逻辑,只负责"接收、分发、协调、收尾"。
在 Java Web 领域,前端控制器模式解决了传统 Servlet 开发中三个根深蒂固的问题:
| 问题 | 传统 Servlet 的表现 | 前端控制器模式的解决方式 |
|---|---|---|
| 重复代码泛滥 | 每个 Servlet 都要写编码处理、认证检查、日志记录 | 中央控制器统一处理,所有请求共享 |
| 配置无限膨胀 | 每增加一个 URL 就要在 web.xml 加一对 servlet-mapping | 一个 Servlet 映射所有请求,路由逻辑内聚到代码中 |
| 横切逻辑分散 | 登录校验、权限检查散落在各个 Servlet 中 | 拦截器链在中央控制器处统一挂载,集中管理 |
与传统 Servlet 的架构对比
传统 Servlet 开发中,每个功能对应一个独立的 Servlet 类,客户端请求直接打到各个 Servlet 上:
上图的致命缺陷在于:编码处理、认证检查、日志记录这些通用逻辑被复制到了每一个 Servlet 中。当白歌架构师要求"所有接口增加请求耗时统计"时,小崔不得不修改 50 个 Servlet 类。
前端控制器模式彻底扭转了这一局面:
关键转变:通用逻辑从"分散在 N 个 Servlet"变成了"集中在 1 个 DispatcherServlet"。新增业务只需增加 Controller 方法,无需触碰中央控制器的任何代码。
生活类比:医院挂号台
想象你去医院看病:
传统 Servlet 模式:医院有 20 个科室,每个科室门口都有一个独立的挂号窗口。你要去骨科就去骨科窗口排队,要去内科就去内科窗口排队。每个窗口都要重复问你"有没有医保卡""有没有过敏史""手机号是多少"——同样的信息采集重复 20 次。如果医院新增"体检中心",就要再开一个窗口、再配一套信息采集流程。
前端控制器模式:医院只有一个总挂号台(DispatcherServlet)。你走到总挂号台,工作人员统一采集你的基本信息(编码处理、认证检查),然后查询科室分布表(HandlerMapping),给你一张导诊单(HandlerExecutionChain),指引你去对应科室(Controller)看病。看完病后,报告单交回总挂号台,总挂号台统一打印、盖章、交付(视图渲染)。如果医院新增科室,只需在科室分布表上增加一条记录,总挂号台本身不需要任何改造。
这个类比的关键在于:总挂号台不看病,它只负责"统一入口、统一预处理、统一分发、统一收尾"。
其他使用前端控制器模式的框架
前端控制器模式并非 Spring MVC 独创,它是 Java Web 框架的通用架构选择:
| 框架 | 前端控制器类 | 映射路径 |
|---|---|---|
| Spring MVC | DispatcherServlet | /(默认) |
| Struts 1.x | ActionServlet | *.do(常见配置) |
| JSF | FacesServlet | /faces/* 或 *.jsf |
| Spring WebFlux | DispatcherHandler | /(响应式版本) |
这些框架的前端控制器职责高度一致:统一接收请求、统一预处理、统一路由分发、统一响应包装。差异只在于内部组件的名称和实现细节。
核心原理
请求流经前端控制器的完整时序
前端控制器模式的核心价值,在于它定义了一套标准化的请求处理流水线。所有请求必须经过相同的预处理、分发、后处理环节:
上图揭示了一个关键设计:前端控制器不是简单的"转发器",而是一个完整的请求处理框架。它定义了预处理、路由、拦截、执行、后处理、清理六个标准阶段,所有请求一视同仁地经过这条流水线。
为什么"一个入口"优于"N 个入口"
从软件工程角度看,前端控制器模式符合单一职责原则的反向应用——不是让每个组件职责单一,而是让同一类职责只在一个地方实现:
| 职责 | 传统 Servlet(分散实现) | 前端控制器(集中实现) |
|---|---|---|
| 请求编码 | 每个 Servlet 手动设置 | DispatcherServlet 统一设置 CharacterEncodingFilter |
| 登录校验 | 每个 Servlet 手动检查 Session | 拦截器链统一挂载 AuthenticationInterceptor |
| 请求日志 | 每个 Servlet 手动打印 | DispatcherServlet 统一记录请求 URI、方法、耗时 |
| 异常处理 | 每个 Servlet 写 try-catch | @ControllerAdvice 全局捕获 |
| 跨域配置 | 每个响应头手动设置 | CORS 过滤器统一处理 |
结论:当系统有 N 个接口时,传统模式把通用逻辑复制了 N 份,前端控制器模式只实现 1 份。N 越大,优势越明显。
完整示例
场景
飞翔科技要开发一个电商平台,预计有 80 多个 REST 接口。CTO 大翔召集技术会议,要求架构师白歌评估"传统 Servlet 方案"和"前端控制器方案"的架构差异。后端工程师小崔、前端工程师黄俪、运维工程师李眉参与讨论。
传统 Servlet 方案(被否决)
小崔先按传统思路写了一个原型:
// ProductServlet.java
public class ProductServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 每个 Servlet 重复:编码处理
req.setCharacterEncoding("UTF-8");
resp.setContentType("application/json;charset=UTF-8");
// 每个 Servlet 重复:登录校验
HttpSession session = req.getSession();
if (session.getAttribute("user") == null) {
resp.setStatus(401);
resp.getWriter().write("{\"error\":\"未登录\"}");
return;
}
// 每个 Servlet 重复:请求日志
System.out.println("[" + new Date() + "] " + req.getMethod() + " " + req.getRequestURI());
// 业务逻辑(这才是真正该写的代码)
String id = req.getParameter("id");
Product product = productService.findById(Integer.parseInt(id));
// 每个 Servlet 重复:JSON 序列化
String json = "{\"id\":" + product.getId() + ",\"name\":\"" + product.getName() + "\"}";
resp.getWriter().write(json);
}
}
// OrderServlet.java
public class OrderServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 同样的编码处理、登录校验、请求日志...又复制了一遍
req.setCharacterEncoding("UTF-8");
resp.setContentType("application/json;charset=UTF-8");
// ... 重复代码省略
}
}
web.xml 配置:
<!-- 80 个接口 = 80 对 servlet + servlet-mapping -->
<servlet><servlet-name>product</servlet-name><servlet-class>com.feixiang.ProductServlet</servlet-class></servlet>
<servlet-mapping><servlet-name>product</servlet-name><url-pattern>/product</url-pattern></servlet-mapping>
<servlet><servlet-name>order</servlet-name><servlet-class>com.feixiang.OrderServlet</servlet-class></servlet>
<servlet-mapping><servlet-name>order</servlet-name><url-pattern>/order</url-pattern></servlet-mapping>
<!-- 还有 78 个... -->
团队反馈:
- 白歌(架构师):"通用逻辑复制了 80 份,一旦要改认证方式(比如从 Session 改 JWT),小崔你要改 80 个文件?"
- 黄俪(前端):"我发现不同接口的错误格式不一样,有的返回 JSON,有的返回 HTML,有的直接 500 空白页。"
- 李眉(运维):"我想统计每个接口的 QPS 和平均耗时,但日志格式不统一,根本没法做监控。"
- 大翔(CTO):"这个方案不可接受。换一个思路。"
前端控制器方案(被采纳)
白歌提出了基于 Spring MVC 前端控制器模式的方案:
// 统一编码过滤器(由 DispatcherServlet 前置处理)
public class EncodingFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
req.setCharacterEncoding("UTF-8");
resp.setContentType("application/json;charset=UTF-8");
chain.doFilter(req, resp);
}
}
// 统一认证拦截器(挂载在 DispatcherServlet 的拦截器链上)
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler)
throws Exception {
if (req.getSession().getAttribute("user") == null) {
resp.setStatus(401);
resp.getWriter().write("{\"error\":\"未登录\"}");
return false; // 拦截,不继续执行
}
return true; // 放行
}
}
// 业务控制器(只写业务逻辑,不写任何通用代码)
@RestController
@RequestMapping("/api")
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping("/products/{id}")
public Product getProduct(@PathVariable int id) {
// 只关注业务:根据 ID 查询商品
return productService.findById(id);
}
}
@RestController
@RequestMapping("/api")
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("/orders/{id}")
public Order getOrder(@PathVariable int id) {
// 只关注业务:根据 ID 查询订单
return orderService.findById(id);
}
}
Spring MVC 配置:
@Configuration
@EnableWebMvc
@ComponentScan("com.feixiang")
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 拦截器统一挂载:所有 /api/** 请求都要经过认证检查
registry.addInterceptor(new AuthInterceptor())
.addPathPatterns("/api/**");
}
}
团队反馈:
- 白歌:"通用逻辑全部集中到 DispatcherServlet 和拦截器链上。改认证方式只需改 AuthInterceptor 一个类。"
- 黄俪:"所有接口的错误格式统一了,401 由 AuthInterceptor 统一返回,500 由 @ControllerAdvice 统一处理。"
- 李眉:"我在 DispatcherServlet 层加了一个耗时统计拦截器,所有接口的 QPS 和 RT 自动输出到日志,格式完全一致。"
- 大翔:"这才是正确的架构方向。小崔,后续所有业务接口都按这个模式写。"
易错场景与面试考点
误区一:前端控制器模式"消灭"了 Servlet
错误认知:"用了 DispatcherServlet,就等于不用学 Servlet 了。"
纠正:DispatcherServlet 本身就是 Servlet,它继承自 HttpServlet,由 Servlet 容器(Tomcat)创建和调用。前端控制器模式不是"替代 Servlet",而是在 Servlet 之上建立了一层架构。不理解 Servlet 生命周期,就无法理解:
- 为什么 DispatcherServlet 需要在
init()中初始化 Spring 容器 - 为什么
service()方法是所有请求的入口 - 为什么 Filter 在 DispatcherServlet 之前执行
误区二:前端控制器模式等于"单例瓶颈"
错误认知:"所有请求都走一个 DispatcherServlet,这不是单点瓶颈吗?"
纠正:DispatcherServlet 虽然是逻辑上的唯一入口,但 Servlet 容器会为它创建多线程并发处理机制。Tomcat 的线程池同时处理多个请求,每个请求在 DispatcherServlet 的 service() 方法中有独立的调用栈。DispatcherServlet 本身是无状态的,不存在线程安全问题。真正可能成为瓶颈的是后端业务逻辑或数据库,而不是 DispatcherServlet。
误区三:任何项目都应该用前端控制器模式
错误认知:"前端控制器模式是最佳实践,所以所有 Web 项目都应该用 Spring MVC。"
纠正:前端控制器模式适合请求类型多样、通用逻辑复杂、需要集中管控的项目。如果一个项目只有 2-3 个接口,且没有复杂的预处理需求,直接使用原生 Servlet 或 JAX-RS 可能更轻量。架构选择取决于问题复杂度,而非"哪个更流行"。
面试高频:前端控制器模式解决了什么问题?
标准回答:
- 消除重复代码:编码处理、认证检查、日志记录等通用逻辑从 N 个 Servlet 集中到 1 个前端控制器
- 统一请求入口:所有请求走同一条预处理流水线,便于集中管控和监控
- 解耦路由与业务:路由逻辑由前端控制器和 HandlerMapping 负责,业务处理器只关注业务
- 标准化横切逻辑:拦截器链机制让登录校验、权限检查、耗时统计等横切逻辑可插拔、可复用
- 降低维护成本:新增业务只需增加处理器,无需修改中央控制器的任何代码
面试高频:Struts 的 ActionServlet 和 Spring MVC 的 DispatcherServlet 有什么区别?
标准回答:两者都是前端控制器模式的具体实现,核心职责相同(接收请求、统一预处理、路由分发)。主要区别在于内部架构:
- ActionServlet 的路由配置依赖 XML(struts-config.xml),处理器必须是 Action 类的 execute 方法
- DispatcherServlet 的路由支持注解(@RequestMapping),处理器可以是任意方法,通过 HandlerAdapter 适配不同签名,扩展性更强
小结
前端控制器模式是 Spring MVC 的架构基石。它用一个中央控制器(DispatcherServlet)替代了传统开发中 N 个独立 Servlet 的分散入口模式,将编码处理、认证检查、日志记录等通用逻辑从业务代码中剥离出来,实现了集中管控、统一标准、解耦复用。
本章与全局的关系:本章从设计模式视角解释了"为什么需要 DispatcherServlet"。下一章"DispatcherServlet"将聚焦这个前端控制器的具体实现——它如何初始化、如何接收请求、如何协调内部组件。再下一章"核心组件协作"将深入讲解 DispatcherServlet 如何与 HandlerMapping、HandlerAdapter、ViewResolver 配合完成请求调度。