【JavaEE10-后端部分】SpringMVC05-综合案例1-从加法计算器看前后端交互:接口文档与HTTP通信详解

前言

在Web开发中,前后端分离已经成为主流。前端负责页面展示和用户交互,后端负责业务逻辑和数据存储。为了让前后端能够顺畅地"对话",我们需要一份接口文档,它就像一份合同,规定了双方如何交换数据。本文将通过一个最简单的加法计算器案例,带你从头到尾理解接口文档的定义、前后端的实现,以及HTTP通信背后的原理。


1. 案例背景与需求

1.1 需求描述

我们需要实现一个加法计算器页面,用户输入两个整数,点击提交后,后端计算它们的和,并将结果以醒目的HTML格式返回显示在页面上。

1.2 技术栈

  • 前端:HTML + 原生表单提交(不使用AJAX)
  • 后端:Spring Boot + SpringMVC

1.3 为什么用原生表单?

因为这是最简单的示例,可以让我们聚焦于HTTP通信的本质,避免AJAX带来的额外概念。

1.4 效果演示

正确演示



错误演示


2. 接口文档的定义

在动手写代码之前,前后端开发人员需要坐在一起(或通过文档)明确接口的细节。接口文档通常包含以下内容:

  • 请求路径:URL
  • 请求方法:GET/POST/PUT/DELETE等
  • 接口描述:说明接口的作用
  • 请求参数:每个参数的名称、类型、是否必须、说明
  • 请求示例:一个具体的请求样子
  • 响应说明:响应内容格式、状态码、示例等

对于加法计算器,我们可以定义如下接口:

加法计算器接口文档

项目 内容
请求路径 /calc/sum
请求方法 POST(也可以用GET,但这里选POST)
接口描述 计算两个整数的和,返回HTML结果
请求参数
参数名 类型 是否必须 说明
num1 Integer 参与计算的第一个数
num2 Integer 参与计算的第二个数

请求示例(表单格式):

复制代码
num1=5&num2=3
响应数据
  • Content-Type : text/html;charset=UTF-8
  • 响应体 : HTML片段,例如 <h1 style='color: #67c23a;'>计算出的结果是:8</h1>

响应示例

html 复制代码
<h1 style='color: #67c23a;'>计算出的结果是:8</h1>

2.1 为什么这样定义?

  • 请求方法:虽然GET也能传参,但POST更适合提交数据,且参数不会暴露在URL中(不过本例中其实没有敏感数据,用GET也行,但为了示范,我们用POST)。
  • 参数格式 :采用 application/x-www-form-urlencoded,这是HTML表单默认的格式,简单易用。
  • 响应格式:返回HTML,因为前端就是普通的页面提交,直接显示结果即可。

3. 前端开发:基于接口文档实现页面

3.1 HTML页面

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>加法计算器</title>
    <style>
        /* 页面整体样式:居中、浅背景、字体优化 */
        body {
            font-family: "Microsoft YaHei", Arial, sans-serif; /* 友好的中文字体 */
            background-color: #f5f5f5; /* 浅灰色背景,避免纯白刺眼 */
            display: flex;
            justify-content: center; /* 水平居中 */
            align-items: center; /* 垂直居中 */
            min-height: 100vh; /* 占满屏幕高度 */
            margin: 0;
        }

        /* 表单容器样式:白色背景、圆角、阴影、内边距 */
        form {
            background-color: white;
            padding: 30px 40px;
            border-radius: 8px; /* 圆角 */
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); /* 轻微阴影,提升层次感 */
            width: 300px; /* 固定宽度,适配移动端 */
        }

        /* 标题样式:居中、颜色、间距 */
        h1 {
            text-align: center;
            color: #333;
            margin-top: 0;
            margin-bottom: 20px;
            font-size: 24px;
        }

        /* 输入框样式:美化、统一尺寸 */
        input[type="text"] {
            width: 100%; /* 占满容器宽度 */
            padding: 10px;
            margin: 8px 0 15px 0; /* 上下间距 */
            border: 1px solid #ddd; /* 浅灰色边框 */
            border-radius: 4px; /* 小圆角 */
            box-sizing: border-box; /* 内边距不影响总宽度 */
            font-size: 16px;
        }

        /* 输入框聚焦样式:高亮边框 */
        input[type="text"]:focus {
            outline: none; /* 去掉默认外边框 */
            border-color: #409eff; /* 蓝色高亮,常用的交互色 */
            box-shadow: 0 0 5px rgba(64, 158, 255, 0.2); /* 轻微发光效果 */
        }

        /* 提交按钮样式:美化、hover效果 */
        input[type="submit"] {
            width: 100%; /* 占满宽度 */
            padding: 12px;
            background-color: #409eff; /* 主色调:蓝色 */
            color: white; /* 文字白色 */
            border: none; /* 去掉默认边框 */
            border-radius: 4px;
            font-size: 16px;
            cursor: pointer; /* 鼠标悬浮变手型 */
            margin-top: 10px;
        }

        /* 按钮悬浮效果:加深颜色,提升交互感 */
        input[type="submit"]:hover {
            background-color: #337ecc;
        }
    </style>
