
与的繁体图解原理:3个坑让你面试挂科
上周有个学员找我吐槽,说面试时被问“与的繁体在数据库里怎么存才不炸”,他愣了半天,只憋出一句“用UTF-8呗”。面试官没说话,直接让他回去等通知。
这就是典型的面试被问原理答不上来。
别以为“与的繁体”是个冷门冷僻字,在金融、政务、港澳台业务对接的系统里,它出现的频率比你想象的高得多。很多开发者只会在前端页面敲个“與”字,真到了后端处理、数据库存储、跨服务传输环节,坑一个接一个。
今天不整虚的,直接上图解原理,把“与的繁体”在开发全流程里的三个致命坑扒个底朝天。全是实战踩出来的血泪教训,看完这篇,你至少能避开80%的编码事故。
坑一:编码混用导致数据“变脸”
现象描述
最经典的场景:前端提交表单,后端接收到的“与”变成了“?”或者乱码;或者数据库里存的是“與”,查出来变成了“???”。
很多初级开发第一反应是“字符集没设对”,于是疯狂改数据库字符集、改连接串、改HTTP Header,折腾半天没卵用。
根本原因
问题的根源不在数据库,而在编码转换的时机。
“与”的简体(U+4E0E)和繁体“與”(U+8207)在Unicode里是两个不同的码点。当数据从浏览器传到服务端,中间经过Nginx、Tomcat、JDBC驱动,每一层都可能做一次编码转换。
如果你的Nginx配置是charset utf-8,Tomcat的Connector配置是URIEncoding=UTF-8,但JDBC连接串里没指定useUnicode=truecharacterEncoding=UTF-8,数据在JDBC层就会被默认编码(通常是平台默认编码,Linux下是POSIX,Windows下是GBK)转换一次。
这时候,UTF-8编码的“與”字节序列被当成GBK解读,就会变成乱码。更隐蔽的是,如果某个中间件只处理了ASCII字符,非ASCII字符直接被替换成“?”,这时候数据已经永久丢失,改字符集也没用了。
正确写法对比
错误写法:依赖平台默认编码
// 错误:JDBC连接串未显式指定编码
String url = jdbc:mysql://localhost:3306/mydb;
Connection conn = DriverManager.getConnection(url, user, pass);// 前端提交:{name: 與}
// 后端接收:name = ? 或 锟斤拷正确写法:全链路显式指定UTF-8
// 正确:JDBC连接串显式指定编码
String url = jdbc:mysql://localhost:3306/mydb?useUnicode=truecharacterEncoding=UTF-8useSSL=false;
Connection conn = DriverManager.getConnection(url, user, pass);// 同时确保:
// 1. Nginx: charset utf-8;
// 2. Tomcat: Connector port=8080 URIEncoding=UTF-8 /
// 3. MySQL: CREATE DATABASE mydb DEFAULT CHARACTER SET utf8mb4;注意:MySQL必须用utf8mb4,不能用utf8。MySQL的utf8是阉割版,只支持3字节UTF-8,而“與”在UTF-8里是3字节,刚好能存;但如果你未来要存emoji,就会炸。统一用utf8mb4是铁律。
复现与修复代码
想复现这个坑?简单:
# 1. 创建测试表
mysql CREATE TABLE test (id INT, name VARCHAR(50)) DEFAULT CHARSET utf8mb4;# 2. 用Java程序插入,JDBC连接串不带characterEncoding
# 在Windows机器上运行,默认编码是GBK
String sql = INSERT INTO test VALUES(1, '與');
stmt.execute(sql);# 3. 查询
mysql SELECT * FROM test;
+----+--------------+
| id | name |
+----+--------------+
| 1 | ??? |
+----+--------------+修复很简单:在JDBC URL里加上characterEncoding=UTF-8,重新插入,数据就正常了。
但如果是生产环境,数据已经污染了,只能从备份恢复,或者写脚本用正则替换“?”为“與”(如果业务上能确定原始值)。
规避建议全链路统一UTF-8:从浏览器到数据库,每一层都显式指定,不要依赖默认值。
用utf8mb4:MySQL建库建表一律用utf8mb4,别省那一个字符的宽度。
单元测试覆盖编码:写个测试,模拟GBK环境下的JDBC连接,验证数据完整性。坑二:ORM框架的隐式转换陷阱
现象描述
用MyBatis或Hibernate,明明数据库里存的是“與”,实体类里定义的字段类型是String,但查出来就是“与”或者空值。
这种坑更隐蔽,因为单元测试在开发机上(通常UTF-8环境)跑得好好的,一上生产(Linux,默认POSIX编码)就炸。
根本原因
ORM框架在映射时,会调用ResultSet.getString()获取数据。这个方法依赖JDBC驱动的编码配置。
但更深层的问题是:Java String内部是UTF-16编码。当JDBC驱动从字节流解码为Java String时,如果编码配置错误,解码出的String本身就是错的。
另一个常见坑:MyBatis的TypeHandler。如果你自定义了TypeHandler,但没有正确处理编码,就会出问题。比如你写了一个StringToChineseTraditionalTypeHandler,里面硬编码了new String(bytes, GBK),那在UTF-8环境下必然出错。
正确写法对比
错误写法:自定义TypeHandler硬编码编码
// 错误:TypeHandler里硬编码GBK
@MappedTypes(String.class)
public class BadTypeHandler extends BaseTypeHandlerString {@Overridepublic void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {ps.setString(i, parameter);}@Overridepublic String getNullableResult(ResultSet rs, String columnName) throws SQLException {byte[] bytes = rs.getBytes(columnName);// 致命错误:硬编码GBKreturn new String(bytes, GBK);}
}正确写法:依赖JDBC驱动的编码配置
// 正确:让JDBC驱动处理编码
@MappedTypes(String.class)
public class GoodTypeHandler extends BaseTypeHandlerString {@Overridepublic void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {ps.setString(i, parameter);}@Overridepublic String getNullableResult(ResultSet rs, String columnName) throws SQLException {// 正确:直接调用getString,依赖驱动层编码return rs.getString(columnName);}
}复现与修复代码
复现步骤:开发机(Windows,默认GBK):用MyBatis插入“與”,查询正常。
部署到Linux服务器(默认POSIX,等效UTF-8):查询出来变成乱码。
检查MyBatis配置,发现自定义TypeHandler里用了new String(bytes, GBK)。修复:删除自定义TypeHandler,或者改为调用rs.getString()。
如果必须自定义TypeHandler,比如需要做繁简转换,应该这样写:
// 正确:使用java.text.Normalizer做Unicode标准化
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {String raw = rs.getString(columnName);if (raw == null) return null;// 如果需要繁简转换,用专门的库,如OpenCCreturn OpenCC4J.convertToTraditional(raw);
}规避建议不要硬编码编码:TypeHandler里永远不要出现new String(bytes, GBK)或new String(bytes, UTF-8),让JDBC驱动处理。
繁简转换用专业库:如果需要处理繁简转换,用OpenCC(官方源码仓库),不要用正则替换,正则替换会漏掉很多异体字。
CI/CD环境一致性:确保开发、测试、生产环境的JVM默认编码一致,或者在启动参数里强制指定-Dfile.encoding=UTF-8。坑三:跨服务传输时的JSON序列化陷阱
现象描述
微服务架构下,A服务调用B服务,传递包含“與”的JSON字符串,B服务接收后变成乱码。
这种坑在REST API和gRPC里都常见。特别是用Jackson或Gson序列化时,如果没配置对,就会出问题。
根本原因
JSON规范本身是UTF-8编码的,但序列化库在处理非ASCII字符时,有两种策略:转义:把“與”转义成\u8207。
直接写入:把UTF-8字节序列直接写入JSON。如果A服务用策略1,B服务用策略2,或者中间经过一个代理服务器(如Nginx)做了转义,就会出现不一致。
更隐蔽的是:某些序列化库在遇到无法识别的字符时,会直接丢弃或替换成“?”。比如Gson默认会把非ASCII字符转义,但如果你配置了disableHtmlEscaping(),又会改变行为。
正确写法对比
错误写法:依赖序列化库默认行为
// 错误:A服务用Jackson默认配置
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(new Data(與));
// 输出:{name:\u8207}// B服务用Gson默认配置
Gson gson = new Gson();
Data data = gson.fromJson(json, Data.class);
// 如果Gson版本旧,可能解析失败或乱码正确写法:统一配置,禁用转义
// 正确:A服务
ObjectMapper mapper = new ObjectMapper();
mapper.configure(JsonGenerator.Feature.ESCAPE_NON_ASCII, false);
String json = mapper.writeValueAsString(new Data(與));
// 输出:{name:與} // 直接写入UTF-8字节// B服务
Gson gson = new GsonBuilder().disableHtmlEscaping().create();
Data data = gson.fromJson(json, Data.class);
// 正常解析复现与修复代码
复现步骤:A服务用Jackson默认配置序列化“與”,得到{name:\u8207}。
中间经过一个日志系统,日志系统把\u8207还原成“與”,但又用GBK编码写入日志文件。
B服务从日志系统读取JSON,解析失败。修复:统一所有服务的序列化配置,禁用非ASCII转义。
中间件(日志系统、消息队列)必须支持UTF-8透传。
如果必须经过GBK环境,在边界处做显式转换:// 在边界处显式转换
String json = {\name\:\與\}; // 带BOM的UTF-8
String jsonWithoutBom = json.substring(1);
byte[] bytes = jsonWithoutBom.getBytes(UTF-8);
// 确保下游能处理UTF-8字节规避建议统一序列化库和配置:整个微服务体系内,统一用Jackson或Gson,并统一配置。
禁用非ASCII转义:在JSON传输中,直接写入UTF-8字节,不要用\uXXXX转义。
中间件UTF-8透传:确保Nginx、Kafka、RabbitMQ等中间件都支持UTF-8,不要做二次编码。总结与互动
这三个坑,编码混用、ORM隐式转换、JSON序列化陷阱,覆盖了“与的繁体”在开发全流程中的主要风险点。
记住几个铁律:全链路UTF-8:从浏览器到数据库,每一层都显式指定。
用utf8mb4:MySQL建库建表一律用utf8mb4。
不要硬编码编码:TypeHandler、序列化器里,让框架处理编码。
繁简转换用专业库:OpenCC是最稳的选择,官方源码仓库 里有很多企业级案例。最后问一句:你公司项目里是怎么处理繁简字符的?有没有踩过更隐蔽的坑?欢迎评论区聊聊。