在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规范,www.rfc-editor.org/rfc/rfc4627…(RFC 4627)转义符号会被当作无效字符给丢弃,说的也很清楚。
很明显编译器也过不了这种规则,但是json数据传输时这串是能成立的
那是数据获取源头产生的问题吗,它是否在运行过程中就是产生了这种string值?动态调试了一番发现在字段赋值的时候的确是没有转义字符的('')。很明显了,就是在转换成json的时候被加上转义符了,这也很难和md5计算扯上联系对吧?关键的点来了,因为一直以来都是在最后的封装环节把数据封装好了数据才进行md5计算,这个思路和方案都没有问题的(不可能提前知晓所有字段和值吧?),那就说明是使用系统json库取值的时候出了问题,让转义符也参与了计算,看源码部分。
//opt是JSONObject
if (opt.getClass().isPrimitive()) {
return opt.toString();
}
问题是定位到了,那这是很神奇的问题啊,让我们从源码来看看~
- Android系统自带json库
//org.json.JSONStringer
private void string(String value) {
out.append("\ ""); for (int i=0 , length=v alue.length(); i < length; i++) { char c=v alue.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 <=0 x1F) { out.append(String.format( "\\u%04x", (int) c)); } else { out.append(c); } break; } } out.append(
"\" ");
}
其转义时会将字符'\'插入需要转义的前一位,下面对比Gson的解析和封装。
- Gson解析json
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=t his.buffer; StringBuilder builder=n ull; while (true) { int p=p os; int l=l imit; /* the index of the first character not yet appended to the builder. */ int start=p
; while (p < l) { int c=b uffer[p++]; if (c==q uote) { pos=p ; int len=p - start - 1; if (builder==n ull) { 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==n ull) { int estimatedLength=( len + 1) * 2; builder=n ew StringBuilder(Math.max(estimatedLength,
16)); } builder.append(buffer, start, len); builder.append(readEscapeCharacter()); p=p os; l=l imit; start=p ; } else if (c=='\n' ) { lineNumber++; lineStart=p ; } } if (builder==n ull) { int estimatedLength=(
p - start) * 2; builder=n ew 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对象,保证每次取到的都是正确的值。
方案一需要重写去除转义的方法,方案二会一定程度的影响到性能。那还是使用方案一来修复;
小结
此坑到此为止~~~!