Java企業級版本,或者說Java EE(以前叫J2EE),對於開發服務器端的應用來說是一個強大的但卻又過於復雜的的平台。從它誕生之日起,過於復雜一直是對使用Java EE猶豫不決的一個重要因素。在JavaWorld的以前的一篇文章”簡化之路”中,我指出了那些讓Java EE應用變復雜的因素,其中很多都是與當前的EJB 2.1規范有關。
在過去的三年中,Java開放源代碼社區,Java社區進程(JCP)以及主要的Java EE供應商,一直致力於讓Java EE更簡單。舉例來說:新的設計范例,比如POJO服務,服務攔截器和依賴注入,已經可以在實際應用中用來簡化Java EE的開發了。還有,新的工具和框架,比如Hibernate, AOP(ASPect-orIEnted programming,面向方面編程),Struts,Xdoclet和Spring, 也已經被廣泛用於同一目的。
簡化不是功能的減少
簡化一個編程模型並沒有減少它的功能。簡化只是把復雜的邏輯隱藏到了框架代碼或可重用的組件中去了。根本上,它是把復雜的東西從需要應用開發者直接管理的地方轉移到了大多數開發者看不到的地方。
上述的模板和工具讓初學者更容易上手,同時也提高了有經驗的Java開發者的生產力,現在它們正在被JCP合並到下一代的Java EE標准中(比如:EJB 3.0)。由Java開發人員Raghu Kodali最近所做的研究顯示:將Java EE的示例程序RosterApp從EJB 2.1轉到EJB 3.0可以減少百分之五十以上的代碼。
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配置文件並且消除了嚴格組件模型需求。
注釋 vs XML
基於XML的布署描述和注釋一起都可以用來在Java EE應用中配置服務的相關屬性。它們的區別在於:XML文檔是與代碼分開處理的,特別是在運行時刻,而注釋是與代碼編譯在一起的並被編譯器檢查的。對於開發者來說這就有了一些重要的含義,正如我下面所列出的:
冗長:XML配置文件是出了名的冗長的。為了配置代碼,XML文件必須復制許多信息:比如代碼中類名字和方法名字。Java注釋則不同,它是代碼的一部分,不需要額外的引用就可以指明配置信息。
強壯性:在XML文件中重復的代碼信息引入了多處出錯的可能。比如,如果你寫錯了方法的名字,那應用直到運行時刻才會出錯垮掉。也就是說,XML配置文件的強壯性就不如注釋,注釋是被編譯器檢查的,並和其它代碼一起被處理的。
靈活性:既然XML文件是在代碼之外被單獨處理的,那也就是說基於XML的配置信息不是“硬編碼”的,是可以以後修改的。部署的靈活性對系統管理員來說是非常非常重要的特性。
注釋是簡單易用的,已證明對大多數應用來說足夠了。XML文件更復雜,但能被用來處理更高級的問題。EJB 3.0允許你通過注釋來配置大多數的應用。EJB 3.0也支持用XML文件來覆蓋默認的注釋,及配置像數據庫聯接這樣的外部資源。
除了替換和簡化XML描述符,注釋也允許我們廢除困擾EJB 1.x, EJB 2.x的嚴格組件模型。
POJO vs 嚴格組件
EJB 組件是容器管理(container-managed)的對象。容器在運行時刻操作Bean的狀態和行為。為了讓行為發生,EJB 2.1規范定義了一個Bean必須遵守的嚴格的組件模型。每一個EJB類必須從某一種抽象類中繼承,並為容器提供了回調的鉤子。既然Java只支持單繼承,嚴格組件模型就限制了開發者使用EJB組件創建一個復雜對象結構的能力。當把復雜的應用數據映射到實體 Bean中的時候,正如我們在第二部分中看到的,這會成為一個很大的問題。
在EJB 3.0中,所有的容器服務都可以通過使用注釋的POJO應用來配置和交付。大多數情況下,並不需要特殊的組件類。讓我們通過JBoss EJB 3.0 TrailBlazer示例看一下如何在EJB 3.0中使用注釋。
開發藕合松散的服務對象
像Java EE這樣的企業級中間件的一個最重要的好處是允許開發者使用藕合松散的組件來開發應用。這些組件僅僅通過他們自己發布的商業接口來藕合。因此這些組件的實現類可以在不改變應用其余部分的情況下改變自己的實現。這將會使應用更加強壯,更容易測試,更易移植。EJB 3.0使得在POJO中創建藕合松散的商業組件變得更簡單了。
Session bean
在EJB 3.0應用中,藕合松散的服務組件的典型應用是Session Bean。一個Session Bean至少有一個接口(也就是:商業接口),其它應用組件通過它獲得服務。下面的代碼為我們的投資計算器服務提供了商業接口。它只有一個方法,根據給定的起始年齡,終止年齡,增長率,月存金額,計算出總投資額。
public interface Calculator {
public double calculate (int start, int end,
double growthrate, double saving);
}
Session bean類簡單地實現了商業接口。你必須通過使用Stateless或Stateful注釋來告訴EJB 3.0容器這個POJO類是一個Session Bean。有狀態(Stateful)的session bean在不同的服務請求間維護著客戶的狀態。相反地,對於無狀態(Stateless)的session bean,每次的請求都是被隨機挑選的session bean實例處理的。這些行為是與EJB 2.1規范中的有狀態和無狀態session bean的定義是一致的。EJB 3.0容器算出何時實例化Bean對象,並通過商業接口讓其可用。下面是session 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;
}
}
你也可以為一個session bean指明多個接口-一個為本地客戶服務,一個為遠程客戶服務。只要使用@Local和@Remote注釋來區分。下面的代碼片斷顯示了同時實現了本地和遠程接口的CalculatorBean。如果你沒有@Local和@Remote注釋,session bean接口默認為本地接口。
@Stateless
@Local ()
@Remote ()
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";
}
}
Session bean用戶通過JNDI得到bean的一個存根(Stub)對象。容器所提供的存根對象實現了session bean的商業接口。所有針對存根的調用都被引向了容器,由容器調用相應的實現類中的接口。對於有狀態的的session bean,你必須自己在客戶端緩存存根對象,這樣在每次的後續調用時,容器才知道要提供相同的的bean實例。下面的片斷顯示如何調用session bean.在後面,你將會學到獲取存根對象的更簡單的方法。
InitialContext ctx = new InitialContext();
cal = (Calculator) ctx.lookup(Calculator.class.getName());
double res = cal.calculate(start, end, growthrate, saving);
Session bean生命周期的管理
為達到藕合松散的目的,應用把session bean實例的創建、緩存、銷毀全部交給EJB 3.0容器(也就是,反向控制設計模式)。應用只和bean的商業接口打交道。
但如果應用需要對session對象更好的控制呢?比如說,應用可能需要在創建session bean的時候初始化數據庫聯接,而在銷毀bean時關閉外部的聯接。上述這些,你都可能通過在bean類中定義生命周期的回調方法來實現。這些方法將會被容器在生命周期的不同階段調用(如:創建或銷毀時)。通過使有下面所列的注釋,EJB 3.0允許你將任何方法指定為回調方法。這不同於EJB 2.1,EJB 2.1中,所有的回調方法必須實現,即使這是空的。EJB 3.0中,bean可以有任意數量,任意名字的回調方法。
@PostConstruct:當bean對象完成實例化後,使用了這個注釋的方法會被立即調用。這個注釋同時適用於有狀態和無狀態的session bean。
@PreDestroy:使用這個注釋的方法會在容器從它的對象池中銷毀一個無用的或者過期的bean實例這前調用。同時適用於有狀態和無狀態的session bean.
@PrePassivate:當一個有狀態的session bean實例空閒過長的時間,容器將會鈍化它,並把它的狀態保存下來。使用這個注釋的方法會在容器鈍化bean實例之前調用。適用於有狀態session bean。
@PostActivate:當客戶端再次使用已經被鈍化的的有狀態session bean時,新的實例被創建,狀態被恢復。使用此注釋的session bean會在bean的激活完成時調用。
@Init:這個注釋指定了有狀態session bean初始化的方法。它區別於@PostConstruct注釋在於:多個@Init注釋方法可以同時存在於有狀態session bean 中,但每個bean實例只會有一個@Init注釋的方法會被調用。這取決於bean是如何創建的(細節請看EJB 3.0規范)。@PostConstruct在@Init之後被調用。
另一個有用的生命周期方法注釋是@Remove,特別是對於有狀態session bean。當應用通過存根對象調用使用了@Remove注釋的方法時,容器就知道在該方法執行完畢後,要把bean實例從對象池中移走。
@Stateful
public class CalculatorBean implements Calculator, Serializable {
// ... ...
@PostConstruct
public void initialize () {
// Initializes the history records and load
// necessary data from database etc.
// 初始化歷史記錄,並從數據庫中裝入必需的數據。
}
@PreDestroy
public void exit () {
// Save history records into database if necessary.
// 如有必要則將歷史記錄保存至數據庫中
}
@Remove
public void stopSession () {
// Call to this method signals the container
// to remove this bean instance and terminates
// the session. The method body can be empty.
// 調用這個方法來通知容器將bean實例移除並中止session.
// 這個方法可以為空。
}
// ... ...
}
消息驅動bean
Session bean服務提供了同步調用的方法。另一個重要的藕合松散服務類型是一種通過進入的消息來觸發的異步服務(比如:email或Java消息服務產生的消息)。EJB 3.0的消息驅動bean(MDB)是設計用來專門處理基於消息請求的組件。
一個MDB類必須實現MessageListener接口。當容器檢測到bean守候的隊列一條消息時,就調用onMessage()方法,將消息作為參數傳入。MDB在OnMessage()中決定如何處理該消息。你可以用注釋來配置MDB偵聽哪一條隊列。當MDB部署時,容器將會用到其中的注釋信息。在下面的例子中,CalculatorBean MDB會在JMS隊列queue/mdb有消息進入時調用。MDB解析消息,並根據消息內容計算投資。
@MessageDriven(activateConfig =
{
@ActivationConfigProperty(propertyName="destinationType",
propertyValue="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來查找存根對象(session bean)或消息隊列(MDB)。JNDI查找是把客戶端與實際的服務端實現解藕的關鍵步驟。但是,直接使用一個字符串來進行JNDI查找並不優雅。有這樣幾個原因:
客戶端與服務端必須有一致的基於字符串的名字。它沒有在編譯時得到認證或在布署時得到檢查。
從JNDI返回的服務對象的類型沒有在編譯時進行檢查,有可能在運行時出現轉換(casting)錯誤。
冗長的查找代碼,有著自己的try-catch代碼塊,在應用之間是重復的和雜亂的
EJB 3.0,對任何POJO,提供了一個簡單的和優雅的方法來解藕服務對象和資源。使用@EJB注釋,你可以將EJB存根對象注入到任何EJB 3.0容器管理的POJO中。如果注釋用在一個屬性變量上,容器將會在它被第一次訪問之前賦值給它正確的值。下面的例了演示了怎樣把CalculatorBean無狀態session bean的存根注入到CalculatorMDB MDB類中。
public class CalculatorMDB implements MessageListener {
@EJB Calculator cal;
// Use the cal variable
// ... ...
}
注釋如果被用在JavaBean風格的setter方法上時,容器會在屬性第一次使用之前,自動地用正確的參數調用bean的setter方法。下面的片斷演示了這是如何做的:
public class CalculatorMDB implements MessageListener {
Calculator cal;
@EJB
public void setCal (Calculator cal) {
this.cal = cal;
}
// Use the cal variable
// 使用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;
此外,你也可以把一個容器管理的持久化管理器(也就是,EntityManager-類似於Hibernate session對象)注入到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 {
// Update the database in a loop.
// 在循環中更新數據庫
// ... ...
// The Operations in the loop must all be successful or
// the database is not updated at all.
// 循環中的操作必須全部成功或者根本不更新。
}
}
安全
容器也提供了安全服務來進行用戶認證和根據用戶規則來限制對POJO的訪問。對每一個POJO來說,你可以通過使用@SecurityDomain注釋為它指定一個安全域, 安全域告訴容器到哪裡去找密碼和用戶角色列表。JBoss中的other域表明文件是classpath中的users.propertes和roles.propertIEs。這樣,對每一個方法來說,我們可以使用一個安全限制注釋來指定誰可以運行這個方法。比如,下面的例子,容器對所有試圖調用addFund()的用戶進行認證,只允許擁有AdminUser角色的用戶實際運行它。如果你沒有登錄或者沒有以管理員的身份登錄,一個安全意外將會拋出。
@Stateless
@SecurityDomain("other")
public class CalculatorBean implements Calculator {
@RolesAllowed()
public void addFund (String name, double growthrate) {
// ... ...
}
@RolesAllowed()
public void addInvestor (String name, int start, int end) {
// ... ...
}
@PermitAll
public Collection getFunds () {
// ... ...
}
// ... ...
@RolesAllowed()
public double calculate (int fundId, int investorId,
double saving) {
// ... ...
}
}
通用攔截器
事務和安全服務都可以被看作是容器管理的運行時刻攔截器。容器攔截了對EJB存根的調用,並在其上應用事務上下文或進行安全限制。
在EJB 3.0中,你可以自己寫攔截器來擴展容器服務。使用@AroundInvoke注釋,你可以將任意bean方法作為攔截器方法在任意bean方法之前和之後運行。下面的例子中,log()方法是一個攔截器,它計算和記錄了其它bean方法的執行