</head>
<body>
     <form action="calc/sum" method="post">
        <h1>计算器</h1>
        数字1:<input name="num1" type="text"><br>
        数字2:<input name="num2" type="text"><br>
        <input type="submit" value=" 点击相加 ">
    </form>
</body>

</html>

3.2 关键点解释

  • <form action="/calc/sum" method="post">:指定提交地址和方式,完全符合接口文档。
  • inputname 属性必须与接口文档中的参数名一致(num1num2)。
  • 没有写任何JavaScript,浏览器会自动处理表单提交。

3.3 浏览器做了什么?

当用户点击提交按钮时,浏览器会自动:

  1. 收集表单中所有带有 name 属性的输入框的值。
  2. 按照 application/x-www-form-urlencoded 格式对数据进行编码(比如中文会被URL编码,数字不变)。
  3. 设置请求头 Content-Type: application/x-www-form-urlencoded
  4. 将编码后的字符串放入请求体,发送POST请求到 /calc/sum

实际发送的HTTP请求报文

复制代码
POST /calc/sum HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
Content-Length: 11

num1=5&num2=3

4. 后端开发:根据接口文档实现接口

4.1 创建Controller

java 复制代码
package com.zhongge.demo;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * @ClassName CalculatorController
 * @Description TODO 实现计算器的控制类
 * @Author 笨忠
 * @Date 2026-02-22 13:22
 * @Version 1.0
 */
@RequestMapping("/calc")
@Controller
public class CalculatorController {

    /**
     * 实现加法计算器
     * @param num1 参数1
     * @param num2 参数2
     * @return 和的结果
     *
     * 他接收的是普通参数的形式
     */
    @RequestMapping("/sum")
    @ResponseBody
    public String sum(Integer num1, Integer num2) {
        //首先判断参数的合法性
        if (num1 == null || num2 == null) {
            return "<h1 style='color: #f56c6c;'>参数不合法,请重新输入</h1>";
        }

        int num = num1 + num2;
        //程序跑到这里,说明参数是合法的
        return "<h1 style='color: #67c23a;'>计算出的结果是:" + num + "</h1>";
    }
}

4.2 关键点解释

  • @Controller:表明这是一个控制器,可以处理HTTP请求。
  • @RequestMapping("/calc"):类级别的路径,所有方法都继承这个前缀。
  • @RequestMapping("/sum"):方法级别的路径,完整路径为 /calc/sum
  • 方法参数 Integer num1, Integer num2 :这里没有加任何注解。SpringMVC会默认从请求参数 中获取同名参数的值,并进行类型转换(字符串转Integer)。为什么能拿到?因为浏览器发送的是表单格式的POST请求,请求体中的 num1=5&num2=3 会被Servlet容器解析为请求参数,SpringMVC就能自动绑定。
  • @ResponseBody:告诉Spring将返回值直接写入响应体,而不是解析为视图(JSP等)。因为我们要返回HTML片段,所以直接返回字符串。
  • 返回值是HTML字符串,Spring会使用 StringHttpMessageConverter 将其写入响应,并设置默认的 Content-Type: text/html;charset=UTF-8(实际上由于返回的是HTML标签,Spring会协商为 text/html)。

4.3 后端处理流程

  1. 请求到达Tomcat,解析为 HttpServletRequest
  2. SpringMVC的 DispatcherServlet 根据 /calc/sum 找到 sum 方法。
  3. 参数解析器看到 num1num2 没有注解,于是从请求参数Map中取值(请求参数包括URL查询参数和POST表单体参数),分别赋值。
  4. 方法执行,返回HTML字符串。
  5. 返回值处理器看到方法有 @ResponseBody,使用 StringHttpMessageConverter 写入响应,并设置响应头 Content-Type: text/html;charset=UTF-8
  6. 浏览器收到响应,根据 text/html 渲染页面,用户看到带颜色的结果。

5. 接口文档的重要性

5.1 为什么需要接口文档?

  • 明确约定:前后端可以并行开发,只要按照文档实现,联调时就不会出现数据对不上的问题。
  • 减少沟通成本:不需要每次都口头沟通,文档就是唯一的真理。
  • 便于维护:后续修改或新增功能,可以追溯原有设计。

