Java 和微服務,第 2 部分: 在 Java 中創建微服務

etblakstn 7年前發布 | 18K 次閱讀 微服務 Java Java開發

系列內容:

此內容是該系列 4 部分中的第 # 部分: Java 和微服務,第 2 部分

https://www.ibm.com/developerworks/cn/views/cognitive/libraryview.jsp?sort_by=&show_abstract=true&show_all=&search_flag=&contentarea_by=%E6%89%80%E6%9C%89%E4%B8%93%E5%8C%BA&search_by=Java+%E5%92%8C%E5%BE%AE%E6%9C%8D%E5%8A%A1&product_by=-1&topic_by=-1&ibm-search=%E6%90%9C%E7%B4%A2

敬請期待該系列的后續內容。

此內容是該系列的一部分: Java 和微服務,第 2 部分

創建由微服務組成的應用程序的前景給所有語言都帶來了一些疑問:

  • 微服務應該有多大?
  • 對于傳統的集中化治理,做一件事的專注服務的概念有何意義?
  • 微服務會如何處理傳統的數據建模方式?

本章將重點介紹如何識別和創建組成應用程序的微服務,特別是如何將識別的候選服務轉換為 RESTful API,然后在 Java 中實現它。

示例應用程序

我們使用兩個示例應用程序來幫助解釋相關概念和提出觀點:

在線零售店

在線零售店是一個在討論應用程序架構時常用的示例。本書將考慮一組微服務,它們共同構建了一個應用程序,用戶可在其中瀏覽產品并下單。

該架構包含以下服務:

  • Catalog 服務:包含可在店內出售的產品的信息。
  • Account 服務:管理商店的每個帳戶的數據。
  • Payment 服務:處理支付。
  • Order 服務:管理當前訂單列表,執行訂單生命周期中需要的任務。

Game On!

Game On! 是一個可擴展的、基于文本的冒險游戲,是使用微服務構建的。不同于在線商店,Game On! 是一個真實的應用程序,可在 GitHub7 上獲得它的代碼,而且有一個 GitBook8 詳細介紹了創建該游戲所使用的架構和工具。這個特意選擇的非標準示例用于與傳統企業應用程序形成對比。

該游戲基本上講是一個由互聯房間組成的無限二維網絡。

房間由第三方提供(例如大會或研討會中的開發人員)。一組核心服務提供了一致的用戶體驗。用戶登錄并使用簡單的文本命令來與房間交互,并在房間之間穿梭。同步和異步通信模式都可以應用。玩家、核心服務和第三方服務之間的交互也受到保護。

Java 平臺和編程模型

新 Java 應用程序應至少應使用 Java 8,而且應采用新語言功能,比如 lambdas、并行操作和流。使用注釋可以通過消除樣板文件簡化代碼庫,但可能產生一種主觀觀點"真是太神奇了"。嘗試在代碼清潔度與可維護性之間建立平衡。

要創建具有完美功能的微服務,只有使用低級 Java 庫。但是,通常建議有一個關注點來為常見的橫切關注點 (cross-cutting concerns) 提供合理的基礎支持。它使服務的代碼庫能專注滿足功能需求。

Spring Boot

Spring Boot 提供的微服務創建機制基于對應使用哪些技術的主觀看法。Spring Boot 應用程序是 Spring 應用程序,它們使用了該生態系統所獨有的編程模型。

Spring Boot 應用程序可打包為以應用服務器托管的應用程序形式運行,或者打包為包含依賴關系和某種嵌入式服務器(通常是 Tomcat)的可運行 .jar 文件。Spring Boot 強調約定優于配置,結合使用注釋和類路徑發現來實現額外的功能。

Spring Cloud 是第三方云技術與 Spring 編程模型之間的一組集成。其中一些是 Spring 編程模型與第三方技術之間的集成。在某些少見的情況下,Spring Cloud 在 Spring 編程模型以外的地方很有用,比如 Spring Cloud Connectors Core

