• MySQL 的jdbc为何不能正确的编码汉字


    作者:emu(黄希彤)从mysql4.1的connector/J(3.1.?版)就有了汉字编码问题。http://www.csip.cn/new/st/db/2004/0804/428.htm 里面介绍了一种解决方法。但是我现在使用的是mysql5.0beta和Connector/J(mysql-connector-java-3.2.0-alpha版),原来的方法不适用了,趁这个机会对Connector/J的源码做一点分析吧。
    mysql-connector-java-3.2.0-alpha的下载地址:http://dev.mysql.com/get/Downloads/Connector-J/mysql-connector-java-3.2.0-alpha.zip/from/pick

    3.2版的connectotJ已经不象 http://www.csip.cn/new/st/db/2004/0804/428.htm 上面描述的样子了。原来的“com.mysql.jdbc.Connecter.java” 已经不复存在了,“this.doUnicode = true; ”在com.mysql.jdbc.Connection.java 中变成了setDoUnicode(true),而这个调用在Connection类中的两次调用都是在checkServerEncoding 方法中(2687,2716),而checkServerEncoding 方法只由 initializePropsFromServer 方法调用            //
                // We only do this for servers older than 4.1.0, because
                // 4.1.0 and newer actually send the server charset
                // during the handshake, and that's handled at the
                // top of this method...
                //
                if (!clientCharsetIsConfigured) {
                    checkServerEncoding();
                }
    它说只在4.1.0版本以前才需要调用这个方法,对于mysql5.0,根本就不会进入这个方法

    从initialize里面找不到问题,直接到ResultSet.getString里面跟一下看看。一番努力之后终于定位到了出错的地方:com.mysql.jdbc.SingleByteCharsetConverter

    193 /**
    194  * Convert the byte buffer from startPos to a length of length
    195  * to a string using this instance's character encoding.
    196  *
    197  * @param buffer the bytes to convert
    198  * @param startPos the index to start at
    199  * @param length the number of bytes to convert
    200  * @return the String representation of the given bytes
    201  */
    202 public final String toString(byte[] buffer, int startPos, int length) {
    203     char[] charArray = new char[length];
    204     int readpoint = startPos;
    205
    206     for (int i = 0; i < length; i++) {
    207         charArray[i] = this.byteToChars[buffer[readpoint] - Byte.MIN_VALUE];
    208         readpoint++;
    209     }
    210
    211     return new String(charArray);
    212 }

    在进入这个方法的时候一切都还很美好,buffer里面放着从数据库拿来的正确的Unicode数据(一个汉字对应着两个byte)
    刚进入方法,就定义了一个char数组,其实相当于就是String的原始形式。看看定义了多少个字符:
    char[] charArray = new char[length];
    嘿嘿,字符数和byte数组长度一样,也就是说每个汉字将转换成两个字符。
    后面的循环是把byte数组里面的字符一个一个转换成char。一样的没有对unicode数据进行任何处理,简单的就把一个汉字转成两个字符了。最后用这个字符数组来构造字符串,能不错吗?把toString方法改造一下:

        public final String toString(byte[] buffer, int startPos, int length) {
            return new String(buffer,startPos,length);
        }

    这是解决问题最简单的办法了吧。但是我们还可以追究一下原因,看看有没有更好的解决方法。

    这个toString方法其实是写来转换所谓的SingleByteCharset,也就是单字节字符用的。用这个方法而不直接new String,目的是提高转换效率,可是现在为什么在转换unicode字符的时候被调用了呢?一路跟踪出来,问题出在com.mysql.jdbc.ResultSet.java的extractStringFromNativeColumn里面:

        /**
      * @param columnIndex
      * @param stringVal
      * @param mysqlType
      * @return
      * @throws SQLException
      */
     private String extractStringFromNativeColumn(int columnIndex, int mysqlType) throws SQLException {
      if (this.thisRow[columnIndex - 1] instanceof String) {
          return (String) this.thisRow[columnIndex - 1];
      }

      String stringVal = null;
      
      if ((this.connection != null) && this.connection.getUseUnicode()) {
          try {
              String encoding = this.fields[columnIndex - 1].getCharacterSet();

              if (encoding == null) {
                  stringVal = new String((byte[]) this.thisRow[columnIndex -
                          1]);
              } else {
                  SingleByteCharsetConverter converter = this.connection.getCharsetConverter(encoding);

                  if (converter != null) {
                      stringVal = converter.toString((byte[]) this.thisRow[columnIndex -
                              1]);
                  } else {
                      stringVal = new String((byte[]) this.thisRow[columnIndex -
                              1], encoding);
                  }
              }
          } catch (java.io.UnsupportedEncodingException E) {
              throw new SQLException(Messages.getString(
                      "ResultSet.Unsupported_character_encoding____138") //$NON-NLS-1$
                   + this.connection.getEncoding() + "'.", "0S100");
          }
      } else {
          stringVal = StringUtils.toAsciiString((byte[]) this.thisRow[columnIndex -
                  1]);
      }

      // Cache this conversion if the type is a MySQL string type
      if ((mysqlType == MysqlDefs.FIELD_TYPE_STRING) ||
              (mysqlType == MysqlDefs.FIELD_TYPE_VAR_STRING)) {
          this.thisRow[columnIndex - 1] = stringVal;
      }

      return stringVal;
     }

    这个方法从fields里面取得编码方式。而fields是在MysqlIO类里面根据数据库返回的数据解析处理字符集代号,这里取回的是数据库的默认字符集。所以如果你在创建数据库或者表的时候指定了字符集为gbk(CREATE DATABASE dbname DEFAULT CHARSET=GBK;)那么恭喜恭喜,你取回的数据不需要再行编码了。

    但是当时我在建数据库表的时候没有这么做(也不能怪我,是bugzilla的checksetup.pl自己创建的库啊),所以现在fields返回的不是我们期望的GBK而是mysql默认的设置ISO8859-1。于是ResultSet就拿ISO8859-1来编码我们GBK编码的数据,这就是为什么我们从getString取得数据以后先getBytes("ISO8859-1")再new String就可以把汉字变回来了。

    其实我们指定了jdbc的编码方式的情况下,jdbc应该明白我们已经不打算使用数据库默认的编码方式了,因此ResultSet应该忽略原来数据库的编码方式的,否则我们设置的编码方式还有什么用呢?可是mysql偏偏就选择了忽略我们的选择而用了数据库的编码方式。解决方法很简单,把mysql那段自作聪明的判断编码方式的代码全部干掉:

        /**
      * @param columnIndex
      * @param stringVal
      * @param mysqlType
      * @return
      * @throws SQLException
      */
     private String extractStringFromNativeColumn(int columnIndex, int mysqlType) throws SQLException {
      if (this.thisRow[columnIndex - 1] instanceof String) {
          return (String) this.thisRow[columnIndex - 1];
      }

      String stringVal = null;
      
      if ((this.connection != null) && this.connection.getUseUnicode()) {
          try {
    //          String encoding = this.fields[columnIndex - 1].getCharacterSet();
              String encoding = null;
              if (encoding == null) {
                  stringVal = new String((byte[]) this.thisRow[columnIndex -
                          1]);
              } else {
                  SingleByteCharsetConverter converter = this.connection.getCharsetConverter(encoding);

                  if (converter != null) {
                      stringVal = converter.toString((byte[]) this.thisRow[columnIndex -
                              1]);
                  } else {
                      stringVal = new String((byte[]) this.thisRow[columnIndex -
                              1], encoding);
                  }
              }
          } catch (java.io.UnsupportedEncodingException E) {
              throw new SQLException(Messages.getString(
                      "ResultSet.Unsupported_character_encoding____138") //$NON-NLS-1$
                   + this.connection.getEncoding() + "'.", "0S100");
          }
      } else {
          stringVal = StringUtils.toAsciiString((byte[]) this.thisRow[columnIndex -
                  1]);
      }

      // Cache this conversion if the type is a MySQL string type
      if ((mysqlType == MysqlDefs.FIELD_TYPE_STRING) ||
              (mysqlType == MysqlDefs.FIELD_TYPE_VAR_STRING)) {
          this.thisRow[columnIndex - 1] = stringVal;
      }

      return stringVal;
     }


    好了,整个世界都清静了,现在不管原来的表是什么编码都按默认方式处理,绕过了爱出问题的针对ISO8859-1的加速代码。上面的toString也可以改回去了,不过改不改都无所谓,它没有机会被执行了。

    可是我的疑惑没有完全消除。数据库表定义的是ISO8859-1编码,为何返回回来的数据却又是GBK编码呢?而且这个编码并不随我在jdbc的url中的设定而改变,那么mysql是根据什么来决定返回回来的数据的编码方式呢?作者:emu(黄希彤)



    作者:emu(黄希彤)
    上面研究的只是Result.getString的编码问题。提交数据的时候有类似的编码问题,但是其原因就更复杂一些了。我发现这样做的结果是对的:

    pstmt.setBytes(1,"我们都是祖国的花朵".getBytes());

    而这样居然是错的:

    pstmt.setString(1,"我们都是祖国的花朵");


    一番努力之后把断点打到了MysqlIO的send(Buffer packet, int packetLen)方法里面:

                    if (!this.useNewIo) {
                        this.mysqlOutput.write(packetToSend.getByteBuffer(), 0,
                            packetLen);
                        this.mysqlOutput.flush();
                    } else {...

    字符串的编码在packetToSend.getByteBuffer()里面还是对的,但是送到数据库里面的时候就全部变成“???????”了。也就是说,数据库接收这组byte的时候重新进行了编码,而且是错误的编码。比较两种方式发送的byte数组,数据差异很小,基本上就是第0、4和16这三个byte的值会有些变化,看起来似乎第15、16个byte里面保存的是一个代表数据类型的int,估计就是这个标记,让mysql服务器对接收到的数据进行了再加工。但是源码里面对这些逻辑也没有写充分的注释(还是看jdk自己的源码比较舒服),看起来一头雾水,算了。作者:emu(黄希彤)

  • 相关阅读:
    Python统计字符串中出现次数最多的人名
    初探CORBA组件化编程
    shell脚本—基础知识,变量
    Java多线程--线程交替
    Qt中采用多线程实现Socket编程
    Python字符串格式化--formate()的应用
    JAVA中浅复制与深复制
    Python这些问题你会吗?
    PHP控制反转(IOC)和依赖注入(DI
    Go 语言指针
  • 原文地址:https://www.cnblogs.com/stonehuang/p/6603233.html
Copyright © 2020-2023  润新知