在Andriod系统应用层开发通常json协议解析使用Gson、jackson当然还公司的fastjson库等,Andriod其实也自带json解析库,集成的是apache的,在一些特定的场景用自带库解析也很方便。
但是,不得不说自带库有个坑踩进去了就会被坑的挺惨,而且很难发现到问题;
一、背景
我们的项目部分模块在http请求时涉及到对参数key value计算出md5,通过json协议数据传输,到了服务端再做md5的校验,正常来说计算md5的规则双方都做了统一保证,满足了一致性的条件。理论上,只要通信过程数据未发生篡改,100%能保证是一致的;但是问题来了,即使中间的通信数据数据未被篡改,双方计算出来的md5还是存在不匹配的情形,而且出现的问题断断续续,一直没有得到有效定位和解决。

然而并没有想的那样100%md5计算相同
二、排查路径
2.1 分析现象
问题出现时会一直提示md5校验失败,说明两边的md5计算结果确实不一样,然而发生的概率很低,低到几乎可以忽略不计,但只要出现问题就能稳定复现。
2.2 定位问题
首先,很容易想到的是双方计算规则不同,计算的层级不同,毕竟Android端对库的依赖和服务端库的依赖存在这差别;然而,将算法统一校准后问题并没有得到解决~
再来从有问题的请求json串入手分析,发现带问题的json数据给到服务端解析后--出现json转化的值一些些特定字符都会被去掉,那问题其实就定位到了,但这个服务端的问题吗?毕竟它每次都会将值里边的某个字符给丢掉。查下json规范,http://www.rfc-editor.org/rfc/rfc4627.txt(RFC 4627)转义符号会被当作无效字符给丢弃,说的也很清楚。

很明显编译器也过不了这种规则,但是json数据传输时这串是能成立的
那是数据获取源头产生的问题吗,它是否在运行过程中就是产生了这种string值?动态调试了一番发现在字段赋值的时候的确是没有转义字符的('')。很明显了,就是在转换成json的时候被加上转义符了,这也很难和md5计算扯上联系对吧?关键的点来了,因为一直以来都是在最后的封装环节把数据封装好了数据才进行md5计算,这个思路和方案都没有问题的(不可能提前知晓所有字段和值吧?),那就说明是使用系统json库取值的时候出了问题,让转义符也参与了计算,看源码部分。
//opt是JSONObject
if (opt.getClass().isPrimitive()) {
return opt.toString();
}
问题是定位到了,那这是很神奇的问题啊,让我们从源码来看看~
//org.json.JSONStringer
private void string(String value) {
out.append("\"");
for (int i = 0, length = value.length(); i < length; i++) {
char c = value.charAt(i);
/*
* From RFC 4627, "All Unicode characters may be placed within the
* quotation marks except for the characters that must be escaped:
* quotation mark, reverse solidus, and the control characters
* (U+0000 through U+001F)."
*/
switch (c) {
case '"':
case '\\':
case '/':
out.append('\\').append(c);//看这
break;
case '\t':
out.append("\\t");
break;
case '\b':
out.append("\\b");
break;
case '\n':
out.append("\\n");
break;
case '\r':
out.append("\\r");
break;
case '\f':
out.append("\\f");
break;
default:
if (c <= 0x1F) {
out.append(String.format("\\u%04x", (int) c));
} else {
out.append(c);
}
break;
}
}
out.append("\"");
}
其转义时会将字符'\'插入需要转义的前一位,下面对比Gson的解析和封装。
https://github.com/google/gson/blob/master/gson/src/main/java/com/google/gson/stream/JsonReader.java
/**
* Returns the string up to but not including {@code quote}, unescaping any
* character escape sequences encountered along the way. The opening quote
* should have already been read. This consumes the closing quote, but does
* not include it in the returned string.
*
* @param quote either ' or ".
* @throws NumberFormatException if any unicode escape sequences are
* malformed.
*/
private String nextQuotedValue(char quote) throws IOException {
// Like nextNonWhitespace, this uses locals 'p' and 'l' to save inner-loop field access.
char[] buffer = this.buffer;
StringBuilder builder = null;
while (true) {
int p = pos;
int l = limit;
/* the index of the first character not yet appended to the builder. */
int start = p;
while (p < l) {
int c = buffer[p++];
if (c == quote) {
pos = p;
int len = p - start - 1;
if (builder == null) {
return new String(buffer, start, len);
} else {
builder.append(buffer, start, len);
return builder.toString();
}
} else if (c == '\\') {//看这
pos = p;
int len = p - start - 1;
if (builder == null) {
int estimatedLength = (len + 1) * 2;
builder = new StringBuilder(Math.max(estimatedLength, 16));
}
builder.append(buffer, start, len);
builder.append(readEscapeCharacter());
p = pos;
l = limit;
start = p;
} else if (c == '\n') {
lineNumber++;
lineStart = p;
}
}
if (builder == null) {
int estimatedLength = (p - start) * 2;
builder = new StringBuilder(Math.max(estimatedLength, 16));
}
builder.append(buffer, start, p - start);
pos = p;
if (!fillBuffer(1)) {
throw syntaxError("Unterminated string");
}
}
}
其写方法
static {
REPLACEMENT_CHARS = new String[128];
for (int i = 0; i <= 0x1f; i++) {
REPLACEMENT_CHARS[i] = String.format("\\u%04x", (int) i);
}
REPLACEMENT_CHARS['"'] = "\\\"";
REPLACEMENT_CHARS['\\'] = "\\\\";
REPLACEMENT_CHARS['\t'] = "\\t";
REPLACEMENT_CHARS['\b'] = "\\b";
REPLACEMENT_CHARS['\n'] = "\\n";
REPLACEMENT_CHARS['\r'] = "\\r";
REPLACEMENT_CHARS['\f'] = "\\f";
HTML_SAFE_REPLACEMENT_CHARS = REPLACEMENT_CHARS.clone();
HTML_SAFE_REPLACEMENT_CHARS['<'] = "\\u003c";
HTML_SAFE_REPLACEMENT_CHARS['>'] = "\\u003e";
HTML_SAFE_REPLACEMENT_CHARS['&'] = "\\u0026";
HTML_SAFE_REPLACEMENT_CHARS['='] = "\\u003d";
HTML_SAFE_REPLACEMENT_CHARS['\''] = "\\u0027";
}
gson是不会对'/'进行转义的,那是否直接使用gson库替换就解决问题?应该还不是这么简单的,这里我们注意到仍然是存在特殊字符需要转义的,最后还是得回到调整取json字符值上面来。到这里问题就定位的很清楚了。
2.3 评估影响面
这个其实影响很大的,一直以来就很难发现潜在的问题(概率低),尤其是作为通信最基础关键的部分。每次请求都可能会触发到特殊字符的转义,那后面再从json对象取到的字符串string值都是带着转义符号过来的。
2.4 解决方案
两个解决方案:
一是,对从json对象去除字符串进行二次加工,调用去除转义的方法把转义符号给去除。
二是,将json对象解析回java对象,保证每次取到的都是正确的值。
方案一需要重写去除转义的方法,方案二会一定程度的影响到性能。那还是使用方案一来修复;
小结
此坑到此为止~~~!