04-处理器方法四种返回类型

核心前提:DispatcherServlet 的作用

在深入之前,必须牢记 DispatcherServlet 是 SpringMVC 的前端控制器,是所有请求的统一入口和调度中心。它的核心职责是:

  1. 接收请求。
  2. 根据请求信息找到能处理该请求的处理器方法 (HandlerMethod)
  3. 调用该处理器方法,并传入所需的参数(如 HttpServletRequest, HttpServletResponse, 命令对象等)。
  4. 接收处理器方法的返回值,并根据返回值的类型注解,采取不同的后续处理策略
  5. 最终生成响应。

我们今天要剖析的,正是第 4 步——DispatcherServlet 如何处理不同的返回值。


1. 返回类型:String

A. 关联了 @ResponseBody

  • 原理解析

    1. 当处理器方法上标注了 @ResponseBody 注解,DispatcherServlet 在调用该方法并得到字符串返回值后,并不会将这个字符串视为一个视图路径。
    2. 它会启动一个消息转换器 (HttpMessageConverter) 机制。DispatcherServlet 会检查请求的 Accept 头(客户端希望接收的数据类型)和服务器端所能提供的数据类型,最终选择一个合适的 HttpMessageConverter(最常用的是 MappingJackson2HttpMessageConverter)。
    3. 这个转换器会负责将返回的 Java 对象(这里是一个 String 对象)转换成一个特定格式的字节流(application/json, text/plain, text/html 等)。
    4. DispatcherServlet 然后会调用 HttpServletResponse,获取其中的 OutputStreamWriter,并将转换后的字节流直接写入到响应体 (Response Body) 中。
    5. 整个过程结束后,请求处理就此终结,不会再有视图渲染的阶段。
  • 与 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

  • 原理解析

    1. 这种情况下,DispatcherServlet 将处理器方法返回的 String 解释为一个逻辑视图路径
    2. DispatcherServlet 会将它交给 ViewResolver(视图解析器) 进行处理。常用的 InternalResourceViewResolver 会为这个路径加上前缀(如 /WEB-INF/views/)和后缀(如 .jsp),拼接成一个完整的服务器端资源路径(例如:/WEB-INF/views/success.jsp)。
    3. 然后,DispatcherServlet 会使用请求转发 (Request Forward) 的方式,将这个请求派发 (Dispatch) 给上述完整的路径。请注意:这个“简化版 URI” (success) 是交给服务器端的 ViewResolverServlet 容器使用的,不是浏览器重定向
    4. 最终由目标资源(如 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

  • 原理解析

    1. 当返回类型是 void 时,相当于开发者告诉 DispatcherServlet:“这个请求的响应由我全权负责,你不用管了”。
    2. DispatcherServlet 在调用完处理器方法后,不会做任何后续处理。它不会尝试寻找视图,也不会尝试写入响应体。
    3. 因此,全部的责任落在了开发者身上。你必须在方法的参数中声明 HttpServletResponse 对象,然后像编写原生 Servlet 一样,自己设置响应头、 content-type,自己获取 PrintWriterOutputStream 并向其中写入数据。
    4. 如果你在方法中什么都没有做,那么客户端(浏览器)将收到一个空的响应
  • 与 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

  • 原理解析

    1. ModelAndView 是一个复合容器,它同时承载了数据 (Model)视图 (View) 信息。这是 SpringMVC 早期比较经典的一种返回方式。
    2. 上层空间 (Model):本质是一个 Map<String, Object>。你可以通过 addObject(“key”, value) 向其中添加数据。
    3. 下层空间 (View):可以设置一个 View 对象,或者更常见的是设置一个视图名称(String),后续由 ViewResolver 解析。
    4. 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(视图名),而通过方法参数传入一个 Model Map 来存放数据,这样更简洁。但 ModelAndView 将数据和视图打包在一个返回值里,在某些场景下仍然有用。


4. 返回类型:Object (POJO, List, Map 等)

  • 原理解析

    1. 这是实现异步请求(AJAX)RESTful API 的核心方式。
    2. 返回一个普通的 Java 对象(POJO)或集合,几乎总是需要与 @ResponseBody 注解配合使用(或者类上有 @RestController 注解)。
    3. DispatcherServlet 看到方法有 @ResponseBody 注解时,无论返回类型是 String 还是 Object,它都会启动消息转换器 (HttpMessageConverter) 机制。
    4. 这个过程与返回 String 类型且带有 @ResponseBody 的原理完全一致,但转换器的工作更重要:
      • DispatcherServlet 会遍历所有配置的 HttpMessageConverter,问:“你能处理这个返回值类型(比如 User.class)并且客户端能接受 application/json 吗?”
      • 找到了合适的转换器(如 MappingJackson2HttpMessageConverter)后,转换器会通过 Jackson 库等工具,将 Java 对象序列化 (Serialize) 为 JSON 字符串
      • 然后,DispatcherServlet 会设置响应的 Content-Typeapplication/json;charset=UTF-8,并将 JSON 字符串直接写入响应体
    5. 重要:这意味着整个处理过程是同步请求/响应模型下的“异步”处理。服务器没有返回一个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
posted on 2025-09-13 21:43  笨忠  阅读(13)  评论(0)    收藏  举报