Java 和微服務,第 4 部分: 處理數據 JPA 反射

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

在微服務系統中,具有混合持久性的環境時,有必要讓數據處理保持可管理。爲了解釋如何實現此目標,本章會簡要介紹微服務在數據處理方面的特徵,然後介紹如何使用基於 Java 的微服務實現此目的。

微服務特定於數據的特徵

識別數據的一種方式是自上而下的方法,數據必須存儲在您的微服務的數據存儲中。從業務級別開始建模您的數據。以下各節將介紹如何識別此數據,如何處理它,以及如何與其他微服務的其他數據存儲共享它。有關自上而下方法的描述,請訪問 網站

領域驅動設計導致產生各種實體

如果採用領域驅動設計中的方法,您會獲得以下對象(和其他對象):

  • 實體

"一種不通過屬性定義,而通過連續性和身份定義的對象。"

  • 價值對象

"一種包含屬性,但沒有概念實體的對象。它們應視爲不可變。"

  • 聚合

"一種通過根實體(也稱爲聚合根)綁定到一起的對象集合。聚合根通過禁止外部對象持有其成員的引用,確保在聚合內執行的更改的一致性。"

  • 存儲庫

"檢索領域對象的方法應委託給專門的存儲庫對象,以便能夠輕鬆地交換備選的存儲實現。"

這些對象應映射到您的持久存儲(僅在實體具有相同的生命週期時才能聚合它們)。這種領域模型是邏輯數據模型和物理數據模型的基礎。

每個微服務都有單獨的數據存儲

每個微服務都應擁有自己的數據存儲(如圖 1),而且與其他微服務及其數據分離。第二個微服務不能直接訪問第一個微服務的數據存儲。

採用此特徵的原因是:

  • 如果兩個微服務共享一個數據存儲,它們將緊密耦合。如果更改一個微服務的數據存儲(比如表)的結構,則可能導致另一個微服務出現問題。如果微服務以這種方式耦合,則必須協調新版本的部署,而這是我們必須避免的。
  • 每個微服務都應該採用最能滿足其需求的數據庫類型(混合持久性,請參閱小節"混合持久性"瞭解更多的細節)。在選擇數據庫系統時,不需要在不同微服務之間進行權衡。這種配置會爲每個微服務提供一個單獨的數據存儲。
  • 從性能角度講,每個微服務都有自己的數據存儲可能很有用,因爲擴展會變得更容易。數據存儲可託管在自己的服務器上。

圖 1 每個微服務都有單獨的數據存儲

Java 和微服務,第 4 部分: 處理數據

對於關係數據庫,可通過以下方式之一實現數據存儲的分離:

  • 每個微服務一種模式

每個服務在數據庫中都有自己的模式(如圖 2)。其他服務可使用同一個數據庫,但必須採用不同的模式。可通過數據庫訪問機制實現這種配置(爲連接的數據庫用戶使用權限),因爲面臨時間壓力的開發人員傾向於使用快捷方式,而且能直接訪問其他模式。

  • 每個微服務一個數據庫

每個微服務都可以有自己的數據庫,但與其他微服務共享數據庫服務器(如圖 2)。使用不同的數據庫,用戶可以連接到數據庫服務器,而且實現了很好的數據庫分離。

圖 2 每個微服務一種模式和每個微服務一個數據庫

Java 和微服務,第 4 部分: 處理數據

  • 每個微服務一個數據庫服務器

這是最高程度的分離。舉例而言,在需要解決性能方面的問題時,這可能很有用(如圖 3)。

圖 3 每個微服務一個數據庫服務器

Java 和微服務,第 4 部分: 處理數據

混合持久性

每個微服務應使用自己的數據存儲,這意味着該服務也可以採用不同的數據存儲技術。NoSQL 運動催生了許多新的數據存儲技術,它們可與傳統關係數據庫一起使用。基於應用程序必須滿足的需求,可選擇不同類型的技術來存儲數據。一個應用程序中擁有各種各樣的數據存儲技術,這被稱爲混合持久性。

對於一些微服務,最好將其數據存儲在關係數據庫中。對於其他具有不同類型的數據(非結構化、複雜和麪向圖形的數據)的服務,可將它們的數據存儲在一些 NoSQL 數據庫中。

多語言編程這個詞的含義是,應用程序應該使用最適合它必須解決的挑戰的編程語言來編寫。

跨微服務的數據共享

在一些情況下,微服務應用程序的用戶可能希望請求不同的服務所擁有的數據。例如,用戶可能想查看他的所有支付和支付的相應狀態。因此,用戶需要在服務中查詢他的帳戶,隨後在服務中查詢他的支付情況。微服務會使用數據庫合併數據,不是一種最佳實踐。要處理此業務案例,您必須實現一個適配器服務(如圖 4),使用它查詢帳戶服務和支付服務,然後將收集到的數據返回給用戶。適配器服務還負責對它收集到的數據執行必要的數據轉換。

圖 4 適配器微服務

Java 和微服務,第 4 部分: 處理數據

更改數據可能變得更復雜。在微服務系統中,一些業務事務可能涵蓋多個微服務。舉例而言,在零售店的微服務應用程序中,可能有一個下單服務和一個支付服務。

因此,如果客戶想在您的商店購買商品並付款,那麼業務事務需要包含兩個微服務。每個微服務都有自己的數據存儲,所以對於包含兩個或更多微服務的業務事務,涉及到兩個或更多個數據存儲。本節將介紹如何實現這些業務事務。

事件驅動架構

您需要一種方法來確保包含兩個或多個微服務的業務事務中涉及的數據的一致性。一種方法是採用分佈式事務,但衆多原因表明,不應在微服務應用程序中採用這種方法。主要原因是,分佈式事務中涉及的微服務是緊密耦合的。如果一個分佈式事務涉及兩個微服務,而且一個服務失敗或出現性能問題,那麼另一個服務必須等到超時後回滾事務。

