欢迎光临
我们一直在努力

MiniSpring框架学习笔记-如何将处理结果返回给前端?|Java对象怎么变成浏览器数据?

ModelAndView:如何将处理结果返回给前端?

  • 11. ModelAndView:如何将处理结果返回给前端?
    • 一、先用一个请求看清总流程
    • 二、DispatcherServlet:先执行,再决定是否渲染视图
    • 三、HandlerAdapter:返回值在这里分流
      • `@ResponseBody` 做了什么?
    • 四、为什么要单独设计 HttpMessageConverter?
    • 五、为什么还要再拆一个 ObjectMapper?
    • 六、Java 对象是怎么变成浏览器数据的?
    • 七、用 `/test7` 再串一次
    • 八、职责拆分总结

教程:https://github.com/YaleGuo/minis 极客时间:手把手带你写一个 MiniSpring

前言:本章的实现内容其实很简单,无非就是将对象转换成 JSON 字符串,写入 HttpServletResponse,再经过 HTTP 传输到浏览器。真正值得关注的并不是这几行代码,而是源码背后的设计思路。重点再感受一下源码设计思路!职责单一、解耦彻底、扩展性强。解耦!解耦!解耦!

11. ModelAndView:如何将处理结果返回给前端?

上一节解决的是“请求参数怎么变成 Java 对象”,这一节反过来解决:

Controller 方法执行完以后,Java 返回值怎么变成浏览器能收到的 HTTP 响应?

当前 MiniSpring 支持两条返回路径:

Controller 返回值
|
v
RequestMappingHandlerAdapter
|
+– 方法有 @ResponseBody
| |
| v
| HttpMessageConverter
| |
| v
| ObjectMapper -> JSON 字符串 -> HTTP 响应体
|
+– 方法没有 @ResponseBody
|
v
ModelAndView -> ViewResolver -> View.render(…)

这一节主要讲现代前后端分离项目常用的第一条路径,也就是把 Java 对象转换成 JSON 后写入响应体。ModelAndView 和 JSP 渲染只保留必要说明,不展开讲。

一、先用一个请求看清总流程

项目里的测试接口如下:

@RequestMapping("/test7")
@ResponseBody
public UserForm doTest7(UserForm user) {
// 用于观察:参数绑定后的对象仍然可以在 Controller 中继续修改。
user.setName(user.getName() + "—");
user.setBirthday(new Date());
return user;
}

发送请求:

http://localhost:8080/test7?name=Tom&age=18&birthday=2026-06-01

完整链路是:

浏览器发送 HTTP 请求

Tomcat 创建 HttpServletRequest 和 HttpServletResponse

DispatcherServlet 根据 URL 找到 HandlerMethod

RequestMappingHandlerAdapter 绑定 UserForm 参数

反射调用 doTest7(user)

得到 UserForm 返回值

发现方法上有 @ResponseBody

HttpMessageConverter 设置响应类型和字符编码

ObjectMapper 把 UserForm 序列化成 JSON 字符串

PrintWriter 把字符写入 HttpServletResponse

Servlet 容器按 UTF-8 编码成字节并通过网络发送

浏览器或 Axios 收到 JSON

注意,doTest7 中重新设置了 birthday,所以请求里的 2026-06-01 会被服务器当天日期覆盖。例如服务器当天是 2026-06-09,当前极简序列化器可能返回:

{"name":"Tom—","age":"18","birthday":"2026-06-09"}

这里的 "age":"18" 也是当前教学版 DefaultObjectMapper 的限制:它把非空字段统一写成了 JSON 字符串。真实项目使用 Jackson 时,数字通常会保留为数字:

{"name":"Tom—","age":18,"birthday":"2026-06-09"}

二、DispatcherServlet:先执行,再决定是否渲染视图

请求入口仍然是 DispatcherServlet.doDispatch(…):

protected void doDispatch(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// 第一步:根据 URL 找到对应的 Controller 方法。
HandlerMethod handlerMethod = this.handlerMapping.getHandler(request);
if (handlerMethod == null) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}

// 第二步:绑定参数、调用 Controller,并处理方法返回值。
ModelAndView mv = this.handlerAdapter.handle(
request, response, handlerMethod);

// @ResponseBody 分支已经把结果写入 response,不需要再渲染页面。
if (mv == null) {
return;
}

// 没有 @ResponseBody 时,才进入页面渲染流程。
render(request, response, mv);
}

这里最容易误解的是:

if (mv == null) {
return;
}

mv == null 不一定表示 Controller 没有返回数据。对于 @ResponseBody 方法,数据已经由 HttpMessageConverter 写进 HttpServletResponse,所以这里只是告诉 DispatcherServlet:

响应已经处理完了,不要再走 ModelAndView 的视图渲染流程。

这也解释了为什么接口返回的是 JSON,却仍然能在代码中看到 ModelAndView:当前 HandlerAdapter 用 ModelAndView 作为统一返回类型,JSON 分支写完响应后返回 null,页面分支才真正返回 ModelAndView。

三、HandlerAdapter:返回值在这里分流

真正决定“返回 JSON 还是渲染页面”的代码在 RequestMappingHandlerAdapter:

@Override
public ModelAndView handle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
return handleInternal(request, response, (HandlerMethod) handler);
}

private ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response,
HandlerMethod handler) throws Exception {
Method method = handler.getMethod();
Object bean = handler.getBean();

// 先完成参数绑定,再反射调用 Controller。
Object result = invokeHandlerMethod(
request, response, handler, method, bean);

// 必须先判断 @ResponseBody。
// 即使返回值是 String,它也应该写入响应体,而不是被当成视图名。
if (method.isAnnotationPresent(ResponseBody.class)) {
this.messageConverter.write(result, response);
return null;
}

// 页面返回路径:Controller 可以直接返回 ModelAndView。
if (result instanceof ModelAndView) {
return (ModelAndView) result;
}

// 没有 @ResponseBody 的 String 返回值会被当成逻辑视图名。
if (result instanceof String) {
return new ModelAndView((String) result);
}

// 当前教学版本暂不支持其他返回值处理方式。
return null;
}

返回规则可以整理成一张表:

Controller 写法处理方式
@ResponseBody + UserForm 对象序列化为 JSON,直接写入响应体
@ResponseBody + String 字符串序列化后写入响应体
无 @ResponseBody + ModelAndView 进入视图渲染流程
无 @ResponseBody + String 把字符串当作逻辑视图名
无 @ResponseBody + 其他类型 当前版本无法处理,返回 null

@ResponseBody 做了什么?

它只是一个运行时标记:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResponseBody {
}

因为保留策略是 RUNTIME,所以框架执行 Controller 时可以通过反射判断:

method.isAnnotationPresent(ResponseBody.class)

当前 MiniSpring 没有实现 @RestController。因此,要让方法返回值进入响应体,必须明确添加 @ResponseBody。

四、为什么要单独设计 HttpMessageConverter?

