对Spring MVC的思考
从Servlet到Spring MVC:一次对"请求分发"的思考
最近连续几个项目都用到了Spring MVC。用得多了,我开始想一个问题:它到底解决了什么,又留下了什么问题?这篇文章就从我自己的学习经历说起。
一、Servlet时代的痛点
最早我用Servlet + JSP开发,做过一个网上云打印店的项目。项目初期功能少,一切都很顺利;但随着功能增多,问题很快出现了:
- Servlet类越来越多。 一个功能点对应一个Servlet,类的数量随需求线性膨胀。
- 配置维护麻烦。 当时没有使用Servlet 3.0的
@WebServlet注解,每新增一个Servlet都要修改web.xml。多人协作时,这个文件成了冲突的高发区。
二、我们自己实现的"请求分发"
既然Servlet太多,能不能减少数量?我们想到的办法是:用一个参数标记操作类型,由同一个Servlet按参数分发到不同方法,例如xxx?action=myMessages:
String action = request.getParameter("action");
if (action == null) {
action = "myMessages";
}
if ("submitMessage".equals(action)) {
submitMessage(request, response);
} else if ("myMessages".equals(action)) {
myMessages(request, response);
} else if ("adminMessages".equals(action)) {
adminMessages(request, response);
} else if ("viewMessage".equals(action)) {
viewMessage(request, response);
} else if ("markMessage".equals(action)) {
markMessage(request, response);
}
// ......
这样,同一模块的操作就集中到了一个类里,这个Servlet也就成了该模块的控制器。映射关系从"URL → 类"变成了"URL → 方法",结构清爽了不少。
但缺点也很明显:分发靠if-else(或switch)硬编码,新增方法就要改分发逻辑,字符串拼写错误还要到运行时才能发现。
后来我才意识到,这其实就是前端控制器(Front Controller)模式的雏形。当年的Struts 1也有类似的DispatchAction。而Spring MVC正是把这个思路做成了通用、可配置的框架,可以说和我们当时的想法不谋而合,于是我开始系统学习它。
三、Spring MVC带来了什么
1. 用注解完成URL到方法的映射
一个@RequestMapping(或@GetMapping、@PostMapping)就能把URL绑定到方法上,不再需要手写分发逻辑,也不需要为每个入口配置web.xml。
2. 参数自动绑定
Servlet时代,获取一个带默认值的分页参数要这样写:
String pageParam = request.getParameter("page");
int page = (pageParam == null) ? 1 : Integer.parseInt(pageParam);
Spring MVC中:
@RequestMapping("/findUserList")
public String findUserList(@RequestParam(defaultValue = "1") int page) {
// ......
}
参数的获取、空值处理、类型转换都由框架完成,业务代码只关心业务。
3. 数据传递与视图解析
Servlet时代,把数据传给JSP并跳转:
request.setAttribute("list", list);
request.getRequestDispatcher("/WEB-INF/views/index.jsp").forward(request, response);
Spring MVC中,控制器只需要往Model里放数据,再返回一个逻辑视图名:
@RequestMapping("/findUserList")
public String findUserList(@RequestParam(defaultValue = "1") int page, Model model) {
List<User> list = userService.findUsers(page);
model.addAttribute("list", list);
return "index"; // 逻辑视图名
}
逻辑视图名如何对应到真实的JSP文件,交给视图解析器:
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"
p:viewClass="org.springframework.web.servlet.view.JstlView"
p:prefix="/WEB-INF/views/"
p:suffix=".jsp" />
配置之后,返回"index"就会对应到/WEB-INF/views/index.jsp。控制器不再硬编码页面路径,更换视图技术(JSP、Thymeleaf、FreeMarker等)时,控制器代码也不用动。
四、背后的原理:DispatcherServlet
用得顺手之后,我开始好奇这些"魔法"是怎么实现的。答案是:Spring MVC本质上是对Servlet的封装,核心是一个DispatcherServlet,它就是前面说的前端控制器。所有请求先到它这里,再由它协调各个组件完成处理:
DispatcherServlet接收请求;- 通过
HandlerMapping找到处理该URL的控制器方法; - 通过
HandlerAdapter调用该方法,并完成参数绑定与类型转换; - 方法返回
ModelAndView(或视图名); - 由
ViewResolver把视图名解析为具体的视图; - 视图结合Model中的数据渲染,响应返回给客户端。
我们当年手写的if-else,对应的正是第2、3步。Spring MVC把它拆成了可替换、可扩展的组件,还在这条链路上提供了拦截器、统一异常处理、参数校验等扩展点。
五、反思:服务端渲染的局限
Spring MVC搭配JSP这类技术时,页面由服务端渲染,这带来了一些问题:
- 前端人员做出的静态HTML,需要再整合成JSP等服务端视图;
- 前端开发需要依赖Java Web环境,前后端难以独立开发、独立部署;
- 视图与后端控制器耦合较紧。
对于需求相对固定的传统企业项目,这种模式简单直接,完全够用。但对于迭代快、前端交互复杂的互联网项目,前后端分离往往是更好的选择。
需要说明的是,这个局限来自"服务端渲染视图"这种用法,而不是Spring MVC本身。Spring MVC同样可以只作为接口层,通过@RestController返回JSON,由Vue、React等前端框架负责页面:
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping
public List<User> list(@RequestParam(defaultValue = "1") int page) {
return userService.findUsers(page);
}
}
这时后端只暴露数据接口,前端独立开发、独立部署,两边通过接口约定协作,耦合度大大降低。
六、总结
回头看这段经历,我有三点体会:
- 框架源于问题。 Spring MVC不是凭空出现的,它解决的正是我们在Servlet时代亲身遇到的问题:入口分散、配置繁琐、样板代码多。带着问题学框架,理解会深得多。
- 别只停留在"会用"。 弄清
DispatcherServlet的处理流程,才能在出问题时定位原因,也才能真正用好拦截器、异常处理等扩展能力。 - 技术选型看场景。 服务端渲染适合需求稳定的传统项目,前后端分离适合变化快的互联网项目,而Spring MVC两种模式都能支持。

浙公网安备 33010602011771号