讓一個業務事務涵蓋多個微服務的最佳方式是,使用事件驅動架構。要更改數據,第一個服務負責更新它的數據,並在同一個(內部)事務中發佈一個事件。第二個微服務(它已訂閱此事件)收到此事件,並對它的數據執行更改。通過使用發佈/訂閱通信模型,兩個微服務是鬆散耦合的。僅在它們交換的消息上存在耦合。該技術使微服務系統能在所有微服務之間保持數據一致性,而無需使用分佈式事務。

如果微服務應用程序中的微服務相互發送許多消息,那麼它們可能非常適合合併到一個服務中。但是需要謹慎一些,因爲此配置可能破壞微服務的領域驅動設計。執行復雜更新(涵蓋多個服務)的適配器服務也可以使用事件實現。

事件驅動架構的編程模型更爲複雜,但 Java 有助於讓該複雜性處於可管理狀態下。

最終一致性

在事件驅動架構中,將消息發送給其他微服務會導致所謂的"最終一致性"問題。這通常是以下情況下導致的運行時問題:微服務 A 更改其數據存儲中的數據,並在同一個內部事務中向微服務 B 發送一條消息。經過較短時間後,微服務 B 收到消息,並更改其數據存儲中的數據。在這個通常很短的時間段內,兩個數據存儲中的數據不一致。例如:服務 A 更新其數據存儲中的訂單數據,並向服務 B 發送一條消息以進行支付。在處理支付之前,有一個未支付的訂單。當消息接收者無法處理消息時,情況就會變遭。在這種情況下,消息系統或接收微服務必須採取一些策略來處理此問題。

在微服務應用程序中,每個微服務擁有自己的數據庫。一次涵蓋多個微服務的業務事務會導致"最終一致性"問題,因爲分佈式事務會阻礙此問題的解決。處理這種業務事務的一種方法如圖 5 所示。訂單微服務將訂單保存到它的數據存儲中,並將一個事件(例如 OrderCreated)發送給支付微服務。在訂單微服務未從支付微服務收到支付確認期間,訂單處於未支付狀態。

支付服務訂閱了 OrderCreated 事件,所以它會處理該事件,並在其數據存儲中執行支付。如果支付成功,它會發佈一個被訂單微服務訂閱的 PaymentApproved 事件。處理 PaymentApproved 事件後,訂單狀態從"未支付"更改爲"已批准"。如果客戶查詢其訂單狀態,就會獲得以下兩種響應之一:訂單未支付或訂單已批准。

圖 5 微服務之間的事件消息

Java 和微服務,第 4 部分: 處理數據

在數據不可用的情況下,服務可能向客戶發送類似下面這樣的消息:"很抱歉,請稍後重試"。

數據複製

數據存儲與從不同數據存儲獲取數據的需求分離,可能導致使用數據庫系統的數據複製機制可以解決問題的想法。舉例而言,跟與多個微服務共享數據庫相比,使用數據庫觸發器、計時存儲過程或其他流程來解決此問題同樣存在缺點。更改複製管道的一端的數據結構會給複製流程帶來問題。在部署服務的新版本時,必須調整該流程。這也是一種緊密耦合形式,必須避免。

前面已經提到,基於事件的處理可將兩個數據存儲分離。如果需要的話,處理事件的服務可執行適當的數據轉換,將數據存儲在它們自己的數據存儲中。

事件尋源和命令查詢職責分離

在事件驅動架構中,可以考慮命令查詢職責分離 (CQRS) 和事件尋源。可結合這兩種架構模式來處理流經您的微服務應用程序的事件。

CQRS 將對數據存儲的訪問拆分爲兩個不同部分:一部分包含讀取操作,另一部分包含寫入操作。讀取操作不會更改系統的狀態。它們僅返回狀態。寫入操作(命令)會更改系統的狀態,但不會返回值。事件尋源存儲在數據發生更改時發生的事件序列。圖 6 顯示了一個使用 CQRS 的示例。

圖 6 使用 CQRS 的示例

Java 和微服務,第 4 部分: 處理數據

如圖 6 所示,事件按順序存儲在事件存儲中。查詢模型中的數據與來自事件存儲的數據同步。要支持事件存儲或查詢模型,可以使用專門的系統來支持微服務的查詢。

此架構也可用於處理微服務應用程序中的事件。

消息系統

可以使用消息系統或面向消息的中間件來支持事件驅動架構。

"面向消息的中間件 (MOM) 是一種軟件或硬件基礎架構,用於支持在分佈式系統之間發送和接收消息。MOM 允許應用程序模塊分佈在異構的平臺上,這降低了開發涵蓋多個操作系統和網絡協議的應用程序的複雜性。中間件創建了一個分佈式通信層,這一層將應用程序開發人員與各種操作系統和網絡接口的細節隔離。跨多樣化的平臺和網絡進行擴展的 API 通常由 MOM 提供。MOM 提供的軟件元素位於客戶端/服務器架構的所有通信組件中,通常支持客戶端與服務器應用程序之間的異步調用。MOM 減少了客戶端/服務器機制的主從性質的複雜性與應用程序開發人員的關聯。"

要與面向消息的中間件進行通信,可以採用不同的協議。以下是最常用的協議:

  • 高級消息隊列協議 (AMQP)

AMQP"規定消息提供者和客戶端的行爲,以便使來自不同供應商的實現可互操作,就像 SMTP、HTTP、FTP 等創建可互操作的系統一樣。"

  • MQ 遙測傳輸 (MQTT)

MQTT 是"基於發佈-訂閱的輕量型"消息協議,用在 TCP/IP 協議之上。它專爲與需要"小代碼體積"或網絡帶寬有限的遠程位置建立連接而設計。"它主要用在物聯網 (IoT) 環境中。

在 Java 世界中,有一個 API 可與面向消息的中間件通信:

Java Message Service (JMS),它包含在 Java EE 規範中。具體的版本是 JMS 2.0。

