MySQL 的jdbc为何不能正确的编码汉字
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是根据什么来决定返回回来的数据的编码方式呢?
上面研究的只是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自己的源码比较舒服),看起来一头雾水,算了。