utf-8不支持emoji导致的bug

165 阅读6分钟

时间 : 2023年3月11日14:08:47

bug描述

发布文章出错。

Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@55105a0c]
org.springframework.jdbc.UncategorizedSQLException: 
​
### Error updating database.  Cause: java.sql.SQLException: Incorrect string value: '\xF0\x9F\x86\x94 \xE8...' for column 'content_html' at row 1
### The error may exist in com/mszlu/blog/dao/mapper/ArticleBodyMapper.java (best guess)
### The error may involve com.mszlu.blog.dao.mapper.ArticleBodyMapper.insert-Inline
### The error occurred while setting parameters
### SQL: INSERT INTO ms_article_body  ( id, content, content_html, article_id )  VALUES  ( ?, ?, ?, ? )
### Cause: java.sql.SQLException: Incorrect string value: '\xF0\x9F\x86\x94 \xE8...' for column 'content_html' at row 1
; uncategorized SQLException; SQL state [HY000]; error code [1366]; Incorrect string value: '\xF0\x9F\x86\x94 \xE8...' for column 'content_html' at row 1; nested exception is java.sql.SQLException: Incorrect string value: '\xF0\x9F\x86\x94 \xE8...' for column 'content_html' at row 1

可以读出,在插入ms_article_body表的时候插入了错误的String value 在content_html 字段。

经过我排查发现,

我发布的文章中有一行语句  PUT /users/:id  中的“:id” 直接转换成了一个emoji

如下图所示,

image-20230311141245274

就是这个emoji导致报错。

PUT /users/🆔

我的数据库使用的字符集是utf-8,不支持emoji

image-20230311141348621

为什么有这么多的character_set

server system database client都是什么呢

  1. character_set_connection:从客户端接收到数据,然后传输的字符集
  2. character_set_database:默认数据库的字符集,无论默认数据库如何改变,都是这个字符集;如果没有默认数据库,那就使用 character_set_server指定的字符集,这个变量建议由系统自己管理,不要人为定义。
  3. character_set_filesystem:把操作系统上的文件名转化成此字符集,即把 character_set_client转换character_set_filesystem, 默认binary是不做任何转换的
  4. character_set_results:结果集的字符集
  5. character_set_server:数据库服务器的默认字符集
  6. character_set_system:存储系统元数据的字符集,总是 utf8,不需要设置

相关知识

因为计算机只能处理数字,如果要处理文本,就必须先把文本转换为数字才能处理。

  1. ASCII编码,占用0 - 127用来表示大小写英文字母、数字和一些符号,这个编码表被称为Ascll表。

  2. Unicode通常用两个字节表示一个字符,原有的英文编码从单字节变成双字节,只需要把高字节全部填为0就可以,将所有语言统一到一套编码里。

  3. utf8是针对Unicode的一种可变长度字符编码,由于对可以用Ascll表示的字符,使用Unicode并不高效,因为Unicode比Ascll占用大一倍的空间,而对ASCII来说高字节的0对他毫无用处。就出现了一些中间格式的字符集,他们被称为通用转换格式,即UTF(Unicode Transformation Format)。

  4. utf8mb4(mb4 = most bytes 4)utf8mb4是utf8的超集,门用来兼容四字节的unicode。

    低版本的MySQL支持的utf8编码,最大字符长度为 3 字节,如果遇到 4 字节的字符就会出现错误了。

    三个字节的 UTF-8 最大能编码的 Unicode 字符是 0xFFFF,也就是 Unicode 中的基本多文平面(BMP)。

    看unicode编码区从1 ~ 126就属于传统utf8区,当然utf8mb4也兼容这个区,126行以下就是utf8mb4扩充区,什么时候你需要存储那些字符,你才用utf8mb4,否则只是浪费空间。

utf8mb4比utf8多了emoji编码支持

总结与解决办法

