核心前提:DispatcherServlet 的作用
在深入之前,必须牢记 DispatcherServlet 是 SpringMVC 的前端控制器,是所有请求的统一入口和调度中心。它的核心职责是:
- 接收请求。
- 根据请求信息找到能处理该请求的处理器方法 (HandlerMethod)。
- 调用该处理器方法,并传入所需的参数(如
HttpServletRequest,HttpServletResponse, 命令对象等)。 - 接收处理器方法的返回值,并根据返回值的类型和注解,采取不同的后续处理策略。
- 最终生成响应。
我们今天要剖析的,正是第 4 步——DispatcherServlet 如何处理不同的返回值。
1. 返回类型:String
A. 关联了 @ResponseBody
-
原理解析:
- 当处理器方法上标注了
@ResponseBody注解,DispatcherServlet在调用该方法并得到字符串返回值后,并不会将这个字符串视为一个视图路径。 - 它会启动一个消息转换器 (HttpMessageConverter) 机制。
DispatcherServlet会检查请求的Accept头(客户端希望接收的数据类型)和服务器端所能提供的数据类型,最终选择一个合适的HttpMessageConverter(最常用的是MappingJackson2HttpMessageConverter)。 - 这个转换器会负责将返回的 Java 对象(这里是一个
String对象)转换成一个特定格式的字节流(application/json,text/plain,text/html等)。 DispatcherServlet然后会调用HttpServletResponse,获取其中的OutputStream或Writer,并将转换后的字节流直接写入到响应体 (Response Body) 中。- 整个过程结束后,请求处理就此终结,不会再有视图渲染的阶段。
- 当处理器方法上标注了
-
与 Servlet 对比:
这完全等价于你在原生的 Servlet 中做的事情:// 原生 Servlet 做法 protected void doGet(HttpServletRequest request, HttpServletResponse response) { response.setContentType("text/plain;charset=UTF-8"); PrintWriter out = response.getWriter(); out.print("这是直接写入响应体的结果"); out.flush(); out.close(); // SpringMVC 帮你做了这一切 } // SpringMVC 做法 @ResponseBody @RequestMapping("/example") public String example() { return "这是直接写入响应体的结果"; }SpringMVC 的优势:它通过
@ResponseBody和消息转换器机制,帮你自动化了设置内容类型、获取流、写入数据、关闭流等一系列模板化代码。你只需要关心返回什么数据,而不用关心如何输出。
B. 没有关联 @ResponseBody
-
原理解析:
- 这种情况下,
DispatcherServlet将处理器方法返回的String解释为一个逻辑视图路径。 DispatcherServlet会将它交给ViewResolver(视图解析器) 进行处理。常用的InternalResourceViewResolver会为这个路径加上前缀(如/WEB-INF/views/)和后缀(如.jsp),拼接成一个完整的服务器端资源路径(例如:/WEB-INF/views/success.jsp)。- 然后,
DispatcherServlet会使用请求转发 (Request Forward) 的方式,将这个请求派发 (Dispatch) 给上述完整的路径。请注意:这个“简化版 URI” (success) 是交给服务器端的ViewResolver和Servlet容器使用的,不是浏览器重定向。 - 最终由目标资源(如 JSP)接手请求,完成后续的渲染工作。
- 这种情况下,
-
与 Servlet 对比:
这等价于在 Servlet 中:// 原生 Servlet 做法 protected void doGet(HttpServletRequest request, HttpServletResponse response) { RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/views/success.jsp"); dispatcher.forward(request, response); // SpringMVC 帮你做了这一切 } // SpringMVC 做法 @RequestMapping("/example") public String example() { return "success"; // 视图解析器会将其解析为 /WEB-INF/views/success.jsp }SpringMVC 的优势:你不再需要编写
RequestDispatcher和调用forward()方法。你返回的只是一个简短的逻辑视图名,与具体视图技术的实现路径解耦,更换视图技术(比如从 JSP 换成 Thymeleaf)通常只需要修改ViewResolver的配置,而不用修改控制器代码。
2. 返回类型:void
-
原理解析:
- 当返回类型是
void时,相当于开发者告诉DispatcherServlet:“这个请求的响应由我全权负责,你不用管了”。 DispatcherServlet在调用完处理器方法后,不会做任何后续处理。它不会尝试寻找视图,也不会尝试写入响应体。- 因此,全部的责任落在了开发者身上。你必须在方法的参数中声明
HttpServletResponse对象,然后像编写原生 Servlet 一样,自己设置响应头、 content-type,自己获取PrintWriter或OutputStream并向其中写入数据。 - 如果你在方法中什么都没有做,那么客户端(浏览器)将收到一个空的响应。
- 当返回类型是
-
与 Servlet 对比:
这几乎就是在写一个原生的 Servlet 方法:// 原生 Servlet 做法 / SpringMVC void 做法 @RequestMapping("/download") public void downloadFile(HttpServletResponse response) { // 1. 设置响应头,告诉浏览器这是一个附件 response.setContentType("application/octet-stream"); response.setHeader("Content-Disposition", "attachment;filename=test.txt"); // 2. 获取输出流并写入数据 try (OutputStream os = response.getOutputStream(); FileInputStream fis = new FileInputStream(new File("/path/to/file"))) { IOUtils.copy(fis, os); // 例如使用 Apache Commons IO os.flush(); } catch (IOException e) { e.printStackTrace(); } // 3. DispatcherServlet 收到 void 返回值,无事可做,处理结束。 }使用场景:通常用于非常底层的操作,如文件下载、 streaming 数据等需要完全手动控制响应输出的场景。在大多数常规 Web 交互中,不推荐使用。
3. 返回类型:ModelAndView
-
原理解析:
ModelAndView是一个复合容器,它同时承载了数据 (Model) 和视图 (View) 信息。这是 SpringMVC 早期比较经典的一种返回方式。- 上层空间 (Model):本质是一个
Map<String, Object>。你可以通过addObject(“key”, value)向其中添加数据。 - 下层空间 (View):可以设置一个
View对象,或者更常见的是设置一个视图名称(String),后续由ViewResolver解析。 DispatcherServlet接收到ModelAndView后,会执行两个核心操作:- 模型处理:它将
Model中的所有数据,逐个取出并以key-value的形式复制到HttpServletRequest的属性集中(即放入请求作用域request.setAttribute(“key”, value))。这样,转发到的 JSP 等视图技术就可以直接通过 EL 表达式${key}来访问这些数据。 - 视图处理:它将
View信息(无论是View对象还是逻辑视图名)交给视图解析器链进行处理,最终解析到一个具体的View对象(如InternalResourceView)。然后,由这个View对象的render()方法来完成渲染,对于 JSP 来说,本质上就是request.getRequestDispatcher(path).forward(request, response)。
- 模型处理:它将
-
与 Servlet 对比:
// 原生 Servlet 做法 protected void doGet(HttpServletRequest request, HttpServletResponse response) { // 1. 准备数据 (Model) String message = “Hello World!”; // 2. 将数据存入请求域 request.setAttribute(“msg”, message); // 3. 转发到视图 RequestDispatcher dispatcher = request.getRequestDispatcher(“/WEB-INF/views/hello.jsp”); dispatcher.forward(request, response); } // SpringMVC ModelAndView 做法 @RequestMapping("/hello") public ModelAndView hello() { ModelAndView mav = new ModelAndView(); // 1. 准备数据并添加到 Model mav.addObject(“msg”, “Hello World!”); // 2. 设置视图名 mav.setViewName(“hello”); // -> ViewResolver -> /WEB-INF/views/hello.jsp return mav; // 3. DispatcherServlet 帮你完成存入请求域和转发的操作 }演进:现在更流行的方式是方法返回
String(视图名),而通过方法参数传入一个ModelMap来存放数据,这样更简洁。但ModelAndView将数据和视图打包在一个返回值里,在某些场景下仍然有用。
4. 返回类型:Object (POJO, List, Map 等)
-
原理解析:
- 这是实现异步请求(AJAX) 和 RESTful API 的核心方式。
- 返回一个普通的 Java 对象(POJO)或集合,几乎总是需要与
@ResponseBody注解配合使用(或者类上有@RestController注解)。 - 当
DispatcherServlet看到方法有@ResponseBody注解时,无论返回类型是String还是Object,它都会启动消息转换器 (HttpMessageConverter) 机制。 - 这个过程与返回
String类型且带有@ResponseBody的原理完全一致,但转换器的工作更重要:DispatcherServlet会遍历所有配置的HttpMessageConverter,问:“你能处理这个返回值类型(比如User.class)并且客户端能接受application/json吗?”- 找到了合适的转换器(如
MappingJackson2HttpMessageConverter)后,转换器会通过 Jackson 库等工具,将 Java 对象序列化 (Serialize) 为 JSON 字符串。 - 然后,
DispatcherServlet会设置响应的Content-Type为application/json;charset=UTF-8,并将 JSON 字符串直接写入响应体。
- 重要:这意味着整个处理过程是同步请求/响应模型下的“异步”处理。服务器没有返回一个HTML页面,而是返回了纯数据(JSON/XML)。浏览器端的 JavaScript 接收到这个数据后,可以用 DOM 操作来局部更新页面,而不是刷新整个页面。所以这里说的“异步请求”指的是 AJAX 这种交互模式。
-
与 Servlet 对比:
// 原生 Servlet 做法 (需要Jackson库) protected void doGet(HttpServletRequest request, HttpServletResponse response) { User user = userService.findById(1); // 获取一个POJO对象 ObjectMapper mapper = new ObjectMapper(); // Jackson 的核心类 response.setContentType(“application/json;charset=UTF-8”); try { // 手动将对象序列化为JSON并写入响应 String json = mapper.writeValueAsString(user); PrintWriter out = response.getWriter(); out.print(json); out.flush(); } catch (Exception e) { e.printStackTrace(); } } // SpringMVC 做法 @ResponseBody @RequestMapping(“/user/{id}”) public User getUser(@PathVariable(“id”) Integer id) { User user = userService.findById(id); return user; // DispatcherServlet 和 Jackson 转换器帮你完成所有序列化和写入操作 }SpringMVC 的巨大优势:这是 SpringMVC 最强大的特性之一。它将对象到 JSON 的转换这一复杂且模板化的过程完全自动化了。开发者彻底从 JSON 处理中解放出来,可以专注于业务逻辑,只需返回业务对象即可。这使得开发 RESTful 服务变得极其简单和高效。
总结对比表
| 返回类型 | 有无 @ResponseBody |
DispatcherServlet 的行为 |
等效的 Servlet 操作 | 主要应用场景 |
|---|---|---|---|---|
| String | 有 | 调用转换器,将字符串直接写入响应体 | response.getWriter().print(“str”) |
返回简单字符串、HTML片段 |
| String | 无 | 将字符串作为视图名,请求转发到对应视图 | request.getRequestDispatcher(...).forward() |
传统的同步页面跳转 |
| void | (无关) | 什么都不做 | 在方法内手动使用 response 对象完成所有响应操作 |
文件下载、完全手动控制的输出 |
| ModelAndView | (无关) | 将 Model 数据存入请求域,然后请求转发到指定视图 | request.setAttribute() + forward() |
传统的同步页面跳转(经典方式) |
| Object | 必须有 | 调用转换器(如 Jackson),将对象序列化为 JSON 等格式并写入响应体 | 手动使用 Jackson 库序列化并输出 | AJAX / RESTful API |
浙公网安备 33010602011771号