JDBC 中古怪的 latin1 到 UTF8 的转换

Whacky latin1 to UTF8 conversion in JDBC

当要求从包含未定义的 latin1 代码页字符的 latin1 列中读取时,

JDBC 似乎插入了一个 utf8 替换字符。此行为不同于 MySQL 的内部函数。

字符编码是一个兔子洞,上周我一直深陷其中,为了不产生 100 个明显的答案,我将通过几个代码示例演示发生了什么。

Mysql:

[admin@yarekt ~]$ echo 'SELECT CONVERT(UNHEX("81") using latin1);' | mysql --init-command='set names latin1' | tail -1| hexdump -C
00000000  81 0a                                             |..|
00000002
[admin@yarekt ~]$ echo 'SELECT CONVERT(UNHEX("81") using latin1);' | mysql --init-command='set names utf8' | tail -1| hexdump -C
00000000  c2 81 0a                                          |...|
00000003

这很明显并且完全符合预期。 0x81 是未定义的 latin1 代码点。它在 UTF8 中表示为 \u0081 或在十六进制中表示为 c2 81 "on disk".

现在怪事来自JDBC,以这个groovy为例:

@GrabConfig(systemClassLoader=true)
@Grab(group='mysql', module='mysql-connector-java', version='5.1.6')
import groovy.sql.Sql
sql = Sql.newInstance( 'jdbc:mysql://localhost/test', 'root', '', 'com.mysql.jdbc.Driver' )
sql.eachRow( 'SELECT CONVERT(UNHEX("C281") using utf8) as a;' ) { println "$it.a --" }

这个查询的输出是两个字节,c2 81符合预期。很容易理解这里发生了什么。 Mysql 连接默认为 UTF8。未十六进制的列也转换为 UTF8(没有编码,因为源是二进制的,CONVERT() 之后的数据仍然是 c2 81)。

现在考虑这个案例。连接仍为 UTF8,默认为 JDBC。我们将 0x81 字节转换为 latin1,因此希望 mysql 将其转换为 c2 81,就像上面 bash 示例中所做的那样。

@GrabConfig(systemClassLoader=true)
@Grab(group='mysql', module='mysql-connector-java', version='5.1.6')
import groovy.sql.Sql
sql = Sql.newInstance( 'jdbc:mysql://localhost/test', 'root', '', 'com.mysql.jdbc.Driver' )
sql.eachRow( 'SELECT CONVERT(UNHEX("81") using latin1) as a;' ) { println "$it.a --" }

运行 这与 groovy latin1_test.groovy | hexdump -C 产生这个:

00000000  ef bf bd 0a                                       |....|
00000004

ef bf bd 是 utf8 替换字符。 utf8 转换失败时使用的字符。

JDBC seems to insert a utf8 replacement character when asked to read from a latin1 column containing undefined latin1 codepage characters

是的,这是 CharsetDecoder instances which by default, when the (byte) input is malformed, will perform a substitution of this unmappable byte sequence with Unicode's replacement character, U+FFFD 的默认行为。

使用此行为的方法示例都是 Reader,还有 String 以字节数组作为参数的构造函数。这就是为什么你不应该使用 String 来存储二进制数据的原因!

造成该错误的唯一解决方案是获取原始字节输入,创建您自己的解码器并在那种情况下将其告诉 fail...