5.2 如何编写接口文档?

  1. 确定业务需求:加法计算器需要两个数,返回和。
  2. 设计请求路径和方法 :RESTful风格建议用 /calc/sum,方法用POST(或GET)。
  3. 设计参数:列出每个参数的名称、类型、是否必须、说明。
  4. 设计响应:包括状态码、Content-Type、响应体示例。
  5. 提供示例:让开发者一目了然。

5.3 前后端如何遵守?

  • 前端 :确保表单的 name 与文档一致,method 正确。
  • 后端:确保接收的参数名与文档一致,返回的内容格式正确。

6. 深入理解HTTP通信【整体的流程】

6.1 HTTP传输的是什么?

HTTP传输的是字节流 。在加法计算器中,前端发送的请求体 num1=5&num2=3 是文本,被编码成UTF-8字节;后端返回的HTML也是文本,同样编码成字节。网络传输的始终是0和1,但我们可以通过 Content-Type 知道如何解读。


1). 用户填写表单并点击提交

  • 用户在两个输入框中分别输入数字,例如 53
  • 点击"提交"按钮,浏览器触发表单提交事件。

2). 浏览器自动构造 HTTP 请求

  • 请求方法POST(因为 <form method="post">)。
  • 请求 URL/calc/sum(由 action 属性指定)。
  • 请求头
    • Content-Type:浏览器自动设置为 application/x-www-form-urlencoded,这是表单的默认编码格式。
    • Accept:浏览器自动添加,通常为 text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,表示浏览器期望接收 HTML 页面(优先级最高),但也接受其他格式。
    • 其他请求头如 HostConnection 等自动添加。
  • 请求体 :浏览器将表单数据编码为 key=value 格式,并用 & 连接,然后进行 URL 编码(但这里数字不需要编码),最终请求体为 num1=5&num2=3

3). 后端服务器接收请求

  • Tomcat 接收到字节流,解析出 HTTP 报文,将请求头、请求体等信息封装成 HttpServletRequest 对象。
  • 对于 application/x-www-form-urlencoded 格式的 POST 请求,Tomcat 会自动解析请求体,将键值对放入请求参数 Map 中(可以通过 request.getParameterMap() 获取)。

4). SpringMVC 处理请求

  • DispatcherServlet 根据 URL /calc/sum 找到对应的控制器方法 sum(Integer num1, Integer num2)
  • 由于方法参数没有注解,SpringMVC 使用 ServletRequestMethodArgumentResolver(或类似解析器)来解析参数。它会从请求参数 Map 中查找名为 num1num2 的参数值,并进行类型转换(将字符串 "5" 转为 Integer 5)。如果找不到,则赋值为 null
  • 方法执行:计算两数之和,并返回一个包含 HTML 标签的字符串。

5). 后端返回响应

  • 方法上有 @ResponseBody(或类上有 @RestController),Spring 知道要将返回值直接写入响应体。
  • 内容协商 :Spring 会考虑客户端的 Accept 头(浏览器期望 text/html)以及返回值类型(String),来决定使用哪个 HttpMessageConverter 以及设置什么 Content-Type
    • StringHttpMessageConverter 支持多种媒体类型,包括 text/plaintext/html。它内部会根据请求的 Accept 头以及返回值内容来选择合适的媒体类型。通常,如果返回值包含 HTML 标签,它会倾向于选择 text/html
    • 最终,Spring 设置响应头 Content-Type: text/html;charset=UTF-8,并将字符串写入响应体。

6). 浏览器接收响应并渲染

  • 浏览器收到响应,看到 Content-Type: text/html,就知道这是一个 HTML 页面(片段),于是将其作为 HTML 解析并渲染。用户看到带颜色的计算结果。

7)补充:后端是如何接受参数的?

