對開發服務器端應用程序而言,Java企業版本即Java EE(以前叫J2EE)是一個功能強大、但又過於復雜的平台。很早以來,過於復雜歷來被認為是阻礙人們采用Java EE的一個重要因素。
但在過去的三年,Java開放源代碼社區、Java社區組織(JCP)以及主要的Java EE開發商都在致力於簡化Java EE。譬如說,實際的應用程序使用新的設計范例來簡化Java EE的開發,譬如普通Java對象(POJO)服務、服務攔截器和依賴注入。而諸多新的工具和框架也得到了廣泛采用,用於同樣的目的,譬如Hibernate、面向方面編程(AOP)、Struts、XDoclet和Spring。
這些模式和工具讓剛入門的開發人員更容易上手,同時提高了經驗豐富的Java開發人員的生產力,目前它們正在被JCP集成到下一代Java EE標准(即EJB 3.0)當中。Java開發人員Raghu Kodali最近開展的一項調查表明,把Sun的Java EE示例應用程序RosterApp從EJB 2.1移植到EJB 3.0可以減少50%以上的代碼。
Java注釋是EJB3.0的重要特性,它把POJO服務、POJO持久性和依賴注入聯系起來,成為完整的企業中間件解決方案。本文使用了一個示例應用程序:JBoss EJB 3.0 TrailBlazer,以演示開發添加注釋的輕便型EJB 3.0 POJO應用程序。TrailBlazer應用程序多次使用EJB 3.0中的不同工具和API,實現了一個投資計算器。示例應用程序在JBoss 應用服務器4.0.3裡面以非傳統方式運行,完全符合最新的EJB 3.0規范(公眾預覽版)。
EJB 3.0的注釋驅動編程模型
從開發人員的角度來看,EJB 3.0廣泛使用Java注釋。注釋有兩個重要優點:它們取代了過多的XML配置文件,而且不需要嚴格的組件模型。
注釋與XML
基於XML的部署描述符和注釋都可以用來配置Java EE應用程序中的服務相關屬性。兩者的區別在於:XML文件與代碼分開處理(往往在運行時);而注釋與代碼一起編譯,而且由編譯器進行檢查。這對開發人員產生了以下這些重要影響:
● 冗長性:XML配置文件以冗長出名。為了配置代碼,XML文件必須從代碼地方復制許多信息,譬如類名稱和方法名稱。另一方面,Java注釋卻是代碼的一部分,不需要另外引用代碼,就可以指定配置信息。
● 健壯性:XML配置文件中的復制代碼信息帶來了多個潛在故障點。譬如說,如果拼錯了XML文件中的方法名稱,應用程序會在運行時出錯。換句話說,XML配置文件不如注釋來得健壯。注釋可以由編譯器來檢查,同代碼的其余部分一起處理。
● 靈活性:因為XML文件與代碼分開處理,所以基於XML的配置信息不是“硬編碼”的,以後可以改動。部署時間的靈活性對系統管理員來說是一項很好的特性。
注釋使用簡單,足以滿足大多數應用程序的要求。XML文件比較復雜,可用來處理更高級的問題。EJB 3.0允許通過注釋來配置大多數應用程序的設置。EJB 3.0還支持XML文件用於取消默認的注釋值、配置外部資源(如數據庫連接)。
POJO與嚴格組件
除了取代及簡化XML描述符外,注釋還可以讓我們棄用曾困擾EJB 1.x和EJB 2.x的嚴格的組件模型。
EJB 組件是容器管理的對象。容器在運行時操縱bean實例的行為和內部狀態。為了讓這種行為出現,EJB 2.1規范定義了bean必須遵守的嚴格的組件模型。每個EJB類必須從為容器提供回調鉤子(callback hook)的某個抽象類繼承而來。因為Java只支持單一繼承,嚴格的組件模型就限制了開發人員使用EJB組件創建復雜對象結構的能力。讀者會在本文下篇分看到,如果映射實體bean中復雜的應用程序數據,這更是個問題。
在EJB 3.0中,所有容器服務都可以通過注釋進行配置,並提供給應用程序裡面的任何POJO。大多數情況下,不需要特殊的組件類。
開發松散耦合的服務對象
Java EE等企業中間件的最重要的好處之一就是,讓開發人員可以使用松散耦合的組件來開發應用程序。這些組件僅僅通過已發布的業務接口來進行耦合。因此,可在不改變應用程序其余部分的情況下,改變組件實現類。這樣使應用程序更健壯、更容易測試,而且更容易移植。EJB 3.0簡化了在POJO中構建松散耦合的業務組件。
會話bean
在EJB 3.0應用程序中,松散耦合的服務組件通常作為會話bean來實現。會話bean要有一個接口(即業務接口),那樣其他應用程序的組件就可以通過它使用其服務。下面的代碼為我們的示例投資計算器服務提供了業務接口。根據投資者開始投資時及終止投資時的年齡、基金增長率及每月儲蓄額,它只有一個方法來計算總的投資回報。
public interface Calculator {
public double calculate (int start, int end, double growthrate, double saving); }
會話bean類僅僅實現了業務接口。必須通過為其添加無狀態或者有狀態的注釋,告訴EJB 3.0容器這個POJO類是會話bean。有狀態的會話bean可以在幾個不同的服務請求期間保持客戶端狀態。與之相反,無狀態的會話bean的請求每次都是由隨機的會話bean實例來處理。其行為與原來EJB 2.1中的有狀態和無狀態的會話bean的行為相一致。EJB 3.0容器計算出什麼時候為bean對象創建實例,然後通過業務接口來提供。下面是會話bean實現類的代碼:
@Stateless
public class CalculatorBean implements Calculator {
public double calculate (int start, int end, double growthrate, double saving) {
double tmp = Math.pow(1. + growthrate / 12., 12. * (end - start) + 1);
return saving * 12. * (tmp - 1) / growthrate; }
}
還可以為一個會話bean指定多個接口-一個用於本地客戶端,一個用於遠程客戶端。只要使用@Local和@Remote注釋,就可以區別接口。下面的代碼片斷顯示了CalculatorBean會話bean同時實現了本地接口和遠程接口。如果你沒有@Local和@Remote注釋,會話bean接口就是默認的本地接口。
@Stateless
@Local ({Calculator.class})
@Remote ({RemoteCalculator.class})
public class CalculatorBean implements Calculator, RemoteCalculator {
public double calculate (int start, int end, double growthrate, double saving) {
double tmp = Math.pow(1. + growthrate / 12., 12. * (end - start) + 1);
return saving * 12. * (tmp - 1) / growthrate; }
public String getServerInfo () {
return "This is the JBoss EJB 3.0 TrailBlazer"; }
}
會話bean用戶通過Java命令和目錄接口(JNDI)得到bean的存根對象。由容器提供的存根對象實現了會話bean的業務接口。針對存根對象的所有調用都被轉向容器,並針對可管理的bean實例進行調用。至於無狀態的會話bean,每次進行調用時,都能獲得新的存根對象。至於有狀態的會話bean,必須把存根對象緩存在客戶端上,那樣容器就知道以後每次調用時為你提供相同的的bean實例。下面的代碼片斷顯示如何調用會話bean。這裡介紹獲得bean存根對象的一種更簡單的方法。
InitialContext ctx = new InitialContext();
cal = (Calculator) ctx.lookup(Calculator.class.getName());
double res = cal.calculate(start, end, growthrate, saving);
會話bean的生命周期管理
為了實現松散耦合,應用程序把會話bean實例的創建、緩存、銷毀全部交給EJB 3.0容器(即反向控制設計模式)。而應用程序只處理業務接口。
但如果應用程序需要對會話對象實行粒度更細的控制,該如何呢?譬如說,應用程序可能需要在容器創建會話bean時執行數據庫初始化,或者在銷毀bean時需要關閉外部連接。只要在bean類中實現生命周期回調方法,就能實現這些操作。這些方法由容器在bean生命周期的不同階段(如bean創建和銷毀)進行調用。在EJB 3.0中,可以指定任何bean方法作為回調,只要為其添加下列注釋。不像EJB 2.1裡面,所有的回調方法都必須加以實現,即便回調方法是空的;EJB 3.0 bean可以有好多回調方法,可以是任何方法名稱。
● @PostConstruct:bean實例創建後,容器立即調用添加了注釋的方法。這個注釋同時適用於有狀態和無狀態的會話bean。
● @PreDestroy:容器從對象池當中銷毀閒置或者過期的bean實例之前,調用添加了注釋的方法。這個注釋同時適用於有狀態和無狀態的會話bean。
● @PrePassivate:如果某個有狀態的會話bean實例閒置時間過長,容器就會將它掛起(passivate),並把其狀態保存在緩存當中。容器將bean實例掛起之前,調用由這個注釋作以標記的方法。這個注釋適用於有狀態的會話bean。
● @PostActivate:如果客戶端再次使用已被掛起的的有狀態的會話bean時,新的實例被創建,bean狀態被恢復。如果被激活的bean實例准備就緒,就調用由該注釋作以標記的方法。這個注釋只適用於有狀態的會話bean。
● @Init:這個注釋為有狀態的會話bean指定了初始化方法。它有別於@PostConstruct注釋之處在於:在有狀態的會話bean中,可以用@Init對多個方法作以標記。不過,每個bean實例只能有一個@Init方法被調用。EJB 3.0容器決定調用哪個@Init方法,具體取決於bean是如何創建的。@PostConstruct方法在@Init方法之後被調用。
生命周期方法的另一個有用注釋是@Remove,對有狀態的會話bean來說更是如此。應用程序通過存根對象調用使用@Remove標注的方法時,容器就知道在該方法執行完畢後,把bean實例從對象池當中移走。下面是這些生命周期方法注釋在CalculatorBean中的一個示例:
@Stateful
public class CalculatorBean implements Calculator, Serializable {
@PostConstruct
public void initialize () {
//初始化歷史記錄,並從數據庫中裝入必要數據。 }
@PreDestroy
public void exit () {
// 若有必要,把歷史記錄保存至數據庫中 }
@Remove
public void stopSession () {
// 調用該方法以通知容器,移除該bean實例、終止會話。方法體可以是空的。}
}
消息驅動的bean
會話bean服務通過同步方法調用來提供。另一種重要的松散耦合的服務就是,由入站消息觸發的異步服務,入站消息包括電子郵件或者Java消息服務(JMS)消息。EJB 3.0消息驅動的bean(MDB)是為了處理基於消息的服務請求而設計的組件。
MDB類必須實現消息監聽器(MessageListener)接口。當容器檢測到該bean的消息後,就調用onMessage()方法,並把入站消息作為調用參數傳遞。MDB會決定在OnMessage()方法中如何處理消息。可以用注釋來配置這個MDB監控哪些消息隊列。MDB部署後,容器使用注釋裡面指定的配置信息。在下面的示例中,當容器檢測到queue/mdb JMS隊列中的入站消息後,就會調用CalculatorBean MDB。MDB會解析消息,並根據消息內容執行投資計算。
@MessageDriven(activateConfig =
{
@ActivationConfigProperty(propertyName="destinationType",
ropertyValue="Javax.jms.Queue"),
@ActivationConfigProperty(propertyName="destination", propertyValue="queue/mdb")
})
public class CalculatorBean implements MessageListener {
public void onMessage (Message msg) {
try {
TextMessage tmsg = (TextMessage) msg;
Timestamp sent = new Timestamp(tmsg.getLongProperty("sent"));
StringTokenizer st = new StringTokenizer(tmsg.getText(), ",");
int start = Integer.parseInt(st.nextToken());
int end = Integer.parseInt(st.nextToken());
double growthrate = Double.parseDouble(st.nextToken());
double saving = Double.parseDouble(st.nextToken());
double result = calculate (start, end, growthrate, saving);
RecordManager.addRecord (sent, result);
} catch (Exception e) {
e.printStackTrace (); }
}
}
依賴注入
在前面一節中,介紹了如何開發松散耦合的服務組件。然而,為了使用這些服務對象,你需要通過服務器的JNDI來查詢存根對象(用於會話bean)或者消息隊列(用於MDB)。JNDI查詢是把客戶端從實際實現的服務對象解除耦合的一個關鍵步驟。不過,基於字符串名的普通JNDI查詢並不方便。以下是幾個原因:
● 客戶端與服務端必須就基於字符串的名字達成一致。這不是由編譯器或者任何部署時間檢查所執行的契約。
● 已獲取的服務對象在編譯時不進行檢查,可能會導致運行時出現數據類型轉換錯誤(casting error)。
● 應用程序裡面一再出現冗長的查詢代碼,該代碼有自己的try-catch代碼塊。
EJB 3.0采用了一種簡單、便利的方法,把解除耦合的服務對象和資源提供給任何POJO使用。你使用@EJB注釋,就可以把EJB存根對象注入到EJB 3.0容器管理的任何POJO中。如果對某字段變量標以注釋,容器會在第一次訪問之前,為該變量賦予正確的值。下面的示例顯示了如何把CalculatorBean無狀態會話bean的存根對象注入到CalculatorMDB MDB類中。
public class CalculatorMDB implements MessageListener {
@EJB Calculator cal;
// 使用cal變量
// ... ... }
如果對某個屬性的JavaBean風格的設置方法標以注釋,屬性第一次使用之前,容器會自動用正確的參數調用屬性設置方法。下面的代碼片斷演示了工作過程:
public class CalculatorMDB implements MessageListener {
Calculator cal;
@EJB
public void setCal (Calculator cal) {
this.cal = cal; }
// 使用cal變量
// ... ... }
除@EJB注釋外,EJB 3.0還支持@Resource注釋注入來自JNDI的任何資源。下面的例子演示了如何注入服務器的默認的TimerService和SessionContext對象,並且演示了如何注入來自JNDI的命名數據庫和JMS資源。
@Resource
TimerService tms;
@Resource
SessionContext ctx;
@Resource (name="DefaultDS")
DataSource myDb;
@Resource (name="ConnectionFactory")
QueueConnectionFactory factory;
@Resource (name="queue/A")
Queue queue;
此外,你還可以把容器管理的持久性管理器(即實體管理器――類似Hibernate會話對象)注入到EJB 3.0 POJO中。
為POJO提供容器服務
除了管理松散耦合的服務對象的生命周期和訪問外,EJB 3.0容器還通過簡單的注釋為可管理的POJO提供運行時服務。
事務
最有用的容器服務可能就是事務服務:萬一應用程序出現錯誤或者異常,它可以保證數據庫的完整性。你只要為POJO方法添加注釋,即可聲明事務屬性。容器可以在適當的事務上下文中運行方法。譬如說,下面的代碼聲明:容器應當創建新的事務來運行updateExchangeRate()方法。如果該方法存在,事務就提交。實際上,從updateExchangeRate()裡面被調用的所有方法也都在同樣的事務上下文中執行,除非以其他方式進行顯示聲明。updateExchangeRate()方法中執行的數據庫操作要麼全部成功,要麼全部失敗。
@Stateless
public class CalculatorBean implements Calculator {
// ... ...
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void updateExchangeRate (double newrate) throws Exception {
// 在循環中更新數據庫。
// ... ...
//循環中的操作必須全部成功,否則數據庫根本不更新。 }
}
安全
容器還能提供驗證用戶身份的安全服務,並且可以根據用戶角色,限制對可管理的POJO的訪問。對每個POJO類而言,你可以使用@SecurityDomain注釋指定安全域,它能告訴容器到哪裡去找密碼和用戶角色列表。JBoss裡面的other域表明文件是類路徑中的users.propertes和roles.propertIEs文件。然後,對於每個方法,你可以使用安全限制注釋來指定誰可以運行這個方法。譬如在下面例子中,容器對所有試圖執行addFund()方法的用戶進行驗證,只允許角色是AdminUser的用戶才能實際運行。如果你沒有登錄,或者不是以管理員的身份登錄,就會引發安全異常。
@Stateless
@SecurityDomain("other")
public class CalculatorBean implements Calculator {
@RolesAllowed({"AdminUser"})
public void addFund (String name, double growthrate) {
// ... ... }
@RolesAllowed({"AdminUser"})
public void addInvestor (String name, int start, int end) {
// ... ... }
@PermitAll
public Collection getFunds () {
// ... ... }
// ... ...
@RolesAllowed({"RegularUser"})
public double calculate (int fundId, int investorId, double saving) {
// ... ... }
}
通用攔截器
事務服務和安全服務都可以被看成是由容器管理的運行時攔截器。容器攔截來自EJB存根對象的方法調用後,為調用添加事務上下文或者安全限制。
在EJB 3.0中,你可以自己編寫攔截器來擴展容器服務。使用@AroundInvoke注釋,就可以把任何bean方法指定為在其他任何bean方法運行前後執行的攔截器方法。在下面例子中,log()方法是分析及記錄其他bean方法的執行時間的攔截器:
@Stateful
public class CalculatorBean implements Calculator {
//被“log()”攔截的bean方法
// ... ...
@AroundInvoke
public Object log (InvocationContext ctx) throws Exception {
String className = ctx.getBean().getClass().getName();
String methodName = ctx.getMethod().getName();
String target = className + "." + methodName + "()";
long start = System.currentTimeMillis();
System.out.println ("Invoking " + target);
try {
return ctx.proceed();
} catch(Exception e) {
throw e;
} finally {
System.out.println("Exiting " + target);
cal.setTrace(cal.getTrace() + "" +"Exiting " + target);
long time = System.currentTimeMillis() - start;
System.out.println("This method takes " + time + "ms to execute");
}
}
}