由於 JMS 的悠久歷史(可追溯到 2001 年),有許多 JMS 消息代理可用作 MOM 系統。不過也有一些實現 AMQP 的消息系統:

– RabbitMQ

– Apache Qpid

– Red Hat Enterprise MRG

所有這些系統都提供了 Java API,所以它們可用在基於 Java 的微服務中。

分佈式事務

大部分(不是所有)消息系統都支持事務。在將消息發送給消息系統和更改事務數據存儲中的數據時,也可以使用分佈式事務。

可在微服務與它的後備存儲之間使用分佈式事務和兩階段提交,但不要在微服務之間使用它們。考慮到微服務的獨立性,特定服務實例之間不得存在關聯,而兩階段提交事務需要這種關聯。

對於涵蓋多個服務的交互,必須添加補救和調解邏輯來保證一致性。

Java 中的支持

在混合持久性環境中,用於實現微服務的編程語言必須處理不同的持久性技術。您的編程語言必須能支持每種持久保存數據的不同方式。作爲編程語言,Java 擁有許多 API 和框架,可幫助開發人員處理不同的持久性技術。

Java Persistence API

"Java Persistence API (JPA) 是一種在 Java 對象/類與關係數據庫之間訪問、持久化和管理數據的 Java 規範。EJB 3.0 規範中將 JPA 定義爲取代 EJB 2 CMP Entity Beans 規範的一種規範。現在,在 Java 行業中,JPA 被視爲對象關係映射 (ORM) 的標準行業方法。

JPA 本身只是一個規範,不是產品;它本身無法無法執行持久化或任何其他操作。JPA 只是一組接口,需要一種實現。有開源和商用的 JPA 實現可供選擇,而且所有 Java EE 5 應用服務器都應支持使用它。JPA 還需要一個數據庫來實現持久化。"

Java Enterprise Edition 7 (Java EE 7) 包含 Java Persistence 2.1 (JSR 338)。

發明 JPA 主要是爲了得到一種對象關係映射器,以便在關係數據庫中持久保存 Java 對象。API 背後的實現(持久化提供程序)可由不同的開源項目或供應商實現。使用 JPA 的另一個好處是,您的持久化邏輯更容易移植。

JPA 定義了自己的查詢語言(Java 持久化查詢語言 (JPQL)),可爲不同數據庫供應商生成查詢。Java 類被映射到數據庫中的表,也可以使用類之間的關係來建立對應的表之間的關係。可使用這些關係建立級聯操作,所以一個類上的操作可能導致其他類的數據上的操作。JPA 2.0 引入了 Criteria API,可幫助在運行時和編譯時獲得正確的查詢結果。在應用程序中實現的所有查詢都有一個名稱,可以通過這個名稱找到它們。這種配置使得在完成編程幾個星期後更容易知道查詢的功能。

JPA 持久化提供程序實現數據庫訪問。以下是最常用的提供程序:

Liberty for Java EE 7 的默認 JPA 提供程序是 EclipseLink。

JPA 概述

下面的簡要介紹和代碼段展示了 JPA 的特性。開始使用 JPA 的最佳方式是創建實體類來持有數據(示例 1)。

示例 1 持有實體數據的 JPA 類

@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String description;
@Temporal(TemporalType.DATE)
private Date orderDate;
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Date getOrderDate() {
return orderDate;
}
public void setOrderDate(Date orderDate) {
this.orderDate = orderDate;
}
……
}

每個實體類都需要一個 @Entity 註釋,然後才能通過持久化提供程序管理。實體類根據名稱映射到數據庫中的對應表(約定優於配置)。也可應用不同的映射。類的屬性根據名稱映射到基礎表的列。也可覆蓋屬性的自動映射 (@Column)。每個實體類必須有一個實體。持久化提供程序必須使用一個或多個身份列(使用 @Id 標註)來將對象的值映射到表中的數據行。數據庫或持久化提供程序可通過不同方式生成身份列的值。實體類的一些屬性必須以特殊方式轉換,才能存儲在數據庫中。例如,數據庫列 DATE 必須使用註釋 @Temporal 映射到實體類中。

用於查詢數據庫的主要 JPA 接口是 EntityManager。它包含從數據庫創建、讀取、更新和刪除數據的方法。可通過不同方式獲取 EntityManager 的引用,具體取決於應用程序運行的環境。在非託管環境中(沒有 servlet、EJB 或 CDI 容器),必須使用類的工廠方法 EntityManagerFactory,如示例 2 所示。

示例 2 如何在 Java SE 環境中獲取 EntityManager

EntityManagerFactory entityManagerFactory =
Persistence.createEntityManagerFactory("OrderDB");
EntityManager entityManager =
entityManagerFactory.createEntityManager();

字符串 OrderDB 是爲其創建 EntityManager 的持久化單元的名稱。持久化單元用於對實體類和相關屬性進行邏輯分組,以配置持久化提供程序(persistence.xml 文件中的配置)。

在託管環境中,情況更簡單。可從容器注入 EntityManager,如示例 3 所示。

示例 3 如何在 Java EE 環境中獲取 EntityManager

@PersistenceContext
EntityManager em;

如果注入持久化上下文時未使用 unitName,也就是配置文件 (persistence.xml) 中配置的持久化單元的名稱,則會使用默認值。如果只有一個持久化單元,那麼該值就是 JPA 使用的默認值。

以下各節將介紹如何使用來自 EntityManager 的方法實現一些簡單的創建、檢索、更新和刪除方法,如示例 4 所示。

示例 4 JPA 創建操作

@PersistenceContext
EntityManager em;
...
public Order createOrder(Order order) {
em.persist(order);
return order;
}

EntityManager 的 persist 方法將執行兩個操作。首先,EntityManager 管理該對象,這意味着它在其持久化上下文中保存該對象。可將持久化上下文視爲一種緩存,在其中保存與數據庫中的行相關的對象。這種關係是使用數據庫事務來建立的。其次,該對象被持久存儲在數據庫中。如果 Order 實體類有一個 ID 屬性,該屬性的值由數據庫生成,那麼該屬性的值將由 EntityManager 在將對象插入數據庫中後設置。正因如此,createOrder 方法的返回值爲對象本身(示例 5)。

