33

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

 4 years ago
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.
neoserver,ios ssh client

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 抓取到的交互报文如下:

eANZviv.png!mobile

通过 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 .

getProperty( "com.mysql.jdbc.faultInjection.serverCharsetIndex"

));

}

}

}

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

     3.1.JDBC5.1.47 官方升级说明

官方升级说明中强调,只要 JDBC 链接字符串中指定了“ characterEncoding=UTF-8   ”,即使 MysqlServer 设置了其它字符集,客户端也会使用 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” 字符集,可参考如下流程图:

JBnYVjj.png!mobile

源码参考 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 返回字符集。

zmYRzqb.png!mobile

源码参考 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 中执行后获得字符集如下:

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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK