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;
读取表单时,字符集来源优先级:
- 请求头
Content-Type中的charset。 - 转换器的默认字符集。
如果请求头是:
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",再记住"普通对象接收不走它",这类问题就能快速判断。