Dropwizard

Dropwizard 是另一種在 Java 中創建微服務的技術。它也是一種主觀方法,其預先選擇的技術組合通過附加功能增強了一個嵌入式的 Jetty servlet 容器。Dropwizard 支持的技術集合相對較小,但第三方或社區提供了插件來填補空白。它們的一些功能非常不錯,特別是它們對應用程序指標的支持。

Java Platform, Enterprise Edition

Java Platform, Enterprise Edition (Java EE) 是一種構建企業應用程序的開放、社區驅動的標準。

Java EE 技術適合創建微服務,尤其是 Java API for RESTful Web Services (JAX-RS) 和 Java EE 上下文與依賴注入 (CDI)。兩種規范都使用了注釋,用于支持依賴注入、輕量型協議和異步交互模式。Java EE7 中的并發性實用程序進一步提供了一種簡單機制,以容器友好的方式使用反應式庫,比如 RxJava 。

現代 Java EE 應用服務器(WebSphere Liberty、Wildfly、TomEE、Payara)輕松滿足了無痛處理應用服務器的要求。所有這些服務器都可在自動化構建過程中生成不可變的、自成一體的、輕量型的工件,而不需要額外的部署步驟。此外,WebSphere Liberty 等可組合的服務器允許顯式聲明規范級依賴關系,能夠升級運行時而不向應用程序引入兼容性問題。

應用程序可使用更少的 Java EE 技術,并與 WebSphere Liberty 等應用服務器打包在一起,這些應用服務器能調整運行時來滿足應用程序的需求。此過程可得到干凈的應用程序,這些應用程序能在環境之間移動,而無需更改代碼、模擬服務或自定義測試工具。

本文中的示例主要使用了 Java EE 技術,比如 JAX-RS、CDI 和 Java Persistence API (JPA)。這些規范很容易使用,而且受到廣泛支持,無需依靠特定于供應商的方法。

帶版本的依賴項

Apache Maven 或 Gradle 等構建工件提供了定義和管理依賴關系版本的輕松機制。顯式聲明版本,可確保工件在生產環境中與在測試環境中一樣運行。

一些工具讓創建使用 Maven 和 Gradle 的 Java 應用程序變得更容易。這些工具接受一些技術作為輸入并輸出項目結構,項目結構中包含構建和發布或部署最終工件所需的 Maven 或 Gradle 構建工件:

識別服務

第 1 部分"概述"中已提到,微服務的描述中使用到了"小"這個詞。服務的大小應反應在它的工作范圍上,而不是它的代碼庫大小。微服務應擁有較小且專注的工作范圍:它應該只做一件事,并將這件事做好。Eric Evans 撰寫的圖書《領域驅動設計》中描述了許多概念,在決定如何將應用程序構建為不同部分的組合時,這些概念很有幫助。接下來的幾節將引用在線零售店和 Game On!,解釋為什么這些概念對長期開發和維護微服務架構很有用。

應用領域驅動設計原理

在領域驅動設計中,領域是一個特定的支持或活動區域。模型是隨著對領域的理解的變化,對不斷出現的領域的重要方面的一種抽象。此模型隨后被用于構建跨團隊通信解決方案。

在整體式系統中,整個應用程序僅有一個統一的領域模型。如果應用程序一直很簡單,此概念會很有用。當應用程序的不同部分以不同方式使用相同元素時,使用單個統一模型會變得不合理。

例如,線零售店示例中的 Catalog 服務專注于待售商品,包括描述、部件號、產品信息、圖片等。相反,Order 服務專注于發票,使用最少的信息表示商品,或許僅包含部件號、摘要和計算的價格。

有界上下文允許將一個領域細分成獨立的子系統。然后,每個有界上下文可以擁有自己的領域概念模型。繼續查看上面的示例,Catalog 和 Order 服務分別擁有最適合其服務需求的獨立商品表示。此系統通常很有效,但是,如果兩個服務擁有同一個概念的不同表示,那么它們如何與其他服務共享信息呢?

