Harry_ray
作者Harry_ray2020-11-05 15:32
系统架构师, 某银行

追根溯源,彻底搞清楚Mysql JDBC对UTF-8的支持

字数 8985阅读 9298评论 0赞 0

1. Mysql 如何支持 UTF8?

1.1. Mysql Server 端配置

原来 mysql 支持的 utf8 编码最大字符长度为 3 字节,如果遇到 4 字节的宽字符就会插入异常了。

Mysql 从 5.5.3 开始支持,通过 utf8mb4(UTF-8 most 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 客户端 (Mysql Connector-j) 在不同的版本中对字符集的支持有一定差异。版本的分界线在 5.1.46 和 5.1.47 。

测试过程中,在重启 Server 情况下字符集都可以生效,而不重启 Server 的情况下只有 5.1.47 在客户端设置了字符集情况下才生效。具体情况如下表:

2.1. Mysql Server 是否重启,到底会影响什么?

官方文档中提到, Server 端的 character_set_server=utf8mb4 设置完成后,客户端如果没有配置“ characterEncoding ”会使用服务端配置的 utf8mb4 字符集。

那为什么 Mysql Server 重启和不重启,会对字符集有影响呢?

2.2.1. MysqlIO.serverCharsetIndex 的使用

从 JDBC 驱动的源码中可以看到,在 com.mysql.jdbc.ConnectionImpl 类的 configureClientCharacterSet() 设置字符集方法中用到了 Mysql Server 返回的服务端字符集,该字符集参数存储于 ” 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 客户端与 Mysql server 进行握手通讯的时候,已经完成了 server 相关信息的获取。

通过 wireshark 抓取到的交互报文如下:


通过 JDBC 报文规范,解析后的报文内容如下,可以看到在未重启 Mysql Server 的情况下,返回的“ character set ”还是“ 33 ”,即“ utf8 ”。

字段取值报文
protocolVersion100a
serverVersion5.7.18-log35 2e 37 2e 31 38 2d 6c 6f 67 00
threadId4751059d3 7e 48 00
auth-plugin-data-partmDv>JkJ05 6d 44 76 3e 4a 6b 4a
filler ([00]) 00
serverCapabilities63487ff f7
character set3321
serverStatus202

“ 33 ”映射为“ utf8 ”,在“ com.mysql.jdbc.CharsetMapping ”类中指定的字符集映射,源码如下:
collation[ 33 ] = new Collation( 33 , "utf8_general_ci" , 1 , MYSQL_CHARSET_NAME_utf8 );

2.2.3. Mysql 不重启,为什么返回报文中还是 utf8 ?

Mysql Server 在执行 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.getProperty("com.mysql.jdbc.faultInjection.serverCharsetIndex"));
}
}

3. 源码解析,解密 JDBC 不同版本的区别

3.1. JDBC5.1.47 官方升级说明

官方升级说明中强调,只要 JDBC 链接字符串中指定了“ characterEncoding=UTF-8 ”,即使 Mysql Server 设置了其它字符集,客户端也会使用 utf8mb4 。

Functionality Added or Changed
The value UTF-8 for the connection property characterEncoding now maps to the utf8mb4 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 to utf8mb4 even if character_set_server has been set to something else on the server. (Before this change, the server must have character_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 of characterEncoding, characterEncoding will be overridden with the encoding corresponding to connectionCollation.

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
if(realJavaEncoding.equalsIgnoreCase("UTF-8") || realJavaEncoding.equalsIgnoreCase("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);
     }
......
}

通过如下代码可以打印系统支持的字符集:

SortedMap<String, Charset> map = Charset.availableCharsets();
for (String alias : map.keySet()) {
    // 输出字符集的别名
    System.out.println(alias);
}

在 windows 64 位操作系统, jdk8 中执行后获得字符集如下:

如果觉得我的文章对您有用,请点赞。您的支持将鼓励我继续创作!

0

添加新评论0 条评论

Ctrl+Enter 发表

作者其他文章

相关文章

相关问题

相关资料

X社区推广