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