概述


数据库字符“乱码”现象,其原因可能跟页面的编码、连接池的字符集、数据库的字符集参数、表和字段的字符集设置有关。而分析“乱码”现象过程中,又会受终端的字符集编码、文件的字符集编码影响。诸多因素导致“乱码”现象扑朔迷离。”眼见不一定为实”,“经验不一定总是正确的”。

本文分享对“字符集”原理和使用场景的思考,以方便快速抓住MySQL数据库字符乱码问题的本质。

字符集原理思考

字符、字符编码和字符集

“字符”可以表示数字、符号、图形、各种文字等等。“字符”的意义是人赋予的。
在计算机的世界里,“字符”只是一个图形符号,没有实际意义,不能参予“运算”。字符在计算机内部是以二进制数存储,计算机能识别的也只有二进制数0和1。
后面为了表示方便,用十六进制说明。

注意:字符集的概念包含字库(源端字符)、字符编码规则、编码字符集(二进制数的集合)。

字符集转换

字符,在GBK编码里值是:0xD6D0,在UTF8编码里是:0xE4B8AD
字符
,在GBK编码里值是:0xB9FA,在UTF8编码里是:0xE59BBD

给一个十六进制的数:0xE4B8ADE59BBD,计算机也不知道它代表的意义。按照UTF-8编码解释这是2个字,按GBK编码解释这是3个字。因此还要提供相应的字符编码。

编码规则可以表示的字符集合就是字符集。​UTF8​是字符集,使用​UTF-8​编码。​GBK​既可以说是字符集,又可以说是字符编码。本文后面不区分字符集和字符编码。

所以,计算机里传递字符数据的时候,一定有编码值(十六进制数)和具体的字符编码(其实也是二进制数)。

字符集转换的时候就是在前一个编码规则里根据编码查找字符,然后在后一个编码规则里根据字符找编码。这个映射规则是人定的。

通常 比较字符集大小 ​​ASCII​​​ < ​​GBK​​​ < ​​UTF8​​​,由小到大转换可以保证前者的编码对应的字符一定能在后者中找到对应的编码;反之,则不一定。前者是后者的子集,后者是前者的超集。当找不到时,通常就是以约定的字符(​​?​​​,编码​​0x3F​​​)指定为转换后的值。也就是通常看到的一类乱码。乱码可能会以​​?​​​显示,但是​​?​​不一定是乱码。映射后的文本不符合人类语言规则也常被认为是乱码。

字符的写入和读出

字符写入文件或者数据库表,或者写入一个流式对象等,都要把编码值转换为该字符在目标端的字符编码规则下的编码值。如果找不到对应的编码,则写入“?”(0x3F)。

如表的字段的字符集是GBK,现在有SQL文本要更新该字段为0xE4B8AD(SQL编码规则是UTF-8),则写入该字段的二进制编码应该是UTF8编码的0xE4B8AD对应的字符在GBK编码里的编码0xD6D0

字符从文件或者数据库表读出,一定会读出具体的编码值(十六进制)和对应的编码规则名,然后转换为接收者的字符集。

然而,从写入一个字符到数据库的表或者从表里读出一个字符到显示器,这其中的链路通常都会发生多次字符传递和转换。

判断字符写入是否正确的唯一绝对有效的办法,就是查看该字符的十六进制。linux下命令是xxd,mysql下函数是​hex()​, oracle下函数是​dump()​

字符的键入和显示

通常,字符的键入都是通过键盘,字符的显示都是通过显示器。目前,没有键盘和显示器的协助,人很难跟计算机交流。

  • 写入流程

有了输入法,人可以将键盘上指定的字母序列跟对应的字符关联起来,在选择好了字符回车的时候,操作系统接收了键盘驱动提供的数据以某个编码规则下的编码保存。这个编码规则的选择跟操作系统的语言设置、终端或编辑器的字符编码设置有关。

如你在终端下键入的是”中国”这个词,如果操作系统的字符编码只有ASCII的话,操作系统根本就不能转换这个词,因此屏幕也不会回显这个词。而当操作系统的语言有中文且选择中文输入法时,操作系统会通过GBK字符集识别这个输入,记录一个0xD6D0B9FA的编码和编码名(GBK)。然后发送给终端。

