【编码日常】微服务接口兼容性升级之序列化

dubbo rpc 接口兼容升级 hessian2 序列化

任何使用微服务架构的团队,作为接口提供方若对扩展性没有考虑周全的话,后续的升级和打补丁绝对是一件让人头疼的事情。要不新增接口与原接口物理隔离,但这会造成接口数量的迅速膨胀和维护困难;要不小心翼翼的修改原接口,尽量考虑兼容到所有依赖系统,而这很容易进入顾此失彼/百密一疏的境地。

本文作者的团队是基于Dubbo微服务架构,所以很多说明和示例都与此有关,我会尽量表述清楚,不妨碍大家理解和阅读。

#兼容性升级常见问题

1. 参数本身或内部成员变量的【类型****修改】

这里的参数包括入参出参, 下文不再特别说明。

既然是兼容升级,修改类定义指的通常是修改为父类。比如MailForm修改为父类BaseForm甚至Object,用来处理类型边界更广的数据。问题随之而来,边界放大了可以收入或者释放更多类型的数据,但是逻辑复杂度也不可避免的升高,稍有不慎万劫不复。此类修改强烈不建议。

2. 参数内部【增删成员变量】

新增应该是最经常发生的。很多时候我们没办法预估到未来的某个需求时,比如要新增一个属性让调用方传递过来,可能是必填也可能是非必填,如果是没有封装为对象只是在入参内一一罗列属性变量,这种低级的设计误区我们就不提了;所以一般是封装为对象作为入参,而这时我们就不得不在对象内新增一个成员变量。

删除一个属性字段的情况很少发生,这里说的场景是:服务方新加了一个属性字段,并提供了一个新的jar包,然后调用方拿着新jar在生产环境发布,而服务方却因为某些原因没有上线,这就出现了好像服务方“删除”了一个字段一样。

3. 修改参数对象的【数据结构】

可能有人会说修改数据结构和兼容性不可能同时出现,这个不能绝对,因为编程中接口定义作为双方约定好的承诺,被单方面破坏的事情偶有发生。比如服务方提供一个接收 Map 类型的方法,如果没约定好具体的实现,调用方使用HashMap、LinkedHashMap、TreeMap的可能性都存在,如果不小心直接使用了TreeMap.firstKey(),只有妥妥的异常。

4. 使用Enum 参数

服务方为了调用方的方便不少人喜欢将参数定义成枚举,殊不知这在RPC接口定义中是一大忌,因为一旦新增枚举值就绝对会导致一片血雨腥风,后文会从序列化角度来说明为何不可。友情提醒,还是老老实实用常量类来实现比较稳妥。

5. 简单总个结

上文主要总结了一些日常会碰到的问题,不少人可能也曾深受其害。接口升级务必充分测试,万不可想当然。举个实际的案例,某个接口的出参是一个Json字符串,有次升级给Json新增了一个Key值,这个Key值很多场景下无关痛痒,只有很特殊的场景才会用这个值。服务方的想法很简单,就算是没有升级的老的调用方解析出这个Key,他的逻辑里也不会对其进行任何操作,理论上没有任何问题。而最后的结果很惊悚,它会导致某些版本的客户端App闪退!

无法免俗的还是不得不提下RPC微服务接口设计和升级的几个要素和建议

a. 接口或者类最好预定义好版本号,可以基于配置(比如dubbo xml),也可以基于类/方法命名。

b. 不能保证万无一失的升级,就尽量新增接口而不是修改,也就是常说的开闭原则。

c. 接口涉及的对象一定要拿捏好边界。不能暴露非必要的属性字段,造成后期的维护升级难度提高;也不能将属性字段定义的太死,不怎么修改的相对稳定的字段可以用确定的类型,而有很大可能性变动的字段建议用模糊的类型定义,比如Map、List等集合类。

d. 用静态常量代替 Enum 类型。

e. 用相对扁平的数据结构,不要使用嵌套过多的集合类型等。

f. 其他...

【Incompatible Changes & Compatible Changes】
 https://docs.oracle.com/javase/8/docs/platform/serialization/spec/version.html#a5172

#serialVersionUID

RPC调用本质无非就是数据的传递,而数据在RPC场景下无非就是序列化的二进制对象,然后成功调用的基本前提就是能够在 Remote 端反序列化恢复成原本的对象。所以本文我们就把接口升级这件事转换到如何保证序列化&反序列化成功率的角度。

先简单把序列化相关基础知识准备一下。谈到序列化大家脑子里先蹦出来的应该是 Serializable 这个Marker标记型接口,和该接口的实现类( IDE通常会提示让你生成 )的一个变量值:

 private static final long **serialVersionUID** = 1L;

【4.6 Stream Unique Identifiers】
 https://docs.oracle.com/javase/7/docs/platform/serialization/spec/class.html#4100

这个值的算法简单说就是对类名,接口名,方法和属性的名称、修饰符以及描述符的64位哈希值。动态代理类和枚举类的该值都是 0L。特别说明:static/transient类型成员变量、私有方法都不参与Hash值计算。

它也是JDK官方定义的所有序列化的类必须设定的,不过如果代码里没有显性设置,也不用担心,JVM会使用相同的算法帮你生成一个。

