over 2 years ago

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.

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

與Java EE整合

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

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

在網頁層,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 Expressions

#{purchaseOrderBean.subtotal}
#{purchaseOrderBean.customer.name}

// Array Expressions

#{purchaseOrderBean.orders[2]}

// Method Expressions

#{purchaseOrderBean.compute}

// Parameterized Method Calls

#{purchaseOrderBean.compute('5')}

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.
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html">
<h:body>
    <h:form>
        <h:outputLabel value="Subtotal:"/>
        <h:inputText value="#{purchaseOrderBean.subtotal}"/>
        <h:outputLabel value="VAT rate:"/>
        <h:inputText value="#{purchaseOrderBean.vatRate}"/>
        <h:commandLink value="Compute" action='#{purchaseOrderBean.compute}'/>
    </h:form>
</h:body>
</html>

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

Listing 3.
@Named
public class PurchaseOrderBean {

    private Float subtotal = 0F;
    private Float vatRate = 5.5F;
    // …

    
    public String compute() {
        Float vat = subtotal * (vatRate / 100);
        Float discount = subtotal * (discountRate / 100);
        total = subtotal + vat  discount;
        return null;
    }

    // ...

}

@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.
public class NumberProducer {

    @Produces
    @VAT
    @Named("vat")
    private Float vatRate = 5.5F;

    @Produces
    @Discount
    @Named("discount")
    private Float discountRate = 2.25f;
}

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

Listing 5.
public class NumberProducer {

    @Produces @VAT @Named("vat")
    private Float vatRate = 5.5F;

    @Produces @VAT @Named("vat") @Alternative
    private Float vatRateAlt = 19.6F;

    @Produces @Discount @Named("discount")
    private Float discountRate = 2.25f;

    @Produces @Discount @Named("discount") @Alternative
    private Float discountRateAlt = 4.75f;
}

狀態管理

我們都習慣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讓這狀態管理的概念更進一步,適用到整個應用程式,不限於HTTP。此外,CDI以描述性的方式做到,使用單一個annotation,bean的狀態交由容器管理。

內建的有效範圍 在開始看一些程式碼前,我們先說明四種內建的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.
@Named
@ApplicationScoped
public class Cache implements Serializable {

    private Map<Object,Object> cache = new HashMap<>();

    public void addToCache(Object key, Object value) {
        // ...

    }

    public Object getFromCache (Object key) {
        // ...

    }

    public void removeFromCache (Object key) {
        // ...

    }
}

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

Listing 7.
@Named
@SessionScoped
public class ShoppingCart implements Serializable {

    private List<Item> cartItems = new ArrayList<>();

    public String addItemToCart() {
        // ...

    }

    public String checkout() {
        // ...

    }
}

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

Listing 8.
@RequestScoped
public class BookService {

    public Book persist(Book book) {
        // ...

    }

    public List<String> findAllImages() {
        // ...

    }

    public List<Book> findByCategory(long categoryId) {
        // ...

    }
}

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需要明確的啟動與結束,或等到超過時限。

Listing 9.
@Named
@ConversationScoped
public class CustomerWizard implements Serializable {

    @Inject
    private Conversation conversation;

    private Customer customer = new Customer();

    public void initProfile () {
        conversation.begin();
        // ...

    }

    public void endProfile () {
        // ...

        conversation.end();
    }
}

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,但大可不必,因為它是預設的有效範圍。

Listing 10.
@Dependent
public class IsbnGenerator {

    public String generateNumber() {
        return "13-84356-" + Math.abs(new Random().nextInt());
    }
}

@RequestScoped
public class BookService {

    @Inject
    private IsbnGenerator generator;
    // ...

}

結論

在本文中,我們示範如何用@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雙月刊的翻譯了。

← 《瘋狂改變世界:我就是這樣創立Twitter的!》書摘 Jython 2.7:結合Python與Java →