如果直接在 HandlerAdapter 里拼 JSON、设置响应头、获取 Writer,代码当然也能运行,但 HandlerAdapter 会同时负责:

  • 参数绑定;
  • 反射调用 Controller;
  • 判断返回值类型;
  • JSON 序列化;
  • 设置 HTTP 响应;
  • 输出响应体。
  • 职责会越来越乱。所以这里抽出 HttpMessageConverter 接口:

    public interface HttpMessageConverter {

    /**
    * 把 Controller 返回值转换后写入 HTTP 响应。
    */

    void write(Object obj, HttpServletResponse response) throws IOException;
    }

    RequestMappingHandlerAdapter 只负责决定:

    这个返回值是不是响应体?

    至于响应体最终写成 JSON、XML 还是纯文本,由具体的 HttpMessageConverter 决定。

    默认实现如下:

    public class DefaultHttpMessageConverter
    implements HttpMessageConverter {

    // JSON 的标准媒体类型是 application/json。
    private String defaultContentType = "application/json";
    private String defaultCharacterEncoding = "UTF-8";
    private ObjectMapper objectMapper = new DefaultObjectMapper();

    public ObjectMapper getObjectMapper() {
    return objectMapper;
    }

    public void setObjectMapper(ObjectMapper objectMapper) {
    this.objectMapper = objectMapper;
    }

    @Override
    public void write(Object obj,
    HttpServletResponse response) throws IOException {
    // 必须在获取 Writer 之前确定响应类型和字符编码。
    response.setContentType(defaultContentType);
    response.setCharacterEncoding(defaultCharacterEncoding);

    writeInternal(obj, response);

    // 提交响应缓冲区,确保结果被发送出去。
    response.flushBuffer();
    }

    private void writeInternal(Object obj,
    HttpServletResponse response) throws IOException {
    // 第一步:Java 对象 -> JSON 字符串。
    String json = this.objectMapper.writeValuesAsString(obj);

    // 第二步:把字符写入响应;最终的字符到字节转换由 Servlet 容器完成。
    PrintWriter writer = response.getWriter();
    writer.write(json);
    }
    }

    项目未提交代码中的默认值如果还是 text/json;charset=UTF-8,建议改成标准写法 application/json,并通过 setCharacterEncoding("UTF-8") 单独设置编码。

    HttpMessageConverter 负责的是 HTTP 层:

    设置 Content-Type
    设置字符编码
    调用 ObjectMapper
    获取 response.getWriter()
    写入并提交响应

    它不应该关心 UserForm 有哪些字段,也不应该自己用反射遍历对象。

    五、为什么还要再拆一个 ObjectMapper?

    ObjectMapper 负责的是对象序列化:

    public interface ObjectMapper {

    void setDateFormat(String dateFormat);

    void setDecimalFormat(String decimalFormat);

    /**
    * 把 Java 对象转换成 JSON 字符串。
    */

    String writeValuesAsString(Object obj);
    }

    两个接口的职责并不一样:

    RequestMappingHandlerAdapter
    |
    v
    HttpMessageConverter
    负责 HTTP 响应格式、编码和输出
    |
    v
    ObjectMapper
    负责 Java 对象到 JSON 字符串的转换

    这样拆分以后:

    • 想把 JSON 换成 XML,可以更换 HttpMessageConverter;
    • 想把手写序列化换成 Jackson,可以更换 ObjectMapper;
    • HandlerAdapter 不需要知道具体序列化细节。

    当前项目中的 DefaultObjectMapper 是一个教学用的极简实现:

    public class DefaultObjectMapper implements ObjectMapper {

    private DateTimeFormatter datetimeFormatter =
    DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private DecimalFormat decimalFormatter =
    new DecimalFormat("#,##0.00");

    @Override
    public void setDateFormat(String dateFormat) {
    this.datetimeFormatter = DateTimeFormatter.ofPattern(dateFormat);
    }

    @Override
    public void setDecimalFormat(String decimalFormat) {
    this.decimalFormatter = new DecimalFormat(decimalFormat);
    }

    @Override
    public String writeValuesAsString(Object obj) {
    if (obj == null) {
    return "null";
    }
    if (obj instanceof String) {
    return "\\"" + escape((String) obj) + "\\"";
    }

    StringBuilder json = new StringBuilder("{");
    Field[] fields = obj.getClass().getDeclaredFields();
    boolean first = true;

    for (Field field : fields) {
    // static 字段属于类,不属于当前对象,不写入 JSON。
    if (Modifier.isStatic(field.getModifiers())) {
    continue;
    }

    field.setAccessible(true);
    Object value = getFieldValue(field, obj);

    if (!first) {
    json.append(",");
    }

    json.append("\\"").append(field.getName()).append("\\":");

    if (value == null) {
    json.append("null");
    } else {
    // 当前教学版本把所有非空字段都按字符串输出。
    json.append("\\"").append(escape(formatValue(value))).append("\\"");
    }
    first = false;
    }

    json.append("}");
    return json.toString();
    }

    private Object getFieldValue(Field field, Object obj) {
    try {
    return field.get(obj);
    } catch (IllegalAccessException e) {
    throw new IllegalStateException("Cannot read field: " + field.getName(), e);
    }
    }

    private String formatValue(Object value) {
    if (value == null) {
    return null;
    }

    if (value instanceof Date) {
    LocalDate localDate = ((Date) value).toInstant()
    .atZone(ZoneId.systemDefault())
    .toLocalDate();
    return localDate.format(this.datetimeFormatter);
    }

    if (value instanceof BigDecimal
    || value instanceof Double
    || value instanceof Float) {
    return this.decimalFormatter.format(value);
    }

    return value.toString();
    }

    private String escape(String text) {
    return text.replace("\\\\", "\\\\\\\\").replace("\\"", "\\\\\\"");
    }
    }

    这段代码的核心过程只有四步:

    拿到对象类型

    反射遍历字段

    读取并格式化字段值

    拼成 JSON 字符串

    不过要明确:这不是完整的 JSON 库。它目前不完善的地方包括:

    • 数字和布尔值也会被加上双引号;
    • 不支持集合、数组和嵌套对象;
    • 字符串转义规则不完整;
    • 没有处理循环引用;
    • 没有 getter、字段忽略等配置能力。

    因此它适合用来学习“对象如何被序列化”,不适合直接用于生产环境。真实 Spring Boot 项目通常由 Jackson 完成这部分工作;当前 MiniSpring 项目并没有引入 Jackson。

    六、Java 对象是怎么变成浏览器数据的?

    Controller 返回的:

    UserForm user

    是 JVM 进程里的对象。变量 user 保存的对象引用只在当前 JVM 内存中有意义,浏览器既不能访问服务器堆内存,也不认识 com.chenhai.test.UserForm,所以不能直接传 Java 对象。

    服务端必须先把对象序列化成双方约定的格式。前后端分离项目最常见的是 JSON:

    JVM 中的 UserForm 对象
    ↓ 序列化
    JSON 字符串
    ↓ 字符编码
    UTF-8 字节
    ↓ HTTP/TCP
    浏览器
    ↓ JSON 解析
    JavaScript 对象

    核心输出代码如下:

    PrintWriter writer = response.getWriter();
    writer.write(json);

    严格来说,PrintWriter 接收的是字符。Servlet 容器再根据 UTF-8 把字符编码成响应体字节,最后通过网络发送:

    JSON String

    PrintWriter 写入字符

    Servlet 容器根据 UTF-8 编码

    响应体字节

    Socket / TCP 发送

    HTTP 响应还要用 Content-Type 告诉客户端如何理解这些字节:

    HTTP/1.1 200 OK
    Content-Type: application/json;charset=UTF-8

    {"name":"Tom—","age":"18","birthday":"2026-06-09"}

    • application/json 表示响应体采用 JSON 格式;
    • charset=UTF-8 表示文本字符按 UTF-8 编码。
    Content-Type客户端通常如何处理
    application/json 按 JSON 数据处理
    text/html 按 HTML 页面处理
    image/png 按 PNG 图片处理
    application/pdf 按 PDF 文件处理

    所有响应最终都会传输字节,但图片等二进制数据不应该使用 PrintWriter,而应该使用 OutputStream 写入原始字节。

    还要注意,当前 MiniSpring 并没有根据请求头 Accept 做内容协商。它的规则很直接:

    方法有 @ResponseBody

    固定使用 DefaultHttpMessageConverter

    返回 JSON

    所以不是“后端自动知道前端想要 JSON”,而是服务端通过 @ResponseBody 和默认转换器明确选择了 JSON。

    浏览器开发者工具会把 JSON 格式化展示。Axios 收到 JSON 后,会把它解析成 JavaScript 值,概念上类似:

    const data = JSON.parse(
    '{"name":"Tom—","age":"18","birthday":"2026-06-09"}'
    );

    console.log(data.name); // Tom—

    所以 Vue 最终使用的是 JavaScript 对象,不是服务器里的 Java 对象:

    Java 对象
    ↓ DefaultObjectMapper
    JSON 文本
    ↓ HttpServletResponse
    HTTP 响应字节
    ↓ Axios 解析
    JavaScript 对象

    Vue 渲染

    七、用 /test7 再串一次

  • HandlerMapping 根据 /test7 找到 doTest7(UserForm user)。
  • WebDataBinder 把 name、age、birthday 绑定到 UserForm。
  • HandlerAdapter 反射调用 Controller,得到 UserForm 返回值。
  • 方法上有 @ResponseBody,所以结果交给 HttpMessageConverter。
  • ObjectMapper 反射读取字段并生成 JSON 字符串。
  • PrintWriter 把 JSON 写入响应,Servlet 容器再编码成字节发送。
  • HandlerAdapter 返回 null,DispatcherServlet 不再执行 render(…)。
  • 八、职责拆分总结

    这一节新增的几个部件,各自只负责一件事:

    部件职责
    DispatcherServlet 组织整个请求分发流程
    RequestMappingHandlerAdapter 绑定参数、调用 Controller、判断返回值走哪条路径
    @ResponseBody 标记方法返回值要写入响应体
    HttpMessageConverter 定义“把返回值写入 HTTP 响应”的能力
    DefaultHttpMessageConverter 设置 JSON 响应头并写入响应体
    ObjectMapper 定义“对象序列化成字符串”的能力
    DefaultObjectMapper 用反射实现教学版 JSON 序列化
    ModelAndView 保存页面渲染所需的模型和视图信息

    最核心的调用链是:

    Controller 返回 Java 对象

    RequestMappingHandlerAdapter 判断 @ResponseBody

    HttpMessageConverter 处理 HTTP 响应

    ObjectMapper 把对象序列化成 JSON

    PrintWriter 写入字符

    Servlet 容器编码为字节并发送

    浏览器或 Axios 解析 JSON

    这一节真正要掌握的不是手写 JSON,而是返回值处理的职责拆分:

    HandlerAdapter 决定怎么处理返回值,HttpMessageConverter 负责怎么写 HTTP 响应,ObjectMapper 负责怎么把对象变成 JSON。

    赞(0)
    未经允许不得转载:171主机测评 » MiniSpring框架学习笔记-如何将处理结果返回给前端?|Java对象怎么变成浏览器数据?
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址