上下文映射(包括開發和使用通用語言)可幫助確保正開發各個服務的不同團隊理解應如何構成整個應用。如果為微服務應用程序建模領域,在確定組成系統的各部分時,一定不要考慮到類或接口層面。對于第一個類,在整體上保持粗粒度級別即可。允許開發各個服務的各個團隊在自己的有界上下文內工作,以擴充細節。

Game On! 中的通用語言

為了提供語言的重要性的具體示例,我們會繞一個小彎,先講述一下 Game On! 是如何開發的。該游戲的構建目的是,讓開發人員更容易直接試驗微服務概念,將快速入門示例的即時滿足與考慮微服務架構的更復雜方面的不同方式相結合。Game On! 是一種基于文字的冒險游戲,開發人員可在其中添加各個服務(房間)來擴展游戲。

該游戲最初被比喻為一家酒店,其中包含 3 種基本元素:

  • Player 服務處理所有與游戲玩家相關的工作
  • 初始 Room 服務是開發人員將構建的房間的占位符
  • 還有一個名為 Concierge 的額外服務,創建它的最初目的是幫助玩家從一個房間過渡到另一個房間

在允許系統的這 3 部分不斷演變后,我們發現開發該游戲的不同人對 Concierge 的應有用途有不同的理解。Concierge 無法找到類比,因為它太模糊了。Concierge 被一個新的 Map 服務取代。Map 是一個房間位于游戲中何處的終極真相之源,單從名稱就能清楚了解它的用途。

將領域元素轉換為服務

領域模型包含一些明確定義的類型:

  • 實體 是一個對象,它有固定的身份,具有明確定義的"連續性線索"或生命周期。通常列舉的示例是一個 Person(一個實體)。大多數系統都需要唯一地跟蹤一個 Person,無論姓名、地址或其他屬性是否更改。
  • 值對象 沒有明確定義的身份,而僅由它們的屬性定義。它們通常不可變,所以兩個相等的值對象始終保持相等。地址可以是與 Person 關聯的值對象。
  • 集合 是一個相關對象集群,這些對象被看作一個整體。它擁有一個特定實體作為它的根,并定義了明確的封裝邊界。它不只是一個列表。
  • 服務 用于表示不是實體或值對象的自然部分的操作或活動。

Game On! 中的領域元素

該游戲是圍繞"玩家"這一明顯實體構建的。玩家有一個固定 ID,壽命與玩家的賬戶一樣長。玩家實體有一組簡單的值對象,包括玩家的用戶名、喜歡的顏色和位置。

游戲的另一個重要元素是房間,房間不會如此明顯地映射到實體。一方面,可以不斷通過不同方式更改房間屬性。這些可更改的屬性通過一個值對象表示,該值對象包含一些屬性,比如房間的名稱、它的入口描述和它的公開末端。此外,房間在地圖中的位置可能不斷改變。如果您將每個可能的房間位置視為一個站點,您最終會認為地圖是唯一站點實體的集合,其中每個站點與可變化的房間屬性相關聯。

將領域元素映射到服務

要將領域元素轉換為服務,可按照以下一般準則來完成此操作:

  1. 使用值對象的表示作為參數和返回值,將集合和實體轉換為獨立的微服務。
  2. 將領域服務(未附加到集合或實體的服務)與獨立的微服務相匹配。
  3. 每個微服務應處理一個完整的業務功能。

本節將探討此流程,將這些準則應用到上面列出的 Game On! 的領域元素,如圖 1 所示。

圖 1. Player 和 Map 服務

Java 和微服務,第 2 部分: 在 Java 中創建微服務

Player 服務提供資源 API 來處理玩家實體。除了標準的創建、讀取、更新和刪除操作之外,Player API 還提供了額外的操作來生成用戶名和喜歡的顏色,以及更新玩家的位置。