如果终端的字符编码选择的是ASCII,则终端接收的是一个乱码(,编码0x3F),屏幕回显的也是该乱码;如果终端的字符编码选择的是GBK,则终端接收的是(0xD6D0B9FA,字符编码规则GBK);如果终端的字符编码选择的是UTF8,则终端接收的是(0xE4B8ADE59BBD,字符编码规则UTF8)。

假设前面数值传递正确的话,如果上面是在终端里面的用vi编辑某个文件,如果vi内置字符集编码是utf8,则vi写入缓冲区的字符为(0xE4B8ADE59BBD,字符编码规则UTF8);如果vi内置字符集编码是cp936,则vi写入缓冲区的字符为(0xD6D0B9FA,字符编码规则cp936)。然后,vi开始保存的时候,如果vi内置字符集编码跟文件字符集编码一致,则直接写入文件;如果二者不一致,将再次转换。

总结:在vi里写入中文并保存,将经历这么多次可能发生字符集转换的环节.判断最终是否正确写入,直接通过命令xxd查看文件的二进制编码!

键盘输入中文字符 

-> OS接收该字符并传递给终端程序

-> 终端程序接收该字符并传递给vi进程

-> vi进程接收该字符并写入到vi缓冲区

-> vi将缓冲区的内容写入到文件中
  • 显示(读取)流程

​vi​​读取显示字符,显示流程是:

OS读取文件内容的字符传递给vi进程 

-> vi进程接收字符传入缓冲区并显示

-> 终端程序接收该显示字符传递到终端界面

-> 终端界面将该字符显示请求传递给OS图形界面

-> OS将显示请求发给显示器

除了键盘和显示器这两个环节外,其他每个环节的前后字符集编码如果不一致,都会发生转换。为了避免转换导致数据丢失(指映射不到),OS的字符编码、终端的字符编码、​​ssh​​​会话的字符编码(影响​​vi​​​的缓冲区字符编码)都设置为​​UTF-8​​​. 文件的编码也建议为​​UTF-8​​。

下面假设OS环节和终端环节的字符编码都是​​UTF-8​​​(不会导致字符转换损失),然后研究​​vi​​的字符乱码

​vi​​​(​​vim​​) 字符集相关分析

参数说明

Vi 有四个跟字符编码方式有关的set选项,即:encodingfileencodingfileencodingstermencoding

  • ​encoding​
  • 缩写为enc。可以通过:set enc 查看当前值。通过:set enc=utf8设置一个值。
    enc值默认会根据终端会话的LANG变量选择一个最接近的字符编码。也可以在当前用户的~/.vimrc里指定。观察发现设置gbk编码的时候,vi会自动选择为cp936编码。

set encoding=utf-8


  • ​​encodingvi进程的内置编码,决定了vi菜单、缓冲区、消息的字符的编码。所以,它也是影响vi界面输入和输出的关键设置。
  • fileencodings缩写为fencs。可以通过 :set fencs 查看当前值。通过 :set fencs=utf8,cp936设置一序列值。fencs可以设置一序列字符集,用于vi打开文件的时候检测该文件的字符编码。按顺序测试。一旦又发现不属于该字符集的字符,就换一下字符集测试。所以,为提高vi打开文件时编码监测效率和准确率,把utf-8设置为第一个。可以在~/.vimrc里指定。
set fileencodings=utf-8,ucs-bom,gbk,gb2312,cp936
  • fileencoding缩写为fenc。可以通过:set fenc 查看当前值。通过:set fenc=utf8 设置一个值。fenc为文件编码,当打开文件的时候,vi通过fencs里指定的字符集一一检测,检测到合适的字符集后就设置它为fenc的值。新建文件的时候,字符集没有设置,可以指定,也可以在保存的那一刻由vi自动从fencs里选择。逻辑跟打开时一样。
  • termencoding
    一般都不会改这个,不用理睬。终端会话的字符集主要还是看终端软件的字符集。

