1. 概述

对终端用户来说,提交表单无非就是填点数据,点一下“提交”按钮,整个过程简单直接。但从工程角度看,这背后其实涉及一套编码机制,确保数据能从客户端可靠地传输到服务端,供后端处理。

本文聚焦于如何在 Spring Web 应用中,处理以 application/x-www-form-urlencoded 内容类型提交的表单数据。这是 Web 开发中最常见的场景之一,但稍不注意就容易踩坑,比如乱码、参数绑定失败等。


2. 表单数据编码机制

表单提交最常用的 HTTP 方法是 POST,但如果是幂等操作(比如搜索),也可以用 GET。方法通过 HTML 表单的 method 属性指定。

  • GET 提交:数据拼在 URL 查询字符串(query string)中发送。
  • POST 提交:数据放在 HTTP 请求体(body)中发送。

对于 POST 请求,可以通过 enctype 属性指定数据的编码方式,常见取值有两个:

  • application/x-www-form-urlencoded(默认)
  • multipart/form-data

2.1 媒体类型 application/x-www-form-urlencoded

这是 HTML 表单的默认编码格式,适用于纯文本数据场景。如果你的表单不涉及文件上传,基本都用它。

它的数据格式非常简单:

  • 键值对用 & 分隔
  • 键和值之间用 = 连接
  • 所有特殊字符(如空格、@、# 等)都会经过 百分号编码(percent-encoding)

例如:

emailId=abc%40example.com&comment=Sample+Feedback

其中:

  • %40@ 的 URL 编码
  • + 代表空格(也可用 %20

⚠️ 注意:虽然 + 在查询字符串中表示空格,但在标准 percent-encoding 中,空格应编码为 %20。不过浏览器和大多数服务器都兼容 +


3. 浏览器中的表单提交

下面我们通过一个“用户反馈”功能,实战演示如何在 Spring 中处理浏览器提交的 URL 编码表单。

3.1 领域模型

反馈表单需要收集用户邮箱和评论内容。定义一个简单的 Feedback 类:

public class Feedback {
    private String emailId;
    private String comment;

    // getter 和 setter 省略
}

很简单,但足以覆盖常见用例。

3.2 创建表单页面

使用 Thymeleaf 模板引擎渲染 HTML 页面。先配置好 Thymeleaf(过程略),然后添加一个 GET 接口返回表单页面:

@GetMapping(path = "/feedback")
public String getFeedbackForm(Model model) {
    Feedback feedback = new Feedback();
    model.addAttribute("feedback", feedback);
    return "feedback";
}

接着创建 feedback.html 模板:

<form action="#" method="post" th:action="@{/web/feedback}" th:object="${feedback}">
    <input type="email" th:field="*{emailId}" placeholder="邮箱" />
    <textarea th:field="*{comment}" placeholder="您的意见"></textarea>
    <button type="submit">提交</button>
</form>

✅ 注意:

  • th:action 指定提交地址
  • th:object 绑定后端对象
  • 无需显式设置 enctype,浏览器默认就是 application/x-www-form-urlencoded

3.3 使用 PRG 模式防止重复提交

用户提交表单后,如果直接返回成功页面,刷新会导致重复提交。解决方案是 POST/Redirect/GET(PRG)模式

先处理表单提交(POST):

@PostMapping(
    path = "/web/feedback",
    consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE})
public String handleBrowserSubmissions(Feedback feedback) {
    // 保存反馈数据(此处省略)
    return "redirect:/feedback/success";
}

然后重定向到一个 GET 接口显示成功信息:

@GetMapping("/feedback/success")
public ResponseEntity<String> getSuccess() {
    return new ResponseEntity<>("感谢您的反馈!", HttpStatus.OK);
}

访问 http://localhost:8080/feedback,可以看到表单页面:

feedback form web

提交后,浏览器会跳转,URL 变为 /feedback/success,刷新也不会重复提交。

抓包可验证请求体数据格式:

emailId=abc%40example.com&comment=Sample+Feedback

✅ 踩坑提醒:如果不加 consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,Spring 可能无法正确识别请求类型,导致绑定失败。


4. 非浏览器请求处理

除了浏览器,客户端也可能是 cURL、Postman 或其他服务调用。这时不需要 HTML 页面,直接提供一个 REST 接口即可。

4.1 接收非浏览器请求

定义一个纯 REST 风格的 POST 接口:

@PostMapping(
    path = "/feedback",
    consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE})
public ResponseEntity<String> handleNonBrowserSubmissions(@RequestBody Feedback feedback) {
    // 保存反馈数据
    return ResponseEntity.ok("感谢您的反馈");
}

关键点:

  • 使用 @RequestBody 注解接收表单数据
  • 必须指定 consumesapplication/x-www-form-urlencoded
  • 不需要 PRG 模式,因为这不是浏览器场景

用 cURL 测试:

curl -X POST \
  http://localhost:8080/feedback \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'emailId=user@example.com&comment=这是一条测试反馈'

返回:

"感谢您的反馈"

4.2 FormHttpMessageConverter 原理

Spring 内部通过 FormHttpMessageConverter 处理 application/x-www-form-urlencoded 类型的数据。它负责解析请求体,并将参数映射到方法参数。

参数绑定的两种方式:

  1. 绑定到 MultiValueMap<String, String>

    可以用 @RequestParam@RequestBody

    @PostMapping("/feedback")
    public ResponseEntity<String> handleWithMap(
        @RequestParam MultiValueMap<String, String> paramMap) {
        // paramMap 包含所有表单字段
        return ResponseEntity.ok("收到");
    }
    

    ✅ 为什么能用 @RequestParam
    因为 Servlet API 把查询参数和表单数据统一放在 request.getParameterMap() 中,Spring 自动解析。

  2. 绑定到自定义对象(如 Feedback)

    必须使用 @RequestBody

    @PostMapping("/feedback")
    public ResponseEntity<String> handleWithObject(@RequestBody Feedback feedback) {
        // Spring 通过 FormHttpMessageConverter + 数据绑定机制自动填充对象
        return ResponseEntity.ok("绑定成功");
    }
    

⚠️ 踩坑重点:
如果错误地使用 @RequestParam Feedback feedback,Spring 会尝试从查询参数或表单字段逐个绑定属性,但不会解析整个请求体,容易导致 null 或绑定失败。


5. 总结

本文系统梳理了 Spring 中处理 application/x-www-form-urlencoded 表单数据的两种场景:

场景 是否需要 PRG 参数绑定方式 注解使用
浏览器表单 ✅ 需要 绑定到 POJO @ModelAttribute(隐式)
非浏览器请求 ❌ 不需要 绑定到 POJO @RequestBody

关键要点:

  • ✅ 表单默认编码为 application/x-www-form-urlencoded
  • ✅ 浏览器场景推荐使用 PRG 模式避免重复提交
  • ✅ 非浏览器请求必须用 @RequestBody 接收对象参数
  • ✅ 显式声明 consumes 类型,避免歧义

完整示例代码已上传至 GitHub:https://github.com/eugenp/tutorials/tree/master/spring-web-modules/spring-web-url


原始标题:Handling URL Encoded Form Data in Spring REST | Baeldung