Map 服務提供資源 API 來處理站點實體,允許開發人員管理游戲中的房間服務注冊。Map 提供了適用于有界上下文內各個站點的創建、檢索、更新和刪除操作。Map 服務中的數據表示不同于在該服務外共享的數據表示,因為一些值是基于站點在地圖中的位置來計算的。

游戲中還有一些服務沒有討論:

1. Mediator:Mediator 服務是從 Player 服務的第一個實現分裂出來的。

它的唯一責任是在以下 WebSocket 連接之間進行調解:

  • 客戶端設備與 Mediator 之間的一個長時間運行的連接。
  • Mediator 與目標獨立房間服務之間的另一個連接。

從領域建模的角度講,Mediator 是一個領域服務。

2. Rooms:各個房間服務呢?開發人員建立他們自己的房間實例。對于建模用途,可將房間(甚至您自己的房間)視為外部作用物。

應用程序和服務結構

整體式 Java EE 應用程序通常是分層(客戶端、Web、業務和數據)構建和部署的,擁有長期運行的應用服務器,以便為應用程序的不同部分托管 Web 存檔(WAR 文件)、企業存檔(EAR 文件)或同時托管二者,如圖 2 的左側所示。命名和打包約定變得很重要,因為用于應用程序的不同部分的 JSP、JSF、servlet 和 EJB 會部署到同一個應用服務器。通用的數據存儲提供了一致的數據表示,但也在如何表示和存儲數據上施加了一些限制。

圖 2. 整體式應用程序或微服務應用程序中包含的 Order 和 Catalog 服務

Java 和微服務,第 2 部分: 在 Java 中創建微服務

微服務架構可能仍擁有這些應用程序分層,但每層的元素可能由在獨立進程中運行的獨立服務提供,如圖 2 的右側所示。請注意以下細節:

  • 通常使用前端的后端模式來支持 17 種具體前端的專門行為。
  • 比如針對特定設備而優化,或者在 Web 應用程序中提供額外的功能。
  • 盡管每個服務都擁有并維護自己的數據存儲,但請記住數據存儲本身是一個獨立服務。

不是所有微服務應用程序都將擁有所有這些元素。例如,使用 JSP 或 JSF 的 Web 應用程序,或者在后端運行的解釋腳本(比如 PHP)。它們可被視為"前端的后端",尤其是在它們與在瀏覽器中運行的 JavaScript 有密切關系時。但是,(在編寫本書時)Game On! 沒有使用前端的后端模式。JavaScript 單頁 Web 應用程序是一組來自某個獨立服務的靜態資源。該應用程序隨后在瀏覽器中運行,并直接調用后端 API。

內部結構

在服務內組織您的代碼,以便提供明確的關注點分離,以方便測試。對外部服務的調用應與領域邏輯分開,后者也應與特定于編組數據的邏輯/支持性數據服務分開。

圖 3 展示了微服務架構的一個簡化版本。此架構將領域邏輯與任何與外部服務交互或理解外部服務的代碼分開。該架構類似于端口和適配器 (Hexagonal) 架構,也類似于 Toby Clemson 在"微服務架構中的測試戰略"中提供的方法。

圖 3. 微服務的內部結構

Java 和微服務,第 2 部分: 在 Java 中創建微服務

示例內部結構考慮了以下元素:

  • 資源將 JAX-RS 資源向外部客戶端公開。這一層處理對請求的基本驗證,然后將信息傳遞給領域邏輯層。
  • 作為一個一般性概念,領域邏輯具有許多形式。在"邊界-實體-控制模式"中,領域邏輯表示實體本身,具有參數驗證、狀態更改邏輯等。
  • 存儲庫是可選的,但可在核心應用程序領域邏輯與數據存儲(如果存在)之間建立有用的抽象。此配置使得在更改或替換支持性數據存儲時,無需大量更改領域邏輯。
  • 服務連接器類似于存儲庫抽象,它們封裝了與其他服務的通信。這一層被用作一個方面或"反腐敗"層,以保護領域邏輯免受對外部資源 API 的更改的影響,或者在 API 連接格式與內部領域模型構造之間轉換。

