emu in blogjava

            BlogJava :: 首頁 :: 新隨筆 :: 聯系 :: 聚合  :: 管理 ::
            171 隨筆 :: 103 文章 :: 1052 評論 :: 2 Trackbacks

          作者: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(黃希彤)

          posted on 2005-06-03 09:08 emu 閱讀(4186) 評論(1)  編輯  收藏

          評論

          # re: MySQL 的jdbc為何不能正確的編碼漢字 2007-01-19 17:21 劉明
          老大,我也遇到類似的問題了,請教一下。加我msn:qlqsh@msn.com或google talk:hopefor@gmail.com,謝謝  回復  更多評論
            


          只有注冊用戶登錄后才能發表評論。


          網站導航:
           
          主站蜘蛛池模板: 台湾省| 蛟河市| 福安市| 曲松县| 百色市| 中阳县| 南岸区| 金昌市| 奎屯市| 龙海市| 民权县| 长子县| 临漳县| 自贡市| 团风县| 南涧| 宜宾县| 徐水县| 峨眉山市| 乡城县| 双城市| 老河口市| 凤城市| 东乡族自治县| 安图县| 广南县| 黑山县| 景德镇市| 栾城县| 平阳县| 南京市| 大埔县| 黔西县| 新闻| 宝兴县| 绥江县| 山阳县| 马公市| 金溪县| 兴城市| 建宁县|