对于许多Java Web开发者来说,Servlet和Spring MVC是两个既熟悉又可能有些模糊的概念。我们知道后者比前者更“高级”,但它们之间确切的关系是什么?Spring MVC是如何在我们看不见的地方与Servlet共舞的?当我们从编写一个原生Servlet转向编写一个Spring MVC控制器时,究竟发生了什么变化,我们又获得了怎样的便利?
今天,让我们通过一个最简单的“根据ID查询用户”的实例,亲手编写两种风格的代码,一步步揭开Servlet的神秘面纱,并深入理解Spring MVC如何站在这个巨人的肩膀上,为我们构建了一个强大而优雅的Web开发世界。
想象一下在2005年,没有Spring Boot的自动化配置,我们需要手动构建一切。我们的目标是创建一个Web应用,当访问 http://localhost:8080/user-servlet-app/user?id=1
时,能显示ID为1的用户信息。
这部分代码与框架无关,是纯粹的业务逻辑。
User.java
(数据模型)
package com.example.servletapp.model;
public class User {
private long id;
private String name;
private String email;
// Getters and setters...
}
UserService.java
(业务服务)
package com.example.servletapp.service;
import com.example.servletapp.model.User;
public class UserService {
// 模拟数据库查询
public User getUserById(long id) {
if (id == 1) {
return new User(1, "Servlet Alice", "[email protected]");
}
return null;
}
}
UserServlet.java
这是我们的核心。一个Servlet就是一个实现了特定接口的Java类,由Web容器(如Tomcat)管理。它需要直接处理原始的HTTP请求和响应对象。
package com.example.servletapp.web;
import com.example.servletapp.model.User;
import com.example.servletapp.service.UserService;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class UserServlet extends HttpServlet {
private final UserService userService = new UserService();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 1. 手动解析请求参数
String userIdStr = req.getParameter("id");
long userId = Long.parseLong(userIdStr);
// 2. 调用业务逻辑
User user = userService.getUserById(userId);
// 3. 将数据存入请求作用域,以便JSP页面可以访问
req.setAttribute("user", user);
// 4. 手动将请求转发到JSP页面进行渲染
req.getRequestDispatcher("/WEB-INF/jsp/user.jsp").forward(req, resp);
}
}
user.jsp
JSP(JavaServer Pages)是一种允许在HTML中嵌入Java代码的技术。我们将它放在WEB-INF
目录下,防止用户直接访问。
/WEB-INF/jsp/user.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="com.example.servletapp.model.User" %>
User Profile
User Details (Servlet)
<%-- 使用JSP脚本和表达式语言(EL)从request中获取数据 --%>
<% User user = (User) request.getAttribute("user"); %>
<% if (user != null) { %>
ID: <%= user.getId() %>
Name: <%= user.getName() %>
Email: <%= user.getEmail() %>
<% } else { %>
User not found.
<% } %>
web.xml
部署描述符最后,我们需要告诉Web容器,哪个URL应该由哪个Servlet来处理。这通过web.xml
文件完成。
/WEB-INF/web.xml
<web-app>
<display-name>Servlet User Appdisplay-name>
<servlet>
<servlet-name>UserServletservlet-name>
<servlet-class>com.example.servletapp.web.UserServletservlet-class>
servlet>
<servlet-mapping>
<servlet-name>UserServletservlet-name>
<url-pattern>/userurl-pattern>
servlet-mapping>
web-app>
至此,一个纯Servlet应用就完成了。它能工作,但显而易见,每增加一个功能,我们就需要重复编写大量模板代码,并在XML中进行配置。
现在,我们用Spring MVC来完成完全相同的功能。你会发现,Spring MVC的强大之处在于它极具灵活性,既能支持传统的“返回视图”模式,也能完美胜任现代的“返回数据”模式。
这种模式下,服务器负责生成完整的HTML页面,直接返回给浏览器。
UserController.java
(视图控制器)
package com.example.mvcapp.controller;
import com.example.mvcapp.model.User;
import com.example.mvcapp.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller; // 注意是 @Controller
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@Controller
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/users/{id}")
public String getUserPage(@PathVariable long id, Model model) {
User user = userService.getUserById(id);
model.addAttribute("user", user);
return "user-profile"; // 返回视图模板的逻辑名称
}
}
/resources/templates/user-profile.html
(Thymeleaf模板)
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
html>
这种方式和我们之前介绍的Spring MVC示例完全一样,重点在于使用@Controller
注解,并返回一个String
作为视图名。
这是现代Web应用(如Vue/React驱动的单页面应用)的主流模式。服务器只作为数据提供方,返回纯粹的JSON数据。
UserApiController.java
(数据接口控制器)
package com.example.mvcapp.controller;
import com.example.mvcapp.model.User;
import com.example.mvcapp.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController; // 注意是 @RestController
@RestController // 这个注解是 @Controller 和 @ResponseBody 的组合
public class UserApiController {
@Autowired
private UserService userService;
// API通常有/api前缀以区分
@GetMapping("/api/users/{id}")
public User getUserData(@PathVariable long id) {
// 直接返回Java对象
// Spring MVC会自动通过Jackson库将其序列化为JSON字符串
return userService.getUserById(id);
}
}
当浏览器访问 http://localhost:8080/api/users/1
时,它不会得到一个HTML页面,而是会收到如下的JSON响应:
{"id":1,"name":"Alice","email":"[email protected]"}
前端的JavaScript代码拿到这个JSON后,可以动态地在页面上构建出任何它想要的UI。
特性 | 返回视图 (前后端不分离) | 返回数据 (前后端分离) |
---|---|---|
核心注解 | @Controller |
@RestController |
方法返回值 | String (视图名), ModelAndView |
Java对象 (POJO), ResponseEntity |
主要目的 | 在服务器端生成完整的HTML | 向客户端提供纯粹的结构化数据 |
数据传递 | model.addAttribute("key", value) |
return dataObject; |
“视图”层 | 服务器上的Thymeleaf/JSP模板 | 客户端的JavaScript代码 (React/Vue/Angular等) |
响应类型 | Content-Type: text/html |
Content-Type: application/json |
适用架构 | 传统的单体应用 | 前后端分离架构、微服务、对外API |
对比完两个例子,一个关键问题浮出水面:如果UserController
和UserApiController
都不是Servlet,那Tomcat这样的Web容器是如何调用到它们的呢?
答案是:Spring MVC在你的应用里只注册了唯一一个核心的Servlet,它就是DispatcherServlet
。
可以把DispatcherServlet
想象成一个高度智能的总调度员或前台接待。
请求的旅程如下:
GET /users/1
或/api/users/1
)到达Tomcat。DispatcherServlet
的管辖范围(在Spring Boot中,它默认接管所有请求/
)。HttpServletRequest
和HttpServletResponse
对象传递给DispatcherServlet
的service()
方法。这是Servlet API与Spring MVC框架交汇的唯一节点。DispatcherServlet
全权接管。它不会自己处理业务,而是扮演调度员的角色。HandlerMapping
),发现请求的URL对应哪个控制器的哪个方法。HttpServletRequest
,提取出路径变量1
,并准备好所需的一切参数(如Model
对象)。DispatcherServlet
的调用栈深处被执行的。"user-profile"
。DispatcherServlet
会把它交给“视图解析器”(ViewResolver
),最终渲染成HTML。User
对象。DispatcherServlet
会把它交给“消息转换器”(HttpMessageConverter
),最终序列化成JSON。DispatcherServlet
通过最初由Tomcat传入的HttpServletResponse
对象,将这个HTML或JSON响应发送回浏览器。所以,Servlet从未消失。 DispatcherServlet
作为一个极其强大的Servlet,为你处理了所有繁琐的、重复的底层工作,让你能专注于编写干净、优雅、只关心业务逻辑的@Controller
或@RestController
。
特性 | 原生Servlet | Spring MVC |
---|---|---|
入口点 | 多个Servlet类,每个对应一个或多个URL | 单一的DispatcherServlet 作为前端控制器 |
配置 | web.xml 中繁琐的声明和映射 |
基于注解的自动化配置,约定优于配置 |
耦合度 | 业务逻辑与Servlet API高度耦合 | POJO编程,业务代码与Web层解耦 |
数据处理 | 手动解析HttpServletRequest ,手动类型转换 |
自动数据绑定和类型转换 |
灵活性 | 模式单一,通常用于服务器端渲染 | 极高,无缝支持视图渲染和数据API两种模式 |
开发者焦点 | 底层HTTP请求/响应处理,流程控制 | 核心业务逻辑实现 |
Servlet是Java Web技术的基石,它强大而底层。而Spring MVC则是站在Servlet这个巨人肩膀上的智者。它没有重新发明轮子,而是将Servlet的能力封装、抽象,并融入了依赖注入、AOP等先进思想,最终为我们提供了一套高效、优雅且极具生产力的Web开发框架,能够灵活适应从传统单体应用到现代前后端分离架构的各种需求。
理解了这段从Servlet到Spring MVC的演进,你便能更深刻地体会到优秀框架设计的精髓:让开发者从重复的劳动中解放出来,专注于创造真正的业务价值。