環境與關係注入:新的Java EE工具箱

與Java EE整合

Du Spirit
Java Magazine 翻譯系列

--

Translated from “Contexts and Dependency Injection: The New Java EE Toolbox — Integration with Java EE” by Antonio Goncalves, Java Magazine, November/December 2015, page 34. Copyright Oracle Corporation.

這系列文章試圖解開環境與關係注入 (Contexts and Dependency Injection, CDI) 的秘密,在前三期的文章中 [譯註:這我恐怕就沒空翻譯了],我探討何為強型的關係注入、如何用 CDI 整合第三方框架、及如何使用攔截器 (interceptor)、裝飾器 (decorator) 與事件 (event) 建立弱耦合,這最終篇將涵蓋 CDI 與 Java EE 的整合。

Java EE 是 Java 執行環境的擴充,他提供一個受控制的環境,在這環境中,容器供應為數不少的服務元件,這些服務包含生命週期管理 (lifecycle management)、安全性 (security)、驗證 (validation)、物件延續性 (persistence) [譯註:這東西有點難直翻成儲存,用資料庫儲存物件只是讓物件延續的眾多方法中的一種],當然,還有注入 (injection)。物件延續性與交易 (transaction) 常常一起用來開發應用程式的後台 (backend)。

在網頁層,Java EE 有 servlet、WebSockets [編按:參考本期文章] 和 JavaServer Faces (JSF) 與使用者介面相關的技術,CDI,如果在前三篇文章所述,可以將網頁層與服務層結合在一起,建立一個同質 [譯註:指都使用 Java 技術]且整合的應用程式。

結合網頁層與服務層

Java EE 有許多技術讓我們建立任何架構,包含網站應用程式、REST 介面、批次處理、非同步傳訊、物件延續等等。如 Figure 1 所示,這些應用都可以組織成數層 (tiers):呈現、商業邏輯、商業模型或與外部服務互動。根據需求,任何架構都可能從無狀態到有狀態的、從 flat layered 到 multitiered [譯註:layer 和 tier 都是分層的概念,所以這就不翻譯了,免得無法分出之間的差別]。一個問題是,網頁層或服務層個別有自己的典範和語言,因此,CDI 便是結合它們很重要的資源。

Figure 1. 一個應用的標準分層

Java 應用在服務層,除了網頁客戶端 (使用HTML) 和資料庫 (使用資料庫定義語言) 外,大多數 Java EE 使用 Java 為主要語言,因此,我們可以在大多數的應用層 (Java Persistence API 存取商業模型實體或在商業邏輯層中一個簡單 bean) 看到 Java,我們甚至可以在部分的呈現層中使用 Java:使用 Java寫 JSF backing beans。

EL 應用在呈現層,我會說 Java 為主要語言,是因為 JSF 頁面主要使用 Facelets 或 Expression Language (EL),EL 提供一個重要的機制,讓呈現層能與應用邏輯溝通,這在 JavaServer Faces 與 JavaServer Pages 中都可以使用,透過 # 符號,如 Figure 2 所示,EL 使用簡單的表達式動態地從元件存取資料,例如,顯示訂單的小記在頁面上,或是當按鈕按下時執行 compute 函式。

CDI 結合服務層與呈現層,使用 @Named,CDI 結合 Java 與 Expression Language,就如同您可以在 Figure 2 看到的,基本上,給予 CDI bean 一個名字,讓它連結在 EL 中,因此當 PurchaseOrderBean 被加註 @Named("po") 時,它以 po 的名字連結在 EL 中。

Figure 2. 使用Expression Language

