FormHttpMessageConverter 详解:Spring MVC 中表单数据的读写转换器

FormHttpMessageConverter 详解:Spring MVC 中表单数据的读写转换器

一、它是什么

FormHttpMessageConverter 是 Spring MVC 内置的 HttpMessageConverter 实现,专门负责 表单数据与 MultiValueMap 之间的转换。

它支持两种表单媒体类型:

媒体类型 用途
application/x-www-form-urlencoded 普通表单提交,键值对
multipart/form-data 带文件的表单提交

它的职责可以概括为:

  • 读:请求体中的表单数据 → MultiValueMap<String, String> 或 MultiValueMap<String, Object>。
  • 写:MultiValueMap → 表单格式的请求体或响应体。

一句话:它是表单报文和 MultiValueMap 之间的翻译官。

二、接口实现

java 复制代码
public class FormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> {

    public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

    private final List<MediaType> supportedMediaTypes = new ArrayList<>();

    private final MultipartHttpMessageWriter partWriter;

    public FormHttpMessageConverter() {
        this.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
        this.supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA);
        // ...
    }

    @Override
    public boolean canRead(Class<?> clazz, MediaType mediaType) {
        if (!MultiValueMap.class.isAssignableFrom(clazz)) {
            return false;
        }
        if (mediaType == null) {
            return true;
        }
        for (MediaType supportedMediaType : getSupportedMediaTypes()) {
            if (supportedMediaType.includes(mediaType)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public boolean canWrite(Class<?> clazz, MediaType mediaType) {
        return MultiValueMap.class.isAssignableFrom(clazz);
    }

    @Override
    public MultiValueMap<String, String> read(Class<? extends MultiValueMap<String, ?>> clazz,
                                              HttpInputMessage inputMessage) throws IOException {
        MediaType contentType = inputMessage.getHeaders().getContentType();
        Charset charset = getCharset(contentType);
        // 解析 x-www-form-urlencoded 或 multipart 数据
        // ...
    }

    @Override
    public void write(MultiValueMap<String, ?> map, MediaType contentType,
                      HttpOutputMessage outputMessage) throws IOException {
        if (isMultipart(map, contentType)) {
            writeMultipart((MultiValueMap<String, Object>) map, contentType, outputMessage);
        } else {
            writeForm((MultiValueMap<String, String>) map, contentType, outputMessage);
        }
    }
}

关键点:

成员 含义
DEFAULT_CHARSET 默认字符集是 UTF-8,比 StringHttpMessageConverter 更友好
supportedMediaTypes 只支持 application/x-www-form-urlencoded 和 multipart/form-data
canRead 目标类型必须是 MultiValueMap,且媒体类型匹配
canWrite 目标类型必须是 MultiValueMap
read 把表单数据解析成 MultiValueMap
write 把 MultiValueMap 写成表单格式

三、它和 @RequestParam、@ModelAttribute 的区别

这是最容易混淆的地方。

1. 普通表单提交用对象接收

java 复制代码
@PostMapping("/login")
public String login(UserForm form) {
    // 不走 FormHttpMessageConverter
    // 走 ModelAttributeMethodProcessor
    return "success";
}

客户端提交 application/x-www-form-urlencoded,Spring MVC 用 ModelAttributeMethodProcessor 把表单参数绑定到 UserForm 对象,不经过 HttpMessageConverter。

2. @RequestParam 接收单个参数

java 复制代码
@PostMapping("/login")
public String login(@RequestParam String username, @RequestParam String password) {
    // 不走 FormHttpMessageConverter
    // 走 RequestParamMethodArgumentResolver
    return "success";
}

@RequestParam 通过 HttpServletRequest.getParameter() 取值,也不经过转换器。

3. @RequestBody MultiValueMap 才会触发

java 复制代码
@PostMapping("/login")
public String login(@RequestBody MultiValueMap<String, String> form) {
    // 走 FormHttpMessageConverter
    String username = form.getFirst("username");
    String password = form.getFirst("password");
    return "success";
}

只有 @RequestBody 且参数类型是 MultiValueMap 时,FormHttpMessageConverter 才会被选中。

4. 对比表

接收方式 是否经过 FormHttpMessageConverter 底层处理器
UserForm form 否 ModelAttributeMethodProcessor
@RequestParam String x 否 RequestParamMethodArgumentResolver
@RequestBody MultiValueMap 是 FormHttpMessageConverter
@RequestBody UserForm 是(但匹配 JSON/XML 转换器) MappingJackson2HttpMessageConverter

四、读表单:请求体转 MultiValueMap

1. 普通表单

java 复制代码
@PostMapping(value = "/form", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public String readForm(@RequestBody MultiValueMap<String, String> form) {
    String username = form.getFirst("username");
    List<String> hobbies = form.get("hobbies");
    return "username=" + username + ", hobbies=" + hobbies;
}

客户端提交:

复制代码
POST /form
Content-Type: application/x-www-form-urlencoded

username=jack&hobbies=java&hobbies=spring

FormHttpMessageConverter 解析后,form 的内容是:

复制代码
username -> [jack]
hobbies  -> [java, spring]

MultiValueMap 的 getFirst() 取第一个值,get() 取所有值。

2. 带文件的表单

java 复制代码
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String upload(@RequestBody MultiValueMap<String, Object> form) {
    String username = (String) form.getFirst("username");
    List<Object> files = form.get("file");
    return "username=" + username + ", fileCount=" + (files == null ? 0 : files.size());
}

multipart/form-data 解析后,普通字段是 String,文件字段是 MultipartFile 或其他实现。

实际项目中更常见的是用 MultipartFile 参数直接接收:

java 复制代码
@PostMapping("/upload")
public String upload(@RequestParam String username,
                     @RequestParam MultipartFile file) {
    return "ok";
}

这种写法走的是 RequestPartMethodArgumentResolver 和 MultipartResolutionDelegate,不经过 FormHttpMessageConverter。

五、写表单:MultiValueMap 转请求体

FormHttpMessageConverter 在服务端作为接收方时用得更多,但在客户端发送表单请求时也会用到。

1. 用 RestTemplate 发送表单

java 复制代码
MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
form.add("username", "jack");
form.add("password", "123456");

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

HttpEntity<MultiValueMap<String, String>> request = 
        new HttpEntity<>(form, headers);

String result = restTemplate.postForObject(
        "http://localhost:8080/login", request, String.class);

RestTemplate 内部会调用 FormHttpMessageConverter.write(),把 MultiValueMap 编码成:

复制代码
username=jack&password=123456

2. 发送带文件的表单

java 复制代码
MultiValueMap<String, Object> form = new LinkedMultiValueMap<>();
form.add("username", "jack");
form.add("file", new FileSystemResource("demo.txt"));

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);

HttpEntity<MultiValueMap<String, Object>> request = 
        new HttpEntity<>(form, headers);

String result = restTemplate.postForObject(
        "http://localhost:8080/upload", request, String.class);

FormHttpMessageConverter 会按 multipart/form-data 格式组装请求体。

六、字符集

FormHttpMessageConverter 的默认字符集是 UTF-8,这一点比 StringHttpMessageConverter 友好。

java 复制代码
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

读取表单时,字符集来源优先级:

  1. 请求头 Content-Type 中的 charset。
  2. 转换器的默认字符集。

如果请求头是:

复制代码
Content-Type: application/x-www-form-urlencoded;charset=GBK

转换器会按 GBK 解码。如果请求头没有 charset,用默认的 UTF-8。

七、和 JSON 转换器的对比

对比 FormHttpMessageConverter MappingJackson2HttpMessageConverter
处理类型 MultiValueMap 任意对象
媒体类型 x-www-form-urlencoded、multipart/form-data application/json
默认字符集 UTF-8 UTF-8
典型场景 传统表单、文件上传 前后端分离 JSON 接口
参数接收 @RequestBody MultiValueMap @RequestBody 对象

一句话记忆:表单走 MultiValueMap,JSON 走对象。

八、完整示例

1. 服务端接收表单

java 复制代码
@RestController
@RequestMapping("/api")
public class FormController {

    @PostMapping(value = "/form", 
                 consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    public Map<String, Object> readForm(
            @RequestBody MultiValueMap<String, String> form) {

        Map<String, Object> result = new HashMap<>();
        result.put("username", form.getFirst("username"));
        result.put("hobbies", form.get("hobbies"));
        return result;
    }
}

请求:

复制代码
POST /api/form
Content-Type: application/x-www-form-urlencoded

username=jack&hobbies=java&hobbies=spring

响应:

json 复制代码
{
  "username": "jack",
  "hobbies": ["java", "spring"]
}

2. 客户端发送表单

java 复制代码
@Service
public class FormClient {

    @Autowired
    private RestTemplate restTemplate;

    public String login(String username, String password) {
        MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
        form.add("username", username);
        form.add("password", password);

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

        HttpEntity<MultiValueMap<String, String>> request = 
                new HttpEntity<>(form, headers);

        return restTemplate.postForObject(
                "http://localhost:8080/login", request, String.class);
    }
}

九、常见问题

1. @RequestBody UserForm 接收表单失败

@RequestBody 默认期望 JSON 转换器处理对象。表单数据不是 JSON,MappingJackson2HttpMessageConverter 读不了,会报 415。

如果要用表单提交,接收方式有两种:

  • 不用 @RequestBody,直接写 UserForm form。
  • 用 @RequestBody MultiValueMap 接收,再手动转成对象。

2. @RequestBody MultiValueMap 报 415

检查请求头 Content-Type 是否为:

复制代码
application/x-www-form-urlencoded

或:

复制代码
multipart/form-data

如果是 application/json,FormHttpMessageConverter.canRead 不匹配。

3. 文件字段类型不是 MultipartFile

multipart/form-data 解析后,文件字段可能是 MultipartFile、Resource、Part 等,取决于具体实现。用 MultiValueMap<String, Object> 接收时,需要判断实际类型。

4. 中文乱码

检查请求头 Content-Type 是否带 charset=UTF-8。没有 charset 时,转换器用默认的 UTF-8,通常不会乱码;但客户端编码和服务端解码不一致时仍可能出问题。

5. 和 @RequestParam 混用

java 复制代码
@PostMapping("/form")
public String form(@RequestBody MultiValueMap<String, String> form,
                   @RequestParam String extra) {
    // form 来自请求体,extra 来自查询参数或表单参数
}

@RequestBody 读请求体,@RequestParam 读参数,两者可以共存,但要注意请求体只能被读一次。如果 @RequestParam 已经触发了请求体解析,@RequestBody 可能拿不到数据。

十、记忆方法

1. 一句话定位

复制代码
表单走 MultiValueMap,
两种媒体类型:urlencoded 和 multipart,
默认 UTF-8,读用 read,写用 write。

2. 两个媒体类型

复制代码
application/x-www-form-urlencoded  → 普通表单
multipart/form-data                → 带文件表单

3. 三个不经过它的场景

复制代码
UserForm form            → ModelAttributeMethodProcessor
@RequestParam String x   → RequestParamMethodArgumentResolver
@RequestBody UserForm    → Jackson 转换器

只有 @RequestBody MultiValueMap 才走 FormHttpMessageConverter。

4. 和 String 转换器的区别

复制代码
String  → StringHttpMessageConverter
表单    → FormHttpMessageConverter
对象    → MappingJackson2HttpMessageConverter

5. 客户端发送表单三步

复制代码
MultiValueMap 装数据
Content-Type 设为 x-www-form-urlencoded
RestTemplate 发请求

十一、总结

维度 说明
作用 表单数据与 MultiValueMap 之间的转换
支持类型 MultiValueMap<String, ?>
支持媒体类型 application/x-www-form-urlencoded、multipart/form-data
默认字符集 UTF-8
触发条件 @RequestBody MultiValueMap 且 Content-Type 是表单类型
不触发的场景 普通对象接收、@RequestParam、@RequestBody 对象
典型用途 传统表单接收、文件上传、RestTemplate 发送表单
常见错误 用 @RequestBody UserForm 接表单导致 415

FormHttpMessageConverter 是 Spring MVC 中专门处理表单的转换器,它的核心类型是 MultiValueMap,支持 urlencoded 和 multipart 两种格式。理解它,就能分清"普通表单绑定"和"@RequestBody 读表单"的区别,也能解释为什么 @RequestBody 接表单对象会报 415。记住"表单走 MultiValueMap、两种媒体类型、默认 UTF-8",再记住"普通对象接收不走它",这类问题就能快速判断。

相关推荐
龙亘川2 小时前
构建城市公共安全数字底座:亘川智城智慧安防系统全维度解读
java·大数据·开发语言·人工智能·信息可视化
金銀銅鐵2 小时前
[Java] 用 CLI 展示 EnumMap 增删键值对时的内部变化
java·源码·ai编程
秋天的怀念_David2 小时前
第二十二夜 · 数字迷雾
java·程序人生·算法
Raas1003 小时前
AI网关部署方案?从部署到上线全流程
java·开发语言·人工智能·gateway·ai网关·企业级ai网关·企业级ai产品
niucloud-admin3 小时前
NIUCLOUD PHP SAAS上门家政使用文档:城市管理
java·前端·php
霸道流氓气质4 小时前
智能客服组件:分层意图识别、RAG Advisor 与人工转接状态机的 Vue3 + Spring AI 生产级实战
java·人工智能·spring
captain3764 小时前
MyBatis操作数据库(入门2)
java·数据库·spring boot·后端·spring·mybatis
心中有你02145 小时前
Java实现Dijkstra最短路径算法(邻接表 + 优先队列)
java·数据库·算法
步行cgn5 小时前
Spring MVC + Thymeleaf 模板解析失败:从 500 错误到页面正常显示
java·spring·mvc