简单总结就是 utf8mb4 >utf8>ASCII ,高级的字符集可以兼容低级的。 utf8mb4 支持emoji,而我的数据库是utf8,我无意间使用了emoji,数据库不支持,无法解析导致报错。

utf8字符更省空间,但是utf8mb4支持emoji。

要么发文章的时候不使用emoji 或者将数据库编码改为utf8mb4,支持emoji。

权衡了一下我决定将数据库编码改为utf8mb4,一个好的博客应该支持emoji才对。

数据库编码改为utf8mb4步骤

1.检查版本

utf8mb4的最低mysql版本支持版本为5.5.3+ ,mysql驱动5.1.34可用,最低不能低于5.1.13

2.修改配置文件

修改mysql配置文件my.cnf(windows为my.ini) my.cnf一般在/etc/mysql/my.cnf位置。找到后请在以下三部分里添加如下内容:

[client] 
default-character-set = utf8mb4 
[mysql] 
default-character-set = utf8mb4 
[mysqld] 
character-set-client-handshake = FALSE 
character-set-server = utf8mb4 
collation-server = utf8mb4_unicode_ci 
init_connect=’SET NAMES utf8mb4’

3.重启数据库检查变量

SHOW VARIABLES WHERE Variable_name LIKE  'character_set_%'  OR Variable_name LIKE 'collation%';
​

image-20230311151350503

4.将已经建立的数据库和表设置为utf8mb4

手动改

更改数据库编码
ALTER DATABASE blog CHARACTER SET `utf8mb4` COLLATE `utf8mb4_general_ci`;
更改表编码
ALTER TABLE `TABLE_NAME` CONVERT TO CHARACTER SET `utf8mb4` COLLATE `utf8mb4_general_ci`; 
​

可以写代码批量转换,涉及到一个包dbutils,写进maven里

image-20230311150458663

写程序