此架構要求您在創建類時遵循一些規則。例如,您用于實現服務的每個類應執行以下任務之一:

  • 執行領域邏輯
  • 公開資源
  • 向外調用其他服務
  • 向外調用數據存儲

這些是代碼結構的一般性建議,不必嚴格遵守。重要目的是減少執行更改的風險。舉例而言,如果您想更改數據存儲,只需更新存儲庫層。您不需要搜索每個類來查找調用數據存儲的方法。如果外部服務的 API 發生更改,這些更改也能以類似方式限制在服務連接器內。這種分離進一步簡化了測試。

使用共享庫還是新服務?

敏捷和面向對象方法中的一個共同原則是"不要復制自己"(DRY)。多次復制相同的代碼,通常表明應該將該代碼轉換為可重用的"東西"。在傳統的面向對象方法中,這個東西是一個對象,也有可能是一個實用程序類。但是,具體來講,在無處不在的實用程序類中,嚴格應用 DRY 原則會導致無法獲得干凈地解耦的模塊。

微服務開發讓這種朝無頭緒代碼的退步變得更困難。硬性的流程邊界使過度共享實現變得更加困難,但不幸的是,仍有可能實現。那么如何處理公共代碼?

  • 接受可能存在一定冗余性的代碼
  • 將公共代碼包裝在一個共享的、帶版本的庫中
  • 創建一個獨立服務

根據代碼的性質,可能最好繼續接受冗余代碼。考慮到能夠且應該獨立演變每個服務,所以微服務的各種需求可能改變最初的公共代碼的外觀。為數據傳輸對象創建共享庫非常具有誘惑,這些庫屬于瘦類,可充當更改連接傳輸格式(通常是 JSON)的工具。此技術可消除大量樣板代碼,但它可能會泄漏實現細節和引入耦合。

通常推薦創建客戶端庫來減少重復,讓 API 的使用變得更容易。此技術一直是數據存儲的標準,包括提供了基于 REST 的 API 的新 NoSQL 數據存儲。客戶端庫可能對其他服務也很有用,尤其是使用二進制協議的服務。但是,客戶端庫會在服務使用者與提供者之間引入耦合。沒有有效維護庫的平臺或語言,可能很難使用該服務。

共享庫也可用于確保一致地處理潛在的復雜算法。舉例而言,對于 Game On!,可使用一個共享庫來封裝請求簽名和簽名驗證。此配置使您能簡化系統內的服務,確保采用一致的方式計算各個值。但是,我們沒有為所有語言提供一個共享庫,所以一些服務仍需要努力以一致方式進行計算。

共享庫何時應是真正的獨立服務?獨立服務不依賴于使用的語言,可單獨擴展,而且允許更新立即生效,而不需要與使用者協調更新。獨立服務還增加了額外的處理負擔,會遇到服務間通信的所有問題。如何進行權衡是一種主觀判斷,但如果一個庫開始需要支持性服務,這通常表明它是一個獨立的服務。

創建 REST API

良好的微服務 API 是什么樣的?設計 API 并創建文檔,對確保它有用且實用很重要。遵守約定并使用良好的版本策略,可讓 API 更容易理解和不斷演變。

自上而下還是自下而上?

在從頭創建服務時,既可以首先定義所需的 API,然后編寫代碼來實現它們,也可以首先編寫服務的代碼,然后通過實施獲得 API。

基本來講,第一個選項始終更好。微服務不會孤立運行。要么是它調用某個東西,要么是某個東西調用它。微服務架構中的服務之間的交互應明確定義和記錄。對于領域驅動設計,API 是有界上下文之間的已知交叉點。

