Hedley

Stay Hungry, Stay Foolish.

MySQL5.5 以下版本通过 Java 转义实现 Emoji 存取

我不输出好文,我只是好文的搬运工。十分钟搞清字符集和字符编码带你走进 UnicodeUTF-8 的前世今生。总而言之,Unicode 是标准,UTF-8 是实现。

MySQL 过招 Emoji

通常 MySQL 的默认字符集都会配置为 UTF-8,只支持单字符不超过 3 bytes 的存储1。常见的 EmojiUnicode 编码值 code point 位于 \u1F601 -- \u1F64F 区间,以 \u1F300 『🌀』举例,它的二进制有 17 位,无法用三字节的 UTF-8 编码表示2。如果通过 JDBC 将此字符尝试插入 MySQL 记录中,会得到以下异常

1
java.lang.RuntimeException: java.sql.SQLException: Incorrect string value: '\xF0\x9F\x8C\x80' for column 'message' at row 1 

Java 过招 Emoji

Java 存储字符的单元是 char,那么问题来了,16 位的 char 如何 handle 17 位的 Unicode 呢?

喜闻乐见,Java 的单个 char 确实无法 handle 🌀。那么 ⚡ 为什么可以通过编译?相信聪明如你,一定可以查到 ⚡ 的 Unicode\u26A1,Tada ~~ 所以 ⚡ 插入数据库中也是妥妥的。

速入 Java’s Unicode Notation 科普一下 Java 对 Unicode 的支持实现,配合通俗易懂版更赞3。原来 Java 存储单个字符的最小单元并不一定是单个 char,而是根据 code point 的范围确定。如果字符 ccode point 二进制表示不超过 16 位那么单个 char 可直接表示4;否则要进行以下处理,以 🌀 举例

  • \u1F300 的二进制表示 0001 11110011 00000000,其高 10 位与低 10 位拆开备用
  • 高位偏移量 W1 : 0xD7C0 与低位偏移量 W2 : 0xDC00 分别与其高低 10 位相加处理,得到 \uD83C\uDF005
  • 两个字符拼接 \uD83C\uDF00 得到 🌀
(EmojiCharacter.java) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package unicode;

public class EmojiCharacter {

    // high offset
    public static final int W1 = Integer.valueOf("D7C0",16);

    // low offset
    public static final int W2 = Integer.valueOf("DC00",16);

    public static void main(String[] args) {
        // 🌀的二进制字符串表示
        String doughnutBinary = Integer.toBinaryString(Integer.valueOf("1F300",16));
        // 拆成高低 10 位表示
        String highSetsBinary = doughnutBinary.substring(0, doughnutBinary.length() - 10);
        String lowSetsBinary = doughnutBinary.substring(doughnutBinary.length() - 10, doughnutBinary.length());
        System.out.println(highSetsBinary);     //    1111100
        System.out.println(lowSetsBinary);      //    1100000000

        // 分别与偏移量相加,得到两个编码值
        String highSetsHexFixed = Integer.toHexString(W1 + Integer.valueOf(highSetsBinary, 2));
        String lowSetsHexFixed = Integer.toHexString(W2 + Integer.valueOf(lowSetsBinary, 2));
        System.out.println(highSetsHexFixed);   //    d83c
        System.out.println(lowSetsHexFixed);    //    df00

        // 拼接这两个编码值,还原字符表示
        char highChar = (char)Integer.valueOf(highSetsHexFixed, 16).intValue();
        char lowChar = (char)Integer.valueOf(lowSetsHexFixed, 16).intValue();
        System.out.println(highChar);           //    ?     
        System.out.println(lowChar);            //    ?
        System.out.println(highChar + "" + lowChar);    //    🌀
    }
}

合体

Client 发送 Emoji 到后台,Java 却无法通过 JDBC 将其存储到 MySQL。如果我们遵循某个特定的转换规则,存储前转义一次,读取时再逆转义一次,那不就妥妥妥的了。你当然可以自己写,但是懒逼如我,更倾向于找现货

引用




  1. MySQL5.5 版本以后有对 4 bytes 单字符的原生支持,字符集配置为 utf8mb4

  2. 请看十分钟系列的实现细节,三字节的 UTF-8 编码上限是 16 位『4 + 6 + 6』,11111 001100 000000 对应的 UTF-8 编码为 11110000 10011111 10001100 10000000,即 \xF0…,MySQL 经常会冒出这种异常

  3. 勘误,通俗版中所说的 W1 : 0xD800 不对,应是 0xD7C0,已在评论中反映给原作者

  4. 比如『⚡』

  5. Unicode 预留了替代区域 \uD800 -- \uDFFF 作为辅助,此区间的单个字符没有实际表示意义