这可能是我遇到过的编程经历中的史诗级巨坑(Android)

前言

写程序的时候,如果一个 BUG 怎么调试都发现不了问题所在,那是最痛苦的,我想绝大部分人都承认这个观点。出现这个现象的原因很多,例如:单元测试不够充分、自己粗心大意犯了低级错误、潜在的框架/库的 BUG 等。但是唯独有一种 BUG 是最让我无法忍受的,这种 BUG 会教会你没有 “绝对” 的可靠,对任何事情都要多一份疑心,哪怕是万分之一的可能。

Java 所谓的跨平台

假设你有这样一个模型类(这个其实是我博客客户端的一个代码部分,评论实体的模型):

data class CommentModel(
        var id: Int = 0,
        var content: String = "",
        @SerializedName("author_nickname") var nickname: String = "",
        @SerializedName("author_email") var email: String = "",
        @SerializedName("author_face") var face: String = "",
        var site: String = "",
        var top: Long = -1L,
        @SerializedName("cor_res_type") var resType: Int = 0,
        @SerializedName("cor_res_id") var resId: Int = 0,
        @SerializedName("create_at") var createAt: Date = Date(),
        @SerializedName("update_at") var updateAt: Date = Date(),
        @SerializedName("resource_status") var resourceStatus: Int = 0
)

有这样的一个 Retrofit 接口方法(Retrofit 是一个安卓项目常用的高度封装接口式定义 API 的 HTTP Cilent):

@POST("/v1/comments/{res_type}/{res_id}")
@Headers(APPLICATION_JSON)
fun publish(@Path("res_type") resType: Int,
            @Path("res_id") resId: Int,
            @Body model: CommentJsonModel): Call<ResultModel>

使用 Retrofit 构造出来的接口实例,调用 publish 方法,在本机会输出:

D/OkHttp: --> POST https://blogger-core-test.bluerain.io/v1/comments/0/10004 http/1.1
D/OkHttp: Content-Type: application/json
D/OkHttp: Content-Length: <size>
{
    "id": 10058,
    "content": "啦啦啦~ 我是一条评论 😅",
    "author_nickname": "绅士喵",
    "author_email": "-",
    "author_face": "https://o4p9r7thl.qnssl.com/app_account_face",
    "site": "http://blog.bluerain.io",
    "top": 1494658500,
    "cor_res_type": 0,
    "cor_res_id": 10077,
    "create_at": "2017-05-13T14:54:45+08:00",
    "update_at": "2017-05-13T14:54:45+08:00",
    "resource_status": 0
}
D/OkHttp: --> END POST (<size>-byte body)

但是在安卓中运行时,应用程序调用会输出:

D/OkHttp: --> POST https://blogger-core-test.bluerain.io/v1/comments/0/10004 http/1.1
D/OkHttp: Content-Type: application/json
D/OkHttp: Content-Length: <size>
{
    "content": "啦啦啦~ 我是一条评论 😅",
    "create_at": "2017-05-13T14:54:45+08:00",
    "update_at": "2017-05-13T14:54:45+08:00",
    "author_nickname": "绅士喵",
    "author_email": "-",
    "author_face": "https://o4p9r7thl.qnssl.com/app_account_face",
    "id": 10058,
    "site": "http://blog.bluerain.io",
    "top": 1494658500,
    "cor_res_type": 0,
    "cor_res_id": 10077,
    "resource_status": 0
}
D/OkHttp: --> END POST (<size>-byte body)

安卓运行时在构造 JSON 时,没有按照类中的属性顺序。导致在本机跑测试的时候,接口正常(包括服务端回应)。但是在应用程序运行的时候,服务端一直失败。

而我那个服务端程序用的是 Go 语言的关注量最高的 https://github.com/ant0ine/go-json-rest 框架搭建的。问题在于本地跑测试的时候 go-json-rest 服务端完全正常的解析客户端发来的 JSON(转换成 struct 对象),但是安卓程序发来的 JSON 解析一直处于失败(struct 对象实例没有正确的根据 JSON 属性赋值)。

其实这个就是两句代码:

c := Comment{}
request.DecodeJsonPayload(&c)

将 Payload 数据中和 Comment struct 对应的字段都相应的赋值给对象 c,所以这段代码之后原本所有属性都为默认值的 c 对象的对应属性都会被填充成请求中携带的数据。c 不在是一个初始对象。

问题就在于凡是安卓发来的请求,c 对象经过 DecodeJsonPayload 以后,仍然是一个 “初始对象”,它没有任何属性被赋到值,这样导致后续会直接失败,所以安卓上对服务端的这个 RESTful 接口一直不能成功调用(也就是成功发表一个评论)。

