【摘要】 本文介绍了CSE强契约的常见问题,以及针对缺省值、控制消息、异常处理的开发建议。

CSE框架基于强契约,服务之间的交互都需要参考和验证契约信息。

CSE强契约模式常见问题和应对策略_CSE学习

缺省值问题

契约本身是一个平台和语言无关的信息,在不同的语言里面,对于契约的解析是不同的。即使同一种语言,对于契约的翻译也可能不同。造成这个差异的因素有很多。以JAVA语言为例,有如下场景可能导致这种差异:

1.       契约的类型在编程语言上有多种表达形式。比如type: “integer”,既可以使用int类型表示,又可以使用Integer类型表示,采用不同类型表示的时候,他们的缺省值会发生变化,分别对应于null和0。在实际的应用开发中,一般没法约束所有客户端使用同样的数据类型。

2.       解析契约的参与方,设置的序列化/反序列化参数机制不一样,而契约不体现序列化方式,所以参与方在序列化/反序列化的时候,可能得到不一样的值。比如Jackson库,可以设置非常多的属性来控制序列化行为,包括不序列化字段为空的属性、不序列化缺省值的属性;反序列化允许单String作为数组元素、忽略或者不允许多余的字段等。序列化行为差异导致的重要问题是对于缺省传值的处理逻辑发生错乱,下面是一些常见的情况:

a.       客户端在Json中不传递某个属性的值,那么服务端得到的缺省值可能不同。假设服务端定义的对象为ServiceC的契约实现,并且ServiceB设置了@JsonInclude(Include.NON_DEFAULT) ,那么index的值为1;如果没有设置,那么index的缺省值为0。

b.       客户端在Json中包含了一个多余的参数,当设置了允许额外的属性的时候,接口会抛出异常;当没有设置的时候,接口使用缺省值。

在传统的业务架构下,服务调用层级简单,服务本身的返回值就是用户最终的返回值,缺省值造成的影响不是很明显,很多业务通过调整序列化/反序列化属性来规避解析问题。在微服务架构下,缺省值可能会导致很多业务异常和非预期行为发生,给业务健壮性埋下隐患,应该尽可能避免通过定制序列化/反序列化属性来满足开发要求。CSE缺省情况下,不开放Jackson序列化接口给开发者定制(最新版本也是可以的,但并不建议)。

l  解决方案

为了避免缺省值带来的潜在问题,从框架层面和业务代码实现层面,建议遵循下面的约束:

1.       框架实现层面,在序列化、反序列化参数设置的时候,尽可能选择更加健壮的策略。比如允许单String作为数组元素、忽略多余的字段等。这样一些容易犯错误的请求可以到达业务服务。对于错误的检查,配合Validation API由业务完成。

2.       业务实现方面,设计业务逻辑的时候,避免依赖于缺省值,并将常见的缺省值情况,当成一致的语义。比如ServiceC的契约实现,应该将name为null,name为””,或者name不传递当成一样的语义;避免在代码定义里面使用index=1赋缺省值,或者将index为0,index=1或者不传递(等价于index=0)当成一样的语义处理。这里的“当成一样的语义处理”的含义,简单的讲,就是如果得到这几种值,在业务处理上,都是同一个逻辑处理分支,比如返回参数错误;或者某次查询返回缺省集合等。

控制消息传递问题

控制消息是独立于契约以外的信息,不会在契约里面体现。但是作为Consumer在访问Provider的时候,需要提供才能够正确访问,最常见的场景莫过于会话管理SessionID和调用链TraceID了。控制消息传递一般都基于运行平台自身提供的能力,比如使用J2EE(Servlet)开发的服务,一般都通过在HttpServletRequest或者HttpServletResponse增加Header信息来实现,各种HTTP协议网关,都会透传Header信息(除了一些HTTP控制头,包括Transfer-Encoding、Content-Length等),这块机制已经比较成熟,一般的业务代码也是基于这种机制。CSE的REST服务构建于HTTP协议之上,同时支持非HTTP协议,因此在传递控制消息的时候,提供了自己的机制。所以刚刚接触CSE的开发者都可能发现通过HTTP协议Header传递的消息,无法通过HttpServletRequest或者,也无法通过HttpServletResponse来写入。

CSE机制是通过InvocationContext来实现的。在HTTP接入层,可以通过HttpServerFilter将HTTP的Header设置到InvocationContext里面去,业务实现代码可以通过InvocationContext获取控制消息,InvocationContext信息会在CSE微服务和网关(Edge Service)之间透明传递,就像HTTP的Header头在代理服务与业务服务之间传递一样[1]。

异常信息处理

异常信息也是独立于契约以外的信息,不会在契约里面体现。异常信息的处理也是依赖于平台能力的。在J2EE(Servlet)场景下,通过HttpServletReponse写入错误码和消息体内容来实现,它的主要问题是作为Consumer,不知道如何处理异常,以及如何解析消息体,通常只能靠测试,处理几个常见的错误码和消息体,然后做一个统一的错误处理机制,给用户报告错误页面。

CSE的异常处理全部都基于异常,能够很好的给Consumer使用,不管Consumer是一个RPC还是HTTP Client,在代码风格上,看起来都具备一致性。业务代码需要返回异常的时候,也通过throw InvocationException实现。

l  使用RPC

try {

         result = controller.add(index, 3);

} catch (InvocationException e) {

         result = e.getCause().getMessage();

}

l  使用RestTemplate(HTTP)

try {

         template.postForEntity(SERVER + "/MultiErrorCodeService/errorCode", request, MultiResponse400.class);

} catch (InvocationException e) {

         t400 = (MultiResponse400) e.getErrorData();

}

除了代码风格,在异常处理上,一个核心问题就是异常内容如何用恰当的数据结构来表示。在微服务架构下,并不是所有Consumer都具备Provider提供的异常结构。这块CSE也提供了很好的处理方法,详细情况可以查询下面的参考资料[2]。

参考资料

1.       在CSE中使用InvocationContext传递控制消息:https://docs.servicecomb.io/java-chassis/zh_CN/general-development/context.html

2.       在CSE中处理异常: https://docs.servicecomb.io/java-chassis/zh_CN/general-development/error-handling.html

来源:华为云社区  作者:liubao68