问题: server端的charset设置如何影响程序的正确性? jdbc url里的characterEncoding呢?
研究方法: 执行一条带有中文字面量的查询语句,并断点跟踪mysql jdbc driver的源码,重点关注字符集的设定、字节与字符之间的互转
研究工具:
1.打开driver的sql日志功能: jdbc:mysql://…/…?
profileSQL=true,以查看c/s之间的所有sql
2.使用wireshark抓取c/s之间的通讯数据包
软件版本:mysql server: 5.1, jdbc driver: connector/j 5.1.8
测试数据及程序:
1. 数据库的character_set_server = gbk, 但character_set_client = utf8, character_set_results = utf8, character_set_connection = utf8
2. 数据库有一张表,这张表里有一个字段name, name字段采用与服务器同样的编码(即gbk),且表里有一条记录,它的name值为汉字“一“
3. 程序伪码:
DriverManager.getConnection(…); //建立连接
String name = executeQuery("select * from … where name = ‘一’"); //查询
System.out.println(name);
具体测试场景及测试结果:
场景一: jdbc url里不指定characterEncoding
1. c/s握手时,driver从服务端回送的报文中得知server charset index = 28 (代表GBK)。这时还没有执行任何SQL (见MysqlIO.doHandShake())
2. 连接后driver会执行下面的SQL,以获知服务器端的配置,包括charset配置(见ConnectionImpl.loadServerVariables() )
SHOW VARIABLES WHERE … Variable_name = ‘character_set_client’ OR Variable_name = ‘character_set_connection’ … OR Variable_name = ‘character_set_server’ … OR Variable_name = ‘character_set_results’
3. 接下来会执行 SET NAMES gbk (见ConnectionImpl.configureClientCharacterSet())
a."gbk"这个值是连接握手时(即第1步)取来的
b. driver发现连接握手时获得的编码(gbk)与server端配置的连接级编码(character_set_client/character_set_connection = utf8)中的并不一致,于是才执行 set names gbk
c. 而SET NAMES gbk 相当于
SET character_set_client = gbk;
SET character_set_results = gbk;
SET character_set_connection = gbk;
也就是说,本次会话使用的charset将覆盖server端的相关配置
4.然后又执行:SET character_set_results = NULL (ConnectionImpl.configureClientCharacterSet())
a.driver发现server上的character_set_results不为空,为了防止server在回送结果作编码转换,将character_set_results置为空
5. 接下来执行正式的sql: select name from t where name = ‘一’
a. 用wireshark捕捉到的字节流是: select name from ali_activity where name = ‘\xd2\xbb’ #d2bb是"一"的GBK编码(16进制)
b. sql字符串转字节数组的代码可见PreparedStatement$ParseInfo.<init>,其中使用了握手时获取到的编码gbk
6. 拿到的查询结果中
a.会包含charset 信息 (见MysqlIO.unpackField()方法里的charSetNumber变量), charset = gbk
b.这个charset值会被放到resultSet的metadata中
c.resultSet拿到的name字段的byte数组为-46, -49,即d2, bb的补码。
d.最后,使用metadata里的gbk编码,将{-46, -49}变成字符串“一” (见 ResultSetImpl.getStringInternal())
7.最后程序打印的结果是“一”,程序是正确的
结论:
jdbc url未指定编码时,
1.driver使用的连接级charset配置和服务器端的character_set_server是一致的
2.彻底忽略了服务器端的character_set_client, character_set_connection
3.character_set_results也被忽略(设成了null),以保证服务端在回送结果前不做转码,以免节外生枝
场景二:jdbc url里指定characterEncoding=UTF-8
1. 连接后driver会执行SQL以获知服务器端的配置
2. driver从jdbc url中取得encoding=utf8
3. 不会执行 SET NAMES utf8,因为driver发现encoding(utf8)和服务器端的character_set_client/character_set_connection一致
4. 会执行:SET character_set_results = NULL
5. 接下来执行正式的sql: select name from t where name = ‘一’
a. 用wireshark捕捉到的字节流是: select name from ali_activity where name = ‘\xe4\xb8\x80’ #e4b880是"一"的UTF8编码(16进制)
6. 拿到的查询结果中
a.mysql包含的charset = gbk
b.resultSet拿到的name字段的byte数组是“一”的gbk编码
7.最后程序打印的结果是“一”,程序是正确的
结论:
若jdbc url中指定的编码与character_set_client/character_set_conn相同,
1.driver将使用url中指定的charset对sql进行编码,再发送给服务器
2.服务器将使用character_set_client/character_set_conn解码客户端的请求
3.character_set_results 被忽略(设成了null),服务端在回送结果时不做转码
场景三:jdbc url里指定characterEncoding=ISO8859_1
1. 连接后driver会执行SQL以获知服务器端的配置
2. driver从jdbc url中取得encoding=latin1
3. 接下来执行 SET NAMES latin1,因为driver发现encoding(latin1)和服务器端的character_set_client/character_set_connection(utf8)不一致
4. 会执行:SET character_set_results = NULL
5. 接下来执行正式的sql: select name from t where name = ‘一’
a. 用wireshark捕捉到的字节流是: select name from ali_activity where name = ‘?’ #latin1字符集无法识别汉字"一"的编码,只好用字符"?"的ascii码作为“一”的字节码
6. 很显然,拿到的查询结果将为空
7. 最后程序打印的结果是null,程序执行失败
结论:
若jdbc url中指定的字符集不支持sql中所传输的字符串字面量,就会导致错误的信息被传输到服务端,最终程序的执行结果不合预期