示例 5 使用 find 方法的 JPA 讀取操作

@PersistenceContext
EntityManager em;
...
public Order readOrder(Long orderID) {
Order order = em.find(Order.class, orderID);
return order;
}

EntityManager 方法 find 在表中搜索以參數形式 (orderID) 提供主鍵的行。結果被轉換爲一個 Order 類型的 Java 類(示例 6)。

示例 6 使用 JPQL 的 JPA 讀取操作

@PersistenceContext
EntityManager em;
...
public Order readOrder(Long orderID) {
TypedQuery<Order> query =
em.createQuery( "Select o from Order o " +
"where o.id = :id", Order.class );
query.setParameter("id", orderID);
Order order = query.getSingleResult();
return order;
}

示例 6 通過使用 JPQL 和一個參數顯示了 find 方法的功能。從 JPQL 字符串 Select o from Order o where o.id = :id 開始,生成一個 TypedQuery。有了 TypedQuery,您就可以在生成結果對象後省略 Java 轉換(參見參數 Order.class)。JPQL 中的參數可按名稱 (id) 進行查找,該名稱使得開發人員很容易理解它。方法 getSingleResult 確保僅在數據庫中找到一行。如果有多個行與 SQL 查詢對應,則拋出一個 RuntimeException。

merge 方法在數據庫中執行更新(示例 7)。參數 order 是一個遊離對象,這意味着它不在持久化上下文中。在數據庫中更新後,EntityManger 返回一個附加的(現在包含在持久化上下文中)order 對象。

示例 7 JPA 更新操作

public Order updateOrder(Order order, String newDesc) {
order.setDescription(newDesc);
return em.merge(order);
}

要刪除數據庫中的一行數據,需要一個附加對象(示例 8)。要將該對象附加到持久化上下文中,可以運行 find 方法。如果該對象已附加,則不需要運行 find 方法。通過使用 remove 方法和附加對象的參數,可以在數據庫中刪除該對象。

示例 8 JPA 刪除操作

public void removeOrder(Long orderId) {
Order order = em.find(Order.class, orderId);
em.remove(order);
}

前面已經提到,必須使用某種配置來告訴持久化提供程序,在何處查找數據庫和如何處理數據庫。這在名爲 persistence.xml 的配置文件中完成。該配置文件需要位於您的服務的類路徑中。

依賴於您的環境(是否是 Java 容器),必須通過兩種方式之一完成配置(示例 9)。

示例 9 Java SE 環境中的 Persistence.xml

<persistence>
<persistence-unit name="OrderDB"
transaction-type="RESOURCE_LOCAL">
<class>com.service.Order</class>
<properties>
<!-- Properties to configure the persistence provider -->
<property name="javax.persistence.jdbc.url"
value="<jdbc-url-of-database" />
<property name="javax.persistence.jdbc.user"
value="user1" />
<property name="javax.persistence.jdbc.password"
value="password1" />
<property name="javax.persistence.jdbc.driver"
value="<package>.<DriverClass>" />
</properties>
</persistence-unit>
</persistence>

要配置持久化提供程序,必須做的第一件事就是定義持久化單元 (OrderDB)。持久化單元的一個屬性是 transaction-type。可設置兩個值:RESOURCE_LOCAL 和 JTA。第一個選項讓開發人員負責在其代碼中執行事務處理。如果您的環境中沒有事務管理器,那麼可以使用該選項。第二個選項是 JTA,它表示 Java Transaction API,包含在 Java Community Process (JCP) 中。此選項告訴持久化提供程序,將事務處理委託給運行時環境中存在的事務管理器。

在 XML 標記 <class></class> 之間,可以列出要在這個持久化單元中使用的實體類。

在該文件的 properties 部分,可以設置值來配置持久化提供程序將處理數據庫的方式。以 javax.persistence.jdbc 開頭的屬性名稱由 JPA 標準定義。示例 9 展示瞭如何設置數據庫 URL(用於建立數據庫連接)、用戶名和密碼。javax.persistence.jdbc.driver 屬性告訴持久化提供程序應該使用哪個 JDBC 驅動程序類。

Java EE 環境的配置文件中的主要區別如示例 10 所示。

示例 10 Java EE 環境中的 Persistence.xml

<persistence>
<persistence-unit name="OrderDB">
<jta-data-source>jdbc/OrderDB</jta-data-source>
<class>com.widgets.Order</class>
...
</persistence-unit>
</persistence>

JPA 中的默認事務處理方法是使用 JTA,所以您不需要在配置文件中設置它。jta-data-source 屬性指向 Java EE 環境的應用服務器中配置的數據源的 JNDI-Name。

要在非 Java EE 環境中執行事務管理,可使用 EntityManager 的一些方法,如示例 5-11 所示。

示例 11 非 Java EE 環境中的事務管理

EntityManagerFactory emf =
Persistence.createEntityManagerFactory("OrderDB");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
em.persist(yourEntity);
em.merge(anotherEntity);
tx.commit();
} finally {
if (tx.isActive()) {
tx.rollback();
}
}

示例 11 給出了非 Java EE 環境中的事務處理過程。請避免自行在 Java EE 環境中執行事務管理。還有更好的管理方式,如第小節"Enterprise JavaBeans"所述。

要分離微服務的數據存儲,可以使用示例 12 中的配置文件來設置關係數據庫的默認模式。

示例 12 在 my-orm.xml 中設置默認模式

<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm orm_2_0.xsd"
version="2.0">
<persistence-unit-metadata>
<persistence-unit-defaults>
<schema>ORDER</schema>
</persistence-unit-defaults>
</persistence-unit-metadata>
</entity-mappings>

