乱码问题主要是字符编码不配对引起的,也就是编码和解码的字符集不一致,就造成了乱码。mysql的字符集编码可以参考:mysql字符集与比较规则
1、一开始看到这个问题,首先我先看了该schema下的字符集:
show variables like '%char%';
都正常,为utf8mb4。主要关注的是character_set_client、character_set_connection、character_set_results
这三个变量。
他们的含义可以看以下例子:
首先client一般会发送一条sql给客户端,比如select * from tableA where name = ‘蜗牛’;,在分析的过程中,我们主要对查询参数“蜗牛”进行分析。
1、首先蜗牛两个字被client端进行编码为二进制流,传输给服务器。
2、服务器收到后,会认为客户端的编码为character_set_client对蜗牛进行解码,获取client发送的字符串sql。
3、解码之后,再把sql转换为character_set_connection格式,使得能与数据库连接的字符集相同。
4、解码成character_set_connection之后,就进行数据比对查询。此时如果要比对的列的字符集与character_set_connection不相等,那显然就是会有问题的。
5、数据查询出来之后,对结果要进行编码,编码的字符集就采用character_set_results字符集,此时如果客户端接收的字符集与character_set_connection不相同,那么客户端解码的数据也会出现问题。
所以,我们现在字符编码都一致,是没有问题的。
那是什么问题呢?
2、我用idea的客户端连接数据库,发现查询出来的数据是正常的。
客户端的编码格式为:
看来如果客户端是正常的utf8编码,查询出来的数据是正常的呀,符合预期。
3、那应该就是我代码连接数据库的字符集有问题了。的确,目前的连接url还是:
jdbc:mysql://ip:3306/数据库名称的格式。druid连接池的配置中也没有配置与字符集相关的配置。
<bean id="nw_DataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="driverClassName" value="${data.datasource.job.JDBC.DRIVER}" />
<property name="url" value="${data.datasource.job.JDBC.URL}" />
<property name="username" value="${data.datasource.job.JDBC.USERNAME}" />
<property name="password" value="${data.datasource.job.JDBC.PASSWORD}" />
<property name="initialSize" value="${data.datasource.job.JDBC.INITIAL.SIZE}" />
<property name="minIdle" value="${data.datasource.job.JDBC.MIN.IDLE}" />
<property name="maxActive" value="${data.datasource.job.JDBC.MAX.ACTIVE}" />
<property name="maxWait" value="60000" />
<property name="timeBetweenEvictionRunsMillis" value="2000" />
<property name="minEvictableIdleTimeMillis" value="600000" />
<property name="maxEvictableIdleTimeMillis" value="900000" />
<property name="validationQuery" value="${data.datasource.job.JDBC.validationQuery}" />
<property name="testWhileIdle" value="true" />
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />
<property name="keepAlive" value="true" />
<property name="phyMaxUseCount" value="1000" />
<property name="filters" value="stat" />
<property name="asyncInit" value="true" />
bean>
按照网上说的思路,我再数据库连接url上加上了字符集的配置:
jdbc:mysql://ip:3306/数据库名?useUnicode=true&characterEncoding=utf8
结果还是不行。
我决定去翻下源码,沿着数据库查询的路子走下去。这是mysql-connector-j的github地址
从com.alibaba.druid.proxy.DruidDriver的connect方法往下走,
public Connection connect(String url, Properties info) throws SQLException {
if (!acceptsURL(url)) {
return null;
}
connectCount.incrementAndGet();
DataSourceProxyImpl dataSource = getDataSource(url, info);
return dataSource.connect(info);
}
走到mysql驱动中com.mysql.cj.jdbc.NonRegisteringDriver#connect:
case SINGLE_CONNECTION:
return com.mysql.cj.jdbc.ConnectionImpl.getInstance(conStr.getMainHost());
其中在创建数据库连接。
进到com.mysql.cj.jdbc.ConnectionImpl的构造方法 -> createNewIO() -> connectWithRetries()->initializePropsFromServer()其中从服务端初始化客户端的一些配置。
this.session.configureClientCharacterSet(false);
// We've already set it, and it might be different than what was originally on the server, which is why we use the "special" key to retrieve it
this.session.getServerSession().configureCharacterSets();
我主要关注了这两个和字符集相关的配置。其中有这样一段:
// We know how to deal with any charset coming back from the database, so tell the server not to do conversion if the user hasn't 'forced' a
// result-set character set
//
String onServer = this.protocol.getServerSession().getServerVariable("character_set_results");
if (characterSetResults.getValue() == null) {
//
// Only send if needed, if we're caching server variables we -have- to send, because we don't know what it was before we cached them.
//
if (onServer != null && onServer.length() > 0 && !"NULL".equalsIgnoreCase(onServer)) {
try {
sendCommand(this.commandBuilder.buildComQuery(null, "SET character_set_results = NULL"), false, 0);
} catch (PasswordExpiredException ex) {
if (this.disconnectOnExpiredPasswords.getValue()) {
throw ex;
}
}
this.protocol.getServerSession().getServerVariables().put(ServerSession.LOCAL_CHARACTER_SET_RESULTS, null);
} else {
this.protocol.getServerSession().getServerVariables().put(ServerSession.LOCAL_CHARACTER_SET_RESULTS, onServer);
}
}
它读取了服务器的character_set_results
变量,如果没有自定义characterSetResults
,则执行SET character_set_results = NULL
将character_set_results
置空了。注释说我们知道怎么转换数据库返回的结果,所以我们强制数据库不要对结果进行转换。 似乎他想提供一个更加灵活的字符集配置的效果。
在下面的configureCharacterSets,它又取出了CHARACTER_SET_RESULTS进行了处理。前面赋值的是null,所以走进了if里面。
public void configureCharacterSets() {
String characterSetResultsOnServerMysql = getServerVariable(LOCAL_CHARACTER_SET_RESULTS);
if (characterSetResultsOnServerMysql == null || StringUtils.startsWithIgnoreCaseAndWs(characterSetResultsOnServerMysql, "NULL")
|| characterSetResultsOnServerMysql.length() == 0) {
String defaultMetadataCharsetMysql = getServerVariable("character_set_system");
String defaultMetadataCharset = null;
if (defaultMetadataCharsetMysql != null) {
defaultMetadataCharset = CharsetMapping.getJavaEncodingForMysqlCharset(defaultMetadataCharsetMysql);
} else {
defaultMetadataCharset = "UTF-8";
}
this.characterSetMetadata = defaultMetadataCharset;
setErrorMessageEncoding("UTF-8");
}
} else {
this.characterSetResultsOnServer = CharsetMapping.getJavaEncodingForMysqlCharset(characterSetResultsOnServerMysql);
this.characterSetMetadata = this.characterSetResultsOnServer;
setErrorMessageEncoding(this.characterSetResultsOnServer);
}
我们发现,它再次读取了CHARACTER_SET_RESULTS变量。如果为空的话,会读取系统变量character_set_system
的值,赋给defaultMetadataCharset,在if里面,没有看到characterSetResult的赋值,说明目前还是null。
我一开始以为我可以从源码里面找到,是不是我没有配置characterSetResult
的字符集,使得系统采用了一个默认的字符集导致了乱码,但结果好像并不如人意。
反而在上面代码的else语句中看到了characterSetResultsOnServer 的赋值:
this.characterSetResultsOnServer = CharsetMapping.getJavaEncodingForMysqlCharset(characterSetResultsOnServerMysql);
所以最后我还是没有发现原因。
还好我把问题是解决了。
我在数据库url上加上了characterSetResults:jdbc:mysql://ip:3306/数据库名称?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
或者你也可以配置在druid连接池的参数中,加上:
<property name="connectionProperties">
<value>clientEncoding=utf-8;serverEncoding=utf-8;characterSetResults=utf-8value>
property>
把编码通通一口气指定了。
我使用的是mysql8.0,mysql驱动为8.0.16,druid的版本为1.1.21:
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.16version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.1.21version>
dependency>
mysql配置文件为:
[mysqld]
port=3306
# 允许最大连接数
max_connections=200
# # 允许连接失败的次数。这是为了防止有人从该主机试图攻击数据库系统
max_connect_errors=10
# # 服务端使用的字符集默认为UTF8
character-set-server=utf8
# # 创建新表时将使用的默认存储引擎
default-storage-engine=INNODB
datadir=/mysql/data
socket=/var/lib/mysql/mysql.sock
log-error=/var/log/mysqld.log
pid-file=/var/run/mysqld/mysqld.pid
lower_case_table_names=1
skip-name-resolve
[mysql]
default-character-set = utf8
[mysql.server]
default-character-set = utf8
[mysqld_safe]
default-character-set = utf8
[client]
default-character-set = utf8
虽然问题看似解决了,但还是有一块疙瘩在这里,因为我记得以前是不需要单独配置characterSetResults的呀,是哪个环节出了问题呢?如果你也碰到这样的问题,指导我一下,不胜感激!!!