一个类能序列化的前提是它内部所引用的所有对象也必须可以被序列化,这点是很容易被忽视的。

有个不怎么常用的 Externalizable,不知道多少人用过,它继承自 Serializable,是个历史遗留接口。有兴趣的可以自己了解下。

对JDK序列化方式本文不会再展开,主流的RPC调用框架一般都不会选择JDK序列化方式,因为它的性能相比hessian2, kryo, FST, protstuff, thrift等等基本没有任何优势。

#Dubbo hessian2 序列化测试

Dubbo框架缺省的dubbo协议默认序列化方式就是Hessian2, 话不多说,直接上针对Hessian2的测试代码,然后出结论。代码设计简述如下:

  1. 使用两个完全不同的Project,保证同一个package对象做修改不会互相影响。(一开始考虑过用自定义的Classloader来隔离和模拟调用方和服务方两个同名不同内容的对象,发现复杂度有点高,放弃)

  2. 不需要真正的发起RPC调用,双方使用本地的二进制文件简单模拟传输通道。

两个工程

  • Project-Provider

  • Project-Consumer

分别实现四个类:

  • HessianUtil(Hessian工具类)

  • SerialBean(序列化的Bean)

  • RequestEnum(枚举类)

  • Main(Main测试主入口)

HessianUtil.java
SerialBean.java
RequestEnum.java
Main.java Provider调用serialize(), Consumer调用deserialize()*

测试案例

代码准备完毕,开始我们的测试案例,下图是笔者的一些用例和测试结果:

图中“绿色对勾”图标表示可以成功序列化和反序列化,“红色叉”图标表示不能完全或者部分行不通(比如存在的枚举值行得通)

image

大家有兴趣可以自己去验证更多的案例,这里直接总结结论:

  1. 所有RPC交换对象必须实现Serializable接口;

  2. serialVersionUID 对hessian2无任何影响;

  3. 构造方法不管是否私有,对hessian2无任何影响;

  4. 类型定义除了比较容易理解的修改成父类没有影响之外,类似Long to Boolean也可以成功运行相信很多人没想到,虽然行得通,但是代码逻辑已经失控,肯定是不会建议大家这么做;

  5. 枚举类中增删新的枚举值,对于双方存在的枚举值不会报错,但是一方不存在的就会反序列化异常;

  6. 其他多层嵌套和集合类的测试交给读者自测吧,笔者就不展开了哈哈哈

Dubbo Http协议的坑

架构组老钱前端时间把Mail基础组件从1.0升级成2.0的时候,分享了一个坑。简单来说就是原有MailForm没有显性的设置serialVersionUID, 后来升级新增了字段导致serialVersionUID的计算结果产生变化,一些依赖Mail1.0的调用方会报错,最后不得不显性的设置serialVersionUID并保持不变,这样老的调用方就不会抛异常了。

后来笔者通过上文的各种测试发现这种情况与结论相悖,因为hessian2压根就无视serialVersionUID的存在,到底是怎么发生的呢?苦思无果下,突然想到Mail1.0版本提供的微服务采用的是http协议不是默认的dubbo协议,会不会http协议采用的默认序列化方式不是hessian2呢?看图!

image

上图是Debug过程中的一张截图,大家可以看89行,对象obj是通过ois.readObject() 反序列化出来的。再看ois这个流对象CodebaseAwareObjectInputStream压根就不是Hessian2体系内的类,而是Spring框架里继承自java.io.ObjectInputStream的一个流对象。到这里就说得通了,因为JDK自带的序列化机制确实会严格比对serianVersionUID是否一致,出现上面的异常也就不足为怪了。

Hessian2 如何处理不存在的类

再送一个知识点,hessian2反序列化的类如果在当前jvm里或者classloader里不存在,会出现什么场景?

image

上图是SerializerFactory用来根据反序列化的类型找反序列化器的源码,很清晰的看到是MapDeserializer,它会把不存在的对象按照成员变量的名称反序列化成一个HashMap结构。

这个问题是笔者在使用自定义classloader模拟测试的时候发现的,一并分享出来。

END

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • JAVA序列化机制的深入研究 对象序列化的最主要的用处就是在传递,和保存对象(object)的时候,保证对象的完整...
    时待吾阅读 10,734评论 0 24
  • 约好天气好一起去看风车展的。果然,和天气预报一致,阳光明媚、天气很好。起床练完了字、看好了书。联系阿晶,还在去财政...
    和小忘阅读 331评论 0 1
  • 只是因为太年轻 ,所以所有的悲伤和快乐都显得那么深刻,轻轻一碰就惊天动地,总有一天你的棱角会被世界磨平,不再为一点...
    冷傲的橘子阅读 120评论 0 0
  • 秋老虎的势头可真厉害!中午十一点左右,高远响晴的天空中,一轮明晃晃的太阳像个巨大的探照灯,射得人眼睛生疼,仿佛要把...
    果枚阅读 322评论 8 8
  • 认知心理学认为人头脑中已有的知识和知识结构对人的行为和当前的认识活动有决定作用,换句话说,意思就是当你开始接触一个...
    李不黑阅读 608评论 1 0