Alan Perlis 曾說過,"除了第一次,每個功能都應自上而下構建。"當執行概念證明時,您可能不清楚 API 提供哪些功能。在早期,可編寫一些代碼,幫助完善關于您嘗試構建什么服務的想法。但是,在更好地了解服務需要執行的操作后,您應該回過頭來完善領域模型,并定義與之匹配的外部 API。

創建 API 的文檔

使用工具創建 API 的文檔,更容易確保發布的文檔的準確性和正確性,然后可在與 API 使用者的討論中使用這些文檔。此文檔也可用于使用者驅動的契約測試。

Open API Initiative (OAI) 是一個專注于標準化 RESTful API 描述的聯盟。OpenAPI 規范基于 Swagger,后者定義了用于創建 RESTful API 的 Swagger 表示的元數據結構和格式。通常在單個可移植文檔中表達該定義(約定的文件為 swagger.json,但也可使用 swagger.yaml 來支持 YAML)。Swagger 定義可使用可視編輯器創建,也可基于應用程序中的掃描注釋來生成這個定義。它可進一步用于生成客戶端或服務器存根。

WebSphere Application Server Liberty 中的 apiDiscovery 功能將對 Swagger 定義的支持集成到運行時中。它在 JAX-RS 應用程序中自動檢測和掃描注釋,并使用注釋動態生成 Swagger 定義。如果存在 Swagger 注釋,則包含來自這些注釋的信息來創建可與代碼一起維護的信息文檔。可以使用系統提供的端點來公開生成的這個文檔,如圖 4 所示。

圖 4. apiDisovery-1.0 功能提供的創建 API 文檔的用戶界面

Java 和微服務,第 2 部分: 在 Java 中創建微服務

Swagger API 定義(尤其是 Web 界面中呈現的定義)對可視化 API 的外部使用者視圖很有用。從這個外部視圖開發或設計 API,有助于確保一致性。這個外部視圖符合使用者驅動契約12,后者是一種提倡在設計 API 時專注于使用者期望的模式。

使用正確的 HTTP 動詞

REST API 應為創建、檢索、更新和刪除操作使用標準 HTTP 動詞,而且應特別注意操作是否冪等(可以安全地重復多次)。

POST 操作可用于創建 (C) 資源。POST 操作的明顯特征是它不是冪等的。舉例而言,如果使用 POST 請求創建資源,而且啟動該請求多次,那么每次調用后都會創建一個新的唯一資源。

GET 操作必須是冪等的且不會產生意外結果。它們不應產生副作用,而且僅應用于檢索 (R) 信息。具體來講,帶有查詢參數的 GET 請求不應用于更改或更新信息(而應使用 POST、PUT 或 PATCH)。

PUT 操作可用于更新 (U) 資源。PUT 操作通常包含要更新的資源的完整副本,使該操作具有冪等性。

PATCH 操作允許對資源執行部分更新 (U)。它們不一定是冪等的,具體取決于如何指定增量并應用到資源上。例如,如果一個 PATCH 操作表明一個值應從 A 改為 B,那么它就是冪等的。如果它已啟動多次而且值已是 B,則沒有任何效果。對 PATCH 操作的支持仍不一致。例如,Java EE7 中的 JAX-RS 中沒有 @PATCH 注釋。

毫無意外,DELETE 操作用于刪除 (D) 資源。刪除操作是冪等的,因為資源只能刪除一次。但是,返回代碼不同,因為第一次操作將成功 (200),而后續調用不會找到資源 (204)。

創建機器友好的描述性結果

富于表情的 REST API 應仔細考慮從啟動的操作返回什么信息。考慮到 API 是由軟件而不是用戶啟動的,所以應采用最有效和最高效的方式將信息傳達給調用方。

舉例而言,過去的常見做法是將狀態代碼 200 (OK) 與解釋錯誤消息的 HTML 一起返回。盡管此技術可能適合查看網頁的用戶(HTTP 狀態代碼已對他們隱藏),但機器很難確定其請求是否成功。