vi 打开、写入和保存的逻辑


a. ​​vi​​​ 启动时,检测​​~/.vimrc​​是否设置enc值。如果没有,就根据LANG的变量值选择接近的字符集。

b. 读取要编辑的文件,根据fencs设置的字符编码逐一探测文件的字符集,并设置​​fenc​​​为最接近的(不一定是最正确的)字符集。如果都不匹配,则默认用​​latin1​​​编码(​​ASCII​​字符集)打开。

c. 对比​​fenc​​​和​​enc​​​值。若不同,则调用​​iconv​​​将文件内容转换为​​enc​​​指定的字符集,转换后的内容放入​​vi​​进程的缓冲区。编辑开始。

d. 编辑时,​​vi​​进程接收的字符(二进制数和对应的编码)会转换为缓冲区的字符集,保存在缓冲区中。

e. 保存时,再次比对​​enc​​​和​​fenc​​​值。若不同,则调用​​iconv​​​将缓冲区的内容转换为​​fenc​​指定的字符集,并保存到文件中。

总结:为了避免vi产生字符乱码,把vi的选项encfencs都设置为utf-8

mysql 字符集相关分析

注意:鉴别是否发生“乱码”的唯一依据是看mysql表的字段里的值的二进制编码,结合字段的字符集设置得到的字符值是否符合预期。通常应用端的展示是一个鉴别方法,但不一定能说明字符写入绝对正确或者绝对错误。

在分析mysql 相关字符乱码问题之前,先理清楚这个字符从输入到写入字段的各个环节,以及从字段读出到显示到显示器的环节。然后分析mysql的字符集参数。再分析jdbc的字符集参数原理。

mysql字段数据写入和读取流程

  • 写入流程

从APP开始输入 

APP页面接收用户输入并传递给后台程序 

-> APP后台打开数据库连接并传递sql文本

-> mysql服务端接收sql文本并解析,传递更新请求给存储引擎

-> mysql存储引擎更新对应字段

从终端里的mysql客户端开始输入

终端接收键盘输入并传递给mysql客户端 

-> mysql客户端发送sql文本给mysql服务端

-> mysql服务端接收sql文本并解析,传递更新请求给存储引擎

-> mysql存储引擎更新对应字段

注意:之所以要列举两种类型的输入,是因为一般人分析APP的乱码时会采取在mysql客户端里去模拟。这两者还是有细微区别。

  • 读取流程

输出到app

mysql存储引擎读出字段字符 

-> mysql服务端从存储引擎接收字符并发送给客户端

-> APP后端接收mysql服务端返回字符发送给APP前端

-> APP前端展现该字符

输出到终端mysql客户端

mysql存储引擎读出字段字符 

-> mysql服务端从存储引擎接收字符并发送给mysql客户端

-> mysql客户端接收mysql服务端返回字符发送给终端显示界面

-> 终端展现该字符(后面省略OS环节)

很显然,mysql字符“乱码”现象就发生在其中一个或者几个环节的字符集转换上。

mysql字符集参数

mysql字符集参数跟四个参数有关系,即:​​character_set_client​​​、​​character_set_connection​​​、​​character_set_results​​​、​​character_set_server​​。 

  • ​character_set_client​标识mysql客户端数据使用的字符集,由客户端建立会话时通过 set names 字符集 的方式设定。注意:不管客户端字符的实际字符集是什么,mysql server端都以这个参数指定的为准。
    如果客户端是通过jdbc连接mysql server,jdbc的characterEncoding参数会指定这个值。
    如果客户端是通过mysql命令行连接mysql server的,客户端的某个 my.cnfcharacter_set_client值会指定这个值。如果都没有指定,那默认mysql server端的my.cnf里指定的这个值。
  • ​character_set_connection​连接层字符集,设置方法跟character_set_client 一样。这个参数作用不明,通常只要跟character_set_client 一致即可。
  • ​character_set_results​​mysql server 返回给客户端的查询结果字符集。
  • ​character_set_server​mysql 默认的内部操作字符集。
  • character_set_server -> character_set_database -> 表的字符集 -> 列的字符集
    通常列的字符集由离它最近的字符集(包含列自己的设置)设置而定。