CDI 應用在管理狀態,CDI 更進一步替我們使用有效範圍 (scope) 管理 bean的狀態,假設在網站的右上角,我們需要顯示登入的使用者,希望這資訊可以保持到 session 結束,針對這情況,只需替 bean 加註 @SessionScoped,CDI 會管理狀態,當 session 結束時銷毀 bean;另一方面,每次更新頁面時,應該計算與顯示訂單的小計,因為 PurchaseOrderBean 的範圍比 session 要短,所以我們可以為它加註 @RequestScoped,CDI 只為每次請求保留 bean 的狀態,這意味請求是無狀態的。透過一些 annotations,CDI 結合網頁層與服務層,減少為膠合而寫的程式碼,讓開發者專注在商業問題上。CDI 為分層架構定義一個一致的模型,在使用者多次請求的互動中提供明確定義的環境。

連結 (Binding)

連結是將網頁層與服務層結合在一起的基本服務,如果我們想在非 Java 但支援 EL 的程式碼 (例如JSF頁面)中參考一個 bean,我們必須為 bean 指定一個 EL 名稱,用 @Named 內建的修飾字指定,然後我們可以輕易地在任何 JSF 頁面中透過 EL 表示是使用 bean。原本,EL 受到 ECMAScript 和 XPath 表示語言的啟發,被引入到 Java EE 中,讓網頁開發者可以存取與操作後端的 Java 程式而不需透過 JavaScript。

