对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,它就是前面说的前端控制器。所有请求先到它这里,再由它协调各个组件完成处理:

  1. DispatcherServlet接收请求;
  2. 通过HandlerMapping找到处理该URL的控制器方法;
  3. 通过HandlerAdapter调用该方法,并完成参数绑定与类型转换;
  4. 方法返回ModelAndView(或视图名);
  5. 由ViewResolver把视图名解析为具体的视图;
  6. 视图结合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);
    }
}

这时后端只暴露数据接口,前端独立开发、独立部署,两边通过接口约定协作,耦合度大大降低。

六、总结

回头看这段经历,我有三点体会:

  1. 框架源于问题。 Spring MVC不是凭空出现的,它解决的正是我们在Servlet时代亲身遇到的问题:入口分散、配置繁琐、样板代码多。带着问题学框架,理解会深得多。
  2. 别只停留在"会用"。 弄清DispatcherServlet的处理流程,才能在出问题时定位原因,也才能真正用好拦截器、异常处理等扩展能力。
  3. 技术选型看场景。 服务端渲染适合需求稳定的传统项目,前后端分离适合变化快的互联网项目,而Spring MVC两种模式都能支持。
posted @ 2016-01-13 02:19  xillkey  阅读(200)  评论(0)    收藏  举报