mysql server端的字符集转换逻辑

  • 1. MySQL Server收到请求时将请求数据从character_set_client转换为character_set_connection
  • 2. 进行内部操作前将请求数据从character_set_connection转换为内部操作字符集,其确定方法如下:
  • a.使用每个数据字段的CHARACTER SET设定值;
    b.若上述值不存在,则使用对应数据表的
    DEFAULT CHARACTER SET设定值;
    c.若上述值不存在,则使用对应数据库的
    DEFAULT CHARACTER SET设定值;
    d.若上述值不存在,则使用
    character_set_server设定值。
    e.若字符转换有损(找不到映射的编码),则报
    warning:1366

    3. 将操作结果从内部操作字符集转换为character_set_results,发送回客户端。

mysql客户端终端乱码分析

回顾前面终端访问mysql的流程可知,除了上面mysql server端可能发生字符集转换外,终端的字符集编码也会导致字符输入或者回显的时候发生转换。终端包括​​SecureCRT​​​ 、 ​​XTerm​​​ 或者Linux的​​shell​​终端等。

建议:任何时候都把终端字符编码设置为UTF-8.

jdbc连接mysql 乱码分析

jdbc连接mysql的流程跟终端连接mysql的流程,在mysql server端都是一致的。不同的之处:

1. 在于app建立jdbc连接的时候会设置这三个参数(character_set_client、character_set_connection、character_set_results)的值。

2. jdbc在接收app发送数据的时候会将字符从app的字符集编码(一般是UTF8)转换为jdbc连接的character_set_client指定的字符集。这个转换如果有损,会以乱码符号(?,编码3F)替换相应字符,然后继续。所以往往被应用忽略。

jdbc的characterEncoding逻辑

根据jdbc连接字符串指定characterEncoding的值分析

  • jdbc url里不指定characterEncoding
  • 1.jdbc driver跟mysql server通讯,获取server charset index (28:代表GBK) .此时还没有开始发送sql。
  • 2. jdbc driver发起sql 获取上面四个字符集参数值。

SQL

 SHOW VARIABLES WHERE ... Variable_name = 'character_set_client' OR Variable_name = 'character_set_connection' ... OR Variable_name = 'character_set_server' ... OR Variable_name = 'character_set_results' 

1. 如果第一步获取的值跟第二步获取的character_set_client和character_set_connection值不同,则jdbc driver发起sql :set names 字符集,字符集的值是第一步取来的。

2. 如果第二步获取的character_set_server值非空,则jdbc driver发起sql :SET character_set_results = NULL ,以避免数据返回时在mysql server端发生转换。换句话说,这个参数对jdbc的连接查询没有影响。

3. jdbc driver 将app请求的sql文本转换为 character_set_client指定的字符集,然后发送给mysql server端。

4. mysql server端的流程跟上面讲的一样。

5. mysql server端将数据发回给jdbc客户端后,jdbc driver将接收的数据(编码和编码对应的字符集)返回给APP。

6. APP在把从jdbc接收的数据转换为APP内部编码,显示到界面上。


  • jdbc url里指定characterEncoding
  • 1.由于jdbc url指定了characterEncoding,所以不需要获取 server charset index
  • 2. 后面逻辑跟上面一致。
  • jdbc url指定了错误的characterEncoding
  • 1.前4步跟上面一样。
  • 2. jdbc driver将app请求的sql文本转换为character_set_client指定的字符集,然后发送给mysql server端。此时,由于characterEncoding设置不当导致转换有损,发出去的sql可能不符合语法。报语法错误。 即使不报错,查询结果也不符合预期,这个后果更严重!

jdbc characterEncoding示例

示例代码 MysqlDemo.java 见文末

  • 构造了一个包含gbk列和utf8列的表

SQL

