
中文乱码这个问题几乎每个做后端开发的同行都遇到过。用户提交的表单数据存进数据库之后页面显示变成了一串“æ‰çš„好”或者“????”新人抓耳挠腮老手也偶尔翻车。我见过不少项目在联调阶段被乱码问题卡了一两天最后发现就是某个环节的编码配置漏了。这篇文章就围绕“存入数据库的数据出现中文乱码”这件事把原理、排查思路、解决方案讲透你照着检查一遍大概率能直接解决问题。1. 乱码的本质字符编码不一致是唯一根源1.1 字符集和编码规则的核心概念先说基础。计算机只能存储二进制你要存一个汉字就必须把它转换成二进制这个转换规则就是字符编码。中文字符常见的有 GBK、GB2312、UTF-8英文世界常用 Latin-1、ASCII。UTF-8 是目前最通用的编码它兼容 ASCII又能表示全世界几乎所有字符。举个例子“中文”这两个字用 UTF-8 编码后是E4 B8 AD E6 96 87十六进制用 GBK 编码后是D6 D0 CE C4。看到没同一个字符串不同的编码规则二进制数据完全不同。如果写入时用 GBK 解析读取时用 UTF-8 解析结果自然就乱了这就是乱码的本质原因——整个数据链路中至少有一个环节的编码设置和其他环节不一致。1.2 乱码为什么表现为“问号”或“菱形符号”乱码有几种常见表现每种指向的原因还不一样。一种是插入后就变成???。这种情况通常是数据库连接或表字段的字符集不支持汉字比如字段用的是latin1汉字在 latin1 里根本没有对应关系只能被替换成问号。这种是“不可逆”的数据在存入那一刻就已经丢了。另一种是显示成类似测试这样的“外星文”。这种通常是数据本身是 UTF-8 的字节但客户端连接、HTTP 请求或数据库会话用了 LATIN1 或 GBK 去解析导致字节被错误解读。这种情况只要调整连接或读取环节的编码数据大概率还能恢复。还有一种情况从数据库命令行查是正常的但网页上显示乱码。这个通常不是存储问题而是响应阶段页面渲染编码和实际字节编码不一致。比如数据存的是 UTF-8页面却用Content-Type: text/html; charsetGBK去解析。理解了这些表现后面的排查就会很有方向感。2. 数据从浏览器到数据库的完整链路每一个环节都可能出错2.1 一次数据入库需要经过的几个关卡你从浏览器表单提交一个“用户名”到它真正进入数据库中间要经过至少四个环节浏览器端页面编码决定了用户输入的文字以什么编码格式发送到服务器。网络传输HTTP 请求头里的Content-Type字段会标注请求体的编码类型charsetUTF-8之类的容器靠这个参数来解码参数。应用服务器如 Tomcat、Jetty容器解析 HTTP 请求参数时用的编码方式取决于配置和请求头。JDBC 数据库连接应用把参数发给数据库驱动时驱动会用连接的characterEncoding来编码字节流。数据库端数据库收到字节流后用库、表、字段的字符集来做最终的存储同时数据库连接会话的字符集也参与交互。这五个环节只要有一处编码设置不统一结果就是乱码。大多数项目出问题是因为只改了数据库的字符集忽略了 JDBC URL 参数和容器配置。2.2 编码设置需要在哪些位置对齐既然环节这么多那要保证不乱码就要让所有环节都用同一种编码来工作。以 UTF-8 为例你需要对齐的是这些设置点页面文件本身保存为 UTF-8并且页面头部声明charsetUTF-8。应用容器的请求/响应编码统一为 UTF-8Tomcat 的URIEncoding、useBodyEncodingForURI等。框架层设置一个编码过滤器如 Spring 的CharacterEncodingFilter强制请求和响应都用 UTF-8。JDBC 连接串里显式加上characterEncodingutf8。数据库的连接字符集、库字符集、表字符集、字段字符集全部是 UTF-8或 UTF-8MB4。这条链路说起来简单实际操作中很容易漏。下面几个章节我按“数据库端 → 应用端 → 已乱码数据修复”的顺序分别展开讲。3. 数据库层面的标准解决方案字符集与连接参数3.1 MySQL 字符集的五个层级要逐级对齐以 MySQL 为例字符集其实是分层的从粗到细分别是服务器级、数据库级、表级、字段级外加一个连接级。你可能把数据库建成了 UTF-8但表在建的时候没有指定就会继承数据库的如果字段在建表时显式指定了latin1那表是 UTF-8 也没用还得看字段的。我建议在实际操作中把每一层都显式指定不要依赖默认继承。建库的时候执行CREATE DATABASE demo_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;建表的时候执行CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_general_ci;对于已经建好的表可以用下面的语句调整表级别和字段级别的字符集ALTER TABLE user CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;CONVERT TO CHARACTER SET会同时修改表和表中所有字符串字段的字符集但注意它会把现有数据从旧字符集转换成新字符集如果旧数据本身就是乱码字节转换后依然可能是乱的这一点后面专门讲。连接级别也要注意。登录 MySQL 客户端后可以查看当前会话的字符集SHOW VARIABLES LIKE character_set%;理想状态下character_set_client、character_set_connection、character_set_results都是utf8mb4。如果不是可以在登录后就执行SET NAMES utf8mb4;在 JDBC 或连接池配置里通常用连接参数来替代这个命令下面会说。3.2 JDBC 连接串最容易忽略的一行参数很多项目辛辛苦苦把库、表、字段都改成了 utf8mb4结果插入中文还是乱码问题往往出在 JDBC URL 上。MySQL 的驱动在建立连接时默认的编码行为取决于驱动版本和服务器设置不完全可靠显式声明是最稳妥的。你的连接串应该至少包含以下参数jdbc:mysql://127.0.0.1:3306/demo_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai重点就是useUnicodetrue和characterEncodingutf8。useUnicodetrue是告诉驱动我要启用 Unicode 支持的逻辑characterEncodingutf8则明确指定了驱动和数据库交互时使用的编码。注意characterEncodingutf8对应的是 MySQL 的utf8mb3如果你想用更完整的utf8mb4在较新版本的 MySQL 驱动8.x里也可以写成characterEncodingUTF-8驱动会自动映射到合适的 MySQL 字符集。如果你用的是连接池Druid、HikariCP、C3P0连接池配置里也要透传这些参数。比如 Druid 的connectionPropertiesspring.datasource.druid.connection-propertiesuseUnicodetrue;characterEncodingutf8如果你用 Spring Boot通常在application.yml里这样写spring: datasource: url: jdbc:mysql://127.0.0.1:3306/demo_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai实操中我发现很多老项目的连接串里只有jdbc:mysql://localhost:3306/demo_db因为早期 MySQL 驱动默认行为在某些版本下“碰巧能用”升级驱动或迁移环境后就暴露了。所以无论项目大小请把连接参数字段写完整不要依赖默认。3.3 数据库版本与排序规则的兼容性陷阱还有一个容易被忽略的陷阱MySQL 5.5 之前不支持utf8mb4只有utf8且 utf8 最多 3 字节。表情符号emoji和一些生僻汉字需要 4 字节存储用 utf8 就会报错Incorrect string value: \xF0\x9F... for column。这种报错信息看起来像乱码问题实际上是字符集容量不足。解决办法就是升级到 MySQL 5.7 或 8.x并把相关字符集全部切换成utf8mb4。另外排序规则建议用utf8mb4_general_ci性能稍好或utf8mb4_unicode_ci排序更精确。一般项目选utf8mb4_general_ci够了如果涉及多语言排序选unicode_ci更严谨。查询当前服务器的最大字节长度可以用SELECT CHARACTER_SET_NAME, MAXLEN FROM INFORMATION_SCHEMA.CHARACTER_SETS WHERE CHARACTER_SET_NAME LIKE utf8%;如果看到utf8mb4的MAXLEN为 4就说明支持 4 字节字符。这是排查“能存普通中文但不能存 emoji”类问题最直接的验证方式。4. 应用服务器与代码层面的拦截与统一4.1 Web 容器编码Tomcat 的 URI 与请求体编码如果数据库连接没问题但通过 HTTP 接口插入的数据依然乱码就要看容器侧了。以 Tomcat 为例它解析 GET 请求的查询参数使用的是URIEncoding这个值默认是 ISO-8859-1。也就是说如果你的 URL 路径或查询参数里有中文Tomcat 默认会用 ISO-8859-1 解码到了应用里就是乱码。修改方式是在 Tomcat 的server.xml里给Connector加上Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 URIEncodingUTF-8 /对于 POST 表单请求体Tomcat 会按请求头里的Content-Type的charset解析。如果客户端没有明确发送 charset容器也可能用 ISO-8859-1。为了保险一般会在应用层加一个编码过滤器强制所有请求的编码为 UTF-8。Spring Boot 内嵌 Tomcat 的情况下可以这样显式声明server: servlet: encoding: charset: UTF-8 enabled: true force: trueforce: true意味着即使请求头里带了别的 charset也强制用 UTF-8 解码这在很多对接场景中能省掉不少麻烦。4.2 编码过滤器用 Spring 的 CharacterEncodingFilter不管你用什么 MVC 框架只要是基于 Servlet 的都可以在web.xml或 Java Config 里注册一个编码过滤器。Spring 自带的CharacterEncodingFilter是最省事的方案filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping用 Spring Boot 的话上面说的server.servlet.encoding.forcetrue就是等价配置框架会自动注册这个过滤器。提示forceEncodingtrue有两个作用一是强制请求编码二是强制响应编码。如果你只设置了请求编码响应的 Content-Type 里可能不会带 charset浏览器就可能会用本地默认编码去解析页面照样显示乱码。4.3 代码层面的编码规范字符串读取与写入除了框架配置代码本身也有一些编码习惯问题。比如你从 HTTP 头、文件、外部接口读取数据时如果显式用到了getBytes()、InputStreamReader、OutputStreamWriter这类 API一定要传入明确的 Charset// 坏习惯不指定编码依赖平台默认 String result new String(bytes); // 好习惯显式指定 String result new String(bytes, StandardCharsets.UTF_8);还有文件读取很多项目配置文件是 UTF-8但 JVM 启动参数没指定导致FileReader用了系统默认编码来读。Linux 服务器上系统 locale 可能是 POSIX 或 C默认编码是 ANSI_X3.4-1968近似 ASCII读 UTF-8 文件就会乱。最稳妥的做法是在应用启动脚本里加上JAVA_OPTS-Dfile.encodingUTF-8不过-Dfile.encoding只影响 Java 默认的文件读写编码不影响 HTTP 参数解析和 JDBC 连接这些还是得靠前面的配置。我见过一个真实案例某团队部署到 Linux 服务器后接口从外部文件导入的数据入库全部乱码本地 Windows 开发环境却完全正常。最后定位就是 JVM 默认文件编码差异导致FileReader读文件时用了错误编码。修复方法是显式用InputStreamReader指定 UTF-8而不是只调FileReader。5. 已入库的乱码数据如何修复5.1 先判断是“可逆乱码”还是“不可逆乱码”一旦数据入库已经乱码了修复前必须先判断它是可逆的还是不可逆的。判定方法很简单用数据库客户端直接查询如果显示的是测试之类的符号串说明原始字节还在只是被错误的编码解读了这种通常可逆如果显示的是???说明字符在存入时就丢失了无法从 DB 里恢复只能从源头重导。想进一步确认用HEX()函数看一下存储的字节SELECT HEX(username) FROM user WHERE id1;如果看到E6 B5 8B E8 AF 95这正好是 UTF-8 的“测试”的十六进制。说明数据库里存的字节确实是 UTF-8只是查询会话用错了编码显示。这种情况下你重新配置所有连接为 UTF-8查询显示就正常了。如果看到3F 3F 3F即???很遗憾原始字节已经不可逆地丢失了。此时任何转换都无力回天只能考虑从备份恢复或重新采集数据。5.2 转换字符集修复法CONVERT 函数的使用对于可逆乱码修复思路就是把字节从当前被误解的编码中转回原始编码再转到目标 UTF-8。举个最常见的场景数据实际是 UTF-8 字节但被驱动用 GBK 或 LATIN1 误解析后存进了utf8字段。现在数据库里显示的乱码其实是“用错误编码解出来的字符串”。修复方式是按相反路径转换。假设某张表的username字段里存的是 Latin1 误读 UTF-8 字节后的结果可以用下面的 SQL 来修复UPDATE user SET username CONVERT(CAST(CONVERT(username USING latin1) AS BINARY) USING utf8mb4) WHERE id 1;拆开解释CONVERT(username USING latin1)把当前字符串当作 latin1 编码转成二进制字节。因为 latin1 是单字节编码每个字符一一对应一个字节所以这一步本质上是把“被误解的字符”还原成原始字节。CAST(... AS BINARY)得到二进制串。CONVERT(... USING utf8mb4)再用 utf8mb4 解码二进制串得到正确的字符串。这套写法在很多 MySQL 乱码修复文章里都出现过核心前提是latin1单字节不会丢字节能完好还原 UTF-8 原文。如果是 GBK 误读了 UTF-8显示为中文乱码如“娴嬭瘯”这类情况转换路径略有不同需要先转回 GBK 字节再用 UTF-8 解码UPDATE user SET username CONVERT(CAST(CONVERT(username USING gbk) AS BINARY) USING utf8mb4) WHERE id 1;注意如果你不确定当初是哪个环节用了哪种错误编码请先用一条SELECT语句验证转换结果是否符合预期再执行UPDATE。千万不要直接在生产库上批量 UPDATE后果自负。5.3 修复前必须做的备份与验证步骤乱码数据修复是高风险的我踩过一次坑批量 UPDATE 之后才发现有些行的转换方向弄反了原本还能勉强识别的数据反而变成了更大的乱码最后只能从备份捞数据。所以不管时间多紧修复前请按这个 checklist 走备份表CREATE TABLE user_bak AS SELECT * FROM user;或者用 mysqldump 导出。定位影响行先写 SELECT 语句用WHERE筛选出乱码行。单行测试挑一条记录执行修复 UPDATE再查询确认。对比备份确认无误后再铺开到其他行。如果是大表建议分批 UPDATE比如一次 1000 行避免长事务锁表影响线上业务UPDATE user SET username CONVERT(CAST(CONVERT(username USING latin1) AS BINARY) USING utf8mb4) WHERE id BETWEEN 1 AND 1000;实测中这种修复方式对大表性能影响明显锁行时间较长最好在业务低峰期执行并监控数据库的锁等待和主从延迟。6. 乱码排查思路与典型场景实战6.1 一分钟快速定位问题环节的方法遇到乱码不要东改一下西改一下。我整理了一套排查流程基本能在一分钟内定位到最关键的那个环节。第一步先在数据库命令行客户端直接插入一条中文记录INSERT INTO user(username) VALUES(测试中文); SELECT username FROM user;如果命令行查询正常说明数据库存储环节没问题问题出在应用侧或连接侧。如果命令行插入就乱码说明服务器字符集或库表字符集配置有问题直接回到第 3 章去检查。第二步在应用日志里打印插入前拿到的参数值。如果日志中文正常但入库乱码问题大概率出在 JDBC 连接串或表字段字符集。如果日志里就已经是乱码那问题在入口检查 HTTP 解码和请求编码。第三步用HEX()函数检查存储字节。根据十六进制内容确认实际字节编码比对期望的 UTF-8 字节就能判断是哪个环节发生了错误转码。6.2 典型场景一GET 请求中文参数乱码这是一个高频问题。页面通过链接传中文参数后端获取后入库乱码。这种情况的典型原因是容器的 URI 解码用了 ISO-8859-1。Tomcat 中解决方案就是设置URIEncodingUTF-8。即使设置了 URIEncoding某些老版本 Tomcat 对 body 参数的解码仍受useBodyEncodingForURI影响。如果这个值为true那么 URI 查询参数会优先使用请求体的编码也就是Content-Type里的 charset 决定。所以如果遇到“GET 乱码POST 正常”优先检查URIEncoding和useBodyEncodingForURI的组合。6.3 典型场景二POST 请求 JSON 数据乱码POST 请求如果Content-Type是application/json; charsetutf-8一般不容易乱码因为 JSON 解码器大多会按 UTF-8 处理。乱码通常出现在application/x-www-form-urlencoded表单提交。处理办法就是第 4.2 节的强制编码过滤器。在 Servlet 容器层面把请求编码强制为 UTF-8让参数在进入业务代码之前就被正确解析。Spring Boot 项目就是server.servlet.encoding.forcetrue传统 SSM 项目就注册CharacterEncodingFilter。6.4 典型场景三从文件导入数据乱码导入 CSV 或 Excel 文件时文件本身的编码和程序读取的编码不一致会导致乱码。Excel 导出的 CSV 文件默认可能用 GBK 或本地编码而代码里却按 UTF-8 读就会乱。解决方式是读文件前先检测 BOM 头或者让用户显式选择文件编码。代码中不要用FileReader这种依赖平台默认编码的类改用InputStreamReader指定编码InputStreamReader reader new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8);如果文件实际是 GBK就把StandardCharsets.UTF_8换成Charset.forName(GBK)。更稳妥的做法是读取时先判断 BOMPushbackInputStream pbis new PushbackInputStream(new FileInputStream(file), 3); byte[] b new byte[3]; int len pbis.read(b); if (b[0] (byte)0xEF b[1] (byte)0xBB b[2] (byte)0xBF) { // UTF-8 with BOM } else { // no BOM, 按 GBK 或默认 UTF-8 处理 }这块的逻辑不复杂但很容易被忽视。尤其是 Windows 环境下生成的 CSVFileReader在本地开发时碰巧正常部署到 Linux 就全部乱码原因就是上面提到的 JVM 默认编码差异。6.5 乱码问题排查速查表现象可能原因检查点存储后显示???字段字符集不支持汉字字段字符集改为 utf8mb4显示测试数据库会话/连接编码与存储编码不一致JDBC URL 加 characterEncodingSET NAMES命令行正常网页乱码响应编码未声明设置响应 Content-Type charsetUTF-8GET 参数乱码POST 正常容器 URI 解码编码错误Tomcat URIEncodingUTF-8POST 表单乱码请求体解码错误添加强制编码过滤器文件导入乱码文件实际编码与程序读取编码不一致显式指定 reader 编码检测 BOMLinux 正常、Windows 乱码或反过来平台默认编码差异JVM-Dfile.encodingUTF-8代码中显式指定编码我个人的体会是乱码问题 90% 以上都是“配置或代码里没有显式声明编码”。凡是依赖默认值的地方换个环境就可能暗藏风险。与其每次出了问题焦头烂额不如从一开始就把所有环节的编码写死。特别是数据库连接串、Tomcat 连接器配置、项目全局编码过滤器这三类在新建项目的第一天就配好后面基本不会再碰乱码这种事了。最后再分享一个小技巧每次写数据库表结构时养成手动指定CHARSETutf8mb4 COLLATEutf8mb4_general_ci的习惯不要觉得默认值就行。默认值意味着不可控而数据库这块最怕的就是不可控。