问题发现
最近在修改一个bug的时候,发现了一个离奇的问题:
String[] str = list1.get(i).getList2().get(j).split("\n");
for (k = 0; k < str.length; k++) {
String a = str[k];
String b = userSubmitAnswer.getUserAnswer().get(i).getUserAnswer()[j];
System.out.println(a);
System.out.println(b);
System.out.println(a.equals(b));
if (a.equals(b)) {
fillSubtableResult.setFillScore(list1.get(i).getList1().get(j));
fillSubtableResult.setFillSingleSituationId(1);
score = score + fillSubtableResult.getFillScore();
break;
}
}
这是一部分填空题的判题逻辑,a为数据库中的答案,b为用户的提交。 这么看来是没有问题的。
但是,当a、b的输出一致时,a.equals(b)的输出结果为false。
然后进行断点调试,发现字符串a的后面还跟了一个/r,导致两个字符串的byte数组都不一样。
后面检查数据库,发现数据库的编码为CHARSET = utf8 我用的是 UTF-8 编码的客户端,服务器也是 UTF-8 编码的,数据库也是,就连用户提交的这个字符串“也是合法的 UTF-8。
问题的症结在于,MySQL 的“utf8”实际上不是真正的 UTF-8。
“utf8”只支持每个字符最多三个字节,而真正的 UTF-8 是每个字符最多四个字节。
MySQL 一直没有修复这个 bug,他们在 2010 年发布了一个叫作“utf8mb4”的字符集,绕过了这个问题。
当然,他们并没有对新的字符集广而告之(可能是因为这个 bug 让他们觉得很尴尬),以致于现在网络上仍然在建议开发者使用“utf8”,但这些建议都是错误的。
解决方案
既然问题出在了编码问题,那么解决方法是什么?
首先,需要解决多余/r的同时,需要将两个字符串保持编码一致。
那么就将两个字符串打为byte数组,在重新编码,利用正则表达式去除/r,这样即可保持编码一致,解决问题。
byte[] aBytes = str[k].getBytes(StandardCharsets.UTF_8);
String a = null;
try {
a = new String(aBytes, "utf8");
// a.replace("\r","");
if (a != null) {
Pattern p = Pattern.compile("\\s*|\t|\r|\n");//这里因为数据库编码问题,导致切割后的字符串后面会多出一个 \r
Matcher m = p.matcher(a);
a = m.replaceAll("");
}
}catch (Exception e) {
e.printStackTrace();
}
byte[] bBytes = userSubmitAnswer.getUserAnswer().get(i).getUserAnswer()[j].getBytes(StandardCharsets.UTF_8);
try {
b = new String(bBytes, "utf8");
if (b != null) {
Pattern p = Pattern.compile("\\s+");//这里因为数据库编码问题,导致切割后的字符串后面会多出一个 \r
Matcher m = p.matcher(b);
b = m.replaceAll("");
}
}catch (Exception e) {
e.printStackTrace();
}
简单概括
-
MySQL 的“utf8mb4”是真正的“UTF-8”。
-
MySQL 的“utf8”是一种“专属的编码”,它能够编码的 Unicode 字符并不多。
我要在这里澄清一下:所有在使用“utf8”的 MySQL 和 MariaDB 用户都应该改用“utf8mb4”,永远都不要再使用“utf8”。