1.  Mysql如何支持UTF8?

端配置

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

Mysql从5.5.3开始支持,通过utf8mb4(UTF-8most bytes 4)字符集支持4-byte的UTF8字符。

对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有不同的表现。

重启与否,不同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

未设置

×

是否重启,到底会影响什么?

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

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

  MysqlIO.serverCharsetIndex的使用

从JDBC驱动的源码中可以看到,在com.mysql.jdbc.ConnectionImpl类的configureClientCharacterSet()设置字符集方法中用到了MysqlServer返回的服务端字符集,该字符集参数存储于”io”成员变量的”serverCharsetIndex”属性中。

this.io.serverCharsetIndex

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

dbvisualizer的mysql驱动 mysql8jdbc驱动_utf8mb4 utf8 区别

通过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 的更新, 返回给客户端协议包中的编码就还是以前的编码。

使用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 indicesif (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不同版本的区别

官方升级说明

     官方升级说明中强调,只要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 characterEncodingcharacterEncoding will  be overridden with the encoding corresponding to connectionCollation.

字符集设置源码解析

5.1.46中,通过Mysql服务器返回的“charset”设置是否使用”utf8mb4”字符集,可参考如下流程图:

dbvisualizer的mysql驱动 mysql8jdbc驱动_python3设置编码为utf8_02

源码参考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";

   }

}

}

字符集设置源码解析

5.1.47中,如果JDBC链接字符串中指定了”characterEncoding=UTF-8”,则会默认使用utf8mb4字符集,不使用server返回的字符集属性;否则,未指定使用server返回字符集。

dbvisualizer的mysql驱动 mysql8jdbc驱动_utf8mb4 utf8 区别_03

源码参考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.  其他字符集问题

字符集参数该使用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,utf8if(realJavaEncoding.equalsIgnoreCase("UTF-8")  || realJavaEncoding.equalsIgnoreCase("UTF8"))  {
  ......}

设置为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 usedtry {    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