mysql jbdc driver的字符集有关问题 - 研究笔记

mysql jbdc driver的字符集问题 -- 研究笔记

问题
: 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中所传输的字符串字面量,就会导致错误的信息被传输到服务端,最终程序的执行结果不合预期