特殊空格 0xC2A0 踩坑日记

UTF-8 中有一些特殊的不可见字符,今天踩坑的是特殊空格。常见的空格编码是 0x20(SPACE),而这次遇到的空格编码则是 0xC2A0(NO-BREAK SPACE)。

问题描述

今天上游系统反馈,从支付中心查询回来的 payErrorCode 无法匹配多语言。

仔细对比,发现查询接口返回的 payErrorCode 后面多了一个空格

修复起来也简单,直接在接口返回的时候 trim 一下即可

原因定位

奇怪的是,代码上线之后,接口依然返回带空格的 payErrorCode!似乎 trim 没有生效。正当我百思不得其解的时候,同事(瑶子)执行的 sql 给了我思路。

使用 MySQL 提供的 length 函数,计算字符串长度。

正常空格应该是占一个字节,现在却占了两个字节。难道是特殊字符?马上用一个 sql 验证自己的猜想。

果然是一个特殊字符,而且占了两个字节,接下来让它现行。

使用 MySQL 提供的 hex 函数,将字符串转成 16 进制展示。

特殊字符为 0xC2A0,通过 UTF-8 编码表 可知,这是一个特殊空格 NO-BREAK SPACE。

Unicode code point characcter UTF-8(hex) name
U+0020 20 SPACE
U+00A0 c2 a0 NO-BREAK SPACE

修复方案

知道问题原因之后,修复起来就很简单了。我们可以通过正则表达式去掉特殊空格字符。

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
public static void main(String[] args) {
String str = "7558";
System.out.println(str + ".length=" + str.getBytes().length);
//平常的空格 0x20 -> SPACE
byte[] spaceBytes = new byte[5];
System.arraycopy(str.getBytes(), 0, spaceBytes, 0, str.getBytes().length);
spaceBytes[4] = (byte) 0x20;
String space = new String(spaceBytes, StandardCharsets.UTF_8);
System.out.println("带有 0x20 -> Space 的字符串:" + space + "**");
System.out.println(space + ".length=" + space.getBytes().length);
System.out.println("使用 trim 去掉 0x20 -> Space:" + space.trim() + "**");

//问题空格 0xC2A0 -> NO-BREAK SPACE
byte[] noBreakSpaceBytes = new byte[6];
System.arraycopy(str.getBytes(), 0, noBreakSpaceBytes, 0, str.getBytes().length);
noBreakSpaceBytes[4] = (byte) 0xC2;
noBreakSpaceBytes[5] = (byte) 0xA0;
String noBreakSpace = new String(noBreakSpaceBytes, StandardCharsets.UTF_8);
System.out.println("带有 0xC2A0 -> NO-BREAK SPACE 的字符串:" + noBreakSpace + "**");
System.out.println(noBreakSpace + ".length=" + noBreakSpace.getBytes().length);
System.out.println("使用 trim 无法去掉 C2A0 -> NO-BREAK SPACE:" + noBreakSpace.trim() + "**");

// 使用正则去掉 0xC2A0 空格 -> NO-BREAK SPACE
byte[] bytes3 = new byte[]{(byte) 0xC2, (byte) 0xA0};
String c2a0 = new String(bytes3, StandardCharsets.UTF_8);
Pattern p = Pattern.compile(c2a0);
Matcher m = null;
m = p.matcher(noBreakSpace);
noBreakSpace = m.replaceAll("");
System.out.println("使用正则去掉 0xC2A0 -> NO-BREAK SPACE:" + noBreakSpace + "**");
}

运行结果

小结

  • 一开始排查问题的思路有问题,想当然的以为 payErrorCode="7558 " 后面带的是普通空格,用 trim 函数就可以去掉。
    • 然而代码上线之后,发现 trim 并没有生效,期间还一度怀疑是代码没有发布成功,耽误了许多排查问题的时间。
  • 这个特殊空格之所以能保存到数据库中,是因为在后台系统页面上存在导入 payErrorCode 的接口,这就存在添加特殊空格的机会。
    • 做后台系统设计的时候一定要谨记:导入是一把双刃剑,一方面能提高编辑效率,另一方面又会引入特殊字符

引用