请看下述简图:


  1. 请求到达后,Tomcat解析请求体

    对于 application/x-www-form-urlencoded 格式的POST请求,Tomcat会自动将请求体中的字符串(如 num1=5&num2=3)按 & 分割成键值对,再按 = 分割出参数名和参数值。

    最终,Tomcat会在 HttpServletRequest 内部维护一个参数Map ,这个Map的类型实际上是 Map<String, String[]>。为什么是数组?因为同一个参数名可能对应多个值(例如复选框 <input type="checkbox" name="hobby" value="读书"> 可以选多个)。对于简单情况,数组里只有一个元素。

  2. SpringMVC的参数解析器登场

    当执行到你的 sum(Integer num1, Integer num2) 方法时,SpringMVC需要给这两个参数赋值。由于参数没有加任何注解,Spring会使用 ServletRequestMethodArgumentResolver(或者更具体的 RequestParamMethodArgumentResolver)来解析。

  3. 从参数Map中查找

    解析器会调用 request.getParameterValues("num1"),得到 String[] 数组,取第一个元素(即 "5")。然后,Spring利用内置的类型转换器 (如 StringToIntegerConverter)将字符串 "5" 转换为 Integer 类型。如果转换失败(例如传入了 "abc"),会抛出 TypeMismatchException

  4. 赋值给形参

    转换后的 Integer 对象被赋值给方法参数 num1。同理处理 num2。如果请求中缺少某个参数,则赋值为 null(对于基本类型 int 会报错,所以用包装类型 Integer 更安全)。


为什么能直接从参数Map中取到值?

关键在于Tomcat的预处理 。Tomcat在解析HTTP请求时,对于 application/x-www-form-urlencoded 格式的POST请求,它会主动读取请求体,解析出参数并填充到请求参数Map中。这意味着,无论是URL查询参数(如 ?num1=5)还是POST表单体中的参数,最终都统一存放在这个Map里。所以SpringMVC可以透明地获取,而不关心参数来自哪里。


重要补充:参数名匹配的细节
  • 严格匹配 :默认情况下,参数名必须与方法形参名完全一致。如果前端传的是 number1 而你写的形参是 num1,就会找不到,导致 num1null。这时可以用 @RequestParam("number1") Integer num1 来指定映射。
  • 大小写敏感 :参数名是大小写敏感的,num1NUM1 不同。

一句话总结就是:

前端以键值对形式传递数据 → 后端容器解析成参数Map → SpringMVC根据形参名从Map中取值并转换 → 赋值给形参。这个机制是SpringMVC的基石之一,掌握它就能轻松应对各种表单提交场景。


7. 常见问题解答

Q1:如果我用GET请求会怎么样?

如果 <form method="get">,浏览器会把参数拼接到URL后面,例如 /calc/sum?num1=5&num2=3,然后发送GET请求。后端同样能通过 Integer num1 接收到,因为参数在URL中也是请求参数的一部分。但GET请求有长度限制,且参数暴露在地址栏,不适合提交大量或敏感数据。

Q2:后端返回的HTML为什么能直接显示?

因为响应头 Content-Type: text/html 告诉浏览器这是HTML,浏览器就会解析并渲染。如果返回纯文本(text/plain),浏览器会把标签当作文本显示出来。

Q3:如果参数名和方法参数名不一致怎么办?

可以使用 @RequestParam("前端参数名") 来指定映射关系。

Q4:为什么没有设置 produces,响应头却是 text/html

SpringMVC的内容协商机制:当返回 String 且内容看起来像HTML时,默认协商为 text/html;如果返回纯文本,可能协商为 text/plain。这是由 StringHttpMessageConverter 的行为决定的。也可以手动指定 produces = "text/html" 强制。

Q5:后端框架如何知道你返回字符串中有HTML标签?然后你的响应类型就是HTML呢?

你看,如图,我们返回的是字符串,但是字符串中有html标签。


此时我们观察到响应中自动将Content-Type设置为text/html


这个是为什么呢? 具体请看下述解释:


我们的前端页面虽然没有手动设置 Content-Type,但浏览器在发送请求时,会自动添加一系列默认的请求头 ,其中就包括 Accept 头。这个 Accept 头告诉服务器:"我(浏览器)能够处理哪些类型的响应内容"。对于普通的表单提交或页面导航,浏览器的默认 Accept 头通常包含 text/html,因为它期望服务器返回一个 HTML 页面来展示。


一、浏览器默认的 Accept 头是什么样的?

当你直接在浏览器地址栏输入网址访问,或者通过 <form> 表单提交(非 AJAX),浏览器会自动在请求头中加入类似这样的内容:

复制代码
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7

