Spring Boot Multipart 表单中文乱码问题全解析
一、问题现象
使用 multipart/form-data 提交表单时,文件上传正常,但文本字段中的中文存入数据库后变成乱码(如"张三"变成"å¼ ä¸‰"或"???")。
典型触发场景:
- 前端通过 FormData 同时上传文件和中文参数
- 后端用
@RequestParam接收文本字段 - 数据库连接已经配置了 UTF-8,表字段也是 utf8mb4
二、底层原理
2.1 HTTP 请求中编码的传递链路
浏览器/客户端 Tomcat 容器 Spring MVC 业务代码
│ │ │ │
│── HTTP请求 ──→ │ │ │
│ Content-Type: │ │ │
│ multipart/form-data; │ │ │
│ boundary=----xxx │ │ │
│ │── 解析请求体 ──→ │ │
│ │ request.getParameter() │ │
│ │ 使用什么编码? │ │
│ │ │── 绑定参数到方法 ──→ │
│ │ │ @RequestParam │
│ │ │ │── 使用参数
2.2 问题出在哪一步
关键在于 Tomcat 解析请求体时使用的字符编码:
- 客户端(浏览器/Postman)以 UTF-8 编码中文字符,写入请求体
- Tomcat 收到请求,调用
request.getCharacterEncoding()获取编码 - 如果
Content-Type头中没有charset声明(multipart 请求通常不带),返回 null - 返回 null 时,Tomcat 默认使用 ISO-8859-1 解码请求体
- UTF-8 编码的字节用 ISO-8859-1 解码 → 乱码
2.3 为什么 JSON 请求不会乱码
application/json 请求通常带有 Content-Type: application/json; charset=UTF-8,明确声明了编码。而 multipart/form-data 的 Content-Type 格式是:
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
没有 charset 声明,所以 Tomcat 不知道该用什么编码,退回到默认的 ISO-8859-1。
2.4 编码转换过程图解
原始文本:"张三"
UTF-8 编码字节: E5 BC A0 E4 B8 89 (6个字节)
用 ISO-8859-1 解码这6个字节:
E5 → å
BC → ¼(实际显示可能不同)
A0 → (不可见)
E4 → ä
B8 → ¸
89 → ‰
结果:乱码字符串 "å¼ ä¸‰"
三、Servlet 规范中的编码机制
3.1 request.setCharacterEncoding()
Servlet 规范定义了 request.setCharacterEncoding(String encoding) 方法,必须在第一次读取参数之前调用才有效。
java
// 正确:在读取参数前设置
request.setCharacterEncoding("UTF-8");
String name = request.getParameter("name"); // 用 UTF-8 解码
// 错误:在读取参数后设置(无效)
String name = request.getParameter("name"); // 已经用默认编码解码了
request.setCharacterEncoding("UTF-8"); // 为时已晚
3.2 CharacterEncodingFilter 的工作原理
Spring 提供的 CharacterEncodingFilter 就是在 Filter 链的最前面调用 request.setCharacterEncoding(),确保在任何参数读取之前设置编码。
源码核心逻辑(简化):
java
public class CharacterEncodingFilter extends OncePerRequestFilter {
private String encoding;
private boolean forceRequestEncoding = false;
private boolean forceResponseEncoding = false;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain) {
// 如果请求没有设置编码,或者强制覆盖
if (this.encoding != null) {
if (this.forceRequestEncoding || request.getCharacterEncoding() == null) {
request.setCharacterEncoding(this.encoding);
}
if (this.forceResponseEncoding) {
response.setCharacterEncoding(this.encoding);
}
}
filterChain.doFilter(request, response);
}
}
3.3 为什么 Filter 顺序很重要
请求 → Filter1(编码) → Filter2(安全) → Filter3(日志) → DispatcherServlet → Controller
如果编码Filter不在第一个位置,安全Filter可能先读取了参数,
此时编码已经用默认ISO-8859-1解码了,后面再设置UTF-8也没用。
所以要设置 registration.setOrder(Integer.MIN_VALUE) 确保最先执行。
四、解决方案对比
| 方案 | 实现方式 | 作用范围 | 推荐度 |
|---|---|---|---|
yml 配置 spring.servlet.encoding |
配置文件 | 全局所有请求 | ⭐⭐⭐⭐⭐ |
| CharacterEncodingFilter + 指定 URL | Java 配置类 | 指定接口 | ⭐⭐⭐⭐ |
| Controller 中手动转码 | 业务代码 | 单个参数 | ⭐⭐ |
| 前端显式设置 charset | 前端代码 | 全局 | ⭐⭐⭐ |
4.1 yml 全局配置
yaml
spring:
servlet:
encoding:
charset: UTF-8
enabled: true
force: true
force: true 等同于 forceRequestEncoding=true + forceResponseEncoding=true。
4.2 Java 配置(指定接口)
java
@Bean
public FilterRegistrationBean<Filter> encodingFilter() {
CharacterEncodingFilter filter = new CharacterEncodingFilter();
filter.setEncoding("UTF-8");
filter.setForceRequestEncoding(true);
filter.setForceResponseEncoding(true);
FilterRegistrationBean<Filter> reg = new FilterRegistrationBean<>();
reg.setFilter(filter);
reg.addUrlPatterns("/api/xxx/import"); // 只对特定接口
reg.setOrder(Integer.MIN_VALUE);
return reg;
}
4.3 Controller 手动转码
java
// 应急方案,不推荐长期使用
String name = new String(
rawName.getBytes(StandardCharsets.ISO_8859_1),
StandardCharsets.UTF_8
);
4.4 前端显式设置
javascript
// 方式一:在 FormData 的 Blob 中指定 type
const formData = new FormData();
formData.append('file', file);
formData.append('name', new Blob([JSON.stringify('张三')], { type: 'text/plain; charset=UTF-8' }));
// 方式二:使用 fetch 时不需要额外设置,浏览器默认用 UTF-8 编码 FormData
fetch('/api/import', { method: 'POST', body: formData });
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
五、完整示例
5.1 场景描述
一个文件上传接口,同时接收文件和中文备注信息,确保中文不乱码。
5.2 Controller
java
package com.example.controller;
import com.example.dto.UploadResultDto;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/**
* 文件上传接口.
*/
@RestController
@RequestMapping("/api/file")
public class FileUploadController {
@PostMapping("/upload")
public UploadResultDto upload(
@RequestParam("file") MultipartFile file,
@RequestParam("remark") String remark,
@RequestParam("uploaderName") String uploaderName) {
// 此时 remark 和 uploaderName 已经是正确的 UTF-8 字符串
UploadResultDto result = new UploadResultDto();
result.setFileName(file.getOriginalFilename());
result.setFileSize(file.getSize());
result.setRemark(remark);
result.setUploaderName(uploaderName);
return result;
}
}
5.3 DTO
java
package com.example.dto;
import lombok.Data;
@Data
public class UploadResultDto {
private String fileName;
private Long fileSize;
private String remark;
private String uploaderName;
}
5.4 编码过滤器配置
java
package com.example.config;
import jakarta.servlet.Filter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.CharacterEncodingFilter;
/**
* 字符编码过滤器,解决 multipart/form-data 中文乱码.
*/
@Configuration
public class EncodingConfig {
@Bean
public FilterRegistrationBean<Filter> uploadEncodingFilter() {
// 1. 创建编码过滤器
CharacterEncodingFilter filter = new CharacterEncodingFilter();
filter.setEncoding("UTF-8");
filter.setForceRequestEncoding(true); // 强制请求编码
filter.setForceResponseEncoding(true); // 强制响应编码
// 2. 注册到 Servlet 容器
FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>();
registration.setFilter(filter);
// 3. 指定生效的 URL(只对上传接口)
registration.addUrlPatterns("/api/file/upload");
// 4. 设置最高优先级,确保在所有其他 Filter 之前执行
registration.setOrder(Integer.MIN_VALUE);
return registration;
}
}
5.5 测试验证
cURL 测试:
bash
curl -X POST http://localhost:8080/api/file/upload \
-F "file=@测试文件.xlsx" \
-F "remark=这是一条中文备注" \
-F "uploaderName=张三"
预期返回:
json
{
"fileName": "测试文件.xlsx",
"fileSize": 12345,
"remark": "这是一条中文备注",
"uploaderName": "张三"
}
不加 Filter 时的错误返回:
json
{
"fileName": "测试文件.xlsx",
"fileSize": 12345,
"remark": "è¿æ¯ä¸æ¡ä¸æå¤æ³¨",
"uploaderName": "å¼ ä¸"
}
六、排查清单
如果配置了 Filter 仍然乱码,按以下顺序排查:
| 检查点 | 排查方法 |
|---|---|
| 1. Filter 是否生效 | 在 Filter 中加日志 log.info("encoding filter executed") |
| 2. Filter 顺序是否正确 | 确认 order 是否为最小值,没有其他 Filter 先读取了参数 |
| 3. URL Pattern 是否匹配 | 打印请求路径,确认和 addUrlPatterns 一致 |
| 4. 数据库连接编码 | 确认 JDBC URL 有 characterEncoding=UTF-8 |
| 5. 数据库表/列编码 | SHOW CREATE TABLE xxx 确认是 utf8mb4 |
| 6. 客户端发送编码 | 用 Wireshark 或 Charles 抓包,确认请求体是 UTF-8 字节 |
| 7. 响应编码 | 确认 Response 的 Content-Type 包含 charset=UTF-8 |
七、总结
问题本质:
multipart/form-data 请求的 Content-Type 不携带 charset
→ Tomcat 用默认 ISO-8859-1 解码
→ UTF-8 字节被错误解码为乱码
解决本质:
在 Tomcat 读取参数之前,告诉它"请用 UTF-8 解码"
→ CharacterEncodingFilter 在 Filter 链最前面调用 request.setCharacterEncoding("UTF-8")
→ 后续所有参数读取都使用 UTF-8