Spring Boot Multipart 表单中文乱码问题全解析

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 解析请求体时使用的字符编码

  1. 客户端(浏览器/Postman)以 UTF-8 编码中文字符,写入请求体
  2. Tomcat 收到请求,调用 request.getCharacterEncoding() 获取编码
  3. 如果 Content-Type 头中没有 charset 声明(multipart 请求通常不带),返回 null
  4. 返回 null 时,Tomcat 默认使用 ISO-8859-1 解码请求体
  5. 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
相关推荐
长栎1 小时前
Spring Boot 自动配置的3种设计模式,比 @Autowired 更值得搞懂
后端
dadaobusi1 小时前
Linux内核完成大量内存/调度/时间子系统初始化的关键阶段
java·linux·前端
长栎2 小时前
读 Kafka 源码才知道,你写的观察者模式就是个玩具
后端
胡萝卜术2 小时前
从零搞懂 AJAX:手把手带你从 XMLHttpRequest 到 fetch,彻底理解前后端数据交互
前端·后端·面试
garmin Chen2 小时前
prompt实战:nof1.ai Alpha Arena
java·人工智能·python·prompt
RuoyiOffice2 小时前
从 0 到 1 搭建 RuoyiOffice:30 分钟跑通后端+前端+移动端
前端·spring boot·uni-app·开源·oa·ruoyioffice·hrm
XovH2 小时前
Redis 从入门到精通:分片之道 —— Redis Cluster
后端
XovH2 小时前
Redis 从入门到精通:Redis Sentinel 哨兵
后端
用户938515635072 小时前
从零实现一个 Todos 应用:原生 Ajax + Node 服务,顺便吃透 JSON.stringify
前端·javascript·后端