2.0.0 新特性介绍: 弱类型契约
开发者发布了一个微服务A,同时发布了一份契约文件:
微服务A的用户可以基于这份契约,使用浏览器访问接口,也可以在自己的微服务B中,使用RestTemplate
调用add
接口,用户不需要知道微服务A的实现细节,也不需要依赖任何微服务A提供的接口,微服务B保持完全 与微服务A独立。只要契约不变,微服务B就可以保持不变。
作为微服务A的其他开发者,可以基于契约开发治理功能,不需要知道add
接口的实现细节。比如可以给add
接口增加流量控制 servicecomb.flowcontrol.Provider.qps.limit.[ServiceA].[Controller].[add]=1000
, 限制其流量为1000 TPS;还可以基于 add
接口的参数做灰度发布,在灰度发布代码里面可能会存在下面的代码片段: invocation.getSwaggerArgument("a")
来获取参数值。
“以契约为中心”是保持微服务”独立性”非常重要的手段,“独立开发、独立交付”是微服务被引入,提升软件工程能力 最重要的价值。
本文重点介绍2.0.0版本引入的“弱类型契约”,以及它与之前“强类型契约”之间的差别与联系。
弱类型契约和强类型契约的共同点都是”以契约为中心”,因此弱类型契约并没有改变 Java Chassis 的整体设计理念, 而是在实现层面发生了一些变化,进而影响到部分开发体验。2.0.0之前的版本是强类型契约。产生强类型契约和一些技术背景 有关系,讨论的起点可以从 Java Chassis Highway 的 ProtoBuffer 讲起。
强类型契约可以定义为:在一个微服务里面,契约必须存在一个或者多个编译时确定的类型与它对应。在2.0.0版本之前,这个对应 的类型是在契约文件里面指定的,比如x-java-interface: org.apache.servicecomb.demo.controller.Controller
。 开发者 可能注意到在消费端代码,可以不依赖任何提供端的类,这是因为消费端的代码会根据契约描述,采用 javassist
工具动态生成 一个类型与契约对应,不同版本的契约需要生成不一样的类型,如果灰度环境版本过多,就可能导致内存过大的问题,特别是在边缘 (Edge Service)服务里面。
强类型契约的一个外在体现就是下面的代码:
Person result = restTemplate.postForObject("/getPerson", null, Person.class);
可能抛出 ClassCastException
异常。 因为 getPerson
的返回值的类型在编译时已经确定, 如果返回值 Person 类型 与编译时的类型不一样,就会报告 ClassCastException
异常。
了解了强类型契约后,弱类型契约的定义就很明显了: 在一个微服务里面,契约可以存在一个或者多个编译时确定的类型与它对应, 也可以不存在编译时确定的类型与它对应。 引入弱类型契约后,下面的代码:
Person1 result = restTemplate.postForObject("/getPerson", null, Person1.class);
Person2 result = restTemplate.postForObject("/getPerson", null, Person2.class);
都不会报告 ClassCastException
异常,只需要 Person1
和 Person2
的定义都能够和契约对应。 由此可以看出,弱类型 契约在使用方式上更加灵活,去掉了动态创建类的过程,降低了内存占用,缩短了微服务启动时间。
弱类型契约不要求提供者与消费者使用一样的类型,在代码书写上提供了很大的方便。比如提供者有如下接口:
interface DiffNames {
int differentName(int x, int y);
}
@RpcReference(microserviceName = "springmvc", schemaId = "weakSpringmvc")
其中接口名称是和契约里面的接口名称一致 differentName
, 而不是和服务端的代码一致 diffNames
。 契约包含 x
和 y
两个参数, 并且契约的参数是顺序无关的, 下面的代码也是可以访问同一个接口的:
interface DiffNames2 {
int differentName(int y, int x);
}
@RpcReference(microserviceName = "springmvc", schemaId = "weakSpringmvc")
private DiffNames2 diffNames2;
需要注意的是,2.0.0 版本要求保留参数名称, 在编译代码的时候,需要加上 -parameters 编译参数。
开发者还可以通过 RestTemplate 调用这个接口:
或者采用 InvokerUtils
调用:
Map<String, Object> args = new HashMap<>();
args.put("x", 2);
args.put("y", 3);
InvokerUtils.syncInvoke("springmvc", "weakSpringmvc", "differentName", args);
弱类型契约在 Invocation
里面提供了独立的方法,让开发者开发治理功能更加容易。
public Map<String, Object> getInvocationArguments() {
return this.invocationArguments;
public Map<String, Object> getSwaggerArguments() {
return this.swaggerArguments;
}
public Object getInvocationArgument(String name) {
return this.invocationArguments.get(name);
}
public Object getSwaggerArgument(String name) {
return this.swaggerArguments.get(name);
}
getSwaggerArguments
始终获取的是和契约对应的参数,如果契约为 x
和 y
两个 query 参数, 那么得到的参数就是 包含 x
和 y
的 Map 。 getInvocationArgument
获取的是实际类型参数, 在服务提供者,这个参数列表的个数和类型 和实际的 Method 的列表对应, 比如可能包含 InvocationContext
, HttpServletRequest
等注入参数。 还有很多情况, 契约参数和类型参数不对应,比如聚合参数的情况,契约参数是多个 query 参数, 而类型参数是一个 POJO; 再比如 POJO 接口 定义的时候, 类型参数可能是多个, 而契约参数只有一个 body 参数。 在服务消费者,如果用户采用 POJO 方式调用服务提供者, 两个接口的返回的值与服务提供者类似,存在语义差别;如果采用 RestTemplate 或者 InvokerUtils 调用, 那么两个接口返回的 内容一样,都是契约参数。在边缘服务, 两个接口返回的都是契约参数。 这种行为体现了弱类型契约的语义: 是否存在编译时 类型与契约对应。