李小e 的 EJB 奮鬥札記(一)Session Bean 的存取模式與介面
前言
Enterprise Java Bean(簡稱 EJB)是 Java EE 技術領域中,負責封裝商業邏輯、流程的伺服器端元件,必須佈署在容器(Container)之中,由容器管理並提供所需的執行環境;在早期,開發EJB的程序、設定、佈署、測試與使用上都有較高的技術門檻,甚至在設計不良的情況下還會帶來效能上的瓶頸,所以總是帶給大家高難度與高成本的刻板印象,使得開發系統的相關人員望之卻步。 的確,從 EJB 1.x 到 2.x 的實務開發經驗中,常可以體會到許多令人抓狂的問題,例如:一個EJB元件必須要建立兩個介面(Home、Business 介面)跟一個 EJB 類別的煩瑣程序、又或是在用戶端程式使用 EJB 時,必須先做好與 JNDI 搏鬥的心理準備等,都會讓開發人員聞之色變,而埋下了 EJB 技術在日後未能普及運用的潛在因素。
隨著 Java 技術的進步,EJB 從 3.0 開始能使用標記(Annotation)進行開發,讓程式的撰寫與其相關設定能一次到位,省去了早期還要額外撰寫、維護佈署描述檔(Deployment Descriptor)的煩人程序;同時,也運用了 JPA 來取代傳統 Entity Bean 及 EJB QL,對永續保存的資料進行處理;這些大幅度的革新,簡化了 EJB 的學習曲線與開發程序。EJB 3.1 規格中的非同步方法(Asynchronous Methods)、標準化的 EJB JNDI 名稱(Global JNDI)、單一實例的 Session Bean(Singleton Session Bean)及 EJB Lite(EJB 規格簡化版)等,讓 EJB 技術更加多元、容易測試及使用。 在 2013/05/28 所推出的EJB 3.2 新規格,雖不像 3.1 時那般的大刀闊斧,但仍舊承襲了簡化 EJB 的精神,將 Entity Bean及 JAX-RPC 為基礎的 Web Service 端點(End Point)等相關技術移出主要規格外,使 EJB 的規格一分為二:核心(Core)與非必要(Optional)兩種;如此一來,EJB 的容器供應商(Container Provider)就可以選擇是否實作非必要的規格,讓 EJB 的架構及容器更為輕盈、有效率。
EJB 是 Java EE 架構下商業元件的標準規格,因此元件的可移植性與技術的整合性自然不在話下,此外,商業元件必須有高度的再用性及維護性,而 EJB 能讓不同性質的應用程式(Web、Stand-alone、Web Service、另一個 EJB)透過本地(Local)及遠端(Remote)的模式存取、操作,這些特性凸顯出了 EJB 重要的價值與地位。EJB 3.x 的規格歷經了種種的改革,無論在開發或是使用上,都變的既簡單又直觀,筆者希望將自身的學習及實務經驗,透過一系列的文章,讓大家重新認識並使用 EJB。 EJB 的存取模式EJB 元件可以分為兩大類:負責主要的商業流程與邏輯 Session Bean 及提供非同步訊息處理架構 Message Driven Bean(簡稱 MDB);其中,MDB 是藉由 JMS 的訊息所觸發並執行,不會讓用戶端進行存取,相較之下 Session Bean 可以開放用戶端操作,依特性又可以分為Stateless(SLSB)、Stateful(SFSB)及 Singlton(SSB)三種,在本篇的文章中並不介紹各種 EJB 元件的特性與應用,先針對 Session Bean 的存取模式與介面進行討論。
Session Bean 元件可以開放讓用戶端進行本地及遠端兩種存取模式;當用戶端與 Session Bean佈署在同一個 JVM 中,稱為本地存取,如下圖: 這種存取模式的用戶端多半為 Web 程式,而我們將 EJB 元件與 Web 程式包裝在同一個企業歸檔檔案(Enterprise Archive File-EAR)中進行佈署,又或者將兩者佈署在一個應用伺服器下的兩個獨立程式。 從 EJB 3.1 開始,因為 EJB Lite 及 Web Profile(Web 程式的常用功能-Java EE 簡化版)等規格的建立,更可以將 EJB 元件直接包裝在 Web 歸檔檔案(Web Archive File-WAR)中,進行佈署及本地存取。本地存取是著重於效能的解決方案,用戶端與 EJB 元件間進行溝通時,會遵循 Java 語言既定的規則,以 By Value 方式傳遞基本型別(Primitive),By Reference 的方式傳遞參考型別(Class、Array 等)。
相較之下,當用戶端與 EJB 元件佈署在不同的 JVM 中則稱為遠端存取,如下圖: 在這種存取模式下,無論是基本或參考型別,都會採用 By Value 的方式進行資料傳遞;此外,物件必須進行序列化與反序列化的程序才能在不同 JVM 間進行傳遞,因此遠端存取需要的成本要比本地存取來得高出許多。 從另一個角度去思考,遠端存取模式能將呈現層與商業層的軟體元件,實質分離在不同的硬體設備中,讓複雜、耗時費工的商業邏輯在運算能力及資源較充足的設備上執行,有助於負載的分散與平衡。 開放用戶端存取 Session Bean 元件容器為了要管理 Session Bean 元件的生命周期及交易、安全、同時存取等相關機制,因此,無論是本地或是遠端的存取模式,都會替 Session Bean 實例建立代理(Proxy)物件,讓用戶端透過代理物件對 Session Bean 擁有透通性(Transparent)的操作,如下圖: 我們建立一個簡單的 SLSB,藉此來說明如何將 Session Bean 開放給用戶端存取;在下列程式中,建立一個 POJO 類別及 test() 方法並以 @Stateless 標註: @Stateless public class MySimpleBean{ public String test() { return "Kitty"; } }
別懷疑,我們已經建立了一個能實際運作的 SLSB;事實上,從 EJB 3.1 開始,Session Bean 可以不需要實作任何的商業界面,預設就可以開放 EJB 內所有的 public 方法讓用戶端以 No-Interface 的方式進行本地存取。其實,以 @LocalBean 標記 Bean 類別才算是比較正規的做法,程式修改如下: @LocalBean @Stateless public class MySimpleBean{ public String test() { return "Kitty"; } } 接著,寫個簡單的 Servlet 作為用戶端,並藉由 @EJB 注入 No-Interface Session Bean 後,就能進行存取: @WebServlet(name="MySimpleServlet", urlPatterns={"/MySimpleServlet"}) public class MySimpleServlet extends HttpServlet { @EJB private MySimpleBean mySimpleBean; protected void doGet(HttpServletRequest request,HttpServletResponse response){ PrintWriter out = response.getWriter(); out.println("<h1>Hello " + mySimpleBean.test() + "</h1>"); } }
在 No-Interface(EJB 3.1)規格建立之前,如果要讓 EJB 能提供本地存取,必須建立商業介面並由 Bean 類別實作,實務上常用的做法有以下兩種。 做法(一)建立 POJI(Plain Old Java Interface)商業介面並以 @Local 標記,程式如下: @Local public interface IMySimple{ public String test() ; } @Stateless public class MySimpleBean implements IMySimple { public String test() { return "Kitty"; } } 對用戶端來說,必須透過 IMySimple 介面存取 EJB 元件,這種做法會使得用戶端與 EJB 相關的API(javax.ejb.Local)產生緊密的依賴關係,程式如下: @WebServlet(name="MySimpleServlet", urlPatterns={"/MySimpleServlet"}) public class MySimpleServlet extends HttpServlet { @EJB private IMySimple mySimpleBean; protected void doGet(HttpServletRequest request,HttpServletResponse response){ PrintWriter out = response.getWriter(); out.println("<h1>Hello " + mySimpleBean.test() + "</h1>"); } } 做法(二)建立 POJI(Plain Old Java Interface)商業介面,在 Bean 類別中以 @Local 標記,程式如下: public interface IMySimple{ public String test() ; } @Local @Stateless public class MySimpleBean implements IMySimple { public String test() { return "Kitty"; } } 上述的做法避免了用戶端對 EJB API 的依賴性,但付出了些許的代價-必須使用傳統 JNDI 方式取得 EJB 元件的參考,就程式的一致性來說,無法(不會)再用 @EJB(javax.ejb.EJB)標記進行注入,程式如下: @WebServlet(name="MySimpleServlet", urlPatterns={"/MySimpleServlet"}) public class MySimpleServlet extends HttpServlet { private IMySimple mySimpleBean = lookup(); private IMySimple lookup(){ try { IMySimple mySimpleBean; String jndi = "....."; InitialContext context = new InitialContext(); mySimpleBean = (IMySimple)context.lookup(jndi); return mySimpleBean; } catch (NamingException ex) { throw new RuntimeException(ex); } } protected void doGet(HttpServletRequest request,HttpServletResponse response){ PrintWriter out = response.getWriter(); out.println("<h1>Hello " + mySimpleBean.test() + "</h1>"); } } 這種做法能帶來其他的好處-開放給用戶端存取的介面都會集中在 Bean 類別內;換句話說,一旦 Session Bean 開放了複合存取模式與多重介面時(將於本篇中介紹),開發人員就不需要在各個介面中進行註記,使程式在未來更容易維護也具有彈性。
No-Interface 簡化了 Local Interface 開發 Session Bean 本地存取的步驟,但是,用戶端會與具有實作的 Bean 類別有著高度的耦合,著實不是件好事;甚至,用戶端可以直接產生 Bean 類別的實例使用,如此一來,除了無法獲得容器所提供的執行環境、安全性及相關資源之外,更使得實作變動時會連帶影響到用戶端必須跟著置換 Bean 類別的窘境。相較之下,Local Interface 的用戶端不需要擁有 Bean 類別,也就不會有上述潛在的問題發生。 一般來說,同一個 EJB 程式(模組)通常會包含許多 EJB 元件,此時 EJB 元件間的協同運作就相當適合使用 No-Interface 的方式達成,又或是直接在 Web 程式(模組)中所開發的 EJB 原件(EJB 3.1),由於上述這兩種情況就已經會有 Bean 類別在模組中,因此可以簡化開發程序並獲得容器管理所帶來的好處。對於不同應用程式(模組)的用戶端來說,為了降低與實作(Bean 類別)間的耦合,較適合以 Local Interface 的方式提供存取。
要開放遠端存取 Session Bean 其實很簡單,只要修改"使用 Local Interface 的建立本地存取模式"所描述的兩種做法,將 @Local 改為 @Remote 標記就可以達成,對 Web 用戶端來說一樣可以使用 @EJB 進行注入或傳統的 JNDI 方式取得 Session Bean 的遠端參考,並沒有甚麼不同。
依據 EJB 規格書中"4.9.7 Session Bean’s Business Interface"章節所描述,無法在商業介面上同時標記 @Local 及 @Remote,當我們需要同時開放本地與遠端存取模式時,就必須建立兩個商業介面,由 Bean 類別同時實作,做法(一)的程式如下: @Local public interface IMySimpleLocal{ public String test() ; } @Remote public interface IMySimpleRemote{ public String test() ; } @Stateless public class MySimpleBean implements IMySimpleLocal,IMySimpleRemote { public String test() { return "Kitty"; } } 做法(二)的程式如下: public interface IMySimpleLocal{ public String test() ; } public interface IMySimpleRemote{ public String test() ; } @Stateless @Local(IMySimpleLocal.class) @Remote(IMySimpleRemote.class) public class MySimpleBean implements IMySimpleLocal,IMySimpleRemote { public String test() { return "Kitty"; } } 由於我們不再於介面中進行註記,所以為了能讓容器能在佈署時辨識所開放的本地及遠端介面,因此,必須要在 Bean 類別的 @Local 及 @Remote 標記中額外指定介面的類別。
在剛剛的討論中,無論是本地、遠端或是複合的存取模式,都是採用了單一介面開放用戶端存取,當 Session Bean 要開放不同的商業介面給用戶端時,我們依舊可以採取上述的兩種做法進行,以本地存取為範例,做法(一)的程式如下: @Local public interface IMySimpleLocalA{ public String testA() ; } @Local public interface IMySimpleLocalB{ public String testB() ; } @Stateless public class MySimpleBean implements IMySimpleLocalA, IMySimpleLocalB { public String testA() { return "Kitty"; } public String testB() { return "Snoopy"; } } 或是採用做法(二)的程式如下: public interface IMySimpleLocalA{ public String testA() ; } public interface IMySimpleLocalB{ public String testB() ; } @Stateless @Local(value={IMySimpleLocalA.class, IMySimpleLocalB.class}) public class MySimpleBean implements IMySimpleLocalA, IMySimpleLocalB { public String testA() { return "Kitty"; } public String testB() { return "Snoopy"; } } 由於要開放兩個存取介面,所以必須再使用 @Local 標記的 value 屬性指定多重介面;從 EJB 3.2 開始,可以省略 value 屬性就能達到多重介面的需求,Bean 類別的程式可以簡化如下: @Stateless @Local/*省略value屬性*/ public class MySimpleBean implements IMySimpleLocalA, IMySimpleLocalB { public String testA() { return "Kitty"; } public String testB() { return "Snoopy"; } } 總結EJB 元件就開發的角度來說,雖然越來越簡單,但要上手仍有些門檻,希望能藉由一系列的文章協助大家釐清觀念、突破學習上的瓶頸。最後,整理出一張簡單的表格,讓大家對於本篇文章中所介紹的主題,有更清楚的概念: 參考資料 |