//获取所有表
Connection conn = null;
​
try {
    conn = DbUtilsTool.openConn("MySQL", "127.0.0.1", "3306", "caitu99", "root", "root");
    String sql = "show tables";
    QueryRunner qr = new QueryRunner();
    List<String> tblNameList= (List<String>) qr.query(conn, sql, new ColumnListHandler(1));
    sql = "ALTER DATABASE caitu99 CHARACTER SET `utf8mb4` COLLATE `utf8mb4_general_ci`";
    qr.update(conn,sql);
    for (String str:tblNameList)
    {
        sql = "ALTER TABLE "+str+" CONVERT TO CHARACTER SET `utf8mb4` COLLATE `utf8mb4_general_ci`";
        qr.update(conn,sql);
    }
}
catch (Exception e)
{
            e.printStackTrace();
            throw new RuntimeException(e);
}
finally {
    if(conn!=null) {
        try {
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
   }
}
​

DbUtilsTool类:

package com.mysql.chartest;
​
​
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
​
import org.apache.commons.dbutils.BasicRowProcessor;
import org.apache.commons.dbutils.BeanProcessor;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.ArrayHandler;
import org.apache.commons.dbutils.handlers.ArrayListHandler;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.ColumnListHandler;
import org.apache.commons.dbutils.handlers.KeyedHandler;
import org.apache.commons.dbutils.handlers.MapHandler;
import org.apache.commons.dbutils.handlers.MapListHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;
​
public class DbUtilsTool {
    private static final QueryRunner runner = new QueryRunner();
​
    /* 打开数据库连接(type: MySQL,Oracle,SQLServer) */
    public static Connection openConn(String type,      //数据库类型
                                      String host,      //主机ip
                                      String port,      //主机端口
                                      String name,      //数据库名
                                      String username,  //用户名
                                      String password)//密码
     {
        Connection conn = null;
        try {
            String driver;
            String url;
            if (type.equalsIgnoreCase("MySQL")) {
                driver = "com.mysql.jdbc.Driver";
                url = "jdbc:mysql://" + host + ":" + port + "/" + name;
            } else if (type.equalsIgnoreCase("Oracle")) {
                driver = "oracle.jdbc.driver.OracleDriver";
                url = "jdbc:oracle:thin:@" + host + ":" + port + ":" + name;
            } else if (type.equalsIgnoreCase("SQLServer")) {
                driver = "com.microsoft.jdbc.sqlserver.SQLServerDriver";
                url = "jdbc:sqlserver://" + host + ":" + port + ";databaseName=" + name;
            } else {
                throw new RuntimeException();
            }
            Class.forName(driver);
            conn = DriverManager.getConnection(url, username, password);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return conn;
    }
​
    /* 关闭数据库连接 */
    public static void closeConn(Connection conn) {
        try {
            if (conn != null) {
                conn.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
​
    /* 查询(返回Array结果) */
    public static Object[] queryArray(Connection conn, String sql, Object... params) {
        Object[] result = null;
        try {
            result = runner.query(conn, sql, new ArrayHandler(), params);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return result;
    }
​
    /* 查询(返回ArrayList结果) */
    public static List<Object[]> queryArrayList(Connection conn, String sql, Object... params) {
        List<Object[]> result = null;
        try {
            result = runner.query(conn, sql, new ArrayListHandler(), params);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return result;
    }
​
    /* 查询(返回Map结果) */
    public static Map<String, Object> queryMap(Connection conn, String sql, Object... params) {
        Map<String, Object> result = null;
        try {
            result = runner.query(conn, sql, new MapHandler(), params);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return result;
    }
​
    /* 查询(返回MapList结果) */
    public static List<Map<String, Object>> queryMapList(Connection conn, String sql, Object... params) {
        List<Map<String, Object>> result = null;
        try {
            result = runner.query(conn, sql, new MapListHandler(), params);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return result;
    }
​
    /* 查询(返回Bean结果) */
    public static <T> T queryBean(Class<T> cls, Map<String, String> map, Connection conn, String sql, Object... params) {
        T result = null;
        try {
            if (map != null) {
                result = runner.query(conn, sql, new BeanHandler<T>(cls, new BasicRowProcessor(new BeanProcessor(map))), params);
            } else {
                result = runner.query(conn, sql, new BeanHandler<T>(cls), params);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return result;
    }
​
    /* 查询(返回BeanList结果) */
    public static <T> List<T> queryBeanList(Class<T> cls, Map<String, String> map, Connection conn, String sql, Object... params) {
        List<T> result = null;
        try {
            if (map != null) {
                result = runner.query(conn, sql, new BeanListHandler<T>(cls, new BasicRowProcessor(new BeanProcessor(map))), params);
            } else {
                result = runner.query(conn, sql, new BeanListHandler<T>(cls), params);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return result;
    }
​
    /* 查询指定列名的值(单条数据) */
    public static <T> T queryColumn(String column, Connection conn, String sql, Object... params) {
        T result = null;
        try {
            result = runner.query(conn, sql, new ScalarHandler<T>(column), params);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return result;
    }
​
    /* 查询指定列名的值(多条数据) */
    public static <T> List<T> queryColumnList(String column, Connection conn, String sql, Object... params) {
        List<T> result = null;
        try {
            result = runner.query(conn, sql, new ColumnListHandler<T>(column), params);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return result;
    }
​
    /* 查询指定列名对应的记录映射 */
    public static <T> Map<T, Map<String, Object>> queryKeyMap(String column, Connection conn, String sql, Object... params) {
        Map<T, Map<String, Object>> result = null;
        try {
            result = runner.query(conn, sql, new KeyedHandler<T>(column), params);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return result;
    }
​
    /* 更新(包括UPDATE、INSERT、DELETE,返回受影响的行数) */
    public static int update(Connection conn, String sql, Object... params) {
        int result = 0;
        try {
            result = runner.update(conn, sql, params);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return result;
    }
}
​

版权声明:本文为CSDN博主「woslx」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:blog.csdn.net/woslx/artic…