必須在 JPA 配置文件 persistence.xml 中引用此文件(示例 13)。

示例 13 persistence.xml 中引用一個映射文件的代碼段

<persistence-unit name="OrderDB">
<mapping-file>custom-orm.xml</mapping-file>

此配置將模式名稱被設置爲所有 JPA 類的 my-orm.xml 映射文件中提供的名稱,在本例中爲 OrderDB。它可確保您僅使用此模式中的表。

結合使用 JPA 與 NoSQL 數據庫

EclipseLink 是開始支持 NoSQL 數據庫的 JPA 提供程序之一(從 2.4 版開始)。從此版本開始,它們開始支持 MongoDB 和 Oracle NoSQL。預計在未來的版本中還會支持其他 NoSQL 數據庫。

MongoDB 是一個面向文檔的 NoSQL 數據庫。它的數據結構有利於具有動態模式的 JSON 式文檔。MongoDB 擁有一個專門的 JSON 格式版本,名爲 BSON 。

EclipseLink 2.5 版的 EclipseLink 解決方案指南給出了一個訪問 MongoDB 的示例。

在決定使用 JPA(像 EclipseLink 一樣)作爲 MongoDB 的提供程序之前,請考慮以下幾點:

  1. SQL 是一種經過多次修訂的特定語言。數據庫供應商已實現此標準,但他們還向 SQL 添加了一些未標準化的特性。JPA 提供了對 SQL 的良好支持,但不是 MongoDB 公開的所有特性都受到支持。如果微服務只需要其中某些特性,那麼您使用 JPA 所獲得的好處將會更少。
  2. JPA 有許多在面向對象的數據庫中沒有意義的特性,但 EntityManager 擁有處理這些特性的方法。所以您必須定義要在服務中使用哪些方法。

如果您熟悉 JPA,而且只需要使用一些簡單功能將數據存儲在 NoSQL 數據庫中,那麼可以開始使用 JPA 提供程序實現此目的。如果數據訪問變得更加複雜,那麼最好使用來自 NoSQL 數據庫的 Java 驅動程序。JPA 並不真的適合 NoSQL 數據庫,但它是您的實現的不錯起點。

要更充分地利用 JPA 提供程序,使用 Spring Data JPA 可能很有幫助。除了 JPA 提供程序之外,Spring Data JPA 還在 JPA 提供程序之上添加了一個額外層:http://projects.spring.io/spring-data-jpa/

JPA 對微服務中的數據處理的適合性

下面列出了爲什麼 JPA 對微服務中的數據處理很有用的一些理由:

  • 從領域驅動設計角度定義微服務,這使得大部分微服務只需簡單查詢即可持久化其實體(簡單的創建、檢索、更新和刪除操作)。JPA 的 EntityManager 包含您所需的創建、檢索、更新和刪除方法:persist、find、merge、delete。要調用這些方法,無需完成太多編程工作。
  • 在一些情況下,查詢變得更爲複雜。這些查詢可使用 JPA 中定義的查詢語言 JPQL 來完成。對複雜查詢的需求應是一種例外情況。具有實體數據分層結構的 JSON 文檔應分開存儲。這使得 ID 和查詢變得很簡單。
  • JPA 已經過標準化,擁有一個優秀的社區來爲您的微服務開發提供支持。所有 Java EE 服務器都必須支持 JPA。
  • 要實現不在 Java EE 容器中運行的微服務,也可以使用 JPA。
  • 從數據庫生成實體類(逆向工程),可減少必須自行編寫的代碼行數。
  • 對於混合持久性,JPA 支持關係數據存儲和麪向文檔的數據存儲 (EclipseLink)。
  • JPA 是關係數據庫的一種抽象,允許您在需要時交換您的關係數據存儲與另一個關係數據存儲。這種可移植性消除了微服務的供應商鎖定。
  • 爲了實現該策略,每個微服務都應在關係數據庫中擁有自己的模式,您可以在 JPA 配置文件 persistence.xml 中爲您的服務設置默認模式。

Enterprise JavaBeans

Enterprise JavaBeans 3.2 (EJB)(在 JSR 345 中指定)包含在 Java EE 規範中。EJB 並不是普通的 Java 類,原因如下:

  • 它們擁有生命週期。
  • 它們由一個 EJB 容器(EJB 的運行時環境)管理。
  • 它們擁有更多很有幫助的特性。

EJB 是服務器端軟件組件。從 EJB 3.0 開始,它不再使用部署描述符。EJB 的所有聲明都可在 EJB 類自身中使用註釋完成。與 CDI 管理 bean 的實現一樣,在 EJB 容器內處理 EJB 的實現也是輕量級的。EJB 的線程處理由 EJB 容器完成(類似於 servlet 容器中的線程處理)。EJB 的一個附加特性是,它們可與 Java EE Security 結合使用。

EJB 可分爲以下類型:

  • 無狀態
  • 有狀態
  • Singleton
  • 消息驅動 bean (MDB)

無狀態 EJB 無法擁有任何狀態,但有狀態 EJB 可以。由於微服務的特徵,微服務中不應使用有狀態 EJB。一個 Singleton Bean 僅在一個 Java EE 服務器中存在一次。異步消息處理中會結合使用 MDB 和 JMS 提供程序。

EJB 可實現多個業務視圖,必須相應地註釋這些視圖:

  • 本地接口 (@Local)

此接口中的方法只能由同一個 Java 虛擬機 (JVM) 中的客戶端調用。

  • 遠程接口 (@Remote)

此接口中列出的方法可由 JVM 外部的客戶端調用。

  • 無接口 (@LocalBean)

與本地接口大體相同,EJB 類的所有公共方法都向客戶端公開。

在輕量型架構中(微服務應擁有這種架構),將 EJB 實現爲無接口 EJB 會很有用。

EJB 提供的主要好處之一是自動事務處理。每次調用一個業務方法時,都會調用 EJB 容器的事務管理器(例外:顯式關閉了事務支持的 EJB)。所以,很容易將 EJB 與事務數據存儲結合使用。將 EJB 與 JPA 相集成也很容易。

