

追根溯源,彻底搞清楚 Mysql JDBC 对 UTF-8 的支持
source link: https://mp.weixin.qq.com/s/KLJ4AJ3C0sSDoZ-lR61DgA
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

1.Mysql 如何支持 UTF8?
1.1.Mysql Server 端配置
原来 mysql 支持的 utf8 编码最大字符长度为 3 字节,如果遇到 4 字节的宽字符就会插入异常了。
Mysql 从 5.5.3 开始支持,通过 utf8mb4(UTF-8most bytes 4) 字符集支持 4-byte 的 UTF8 字符。
1.2.JDBC 对 utf8mb4 的支持
在服务端支持 utf8mb4 之后, JDBC 客户端也相应的进行了升级。从笔者最近的实践来看,建议使用 5.1.47 以上版本。
官方对 JDBC 驱动的说明如下:
JDBC client 与 Mysqlserver 默认是自动进行检测的。如果服务器端指定了 character_set_server 变量 , 则 JDBC 驱动会自动使用该字符集 ( 在不指定 JDBC URL 参数 characterEncoding 和 connectionCollation 的情况下 ) 。
可以通过 characterEncoding ( 该参数值是使用 Java 风格的形式指定 . 例如 UTF-8 ) 来进行手工指定 , 而不是自动检测。
为了在 MySQL JDBC 驱动版本 5.1.46 及之前的版本中使用 utf8mb4, 则服务器端必须配置 character_set_server=utf8mb4, 否则 JDBC URL 参数 characterEncoding=UTF-8 表示的是 MySQL 的 utf8, 而不是 utf8mb4 。
2. Mysql Server 不重启无法使用 utf8mb4 的分析
然而,在笔者进行测试过程中发现,不同版本 JDBC 驱动在 Mysql Server 设置了字符集参数“重启 / 不重启”不同情况下,能否支持 utf8mb4 有不同的表现。
2.1.MysqlServer 重启与否,不同 JDBC 版本的表现
Mysql JDBC 客户端 (MysqlConnector-j) 在不同的版本中对字符集的支持有一定差异。版本的分界线在 5.1.46 和 5.1.47 。
测试过程中,在重启 Server 情况下字符集都可以生效,而不重启 Server 的情况下只有 5.1.47 在客户端设置了字符集情况下才生效。具体情况如下表:
JDBC 客户端
Mysql Server
版本
characterEncoding 参数
不重启
重启
5.1.46
UTF-8
×
√
未设置
×
√
5.1.47
UTF-8
√
√
未设置
×
√
2.2.Mysql Server 是否重启,到底会影响什么?
官方文档中提到, Server 端的 character_set_server=utf8mb4 设置完成后,客户端如果没有配置“ characterEncoding ”会使用服务端配置的 utf8mb4 字符集。
那为什么 Mysql Server 重启和不重启,会对字符集有影响呢?
2.2.1. MysqlIO.serverCharsetIndex 的使用
从 JDBC 驱动的源码中可以看到,在 com.mysql.jdbc.ConnectionImpl 类的 configureClientCharacterSet() 设置字符集方法中用到了 MysqlServer 返回的服务端字符集,该字符集参数存储于 ” io ” 成员变量的 ” serverCharsetIndex ” 属性中。
this.io.serverCharsetIndex
2.2.2. MysqlIO.serverCharsetIndex 的获取
对于 serverCharsetIndex 的赋值,是在 com.mysql.jdbc.MysqlIO.doHandshake() 方法中。
/**
* Initialize communications with the MySQL server. Handles logging on, and
* handling initial connection errors.
*/
void doHandshake(String user, String password, String database) throws SQLException{
......
/* New protocol with 16 bytes to describe server characteristics */
// read character set (1 byte)
this.serverCharsetIndex= buf.readByte() &0xff;
......
}
从该方法的名称即可发现,在 JDBC 客户端与 Mysqlserver 进行握手通讯的时候,已经完成了 server 相关信息的获取。
通过 wireshark 抓取到的交互报文如下:
通过 JDBC 报文规范,解析后的报文内容如下,可以看到在未重启 Mysql Server 的情况下,返回的“ character set ”还是“ 33 ”,即“ utf8 ”。
字段
取值
报文
protocolVersion
10
0a
serverVersion
5.7.18-log
35 2e 37 2e 31 38 2d 6c 6f 67 00
threadId
4751059
d3 7e 48 00
auth-plugin-data-part
mDv>JkJ
05 6d 44 76 3e 4a 6b 4a
filler ([00])
00
serverCapabilities
63487
ff f7
character set
33
21
serverStatus
2
02
“33”映射为“utf8”,在“com.mysql.jdbc.CharsetMapping”类中指定的字符集映射,源码如下:
collation[33] = new Collation(33, "utf8_general_ci", 1, MYSQL_CHARSET_NAME_utf8);
2.2.3. Mysql 不重启,为什么返回报文中还是 utf8 ?
MysqlServer 在执行send_server_handshake_packet()方法中,返回给客户端的字符集从“default_charset_info”变量中获取。
static boolsend_server_handshake_packet(MPVIO_EXT *mpvio, const char *data, uintdata_len)
{
int2store(end, mpvio->client_capabilities);
/* write server characteristics: up to 16 bytes allowed */
end[2]= (char) default_charset_info->number;
int2store(end + 3, mpvio->server_status[0]);
}
default_charset_info 仅在MySQL Server启动的时候进行初始化使用,其值为 character-set-server 的参数值。修改正在运行的数据库的编码并不会触发 default_charset_info 的更新, 返回给客户端协议包中的编码就还是以前的编码。
2.3. 使用 5.1.46 及之前版本、不重启 Mysql Server 的解决方案
应用如果使用 JDBC 的 5.1.46 以及之前版本,由于种种原因无法重启 Mysql Server 的情况下同时又不升级 JDBC 驱动到 5.1.47 的情况下,如果需要支持 utf8mb4 ,则可以在 JDBC 链接字符串中添加“ com.mysql.jdbc.faultInjection.serverCharsetIndex=45 ”,直接指定“服务器字符集”。
具体参数设置如下:
jdbc:mysql://xxx:3306?com.mysql.jdbc.faultInjection.serverCharsetIndex=45
为什么设置为“45”,则是在“com.mysql.jdbc.CharsetMapping”类中指定的字符集映射,源码如下:
collation[45] = new Collation(45, "utf8mb4_general_ci", 1, MYSQL_CHARSET_NAME_utf8mb4);
当指定了该参数后,com.mysql.jdbc.ConnectionImpl的configureClientCharacterSet()方法会覆盖从Mysql server获取到的字符集,具体源码如下:
private booleanconfigureClientCharacterSet(booleandontCheckServerMatch) throws SQLException{
//从设置参数取值覆盖Mysql Server返回的字符集
// Fault injection for testing server character set indices
if (this.props!= null &&
this.props.getProperty("com.mysql.jdbc.faultInjection.serverCharsetIndex") != null) {
this . io . serverCharsetIndex = Integer . parseInt ( this . props .
));
}
}
}
3. 源码解析,解密 JDBC 不同版本的区别
3.1.JDBC5.1.47 官方升级说明
官方升级说明中强调,只要
JDBC 链接字符串中指定了“
characterEncoding=UTF-8
”,即使
MysqlServer 设置了其它字符集,客户端也会使用
utf8mb4 。
Functionality Added or Changed
-
The value
UTF-8
for the connection propertycharacterEncoding
now maps to theutf8mb4
character set on the server and, for MySQL Server 5.5.2 and later,characterEncoding=UTF-8
can now be used to set the connection character set toutf8mb4
even ifcharacter_set_server
has been set to something else on the server. (Before this change, the server must havecharacter_set_server=utf8mb4
for Connector/J to use that character set.) -
Also, if the connection property
connectionCollation
is also set and is incompatible with the value ofcharacterEncoding
,characterEncoding
will be overridden with the encoding corresponding toconnectionCollation
.
3.2.JDBC5.1.46 字符集设置源码解析
5.1.46 中,通过 Mysql 服务器返回的“ charset ”设置是否使用 ”utf8mb4” 字符集,可参考如下流程图:
源码参考 com.mysql.jdbc.ConnectionImpl 类的 configureClientCharacterSet() 方法,如下所示:
if (getUseUnicode()) {
//1. 如果JDBC链接字符串指定了”characterEncoding=UTF-8”,根据Mysql Server返回的字符集确定是使用utf8或者utf8mb4
if (realJavaEncoding != null) {
// Now, inform the server what character set we will be using from now-on...
if (realJavaEncoding.equalsIgnoreCase( "UTF-8" ) || realJavaEncoding.equalsIgnoreCase( "UTF8" )) {
boolean utf8mb4Supported = versionMeetsMinimum(5, 5, 2);
Boolean useutf8mb4 = utf8mb4Supported && (CharsetMapping.UTF8MB4_INDEXES.contains( this.io.serverCharsetIndex ));
}
} else if (getEncoding() != null) {
//2. 如果JDBC链接字符串未指定”characterEncoding”参数,则会使用Mysql Server返回字符集
String mysqlCharsetName = getServerCharset();
if (getUseOldUTF8Behavior()) {
mysqlCharsetName = "latin1";
}
}
}
3.3.JDBC5.1.47 字符集设置源码解析
5.1.47 中,如果 JDBC 链接字符串中指定了 ”characterEncoding=UTF-8” ,则会默认使用 utf8mb4 字符集,不使用 server 返回的字符集属性;否则,未指定使用 server 返回字符集。
源码参考 com.mysql.jdbc.ConnectionImpl 类的 configureClientCharacterSet() 方法,如下所示:
if (getUseUnicode()) {
//1. 如果JDBC链接字符串指定了”characterEncoding=UTF-8”,则会默认使用utf8mb4字符集
` (realJavaEncoding != null) {
// Now, inform the server what character set we will be using from now-on...
if (realJavaEncoding.equalsIgnoreCase( "UTF-8" ) || realJavaEncoding.equalsIgnoreCase( "UTF8" )) {
// charset names are case-sensitive
boolean utf8mb4Supported = versionMeetsMinimum(5, 5, 2);
String utf8CharsetName = connectionCollationSuffix.length() > 0 ? connectionCollationCharset
: (utf8mb4Supported ? "utf8mb4" : "utf8") ;
}
} else if (getEncoding() != null) {
//2. 如果JDBC链接字符串未指定”characterEncoding”参数,则会使用Mysql Server返回字符集
// Tell the server we'll use the server default charset to send our queries from now on....
String mysqlCharsetName = connectionCollationSuffix.length() > 0 ?
connectionCollationCharset : (getUseOldUTF8Behavior() ?
"latin1 " : getServerCharset() );
}
}
4. 其他字符集问题
4.1. 字符集参数该使用 utf8,UTF8,utf-8,UTF-8 中的哪个?
在 JDBC 链接字符串中,通过“ characterEncoding ”设置字符集,那么我们应该选择“ utf8 、 UTF8 、 utf-8 、 UTF-8 ”中的哪一个?
实际上,上述 4 种设置方式都可以。
在 JDBC 的源码“ com.mysql.jdbc.ConnectionImpl.configureClientCharacterSet() ”方法中,对这四种配置方式都进行了兼容。
//兼容UTF-8,utf-8,UTF8,utf8
)) {
......
}
4.2.characterEncoding 设置为 utf8mb4 为什么报错?
有人会尝试将 JDBC 链接字符串“ characterEncoding ”设置为“ utf8mb4 ”,以此来支持 UTF8 ,却收获了如下报错:
java.sql.SQLException: Unsupported character encoding 'UTF-8mb4'.
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:965)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:898)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:887)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:861)
at com.mysql.jdbc.ConnectionPropertiesImpl.postInitialization(ConnectionPropertiesImpl.java:2575)
其实,在 JDBC 的“ com.mysql.jdbc.ConnectionPropertiesImpl ”类中,对配置的字符集通过“ StringUtils . getBytes ( testString , testEncoding ) ”进行了检查,代码如下:
protected void postInitialization() throws SQLException{
if (testEncoding!= null) {
// Attempt to use the encoding, and bail out if it can't be used
try {
String testString= "abc";
StringUtils.getBytes(testString, testEncoding);
} catch (UnsupportedEncodingExceptionUE) {
throw SQLError.createSQLException(
Messages.getString("ConnectionProperties.unsupportedCharacterEncoding", new Object[] { testEncoding}),
"0S100", getExceptionInterceptor());
}
}
}
而 com.mysql.jdbc.StringUtils 最终调用了 java 标注类库里“ java.nio.charset.Charset ”类的 findCharset 方法,“ Charset . forName (alias) ”方法无法找到“ utf8mb4 ”。
static Charset findCharset(String alias) throws UnsupportedEncodingException{
try {
Charset cs = charsetsByAlias.get(alias);
if (cs == null) {
cs = Charset.forName(alias);
}
...... }
通过如下代码可以打印系统支持的字符集:
. availableCharsets ();
for ( String alias : map.keySet()) {
//输出字符集的别名
System . out .println( alias);
}
在 windows 64 位操作系统, jdk8 中执行后获得字符集如下:
GB2312
GBK
IBM-Thai
IBMxxxxxx
ISO-2022-xx
ISO-8859-xxx
JIS_X0201
JIS_X0212-1990
KOI8-R
KOI8-U
Shift_JIS
TIS-620
US-ASCII
UTF-16
UTF-16BE
UTF-16LE
UTF-32
UTF-32BE
UTF-32LE
UTF-8
Big5
Big5-HKSCS
CESU-8
EUC-JP
EUC-KR
GB18030
windows-xxxx
x-Big5-HKSCS-2001
x-Big5-Solaris
x-euc-jp-linux
x-EUC-TW
x-eucJP-Open
x-IBMxxxxx
x-ISCII91
x-ISO-2022-CN-CNS
x-ISO-2022-CN-GB
x-iso-8859-11
x-JIS0208
x-JISAutoDetect
x-Johab
x-MacArabic
x-Macxxxxxxxx
x-MS932_0213
x-MS950-HKSCS
x-MS950-HKSCS-XP
x-mswin-936
x-PCK
x-SJIS_0213
x-UTF-16LE-BOM
X-UTF-32BE-BOM
X-UTF-32LE-BOM
x-windows-50220
x-windows-50221
x-windows-874
x-windows-949
x-windows-950
x-windows-iso2022jp
Recommend
-
26
【笑出鹅叫】昆汀在中国拍《杀死比尔》时,跟着剧组工作人员学会了说“牛逼”,说的形象生动极其到位。不过当昆导好奇心发作,追根溯源,纠结牛逼的真实含义后,就突然不知道要怎么面对这个词了
-
19
Executing Multiple Statement MySQL Scripts in Java JDBC You're running your Integration Tests, populate your MySQL database with a multi-statement MySQL Script and see the following response: You have an error in...
-
4
【快讯】 1月13日, incaseformat病毒在全网集中爆发,中毒用户C盘以外所有文件被删除。火绒工程师在该事件首次报告的逆向分析中,推测病毒程序制作存在错误,导致其爆发时间推迟到今年1月13日。而同行厂商也在后续...
-
9
JDBC简介JDBC(Java DataBase Connectivity)即Java数据库连接,是Java语言中用来规范客户端程序如何来访问数据库的应用程序接口,提供了诸如查询和更新数据库中数据的方法。 一般格式: jdbc://driver://host:port/database?配置name...
-
7
写在前面: 大家好,我是时光。 今天给大家带来的是排序算法中的堆排序,这种排序跟二叉树相关。我采用图解方式讲解,争取写透彻。话不多说,开始! 思维导图:
-
5
JDBC编程是什么?
-
3
How to trace JDBC Statements performance in MySQL 6 April 2022 by F.Marchioni In this quick article we will learn an handy option which is...
-
6
给 Lua 在 windows 下换上 utf-8 文件名支持 最近在 windows 做开发比较多,lua 原生库使用的都是 C 标准库中的函数,比如文件操作就是用的 fopen 打...
-
4
MySQL und UTF-8
-
4
MySQL之JDBC 一、JDBC是什么 Java DatabaseConnectivity (java语言连接数据库) 二、JDBC的本质 JDBC是SUN公司制定的一套接口(interface)。 接口都有调用者和实现...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK