沒關注?伸出手指點這裏---
作者|劉寶
弱類型契約
"以契約爲中心" 是 Java Chassis 的核心設計理念。“契約” 扮演着用戶與開發者,開發者與開發者之間的溝通橋樑。舉幾個簡單的例子。
開發者發佈了一個微服務A,同時發佈了一份契約文件:
swagger: '2.0'
info:
title: rest api
version: 1.0.0
basePath: /controller
produces:
- application/json
paths:
/add:
get:
operationId: add
parameters:
- name: a
in: query
required: true
type: integer
format: int32
- name: b
in: query
required: true
type: integer
format: int32
responses:
"200":
description: add numer
schema:
type: integer
format: int32
微服務A的用戶可以基於這份契約,使用瀏覽器訪問add
接口,也可以在自己的微服務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版本引入的“弱類型契約”,以及它與之前“強類型契約”之間的差別與聯繫。
弱類型契約 vs 強類型契約
弱類型契約和強類型契約的共同點都是"以契約爲中心",因此弱類型契約並沒有改變 Java Chassis 的整體設計理念, 而是在實現層面發生了一些變化,進而影響到部分開發體驗。2.0.0之前的版本是強類型契約。產生強類型契約和一些技術背景 有關係,討論的起點可以從 Java Chassis Highway 的 ProtoBuffer 講起。
熟悉 ProtoBuffer 的開發者都知道, ProtoBuffer 主要被用於 gRPC, 其他處理 ProtoBuffer 編解碼的庫還有 ProtoStuff。gRPC 需要寫 IDL 文件, 然後根據 IDL 生成運行時的代碼,在gRPC的運行過程中,處理編解碼的類在編譯時間就已經確定,無法 更改。ProtoStuff 提供的類庫比 gRPC 更加靈活,對於同樣的 IDL 文件,可以在運行時指定不同的類進行序列化和反序列化。無論如何,在一個微服務裏面,如果存在一個契約(IDL文件),那麼必須在編譯時就確定這個契約對應的類是什麼。這個類可以 有一個,也可以有多個,但是必須在編譯的時候確定類的類型。
強類型契約可以定義爲:在一個微服務裏面,契約必須存在一個或者多個編譯時確定的類型與它對應。在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
的定義都能夠和契約對應。由此可以看出,弱類型 契約在使用方式上更加靈活,去掉了動態創建類的過程,降低了內存佔用,縮短了微服務啓動時間。
利用弱類型契約增強寫代碼的靈活性
弱類型契約不要求提供者與消費者使用一樣的類型,在代碼書寫上提供了很大的方便。比如提供者有如下接口:
@RestSchema(schemaId = "weakSpringmvc")
@RequestMapping(path = "/weakSpringmvc", produces = MediaType.APPLICATION_JSON_VALUE)
public class WeakSpringmvc {
@GetMapping(path = "/diffNames")
@ApiOperation(value = "differentName", nickname = "differentName")
public int diffNames(@RequestParam("x") int a, @RequestParam("y") int b) {
return a * 2 + b;
}
@GetMapping(path = "/genericParams")
@ApiOperation(value = "genericParams", nickname = "genericParams")
public List<List<String>> genericParams(@RequestParam("code") int code, @RequestBody List<List<String>> names) {
return names;
}
@GetMapping(path = "/genericParamsModel")
@ApiOperation(value = "genericParamsModel", nickname = "genericParamsModel")
public GenericsModel genericParamsModel(@RequestParam("code") int code, @RequestBody GenericsModel model) {
return model;
}
@GetMapping(path = "/specialNameModel")
@ApiOperation(value = "specialNameModel", nickname = "specialNameModel")
public SpecialNameModel specialNameModel(@RequestParam("code") int code, @RequestBody SpecialNameModel model) {
return model;
}
}
而消費者只需要訪問其中一個接口diffNames,只需要定義一個非常簡單的接口:
interface DiffNames {
int differentName(int x, int y);
}
@RpcReference(microserviceName = "springmvc", schemaId = "weakSpringmvc")
private DiffNames diffNames;
其中接口名稱是和契約裏面的接口名稱一致 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 調用這個接口:
restTemplate.getForObject("cse://springmvc/weakSpringmvc/diffNames?x=2&y=3", Integer.class)
或者採用 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 調用, 那麼兩個接口返回的 內容一樣,都是契約參數。在邊緣服務, 兩個接口返回的都是契約參數。這種行爲體現了弱類型契約的語義:是否存在編譯時 類型與契約對應。
弱類型契約帶來的一些變更
總體而言,對於 REST
通信模式, 弱類型契約不僅增強了寫代碼的靈活性, 還完整保留了強類型契約的寫代碼方式,幾乎不 存在用戶需要感知的變更。對於 HIGHWAY
通信模式, 由於底層採用 ProtoBuffer 編碼, 而 ProtoBuffer 天然就是一種 強類型契約的編解碼過程, java-chassis 爲了支持弱類型契約, 做了大量努力, 在一些邊界條件處理上與弱類型契約存在 變更,兩個版本的編解碼是不兼容的,需要同時升級提供者和消費者。在編碼方式上,差異主要體現在對於缺省值的處理,對於 null
的處理等問題上, 詳細參考1.x遷移2.x指南(https://docs.servicecomb.io/java-chassis/zh_CN/upgrading/1_to_2/) 。
如您對開源開發,微服務專家
歡迎掃描下方二維碼添加
ServiceComb小助手
咱們一起做點有意思的事情〜
武漢加油
在看一點,病毒退散