示例 14 中的代碼段給出了一個將 EJB 與 JPA 框架結合的示例。

示例 14 帶 PersistenceContext 的無狀態(無接口)EJB

@Stateless
@LocalBean
public class OrderEJB {
@PersistenceContext
private EntityManager entityManager;
public void addOrder(Order order) {
entityManager.persist(order);
}
public void deleteOrder(Order order) {
entityManager.remove(order);
}
...
}

根據小節"Java Persistence API"中的介紹,注入 EntityManager。

EJB 可擁有以下事務屬性之一來處理事務數據存儲(必須由 EJB 容器實現):

REQUIRED(默認)

MANDATORY

NEVER

NOT_SUPPORTED

REQUIRES_NEW

SUPPORTS

這些所謂的容器管理事務 (CMT) 可用在 EJB 的任何業務方法上。應避免 Bean 管理事務 (BMT),它們也可用在 EJB 中。可在類級別上設置註釋 TransactionAttribute,使該類的每個業務方法都有自己的事務屬性(示例 15)。如果不執行任何設置,那麼所有方法都將擁有默認事務級別 (REQUIRED)。方法級別的事務屬性會覆蓋類屬性。

示例 15 在 EJB 中顯式設置事務屬性

@TransactionAttribute(REQUIRED)
@Stateless
@LocalBean
public class OrderEJB {
...
@TransactionAttribute(REQUIRES_NEW)
public void methodA() {...}
@TransactionAttribute(REQUIRED)
public void methodB() {...}
}

未實現爲 EJB 的 REST 端點

在事務管理其決定提交時,會執行一些數據庫更改或驗證。在某些情況下,驗證數據庫中的檢查約束是提交事務之前的最後一步。如果驗證失敗,JPA 提供程序將拋出一個 RuntimeException,因爲 JPA 使用運行時異常來報告錯誤。如果使用 EJB 執行事故管理,則捕獲 RuntimeException 的位置位於 EJB 的存根代碼中,EJB 容器將在這裏執行事故管理。存根代碼由 EJB 容器生成。因此,您無法處理此 RuntimeException,該異常被進一步拋出到它會被捕獲到的地方。

如果將 REST 端點實現爲 EJB,就像一些人喜歡的那樣,則必須在 REST 提供程序中捕獲該異常。REST 提供程序擁有異常映射器,可將異常轉換爲 HTTP 錯誤代碼。但是,當在數據庫提交期間拋出 RuntimeException 時,這些異常映射器不會進行干預。因此,REST 客戶端會收到 RuntimeException,應該避免這種情況。

處理這些問題的最佳方式是,將 REST 端點實現爲 CDI 管理的請求範圍的 bean。在這個 CDI bean 中,可以使用與 EJB 內相同的注入方式。所以很容易將 EJB 注入 CDI 管理的 bean 中(示例 16)。

示例 16 實現爲 CDI 管理的 bean 且注入 EJB 的 REST 端點

@RequestScoped
@Path("/Order")
public class OrderREST {
@EJB
private OrderEJB orderEJB;
...
}

也可以將 EJB 與 Spring 集成(Enterprise JavaBeans (EJB) 集成 - Spring),而且如果您願意的話,可以使用 Transaction Management Spring 執行事務管理。但是,在 Java EE 領域,將事務管理委託給服務器會更好一些。

BeanValidation

BeanValidation 也包含在 Java EE 7 規範中:Bean Validation 1.1 JSR 349。Bean Validation 的用途是在 bean 數據上輕鬆地定義和執行驗證約束。在 Java EE 環境中,Bean 驗證由不同的 Java EE 容器自動完成。開發人員只需要在屬性、方法或類上以註釋形式設置約束條件。驗證會在調用這些元素時自動完成(如果已配置)。驗證也可以在源代碼中顯式完成。有關 BeanValidation 的更多信息,請訪問 網站

javax.validation.constraints 包中的內置約束示例如示例 17 所示。

示例 17 BeanValidation 中的默認內置約束條件

private String username; // username must not be null
@Pattern(regexp="\\(\\d{3}\\)\\d{3}-\\d{4}")
private String phoneNumber; // phoneNumber must match the regular expression
@Size(min=2, max=40)
String briefMessage; // briefMessage netween 2 and 40 characters

也可以組合使用多個約束條件,如示例 18 所示。

示例 18 約束條件的組合

@NotNull
@Size(min=1, max=16)
private String firstname;

還可以擴展約束條件(自定義約束條件)。示例 19 展示瞭如何自行執行驗證。

示例 19 以編程方式進行驗證

Order order = new Order( null, "This is a description", null );
ValidatorFactory factory =
Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<Order>> constraintViolations = validator.validate(order);
assertEquals( 2, constraintViolations.size() );
assertEquals( "Id may not be null",
constraintViolations.iterator().next().getMessage() );
assertEquals( "Order date may not be null",
constraintViolations.iterator().next().getMessage() );

可以通過配置 JPA 來自動執行 Bean 驗證。JPA 規範要求,持久化提供程序必須驗證所謂的託管類(參見示例 20)。從 JPA 的意義上講,託管類是實體類。用於 JPA 編程的所有其他類也必須驗證(例如嵌入式類、超類)。此過程必須在這些託管類參與的生命週期事件中完成。

示例20 開啓了 Bean 驗證的 Persistence.xml

<persistence>
<persistence-unit name="OrderDB">
<provider>
org.eclipse.persistence.jpa.PersistenceProvider
</provider>
<class> ...</class>
<properties>
<property name="javax.persistence.validation.mode"
value="AUTO" />
</properties>
</persistence-unit>
</persistence>

使用的來自 Java EE 產品棧的所有其他框架都可用於自動驗證 bean(例如 JAX-RS、CDI),也可以使用 EJB,如示例 21 所示。

示例 21 EJB 中的 Bean 驗證

