Apache ServiceComb Java Chassis 2.0.0 新特性重磅出擊: 弱類型契約

沒關注?伸出手指點這裏---

作者|劉寶

弱類型契約

"以契約爲中心" 是 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 的列表對應, 比如可能包含 InvocationContextHttpServletRequest 等注入參數。還有很多情況, 契約參數和類型參數不對應,比如聚合參數的情況,契約參數是多個 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小助手

咱們一起做點有意思的事情〜

武漢加油

在看一點,病毒退散

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章