原来 mysql 支持的 utf8 编码最大字符长度为 3 字节,如果遇到 4 字节的宽字符就会插入异常了。
Mysql 从 5.5.3 开始支持,通过 utf8mb4(UTF-8 most bytes 4) 字符集支持 4-byte 的 UTF8 字符。
在服务端支持 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。
然而,在笔者进行测试过程中发现,不同版本 JDBC 驱动在 Mysql Server 设置了字符集参数“重启 / 不重启”不同情况下,能否支持 utf8mb4 有不同的表现。
Mysql JDBC 客户端 (Mysql Connector-j) 在不同的版本中对字符集的支持有一定差异。版本的分界线在 5.1.46 和 5.1.47 。
测试过程中,在重启 Server 情况下字符集都可以生效,而不重启 Server 的情况下只有 5.1.47 在客户端设置了字符集情况下才生效。具体情况如下表:
官方文档中提到, Server 端的 character_set_server=utf8mb4 设置完成后,客户端如果没有配置“ characterEncoding ”会使用服务端配置的 utf8mb4 字符集。
那为什么 Mysql Server 重启和不重启,会对字符集有影响呢?
从 JDBC 驱动的源码中可以看到,在 com.mysql.jdbc.ConnectionImpl 类的 configureClientCharacterSet() 设置字符集方法中用到了 Mysql Server 返回的服务端字符集,该字符集参数存储于 ” io ” 成员变量的 ” serverCharsetIndex ” 属性中。
this.io.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 ”。
字段 | 取值 | 报文 |
---|---|---|
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 );
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 的更新 , 返回给客户端协议包中的编码就还是以前的编码。
应用如果使用 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"));
}
}
官方升级说明中强调,只要 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.
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";
}
}
}
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());
}
}
在 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")) {
......
}
有人会尝试将 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);
}
如果觉得我的文章对您有用,请点赞。您的支持将鼓励我继续创作!
赞0
添加新评论0 条评论