Android自带Json库使用引发的问题

2,140 阅读5分钟
原文链接: click.aliyun.com

在Andriod系统应用层开发通常json协议解析使用Gson、jackson当然还公司的fastjson库等,Andriod其实也自带json解析库,集成的是apache的,在一些特定的场景用自带库解析也很方便。
但是,不得不说自带库有个坑踩进去了就会被坑的挺惨,而且很难发现到问题;

一、背景

我们的项目部分模块在http请求时涉及到对参数key value计算出md5,通过json协议数据传输,到了服务端再做md5的校验,正常来说计算md5的规则双方都做了统一保证,满足了一致性的条件。理论上,只要通信过程数据未发生篡改,100%能保证是一致的;但是问题来了,即使中间的通信数据数据未被篡改,双方计算出来的md5还是存在不匹配的情形,而且出现的问题断断续续,一直没有得到有效定位和解决。

18_08_39__06_07_2018.jpg

然而并没有想的那样100%md5计算相同

二、排查路径

2.1 分析现象

问题出现时会一直提示md5校验失败,说明两边的md5计算结果确实不一样,然而发生的概率很低,低到几乎可以忽略不计,但只要出现问题就能稳定复现。


2.2 定位问题

首先,很容易想到的是双方计算规则不同,计算的层级不同,毕竟Android端对库的依赖和服务端库的依赖存在这差别;然而,将算法统一校准后问题并没有得到解决~

再来从有问题的请求json串入手分析,发现带问题的json数据给到服务端解析后--出现json转化的值一些些特定字符都会被去掉,那问题其实就定位到了,但这个服务端的问题吗?毕竟它每次都会将值里边的某个字符给丢掉。查下json规范,www.rfc-editor.org/rfc/rfc4627…(RFC 4627)转义符号会被当作无效字符给丢弃,说的也很清楚。

18_32_19__06_07_2018.jpg
很明显编译器也过不了这种规则,但是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对象,保证每次取到的都是正确的值。

方案一需要重写去除转义的方法,方案二会一定程度的影响到性能。那还是使用方案一来修复;


小结

此坑到此为止~~~!