HTTP 狀態代碼應相關且有用。在一切正常時使用 200 (OK)。當沒有響應數據時,使用 204 (NO CONTENT)。除了該技術之外,對于導致創建一個資源的 POST 請求,還應使用 201 (CREATED),無論是否有響應內容。當同時執行的更改發生沖突時,使用 409 (CONFLICT);當參數格式錯誤時,返回 400 (BAD REQUEST)。HTTP 狀態代碼集合很可靠,應使用它來盡可能明確地表達請求的結果。

您還應該考慮要在響應中返回哪些數據,以保持高效通信。例如,當使用 POST 請求創建資源時,響應應在一個 Location 標頭中包含新創建資源的位置。不需要在響應中返回該資源的其他信息。但實際上,創建的資源通常包含在響應中,因為這消除了調用方發出額外的 GET 請求來抓取所創建資源的需求。同樣的原則也適用于 PUT 和 PATCH 請求。

資源 URI 和版本控制

人們對 RESTful 資源 URI 的某些方面有著不同的看法。一般而言,資源應該是名詞而不是動詞,端點應該是復數。此技術可得到創建、檢索、更新和刪除操作的清晰結構:

POST /accounts 創建一個新項目
GET /accounts 檢索一個項目列表
GET /accounts/16 檢索一個具體項目
PUT /accounts/16 更新一個具體項目
PATCH /accounts/16 更新一個具體項目
DELETE /accounts/16 刪除一個具體項目

一般共識是,保持 URI 中的內容盡量簡單。如果創建一個應用程序來跟蹤一種特定的嚙齒動物,URI 將為 /mouses/ 而不是 /mice/,即使后者更讓您害怕。

關系是通過嵌套 URI 來實現建模的,例如,用于管理與一個帳戶關聯的憑證的 /accounts/16/credentials。

如果與資源關聯的操作應產生什么結果并未達成一致,該操作就不應包含在這種普通結構中。管理這些操作并沒有唯一的正確方式。請選擇最適合 API 使用者的方式。

例如,在 Game On! 中,Player 服務提供幫助生成用戶名和最喜歡的顏色的操作。它也可以批量檢索游戲中每個玩家的位置。選擇的結構反映了涵蓋所有玩家的操作,同時仍然允許您處理特定玩家。此技術有其局限性,但闡明了要點:

GET /players/accounts 檢索所有玩家的列表

POST /players/accounts 創建一個新玩家

GET /players/accounts/{id} 檢索一個具體玩家

GET /players/accounts/{id}/location 檢索一個具體玩家的位置

PUT /players/accounts/{id}/location 更新一個具體玩家的位置

GET /players/color 返回 10 種生成的顏色

GET /players/name 返回 10 個生成的名稱

GET /players/locations 檢索所有玩家的位置

版本控制

微服務的一個主要優勢是,允許服務獨立地演變。考慮到微服務會調用其他服務,這種獨立性需要引起高度注意。您不能在 API 中制造破壞性更改。

接納更改的最簡單方法是絕不破壞 API。如果遵循穩健性原則,而且兩端都保守地發送數據,慷慨地接收數據,那么可能很長一段時間都不需要執行破壞性更改。當最終發生破壞性更改時,您可選擇構建一個完全不同的服務并不斷退役原始服務,原因可能是領域模型已進化,而且一種更好的抽象更有意義。

如果需要對現有服務執行破壞性的 API 更改,請決定如何管理這些更改:

  • 該服務是否會處理 API 的所有版本?
  • 您是否會維護服務的獨立版本,以支持 API 的每個版本?
  • 您的服務是否僅支持 API 的最新版本,依靠其他適應層來與舊 API 來回轉換?

在確定了困難部分后,如何在 API 中反映該版本是更容易解決的問題。通常可通過 3 種方式處理 REST 資源的版本控制:

  • 將版本放入 URI 中
  • 使用自定義請求標頭
  • 將版本放在 HTTP Accept 標頭中,并依靠內容協商

