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

Leave a Comment

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.