mysql jdbc 中文不能写入_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(黄希彤)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值