Expression Language EL的語法相當簡單,使用井字符號 (#) 與大括號標示一個需要被解析的表示式,這些表示式可以很複雜或很簡單 (見 Listing 1),也可以使用數學運算、lambda 表示法等等。

Listing 1

value expression 最常用因為可以讀取與寫入資料,因此,我們的頁面可以取得 PurchaseOrderBeansubtotal 屬性或是客戶的 name 屬性。語法同樣允許存取陣列或 list 的元素:使用角括弧指定索引,如此,表示式回傳 bean 當中第二筆消費交易。另一有用的 EL 功能是 method expression,可以執行 bean 可回傳結果的公開函式,所以,表示式執行 PurchaseOrderBeancompute函式。參數化的函示可以接受參數,例中,5 被當成參數傳入。

JSF pages 回到我們的呈現層,EL 以另一種形式出現在 JSF 頁面中,以Listing 2 為例,value expression 用來顯示小記與訂單的附加稅 (VAT) 稅率,這連結是雙向的,意思是表示式可以修改屬性的值,當網頁被傳送給伺服器。當我們需要在按鈕被按下時執行某個動作時,method expression 是很有用的,在這案例中,點擊 compute 按鈕將會執行 PurchaseOrderBeancompute 函式。

Listing 2

CDI beans Listing 3 中的 PurchaseOrderBeansubtotalvatRate 屬性與對應的 getter 及 setter,還有一個 compute 函式,負責計算訂單在指定稅率下的總價,這程式除了加註 @Named 外沒什麼特別的,但沒有它,這個 bean 將無法有 EL 名稱,無法被連結到頁面中。

Listing 3.

@Named @Named讓 EL 能參考到 bean 的屬性與函式,我們可以在使用@Named 時不指定名稱,讓 CDI 為我們取名,預設名稱是類別名稱將第一個字母變小寫,就如此例中,以小寫 p 開頭的 purchaseOrderBean。但是我們可以在使用 @Named 時指定非預設的名稱,當使用 @Named("order"),前面的表示式也必須對應的改名 [譯註:Listing 2中的 purchaseOrderBean 要改成order]。

連結 Producers 與 Alternatives

如我們剛所見,@Named 可以將表示式與 bean 連結起來,搭配 producer,EL 可以參考任何東西,例如,我們產生一個整數,給予名稱,然後就可以在表示中餐考到它。除了 Java,EL 同樣能用 alternative 來切換實作。

為 producer 命名 為了解釋有名字的 producer 與 alternative,我們看一下 NumberProducer 類別,角色是用來產生數值 (見Listing 4),它有 vatRatediscountRate 屬性,兩者的型別都是 Float,計畫是產生這些屬性讓 CDI 管理並可以被注入到某處,就您現在所知,這程式可能會含糊不清,因兩個屬性的型別都是 Float。為區別它們,我們為一個屬性加註 @VAT,另一個加註 @Discount,現在,想在 JSF 頁面中取得附加稅率,只需在產生的屬性加註 @Named,預設,EL 名稱是 vatRate,如此一來,JSF 頁面可直接參考 vatRate,不需將 NumberProducer 類別名稱放在前面 (<h:inputText value="#{vatRate}"/>)。記住,@Named 使用的預設名稱可以被覆寫,例如,我們可以用 vat 取代vatRate,然後用 (<h:inputText value="#{vat}"/>)表示式參考到它。

Listing 4.

Alternative producer 現在,假設我們有另一個使用案例,附加稅和折扣會根據外部的設定而改變,例如,附加稅率在某些國家是 5.5%,但在其他國家是 19.6%,或是在平日折扣是 2.25%,但在聖誕節期間是 4.75%,這是 alternatives 常見的使用案例。首先,我們仍需要產生、修飾與命名附加稅率與折扣屬性 (見 Listing 5),然後我們加註 @Alternative,如您所見,CDI 是非常具表達性的,每個 annotation 都有自己的意義,讓讀程式時相當容易理解,然後,要做的只是在 benas.xml 中指定開啟或關閉 alternatives。

Listing 5.

狀態管理

我們都習慣 HTTP session 與 HTTP 請求的概念,有兩個日常問題的例子是關於管理狀態,與特定 context 有關,當該 context 不再需要時,須確保所有必要的清理工作被執行,例如,HTTP session 結束時,session 需要被清除。傳統上,透過取得與修改 servlet session 及 request 屬性,這狀態管理已被用手動的方式實作。CDI 讓這狀態管理的概念更進一步,適用到整個應用程式,不限於 HTTP。此外,CDI 以描述性的方式做到:使用 annotation,bean 的狀態交由容器管理。不再有因應用程式無法清理 session 屬性造成的記憶體洩漏,CDI 自動完成這些清理工作。CDI 將 Servlet 規範中定義的 context 模型:application、session、request 擴充成另一種context:conversation,然後,將這環境套用到整個商業邏輯層,不只是網頁層。

內建的有效範圍 在開始看一些程式碼前,我們先說明四種內建的 CDI 有效範圍 (如 Figure 3 所示)。假設我們有一個應用程式,其生命週期有數個月,我們開啟伺服器然後在關機前讓它執行數個月,在這例子中,application scope (應用程式層級的有效範圍) 非常長,一個使用者登入,且保持登入狀態數分鐘,session scope (session 層級的有效範圍) 從他登入瞬間開始持續到他登出瞬間,第二個使用者登入但她持續較長的 session。每個 session 都是彼此獨立專屬於單一使用者,生命週期也完全不同。在這期間,使用者都點擊他們專屬的空間,每個點擊都建立一個請求,由伺服器處理。最後一個有效範圍是 conversation,它相當特異,因為它可以視需求維持生命週期,只要開啟一個 conversation 就行,它可以跨越多個請求,然後結束,每個使用者將會有他/她專屬的 conversation。每個有效範圍都用 annotation 表達。

Figure 3. 四種CDI內建的有效範圍

Application scope 例如,假設應用程式需要一個全域的快取,由一個 key-value 的 map 物件、幾個新增物件到快取、用 key 取值和移除物件的函式組成,我們希望所有與應用程式互動的使用者都可以使用這個快取。為此,我們為此 bean 加註 @ApplicationScoped (見 Listing 6),當需要使用這快取時,CDI 容器會自動建立它,當建立它的環境結束時(即伺服器關機),會自動被消滅。如果想在 JSF 頁面中直接參考到這快取,只要加註 @Named

Listing 6.

Session scope 應用程式層級有效範圍的 bean 在整個應用程式生命週期間存活,且分享給所有使用者。而 Session-scoped 的 bean 則只在 HTTP session 的生命週期間存活,且只屬於當前的使用者,這有效範圍十分有用,例如,設計一個購物車模型 (見 Listing 7),每個使用者有自己的購物清單,當他登入,可以加物品到購物車,然後在結束時結帳離開。當 session 建立時,這購物車實體會自動被建立,然後在 session 結束時被消滅,這時體會與使用者的 session 連結,然後分享於 session 的所有請求中。同樣,加註 @Named,如果想在 EL 中使用。

Listing 7.

Request scope. 到目前為止,我們描述的所有有效範圍都在處理狀態,對於無狀態的應用程式,我們可以使用 HTTP 請求與 request scope 的 bean,這些 bean 通常是沒有狀態的 services (見 Listing 8) 和 controller,例如,建立一本書、取得所有書的封面圖片、取得某分類的書籍清單。通常,他們都會加註 @Named,因為可以在頁面上的按鈕點擊時被執行。一個被定義成 @RequestScoped 的物件在每次請求時被建立,而且不需是可被序列化的 (serializable)。

Listing 8.

Conversation scope 最後一個內建的有效範圍是 conversation scope,和 session scope 有點像,可以保持某個使用者的狀態且可以跨越多個請求,但是,和 session scope 不同的是,conversation scope 是由應用程式明確劃分的。假設我們使用好幾個網頁組成一個精靈,讓顧客建立一個 profile (見Listing 9),為了管理 conversation 的生命週期 [譯註:透過好幾個網頁的推進,就好像伺服器與顧客在對談],CDI 給我們一個 Conversation API,用注入的方式取得。所以,當使用者開始建立 profile,呼叫 begin 函式開啟一個 conversation,使用者可以走訪頁面,回到前個頁面或到下個頁面,直到 conversation 結束,如您所見,conversation scope 是唯一需要明確劃分的。其他有效範圍的 bean 都由 CDI 容器自動清理,而 conversations 需要明確的啟動與結束,或等到超過時限。

Dependent scope 所有我們剛才看的有效範圍都與情境相關,這意味他們的生命周期都由容器管理,注入的 bean 都與情境相關,CDI 容器確保在正確的時間建立物件與注入物件,時間點由物件所指定有效範圍所決定。dependent scope 與情境無關,實際上是一個虛擬的有效範圍,dependent scope 是 CDI bean 的預設有效範圍,如果一個 CDI bean 沒有指定上述任何一個有效範圍,則會被注入成一個 dependent-scoped bean,這指的是它的有效範圍與它所屬物件的有效範圍相同。例如,在 Listing 10 中,一個 request-scoped 的服務 (BookService) 注入一個 dependent 的 IsbnGenerator 物件,則被注入的 IsbnGenerator 物件的有效範圍也是 request scope。一個 dependent scope 的 bean 實體的緊緊相依於另一個物件,IsbnGeneratorBookService 建立時被實體化,在 BookService 被消滅時一併被消滅。我們可以總是使用 @Dependent,但大可不必,因為它是預設的有效範圍。

結論

在本文中,我們示範如何用 @Named 與有範圍的狀態管理將網頁層與服務層連結在一起,當使用 CDI,呈現層的元件與商業邏輯層的元件並沒有差異,都可以被限定範圍、注入、或是在 EL 中使用。我們可以將應用程式根據我們所需的任意架構分層,不用擔心應用程式邏輯屈從技術的分層。如果架構的分層太扁平,沒有甚麼可以阻擋我們使用 CDI 建立一個等效的分層架構。撰寫一個所有物件都是 CDI bean 的 Java EE 應用程式是有可能的。 [譯註:那我應該會瘋掉...]

LEARN MORE
CDI specification
Beginning Java EE 7
PluralSight course on CDI 1.1
Weld CDI reference implementation

譯者的告白
其實每次看到 container based 的技術時,心裡總是有些矛盾,它確實很好用,加速開發,但它同時汙染了 domain model (好吧~大概只有我這麼龜毛,我認為 domain model 應該與任何 framework 保持距離),也許改天應該來寫一篇文章關於這內心的糾結。不過有沒有時間就不知道了,Java Magazine 的 2015年11–12 雙月刊還剩下一篇,接下來將邁入 2016年1–2 雙月刊的翻譯了。

--

--