mysql> show create table ctest\G
*************************** 1. row ***************************
Table: ctest
Create Table: CREATE TABLE `ctest` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name_in_gbk` varchar(50) CHARACTER SET gbk DEFAULT NULL,
`name_in_utf8` varchar(50) DEFAULT NULL,
`comment` varchar(200) DEFAULT NULL,
`gmt_create` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8
  • 指定不同的characterEncoding测试
  • 输出如下:
/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/bin/java "-javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=62916:/Applications/IntelliJ IDEA.app/Contents/bin" -Dfile.encoding=UTF-8 -classpath /Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/deploy.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/ext/dnsns.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/ext/jaccess.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/ext/localedata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/ext/nashorn.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/ext/sunec.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/ext/zipfs.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/javaws.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/jfxswt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/management-agent.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/plugin.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/lib/ant-javafx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/lib/dt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/lib/javafx-mx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/lib/jconsole.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/lib/packager.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/lib/sa-jdi.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/lib/tools.jar:/Users/qing.meiq/git/MysqlDemo/out/production/MysqlDemo:/Users/qing.meiq/git/MysqlDemo/mysql-connector-java-5.1.46-bin.jar MysqlDemo

Load mysql jdbc driver successfully!

---------------------------------------------------------------

Init db connection with charset '' successfully!

Create table ctest successfully!

CREATE TABLE `ctest` (

`id` bigint(20) NOT NULL AUTO_INCREMENT,

`name_in_gbk` varchar(50) CHARACTER SET gbk DEFAULT NULL,

`name_in_utf8` varchar(50) DEFAULT NULL,

`comment` varchar(200) DEFAULT NULL,

`gmt_create` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

PRIMARY KEY (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8





show session character set variables :

character_set_client latin1

character_set_connection latin1

character_set_results



run insert sql [insert into ctest(name_in_gbk, name_in_utf8, comment) values('中国', '中国', 'file:UTF-8 Unicode;jdbc:characterEncoding:'); ]



query results [select id, name_in_gbk, hex(name_in_gbk), name_in_utf8, hex(name_in_utf8), comment, gmt_create from ctest order by id desc limit 1;] :

id name_in_gbk hex(name_in_gbk) name_in_utf8 hex(name_in_utf8) comment gmt_create

1 ?? 3F3F ?? 3F3F file:UTF-8 Unicode;jdbc:characterEncoding: 2019-03-26 21:53:42.0



---------------------------------------------------------------

Init db connection with charset 'gbk' successfully!

show session character set variables :

character_set_client gbk

character_set_connection gbk

character_set_results



run insert sql [insert into ctest(name_in_gbk, name_in_utf8, comment) values('中国', '中国', 'file:UTF-8 Unicode;jdbc:characterEncoding:gbk'); ]



query results [select id, name_in_gbk, hex(name_in_gbk), name_in_utf8, hex(name_in_utf8), comment, gmt_create from ctest order by id desc limit 1;] :

id name_in_gbk hex(name_in_gbk) name_in_utf8 hex(name_in_utf8) comment gmt_create

2 中国 D6D0B9FA 中国 E4B8ADE59BBD file:UTF-8 Unicode;jdbc:characterEncoding:gbk 2019-03-26 21:53:42.0



---------------------------------------------------------------

Init db connection with charset 'utf8' successfully!

show session character set variables :

character_set_client utf8

character_set_connection utf8

character_set_results



run insert sql [insert into ctest(name_in_gbk, name_in_utf8, comment) values('中国', '中国', 'file:UTF-8 Unicode;jdbc:characterEncoding:utf8'); ]



query results [select id, name_in_gbk, hex(name_in_gbk), name_in_utf8, hex(name_in_utf8), comment, gmt_create from ctest order by id desc limit 1;] :

id name_in_gbk hex(name_in_gbk) name_in_utf8 hex(name_in_utf8) comment gmt_create

3 中国 D6D0B9FA 中国 E4B8ADE59BBD file:UTF-8 Unicode;jdbc:characterEncoding:utf8 2019-03-26 21:53:42.0



run insert sql [insert into ctest(name_in_gbk, name_in_utf8, comment) values('中国????', '中国????', 'file:UTF-8 Unicode;jdbc:characterEncoding:utf8'); ]



java.sql.SQLException: Incorrect string value: '\xF0\x9F\x98\x85' for column 'name_in_gbk' at row 1

---------------------------------------------------------------

at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:965)

at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3976)

at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3912)

at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2530)

at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2683)

at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2482)

at com.mysql.jdbc.StatementImpl.executeUpdateInternal(StatementImpl.java:1552)

at com.mysql.jdbc.StatementImpl.executeLargeUpdate(StatementImpl.java:2607)

at com.mysql.jdbc.StatementImpl.executeUpdate(StatementImpl.java:1480)

at MysqlDemo.test_case_01(MysqlDemo.java:159)

at MysqlDemo.main(MysqlDemo.java:229)

Init db connection with charset 'latin1' successfully!

show session character set variables :

character_set_client latin1

character_set_connection latin1

character_set_results



run insert sql [insert into ctest(name_in_gbk, name_in_utf8, comment) values('中国CHINA', '中国CHINA', 'file:UTF-8 Unicode;jdbc:characterEncoding:latin1'); ]



query results [select id, name_in_gbk, hex(name_in_gbk), name_in_utf8, hex(name_in_utf8), comment, gmt_create from ctest order by id desc limit 1;] :

id name_in_gbk hex(name_in_gbk) name_in_utf8 hex(name_in_utf8) comment gmt_create

4 ??CHINA 3F3F4348494E41 ??CHINA 3F3F4348494E41 file:UTF-8 Unicode;jdbc:characterEncoding:latin1 2019-03-26 21:53:43.0



---------------------------------------------------------------



Process finished with exit code 0


分析:

程序中报错“java.sql.SQLException: Incorrect string value”,这个是由于往GBK列插入了一个表情字符,在字符集转换时mysql报错导致。


测试程序源码:


import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Connection;
import java.sql.Statement;


public class MysqlDemo {
private String s_conn_ ;
private String s_charset_ ;
private Connection obj_conn_ ;
private Statement obj_stmt_;
private ResultSet obj_rs_;

MysqlDemo(String conn, String charset){
s_conn_ = conn;
s_charset_ = charset ;

init();
System.out.println("Init db connection with charset '" + s_charset_.toString() + "' successfully! ");
}
protected void finalize(){
destroy();
}


private int init(){
int ret = 1;
try{
obj_conn_ = DriverManager.getConnection(s_conn_);
obj_stmt_ = obj_conn_.createStatement();
} catch (Exception e){
e.printStackTrace();
ret = 0;
}

return ret;
}

private void destroy(){
if (obj_stmt_ != null){
try{
obj_stmt_.close();
} catch(Exception e){
e.printStackTrace();
}

}

if (obj_rs_ != null) {
try{
obj_rs_.close();
} catch(Exception e){
e.printStackTrace();
}

}
if (obj_conn_ != null){
try{
obj_conn_.close();
} catch(Exception e){
e.printStackTrace();
}

}

}

private int print_session_character(){
int ret = 1;
String sql;

try{
sql = "show session variables where variable_name in ('character_set_client', 'character_set_connection', 'character_set_results');";
obj_rs_ = obj_stmt_.executeQuery(sql);
System.out.println("show session character set variables :");
while(obj_rs_.next()){
System.out.println(obj_rs_.getString(1) + "\t" + obj_rs_.getString(2) );
}
obj_rs_.close();

} catch (Exception e){
e.printStackTrace();

ret = 0;
} finally {
if (obj_rs_ != null){
try{
obj_rs_.close();
} catch(Exception e) {
e.printStackTrace();
}

}
}

System.out.println();

return ret;
}

private int init_table(){
String sql;
int result;
int ret = 1;

try{

sql = "drop table if exists ctest;";
result = obj_stmt_.executeUpdate(sql);

sql = "create table ctest(id bigint not null auto_increment primary key, name_in_gbk varchar(50) character set gbk, name_in_utf8 varchar(50) character set utf8, comment varchar(200) , gmt_create timestamp not null DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) engine=innodb default charset=utf8;";
result = obj_stmt_.executeUpdate(sql);

if (result!= -1){
System.out.println("Create table ctest successfully!");
sql = "show create table ctest";
obj_rs_ = obj_stmt_.executeQuery(sql);
while (obj_rs_.next()) {
System.out.println(obj_rs_.getString(2));
}
obj_rs_.close();

}

System.out.println();


} catch (Exception e) {
e.printStackTrace();

ret = 0;
} finally {
if (obj_rs_ != null){
try{
obj_rs_.close();
} catch (Exception e){
e.printStackTrace();
}

}
}

System.out.println();

return ret;

}

private int test_case_01(String msg){
String sql;
int result;
int ret = 1;

try {

sql = "insert into ctest(name_in_gbk, name_in_utf8, comment) values('" + msg.toString() +"', '" + msg.toString() + "', 'file:UTF-8 Unicode;jdbc:characterEncoding:" + s_charset_.toString() + "'); ";
System.out.println("run insert sql [" + sql.toString() + "]");
result = obj_stmt_.executeUpdate(sql);
System.out.println();


sql = "select id, name_in_gbk, hex(name_in_gbk), name_in_utf8, hex(name_in_utf8), comment, gmt_create from ctest order by id desc limit 1;" ;
System.out.println("query results [" + sql.toString() + "] :");
obj_rs_ = obj_stmt_.executeQuery(sql);
System.out.println("id\tname_in_gbk\thex(name_in_gbk)\tname_in_utf8\thex(name_in_utf8)\tcomment\t\t\t\t\tgmt_create");

while (obj_rs_.next()) {
System.out.println(obj_rs_.getInt(1) + "\t" + obj_rs_.getString(2) + "\t\t" + obj_rs_.getString(3) + "\t\t" + obj_rs_.getString(4) + "\t\t" + obj_rs_.getString(5)+ "\t\t" + obj_rs_.getString(6) + "\t" + obj_rs_.getString(7));
}
obj_rs_.close();

} catch (Exception e){
e.printStackTrace();

ret = 0;
} finally {
if (obj_rs_ != null){
try{
obj_rs_.close();
} catch (Exception e){
e.printStackTrace();
}

}
}

System.out.println();

return ret;

}





public static void main(String[] args) throws Exception {
String sql;

String url = "jdbc:mysql://10.15.246.84:3306/test?user=mq&password=123456&useUnicode=true";
String url_gbk = "jdbc:mysql://10.15.246.84:3306/test?user=mq&password=123456&useUnicode=true&characterEncoding=GBK";
String url_utf8 = "jdbc:mysql://10.15.246.84:3306/test?user=mq&password=123456&useUnicode=true&characterEncoding=UTF8";
String url_latin1 = "jdbc:mysql://10.15.246.84:3306/test?user=mq&password=123456&useUnicode=true&characterEncoding=latin1";

try {
Class.forName("com.mysql.jdbc.Driver");
System.out.println("Load mysql jdbc driver successfully!");

System.out.println("---------------------------------------------------------------");

MysqlDemo test01 = new MysqlDemo(url, "");
test01.init_table();
test01.print_session_character();
test01.test_case_01("中国");


System.out.println("---------------------------------------------------------------");

MysqlDemo test02 = new MysqlDemo(url_gbk, "gbk");
test02.print_session_character();
test02.test_case_01("中国");

System.out.println("---------------------------------------------------------------");

MysqlDemo test03 = new MysqlDemo(url_utf8, "utf8");
test03.print_session_character();
test03.test_case_01("中国");
test03.test_case_01("中国????");

System.out.println("---------------------------------------------------------------");

MysqlDemo test04 = new MysqlDemo(url_latin1, "latin1");
test04.print_session_character();
test04.test_case_01("中国CHINA");

System.out.println("---------------------------------------------------------------");


} catch (Exception e) {
e.printStackTrace();
} finally {

}

}

}

参考

       本文是篇旧文,jdbc mysql驱动的字符集分析参考了网友的文章,时间久远找不到原文出处。