三豐 soft張三丰 html
衆所周知編程語言有強弱類型之分,進一步還有動態和靜態之分。好比 Java、C# 是強類型的(strongly typed)靜態語言,Javascript、PHP 是弱類型的(weakly typed)動態語言。前端
強類型靜態語言經常被稱爲類型安全(type safe)語言,通常在會編譯期間進行強制類型檢查,提早避免一些類型錯誤。弱類型動態語言雖然也有類型的概念,可是比較鬆散靈活,並且大可能是解釋型語言,通常沒有強制類型檢查,類型問題通常要在運行期才能暴露出來。java
強弱類型的語言各有優劣、相互補充,各有適用的場景。好比服務端開發常常用強類型的,前端 Web 界面常常會用 Javascript 這種弱類型語言。web
對於服務 API 也有強弱類型之分,傳統的 RPC 服務通常是強類型的,RPC 一般採用訂製的二進制協議對消息進行編碼和解碼,採用 TCP 傳輸消息。RPC 服務一般有嚴格的契約(contract),開發服務器前先要定義 IDL(Interface Definition Language),用 IDL 來定義契約,再經過契約自動生成強類型的服務端和客戶端的接口。服務調用的時候直接使用強類型客戶端,不須要手動進行消息的編碼和解碼,gRPC 和 Apache Thrift 是目前兩款主流的 RPC 框架。spring
而如今的大部分 Restful 服務一般是弱類型的,Rest 一般採用 Json 做爲傳輸消息,使用 HTTP 做爲傳輸協議,Restful 服務一般沒有嚴格的契約的概念,使用普通的 HTTP Client 就能夠調用,可是調用方一般須要對 Json 消息進行手動編碼和解碼的工做。在現實世界當中,大部分服務框架都是弱類型 Restful 的服務框架,比方說 Java 生態當中的 SpringBoot 能夠認爲是目前主流的弱類型 Restful 框架之一。編程
固然以上區分並非業界標準,只是我的基於經驗總結出來的一種區分的方法。設計模式
強類型服務接口的好處是:接口規範、自動代碼生成、自動編碼解碼、編譯期自動類型檢查。強類型接口的好處也帶來不利的一面:首先是客戶端和服務端強耦合,任何一方升級改動可能會形成另外一方 break,另外自動代碼生成須要工具支持,而開發這些工具的成本也比較高。其次強類型接口開發測試不太友好,通常的瀏覽器、Postman 這樣的工具沒法直接訪問強類型接口。api
弱類型服務接口的好處是客戶端和服務器端不強耦合,不須要開發特別的代碼生成工具,通常的 HTTP Client就能夠調用,開發測試友好,不一樣的瀏覽器、Postman 能夠輕鬆訪問。弱類型服務接口的不足是須要調用方手動編碼解碼消息、沒有自動代碼的生成、沒有編譯器接口類型檢查、代碼不容易規範、開發效率相對低,並且容易出現運行期的錯誤。瀏覽器
有沒有辦法結合強弱類型服務接口各自的好處同時又規避他們的不足呢?安全
咱們的作法是在 Spring Rest 弱類型接口的基礎上藉助 Spring Feign 支持的強類型接口的特性實現強類型 Rest 接口的調用機制,同時兼備強弱類型接口的好處。
首先咱們來介紹下 Spring Feign,Spring Feign 本質上是一種動態代理機制(Dynamic Proxy),只須要咱們給出 Restful API 對應的 Java 接口,它就能夠在運行期動態的拼裝出對應接口的強類型客戶端。拼裝出的客戶端的結構和請求響應流程以下圖所示:
1.客戶應用發起一個請求並傳入一個 Request Bean,這個請求經過 Java 接口首先被動態代理截獲
2.經過相應的編碼器(Encoder)進行編碼,成爲 Request Json
3.Request Json 根據須要能夠通過一些攔截器(Interceptor)作進一步處理
4.處理完以後傳遞給 HTTP Client,HTTP Client 將 Request Json經過 HTTP 協議發送至服務器端
5.當服務端響應回來後,相應的 Response Json 會被 HTTP Client 接收到
6.通過一些攔截器作一些響應處理
7.轉發給解碼器(Decoder)解碼爲 Response Bean
8.最後 Response Bean 經過 Java 接口返回給調用方
整個請求響應流程的關鍵步驟是編碼和解碼,也就是 Java 對象和 Json 消息的互轉,這個過程也被稱爲序列化和反序列化,另一種叫法爲「Json 對象綁定」。對於一個服務框架而言,序列化、反序列化器的性能對於服務框架性能影響是最大的,也就是說能夠認爲 Decoder 和 Encoder 決定了服務框架整體的性能。
雖然咱們開發出來的服務是弱類型的 Restful 服務,可是由於有 Spring Feign 的支持,咱們只要簡單的給出一個強類型的 Java API 接口就自動得到了一個強類型客戶端,也就是說利用 Spring Feign 咱們能夠同時得到強弱類型的好處(編譯器自動類型檢查、不須要手動編碼解碼、不須要開發代碼生成工具、客戶端和服務器端不強耦合),這樣能夠同時規範代碼風格,提高開發測試效率。
咱們能夠在項目內爲每一個微服務提供兩個模塊,一個是 API 接口模塊(如 mail-api),另外一個是服務實現模塊(如 mail-svc)。API接口模塊內就是強類型的 Java API 接口(包括請求響應的 DTO),能夠直接被 Spring Feign 引用並動態拼裝出強類型客戶端。
項目結構以下:
. ├── README.md ├── account │ ├── Dockerfile │ ├── pom.xml │ └── src │ ├── main │ └── test ├── account-api │ ├── pom.xml │ └── src │ └── main ├── mail │ ├── Dockerfile │ ├── mail.iml │ ├── pom.xml │ └── src │ ├── main │ └── test ├── mail-api │ ├── pom.xml │ └── src │ └── main └── pom.xml
注:我這裏沒有采用 xxx-api 和 xx-svc 的命名方式,直接是 xxx-api 表示 API Client 模塊, xxx 爲服務實現模塊。
咱們以用戶註冊後發送通知郵件來寫一個簡單的例子。發送郵件 client 定義以下:
// mail-api/src/main/java/com/demo/mail/client/MailClient.java import com.demo.common.api.dto.Response; import com.demo.mail.dto.EmailSendDTO; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import javax.validation.Valid; @FeignClient(name = "mail") public interface MailClient { @PostMapping(path = "/send") Response<Boolean> send(@RequestBody @Valid EmailSendDTO mailSendDTO); }
其中 Response 定義以下:
@Data public class Response<T> { private int code = 0; private String message = "OK"; private T data; }
用戶服務調用發郵件 API 實現以下:
// account/src/main/java/com/demo/account/service/UserService.java public class UserService { @Autowired private final UserRepository userRepository; @Autowired private MailClient mailClient; public boolean register(UserDTO userVO) { // 忽略參數驗證部分代碼... User user= userRepository.save(UserDTOConvert.convertTo(userVO)); EmailSendDTO mail = EmailSendDTO.builder() .to("user.getEmail()") .subject("welcome!") .htmlBody("hello," + user.getName()).build(); try { Response<Boolean> response = mailClient.send(mail); } catch (Exception e) { log.error(e.getMessage()); throw new AppException(SysErrorEnum.SYSTEM_ERROR); } if (response.getCode() != 0) { throw new ServiceException(response.getCode(), response.getMessage()); } else if (!response.getData()) { throw new ServiceException(AccountErrorEnum.MAIL_SEND_ERROR); } return true; } }
爲了體現異常處理流程,上邊代碼僅用於演示,在生產環境下發郵件應該爲異步處理,無需檢查發送結果。咱們在服務內加了全局異常處理,因此直接向上拋出便可。
最後再補充一點,業界 Restful API 的設計一般採用 HTTP 協議狀態碼來傳遞和表達錯誤語義,可是咱們的設計是將錯誤碼打包在一個正常的 Json 消息當中,也就是 Response 當中,這一種稱爲封裝消息 + 捎帶的設計模式,這樣設計的目標是爲了支持強類型的客戶端同時簡化和規範錯誤處理,若是借用 HTTP 協議狀態碼來傳遞和表達錯誤語義,雖然也能夠開發對應的強類型客戶端,可是內部的調用處理邏輯就會比較複雜,須要處理各類 HTTP 的錯誤碼,開發成本會比較高。