將版本放入 URI 中

將版本添加到 URI 中,這是指定版本的最簡單方法。此方法有以下優點:

  • 很容易理解。
  • 很容易在您的應用程序中構建服務時實現。
  • 與 API 瀏覽器工具(比如 Swagger)和命令行工具(比如 curl)兼容。

如果將版本放在 URI 中,版本應該會應用于整個應用程序,所以使用 /api/v1/accounts 而不是 /api/accounts/v1。超媒體即應用狀態引擎 (Hypermedia as the Engine of Application State, HATEOAS) 是一種向 API 使用者提供 URI 的方式,可以讓使用者無需自行負責構造 URI。舉例而言,出于這個原因,GitHub 在其響應中提供了超媒體 URL。13如果不同的后端服務可能在其 URI 中擁有獨立的、不同的版本,則很難(甚至無法)實現 HATEOAS。

如果決定將版本放在 URI 中,那么您可以通過不同方式管理它,具體方式從某種程度上講取決于在系統中路由請求的方式。但是,暫時假設您的網關代理將外部 /api/v1/accounts URI 轉換為 /accounts/v1,后者映射到您的服務提供的 JAX-RS 端點。在這種情況下,/accounts 是一個自然的上下文根。完成轉換后,您可以將版本包含在 @ApplicationPath 注釋中,如示例 1 所示,這在應用程序僅支持一個 API 版本時非常有用。

示例 1 使用 @ApplicationPath 注釋向 URI 添加版本

package retail_store.accounts.api;
      @ApplicationPath("/v1")
      public class ApplicationClass extends Application {}

如果服務要支持應用程序的多個版本,也可以將版本推送到 @Path 注釋,如示例 2 所示。

示例 2 @ApplicationPath 定義了一個 REST 端點而不更改 URI

package retail_store.accounts.api.v1;
      @ApplicationPath("/")
      public class ApplicationClass extends Application {}

示例 3 給出了添加到注釋中的版本。

示例 3 使用 @Path 注釋將版本添加到 API 中

package retail_store.accounts.api.v1;
      @Path("v1/accounts")
      public class AccountsAPI {
      @GET
      @Path("{id}")
      @Produces(MediaType.APPLICATION_JSON)
      public Response getAccounts(@PathParam("id") String id) {
      getId(id);
      ……
      }
      }

對將版本包含在 URL 中的一種反對觀點是,HTML 標準嚴格規定 URL 應表示實體,如果表示的實體未更改,那么 URL 也不應更改。

另一種擔憂是,將版本放在 URI 中需要使用者更新其 URI 引用。通過允許發出不帶版本的請求,然后將請求映射到最新版本,可在一定程度上解決此擔憂。但是,在最新版本發生改變時,此方法容易產生意外行為。

添加自定義請求標頭

可以添加自定義請求標頭來表明 API 版本。在將流量路由到特定后端實例時,路由器和其他基礎架構可能會考慮使用自定義標頭。但是,此機制不容易使用,原因與 Accept 標頭不容易使用相同。此外,它是一個僅適用于您的應用程序的自定義標頭,這意味著使用者需要學習如何使用它。

修改 Accept 標頭以包含版本

Accept 標頭是一個定義版本的明顯位置,但它是最難測試的地方之一。URI 很容易指定和替換,但指定 HTTP 標頭需要更詳細的 API 和命令行調用。

以下是本章得出的最重要結論:

  • 從使用者角度設計 API。
  • 擁有處理 API 更改的戰略。
  • 在應用程序中的所有服務中使用一致的版本控制技術。

總結

本文重點介紹了如何在 Java 中創建微服務并使用它。下一部分將更深入的探討微服務的演化架構。好了,學習愉快,下次再見!

參考資源 (resources)

 

來自:http://www.ibm.com/developerworks/cn/java/j-cn-java-and-microservice-2/index.html?ca=drs-

 

 本文由用戶 etblakstn 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!