要知道,我是经历了各种调试各种可能测试一一被推翻最终实在是没办法将日志中的 Payload 拿出来,失败的和成功的一一比对,究竟是哪里不一样会导致这样的结果。最终却发现除了顺序没有 “不一样”,谁会料到这样的一个 JSON 字段顺序的差异会导致服务端问题…
(凡是有最基本的 RESTful 接口开发经验的人都知道 JSON 字段排列顺序不一致是完全不要紧的,毫不相关)

其实原因可以说相关也可以说不相关,JSON 顺序对服务端构造相应的 struct 实例数据包装毫无影响,包括我所用的 go-json-rest 框架。但是,go-json-rest 的一个 BUG 却着被 JSON 属性顺序影响到了。
在 go-json-rest 中,如果请求中的 Payload 和 Decode 的 Struct 中的对应的 Date 类型字段无法正确转换,例如 Comment 中的 create_at 是 Date 类型的,但是请求 Payload 中的 create_at 无法被解析为一个时间对象,那么这 Struct 后续的属性不会继续被赋值了。

所以,当安卓中将 create_at 和 update_at 等时间字段放在前面的时候,导致后续关键的 content 和 author* 等字段在服务端对应的 Struct 对象属性上都会转换失败(或者说干脆不转换了)。
就是这样导致了一个仍然是初始状态的 Struct 对象,相当于没有根据请求 Payload 接收到任何值,自然不能成功调用了。

看法

虽然这罪魁祸首是 go-json-rest 框架,如此令人无语、意想不到的 BUG,毕竟谁会考虑是不是 JSON 中的字段排列不一致?我是实在没别的办法了,只能试一试,哎,发现将时间类型的字段放在前头就失败… 放在后头就成功…

但是让我如此头疼的还是 Java 程序的运行状态不一致!Gson + Retrofit 在本机(Linux 64bit、HotSpot™ 64-Bit Server VM、Java 8),Payload 中的 JSON 字段顺序和定义属性时的顺序是一致的,不会随机。
但是一个环境下编译出来的程序,在安卓中运行,请求 Payload 中的 JSON 字段顺序就乱了,简直莫名其妙。

如果说问题所在,自然是后端的问题,因为前端无论发怎样字段顺序的 JSON 数据,只要结构是对应的,就一定能保证一致的结果。
但是毕竟 go-json-rest 只众多框架中的一个,它出点 BUG 比较正常,因为我也没觉得它会多可靠,在使用的过程中已经有一些东西都是我自己调试以及修改这个框架自身的小毛病。所以我确实没觉得它可靠。

但是 Android 中的 VM 和 OracleJava 的 VM 调用同一段程序有如此大的差异,真是令人后怕。

经历

让我想起了曾经,遇到过的一个 BUG:在不同的 Java 版本上程序运行状态不一致。
具体就是在 Java 版本 A 上,这里会抛出 X 异常,但是在 Java 版本 B 上,这里会抛出 Y 异常。当然 Catch 怎样的运行时异常肯定是开发中调试和测试出来的。我在开发机上测试这个问题会导致 X 异常,但是在服务器 B 版本的 Java 上同样的问题会导致 Y 异常,于是没有被 Catch 到,结果当然就 GG 了。

为什么我用 A、B 来做版本呢,因为我当时没有记录具体版本,现在当然记不得了。但是我记得同是 Jdk 1.7 环境,只有子版本号区别。仅仅子版本不同居然就让程序产生了不一样的结果… 也许是极少的现象被我碰到了,但是当时还是让我明白到了,什么完全的向下兼容、跨平台运行(一次编译到处调试真不是吹的),真的不要当真,至少绝对的不能 100% 当真。那个问题也是花了很长时间在服务器上逐步调试最后才确认代码无任何问题,升级成一致的 Java 版本就解决了。

最后

所以,我想说的就是这种 BUG。这种本应该靠谱,被认为可能是导致 BUG 的几率最低的地方,你的环境、你的运行时,居然都不可靠了。
就好像我在 Windows 下写了一个程序,但是始终达不到目的,我可能从代码、框架/类库、外在因素(文件、配置、网络等)一一排查,但是仍然没有发现问题,最后发现却是最最不可能出问题的 Windows 系统 BUG,这简直让人不能更加恼火。

我完全不是针对 Java,跨平台的语言和技术多了去了,只不过 Java 和我工作接触比较深,所以相对更加深入。也许 Python、Ruby 甚至 Javascript 都会有这种现象,它们可能埋藏在某个角落等待你去发现。或许之后会被标记为 Bug 在后续版本修复,也可能 Wontfix 被认为是你的问题,或者做不到的结果 。总之,它们绝对不是 100% 可靠的,在面对无法解释的 BUG 面前请保持对它们可靠性的那份怀疑。