@Stateless
@LocalBean
public class OrderEJB {
public String setDescription(@Max(80) String newDescription){
...
}
}

JPA 和 BeanValidation 的架構分層方面

據微服務中實現的層,需要解決一些方面的問題。

在僅包含少量層的服務中,使用 JPA 實體類作爲數據傳輸對象 (DTO) 也更容易。將 JPA 對象與它的持久化上下文分離後,可以將它用作簡單 Java 對象 (POJO)。還可以將這個 POJO 用作 DTO,以便將數據傳輸到 REST 端點。以這種方式傳輸數據有一些缺點。BeanValidation 註釋與 JPA 註釋混合在一起,這可能導致 Java 類包含大量註釋,而且您的 REST 端點與數據庫的關係更緊密。

如果微服務稍大一點或需要處理更復雜的數據模型,那麼最好使用一個單獨層來訪問數據庫。這個額外層基於 JPA 類來實現所有數據訪問方法。此層中的類是數據訪問對象 (DAO)。可以使用 DAO 類爲 REST 端點生成 DTO 類,一方面關注數據模型 (DAO),另一方面關注客戶端 (DTO)。這種模式的缺點是,必須將 DAO 層中處理的 JPA 類轉換爲 DTO,並轉換回來。爲了避免創建大量樣板代碼來執行此任務,可以使用一些框架來幫助轉換。可以使用以下框架來轉換 Java 對象:

要增加 BeanValidation 帶來的可能性,使用 Spring 獲得額外的特性可能很有用。

上下文和依賴注入

如果您的微服務不打算將數據存儲在事務數據存儲中,可以考慮使用 CDI 管理 bean 代理 EJB。CDI 1.1 是在 JSR 346 中指定的,包含在 Java EE 7 規範中。CDI 管理 bean 可能是這些情況下的不錯替代方案。

有關 CDI 管理 bean 的更多信息,請訪問下面這個網站:http://docs.oracle.com/javaee/6/tutorial/doc/giwhl.html

與 EJB 相比,CDI 本身沒有 Java EE 安全機制,沒有注入持久化上下文。此過程必須由開發人員自己完成,或使用其他框架完成。舉例而言,Apache DeltaSpike 有許多模塊可用於擴展 CDI 的功能。有關 Apache DeltaSpike 的更多信息,請訪問:

http://deltaspike.apache.org/index.html

可使用其他框架來擴展 CDI 管理 bean 的功能。EJB 有一個可在應用服務器中管理的線程池。CDI 目前沒有與此功能相對應的功能。能夠配置線程池,這在高負載的環境中很有幫助。

爲了實現不在 Java EE 應用服務器中運行的微服務,CDI 和其他模塊提供了許多對這些環境有用的功能。

Java Message Service API

爲了在 Java 領域實現事件驅動架構,JMS API 提供了相關支持,Java Message Service 2.0 JSR 343 中也指定了該 API。JMS 用於與必須通過面向消息的中間件 (MOM) 實現的消息提供程序通信。

當 JMS 從 1.1 版更新到 2.0 版(包含在 Java EE 7 規範中)時,進行了大量返工來讓 API 更容易使用。JMS 2.0 兼容更低的版本,所以您可以對新微服務使用現有代碼或使用新的簡化 API。下一個版本不會棄用舊 API。

依據智能端點和啞管道方法,基於 Java 的微服務必須將 JSON 消息發送到 JMS 提供程序託管的端點。消息的發送方被稱爲生成者,消息的接收方被稱爲使用者。這些端點可具有以下類型:

  • 隊列

一個隊列中的消息僅由一個使用者使用。隊列中的消息序列可按不同的順序使用。隊列採用端到端的語義使用。

  • 主題

這些消息可供多個使用者使用。這是發佈/訂閱語義的實現。

在基於 REST 的微服務中(其中基於 JSON 的請求由客戶端發出),對消息系統也採用 JSON 格式是一個不錯的主意。其他消息應用程序使用了 XML。如果您的微服務系統中僅有一種格式,則更容易實現 JSON。

示例 22 生成者使用 EJB 將消息發送到 JMS 隊列

@Stateless
@LocalBean
public class OrderEJB {
@Inject
@JMSConnectionFactory("jms/myConnectionFactory")
JMSContext jmsContext;
@Resource(mappedName = "jms/PaymentQueue")
Queue queue;
public void sendMessage(String message) {
jmsContext.createProducer().send(queue, message);
}

需要一個 JMSContext 和一個 Queue 才能發送消息(示例 22)。如果消息發送方在 Java EE 應用服務器中運行,則會注入這些對象。示例 22 使用一個 EJB,所以注入了這些資源。必須在應用服務器中對注入的對象進行配置。如果發生異常,則會拋出一個運行時異常 JMSRuntimeException。

注入的 JMSContext 在 JTA 事務中的範圍爲 transaction。所以如果您的消息生成者是 EJB,您的消息將傳送到事務的上下文中,這樣做可以避免鬆散的消息。

要使用來自隊列的消息,使用消息驅動 EJB (MDB) 就能輕鬆實現,如示例 23 所示。使用 MDB 的另一個優勢是,消息的使用在事務中完成,所以不會丟失消息。

示例 23 使用者 – 負責處理來自 JMS 隊列的消息

@MessageDriven(
name="PaymentMDB",
activationConfig = {
@ActivationConfigProperty(
propertyName="messagingType",
propertyValue="javax.jms.MessageListener"),
@ActivationConfigProperty(
propertyName = "destinationType",
propertyValue = "javax.jms.Queue"),
@ActivationConfigProperty(
propertyName = "destination",
propertyValue = "PaymentQueue"),
@ActivationConfigProperty(
propertyName = "useJNDI",
propertyValue = "true"),
}
)
public class PaymentMDB implements MessageListener {
@TransactionAttribute(
value = TransactionAttributeType.REQUIRED)
public void onMessage(Message message) {
if (message instanceof TextMessage) {
TextMessage textMessage = (TextMessage) message;
String text = message.getText();
...
}
}
}

必須使用 @MessageDriven 註釋將 EJB 類型聲明爲消息驅動 EJB。在此註釋內,可以設置 MDB 的名稱和一些激活配置。激活配置的屬性將 MDB 與處理隊列或主題的 JMS 消息系統相關聯。在託管您的 MDB 的應用服務器環境中,很容易配置這些元素。MDB 本身會實現一個 MessageListener 接口,該接口只有一個方法:onMessage。每當 JMS 提供程序有消息要處理時,它就會調用此方法。使用一個事務屬性註釋該方法,以表明它是在一個事務內調用的。MDB 的默認事務屬性是 TransactionAttributeType.REQUIRED。在該方法內,必須轉換消息對象,而且消息可以提取爲字符串。也可使用其他消息類型。

僅使用 MDB 處理消息是一種不錯的做法。將 MDB 保持爲一個技術類。您的剩餘業務代碼應在 MDB 調用的 Java POJO 中實現。此配置使業務代碼更容易在 JUnits 中測試。

前面已經提到過,每個 MDB 在一個事務內運行,所以不會丟失消息。如果在處理消息期間發生錯誤,而且 EJB(處理 MDB)收到此運行時異常,那麼該消息會重新傳送到 MDB(錯誤循環)。可在 JMS 提供程序中配置重試次數,它指定了發生此錯誤的頻率。在達到重試上線次數後,消息通常被放入一個錯誤隊列中。錯誤隊列中的消息必須單獨處理。

如果一個業務事務涵蓋多個微服務,可使用事件驅動架構(參見小節"跨微服務的數據共享")。這意味着發送事件的微服務必須執行以下任務:

  • 更改其數據存儲中的數據
  • 將消息發送到第二個微服務

接收微服務必須執行以下任務:

  • 從隊列接收消息
  • 更改它的數據存儲中的數據

爲了保持一致,如果數據存儲是事務性的,這兩個操作必須在一個事務中完成。對於消息系統的生成者和使用者,也要滿足此要求。在這些情況下,必須使用分佈式事務。事務夥伴是數據存儲和 JMS 提供程序(不是兩個微服務的兩個數據存儲)。

要跟蹤生成和使用的消息,使用關聯 ID 很有用。消息生成者指定的關聯 ID 與使用者使用的消息相關聯。這個關聯 ID 也可用在微服務的日誌記錄中,以便獲得微服務調用的完整通信路徑。

Java 提供了一個類來生成唯一 Id:UUID。這個類可用於生成關聯 ID。示例 24 展示瞭如何設置關聯 ID。

示例 24 在 JMS 消息中設置關聯 ID

// JMSContext injected as before
JMSProducer producer = jmsContext.createProducer();
producer.setJMSCorrelationID(UUID.randomUUID().toString());
producer.send(queue, message);

示例 25 展示瞭如何獲取關聯 ID。

示例 25 從 JMS 消息獲取關聯 ID

// message received as before
String correlationId = message.getJMSCorrelationID();

有關 UUID 的更多信息,請訪問下面這個網站:

http://docs.oracle.com/javase/7/docs/api/java/util/UUID.html

如果使用非 JMS 提供程序作爲面向消息的中間件,JMS 可能不是向此係統發送消息的正確方法。可結合使用 RabbitMQ(一個 AMQP 代理)和 JMS,因爲 Pivotal 爲 RabbitMQ 實現了一個 JMS 適配器。有關 RabbitMQ 的更多信息,請訪問下面這個網站:

http://www.rabbitmq.com/

Apache Qpid 也爲 AMQP 協議實現了一個 JMS 客戶端。這些只是一些示例,表明使用 JMS 與非 JMS 提供程序通信也很有用。但是,根據您的需求,使用消息系統的原生 Java 驅動程序有可能會更好一些。有關 Apache Qpid 的更多信息,請訪問下面這個網站:

http://qpid.apache.org/index.html

Spring 對 JMS 消息提供程序的支持是處理 JMS 消息的另一種方案。有關 Spring 的更多信息,請訪問下面這個網站:

http://docs.spring.io/spring/docs/current/spring-framework-reference/html/jms.html

Java 和其他消息協議

根據您需要的面向消息的中間件中的特性,您可能需要一個非 JMS 消息提供程序系統。MQTT 是一種最適合物聯網 (IoT) 和 AMQP 的需求的消息協議。它是爲實現跨供應商移植而開發的。

JMS 不能用於與這些系統通信。通過使用它們提供的客戶端,可以使用它們提供的所有特殊功能。有關 MQTT 的更多信息,請訪問下面這個網站:http://mqtt.org/

Apache Kafka 是一個非 JMS 提供程序,但它提供了一個 Java 驅動程序。人們提出了一種增強請求,希望實現一個適配器,讓客戶端能與 Apache Kafka 傳輸 JSM 消息,但此問題仍待解決。所以最好使用 Kafka 提供的 Java 驅動程序。有關 Kafka 的更多信息,請訪問下面這個網站:http://kafka.apache.org/

RabbitMQ 是一個消息代理系統,它實現了 AMQP 協議,而且還提供了一個 Java 客戶端。有關 RabbitMQ 的更多信息,請訪問下面這個網站:https://www.rabbitmq.com/

Spring 有一個與基於 AMQP 的消息系統通信的庫。Spring 還提供了對 MQTT 協議的支持。有關 Spring 的更多信息,請訪問下面這個網站:

http://projects.spring.io/spring-amqp/

可以看到,支持結合使用 Java 和非 JMS 消息系統。

總結

本文重點介紹瞭如何使用基於 Java 的微服務實現微服務在數據處理方面保持可管理。下一部分我們將回到第一部分中講到的演化策略,將介紹可在實踐中考慮和應用的可能戰略。好了,學習愉快,下次再見!

參考資源 (resources)

 

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

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