这个头的意思是:

  • 我最希望收到 text/html(HTML 页面)。
  • 如果不行,application/xhtml+xml(XHTML)也可以。
  • 再不行,application/xml(XML)也能接受,但权重稍低(q=0.9)。
  • 图片格式(image/avif, image/webp 等)也能处理。
  • 最后,*/* 表示任何类型都行,但权重最低(q=0.8)。

所以,浏览器天生就是为浏览网页设计的,它默认就期望服务器返回 HTML。即使你没有写任何 AJAX 代码,浏览器也会在请求头里带上这些信息。


二、后端 SpringMVC 如何利用 Accept 头?

在我们的后端代码中,你返回的是一个包含 HTML 标签的字符串:

java 复制代码
return "<h1 style='color: #67c23a;'>计算出的结果是:" + num + "</h1>";

SpringMVC 在处理方法返回值时,会进行 内容协商(Content Negotiation),步骤大致如下:

  1. 查看客户端期望的响应格式 :从请求头 Accept 中获取。
  2. 查看可用的消息转换器 :每个转换器能写的媒体类型不同。
    • StringHttpMessageConverter 可以写 text/plaintext/html(以及 */*)。
    • MappingJackson2HttpMessageConverter 可以写 application/json
  3. 匹配最合适的媒体类型 :因为客户端 Accept 中有 text/html,且 StringHttpMessageConverter 支持 text/html,所以协商结果为 text/html
  4. 设置响应头Content-Type: text/html;charset=UTF-8,然后用该转换器将字符串写入响应体。

浏览器收到响应后,看到 Content-Type: text/html,就知道这是 HTML 内容,于是把它当作网页渲染,你就能看到带样式的标题了。


三、如果浏览器期望的是 JSON 怎么办?

如果前端是用 JavaScript 发起的 AJAX 请求(比如 fetchaxios),通常浏览器不会自动添加 text/htmlAccept 头,而是默认接受 */*,或者你可以手动设置 Accept: application/json。此时,后端内容协商的结果就会不同:

  • 如果 Acceptapplication/json,Spring 会优先选择能写 JSON 的转换器(如 MappingJackson2HttpMessageConverter),即使你返回的是字符串,也可能会尝试将其转换为 JSON(如果是字符串,可能会用 StringHttpMessageConverter 协商出 text/plain,但最终取决于配置)。
  • 但更常见的是,后端返回一个 Java 对象,Spring 自动序列化成 JSON,响应头就是 application/json

四、总结:你无需手动设置,浏览器已经帮你做了
  • 前端没有手动设置 Accept,但浏览器默认加了,因为它认为你在进行网页浏览。
  • 后端没有手动指定 produces,但 SpringMVC 自动根据 Accept 协商出了 text/html
  • 这一切都是 Web 标准和框架的默认行为,让开发者可以专注于业务逻辑。

所以,我们的例子完美展示了 浏览器与服务器之间基于 HTTP 头部的自动协商机制。如果我们希望后端强制返回 JSON,即使浏览器期望 HTML,我们也可以:

  • @RequestMapping 中添加 produces = "application/json"
  • 或者返回 ResponseEntity 并手动设置 Content-Type
  • 或者前端修改请求,让 Accept 头只包含 application/json(例如用 AJAX 请求)。

8. 总结

好了,老铁们通过这个最简单的加法计算器案例,我们看到了:

  • 接口文档是如何定义前后端契约的。
  • 前端如何按照文档实现表单提交。
  • 后端如何按照文档接收参数并返回结果。
  • HTTP通信的底层细节:字节流、Content-Type、参数绑定等。

加法计算器虽然简单,但揭示了Web开发的核心思想:约定优先,文档先行。后续的案例将引入AJAX和JSON,进一步展示更复杂的交互。老铁们别忘了点赞👍、关注、加收藏,咱们下期见👋~

写在最后

最后或许我们网络通信大家还有一点迷糊,于是呢我去向AI查了一些相关的资料:彻底搞懂 HTTP 通信:从字节流到对象,前后端数据交换全解析

相关推荐
予枫的编程笔记2 小时前
【Kafka进阶篇】Kafka延迟请求处理核心:时间轮算法拆解,比DelayQueue高效10倍
java·kafka·高并发·时间轮算法·delayqueue·延迟任务·timingwheel
躲在云朵里`2 小时前
同一账号在同一客户端类型只能登录一次
前端·spring·bootstrap
西门吹雪分身2 小时前
JUC之公平锁与非公平锁
java·并发·juc·
张铁铁是个小胖子2 小时前
mysql事务的隔离性如何保证
java·开发语言
troublea2 小时前
Laravel5.x核心特性全解析
数据库·spring boot·后端·mysql
lonelyhiker2 小时前
新版idea的structure卡顿
java·ide·intellij-idea
没有bug.的程序员2 小时前
依赖治理之巅:Maven 与 Gradle 依赖树分析内核、冲突判定博弈与工程自愈实战指南
java·gradle·maven·依赖治理·冲突判定·依赖树
毕设源码-邱学长2 小时前
【开题答辩全过程】以 前缘农产品销售系统的设计与实现为例,包含答辩的问题和答案
java
程序员南飞2 小时前
排序算法举例
java·开发语言·数据结构·python·算法·排序算法