本文的大體思路是展示了一次業(yè)務(wù)交易如何動(dòng)態(tài)地為子系統(tǒng)處理過程觸發(fā)業(yè)務(wù)事件。本文所示的例子使用Spring框架和Spring AOP有效地解耦業(yè)務(wù)服務(wù)和子系統(tǒng)處理功能。現(xiàn)在讓我們仔細(xì)看看業(yè)務(wù)需求。
業(yè)務(wù)需求
客戶注冊(cè)系統(tǒng)(CRS——customer registration system)在其完成客戶在線注冊(cè)后需要發(fā)送通知給它的客戶,并傳送他們的地址數(shù)據(jù)到發(fā)票系統(tǒng)(invoice system),以便為付費(fèi)產(chǎn)生發(fā)票。
技術(shù)設(shè)計(jì)
讓我們把上面提到的業(yè)務(wù)需求分解成技術(shù)設(shè)計(jì)。本例中,我們將定義一個(gè)自定義業(yè)務(wù)事件以標(biāo)示客戶注冊(cè)過程。
一個(gè)事件可以被認(rèn)為是在特定時(shí)間間隔發(fā)生的一些事情。本例中,它是客戶注冊(cè)過程。典型地,當(dāng)事件發(fā)生時(shí),一個(gè)單一事件可能包含了一個(gè)或多個(gè)需要發(fā)生的動(dòng)作。按照業(yè)務(wù)需求,我們已經(jīng)確定了如下兩個(gè)動(dòng)作:
- 發(fā)送郵件通知客戶。
- 傳送客戶地址數(shù)據(jù)到發(fā)票系統(tǒng)。
我們現(xiàn)在將設(shè)計(jì)事件數(shù)據(jù)結(jié)構(gòu)以持有存儲(chǔ)在事件數(shù)據(jù)庫表中的信息。以下事件屬性被確定下來。
- 事件標(biāo)識(shí)符:1
- 事件描述:客戶注冊(cè)事件
- 動(dòng)作代碼:MT
該事件標(biāo)識(shí)符是映射到數(shù)據(jù)庫中的主鍵。事件描述定義了關(guān)于事件的描述信息。最后一個(gè)是動(dòng)作代碼,當(dāng)事件發(fā)生時(shí),它代表了要發(fā)生的不同動(dòng)作。該動(dòng)作代碼定義在動(dòng)作代碼對(duì)照表中。
上面提到的事件,其動(dòng)作代碼標(biāo)識(shí)為M和T。M代表發(fā)送一個(gè)郵件通知到客戶,T代表傳送客戶地址數(shù)據(jù)到發(fā)票系統(tǒng)。
Example: Event.java
/**
*Event.java - The event domain object
*@author - Vigil Bose
*/
public class Event implements Serializable {
private Integer eventId;
private String eventDesc;
private String eventActionCodes;
private static final long serialVersionUID = 1L;
/** The cached hash code value for this instance. Settting to 0 triggers
re-calculation. */
private int hashValue = 0;
/**
*@return the eventActionCodes
*/
public String getEventActionCodes(){
return eventActionCodes;
}
/**
* @param eventActionCodes the eventActionCodes to set
*/
public void setEventActionCodes(String eventActionCodes) {
this.eventActionCodes = eventActionCodes;
}
/**
* @return the eventDesc
*/
public String getEventDesc() {
return eventDesc;
}
/**
* @param eventDesc the eventDesc to set
*/
public void setEventDesc(String eventDesc) {
this.eventDesc = eventDesc;
}
/**
* Return the simple primary key value that identifies this object.
* @return the eventId
*/
public Integer getEventId() {
return eventId;
}
/**
* Set the simple primary key value that identifies this object.
* @param eventId the eventId to set
*/
public void setEventId(Integer eventId) {
this.hashValue = 0;
this.eventId = eventId;
}
/**
*Implementation of the equals comparison on the basis of equality
*of the primary key values.
* @param rhs
* @return boolean
*/
public boolean equals(Object rhs){
if (rhs == null)
return false;
if (! (rhs instanceof Event))
return false;
Event that = (Event) rhs;
if (this.getEventId() == null || that.getEventId() == null)
return false;
return (this.getEventId().equals(that.getEventId()));
}
/**
* Implementation of the hashCode method conforming to the Bloch pattern with
* the exception of array properties (these are very unlikely primary key types).
* @return int
*/
public int hashCode(){
if (this.hashValue == 0){
int result = 17;
int eventIdValue = this.getEventId() == null ? 0 :
this.getEventId().hashCode();
result = result * 37 + eventIdValue;
this.hashValue = result;
}
return this.hashValue;
}
}
現(xiàn)在我們已經(jīng)設(shè)計(jì)了事件領(lǐng)域?qū)ο笠源砜蛻糇?cè)事件。現(xiàn)在我們轉(zhuǎn)而設(shè)計(jì)Web層與業(yè)務(wù)服務(wù)層之間的API契約。設(shè)計(jì)的約束之一是改變領(lǐng)域模型不能破壞Web層與業(yè)務(wù)服務(wù)層之間的API契約。為了滿足該設(shè)計(jì)約束,確定了兩個(gè)數(shù)據(jù)包裝類:AbstractData和UserData。AbstractData本質(zhì)上定義了一些行為的抽象。UserData是AbstractData的子類,并提供了其他行為。比如,如果你有一個(gè)應(yīng)用框架,抽象類可以提供諸如事件和消息處理的默認(rèn)服務(wù)。
在本例中,AbstractData負(fù)責(zé)收集各種業(yè)務(wù)事件。AbstractData的子類是UserData,用來持有我們的主要領(lǐng)域?qū)ο螅ㄓ脩魧?duì)象)。
用戶領(lǐng)域?qū)ο笥刹煌瑢傩越M成,這些屬性是用來識(shí)別用戶的,如userId、firstName、lastName和加過密的password。它還包含了地址領(lǐng)域?qū)ο螅搶?duì)象有多個(gè)地址屬性,如address line 1、address line 2、city、state等等。
Example: AbstractData.java
/**
*AbstractData.java - A template pattern like class that implments
*the event collection behavior. This class is used by all data
*transfer wrapper objects between UI Layer and Server side Layer
*@author - Vigil Bose
*/
public abstract class AbstractData{
/**
*Stores all the events identified during the transaction
*Processing.
*/
private Set eventSet = Collections.synchronizedSet(new HashSet());
/**
* @return Returns the eventSet.
*/
public Set getEventSet() {
return eventSet;
}
/**
*@param event - An instance of a business event resulted from a particular
*business transaction
*/
public void addToEventSet(Event event) {
this.eventSet.add(event);
}
}
在AbstractData中聲明一個(gè)集合(Set)的原因,是為了避免在給定時(shí)間點(diǎn)集合中有同一重復(fù)事件。讓我們看看UserData長什么樣吧。UserData包含了實(shí)際User領(lǐng)域?qū)ο蟆R虼酸槍?duì)User領(lǐng)域?qū)ο蟮娜魏胃淖兌急幌拗圃谶@個(gè)包裝類中,不會(huì)破壞客戶端和業(yè)務(wù)服務(wù)層之間的接口契約。
Example: UserData.java
/**
*UserData.java - A concrete POJO data wrapper whose responsibility is to
*holds the main domain object reference and used between client and business
*service layers.
*@author - Vigil Bose
*/
public class UserData extends AbstractData{
private User user;
/**
* @return The user domain object instance
*/
public Users getUsers(){
return this.users;
}
/**
*@param The user instance to set.
*/
public void setUser(User user){
this.user = user;
}
}
讓我們看看業(yè)務(wù)服務(wù)接口。本例中,我們將在IRegistrationService接口中定義一個(gè)叫做doRegister()的API契約,以完成用戶注冊(cè)業(yè)務(wù)過程。該API本質(zhì)上是事務(wù)型的,因?yàn)樗蚨鄠€(gè)數(shù)據(jù)庫表插入記錄。客戶層和業(yè)務(wù)層通過該接口進(jìn)行交互。
Example: IRegistrationService.java
/**
*IRegistrationService.java - A classic example of EJB's business
*methods interface pattern that exposes all the Registration
*related business methods that can be implemented by both the
*enterprise session bean as well as the Pojo service. Moreover
*this pattern allows us to switch to POJO implementation later
*if it makes sense to do so.
*@author - Vigil Bose
*/
public interface IRegistrationService{
/**
*API to complete the registration business process
*@param userData - The data wrapper object
*/
public void doRegister(AbstractData userData);
}
為了簡單起見,本例中我們只用了POJO(Plain Old Java Object)服務(wù)。業(yè)務(wù)服務(wù)實(shí)現(xiàn)則實(shí)現(xiàn)了完成客戶注冊(cè)過程的業(yè)務(wù)邏輯。本例中,服務(wù)實(shí)現(xiàn)的唯一職責(zé)是將調(diào)用委派給數(shù)據(jù)訪問層(Data Access Layer),以在適當(dāng)?shù)臄?shù)據(jù)庫表中記錄客戶注冊(cè)交易的狀態(tài)。
Example: RegistrationServiceImpl.java
/**
*The primary business method implementation of Customer Registration Service.
*This is a POJO. It does not depend on any Spring APIs. It's usable outside a
*Spring container, and can be instantiated using new in a JUnit test. However,
*we can still apply declarative transaction management to it using Spring AOP.
*@author - Vigil Bose
*/
public class RegistrationServiceImpl implements IRegistrationService{
private IRegistrationServiceDao registrationServiceDao;
/**
* A setter method of dependency injection
* @param registrationServiceDao - The registrationServiceDao to set.
*/
public setRegistrationServiceDao(IRegistrationServiceDao
registrationServiceDao){
this.registrationServiceDao = registrationServiceDao;
}
/**
* API to register the user
* @param user - The user domain object
*/
public void doRegister(AbstractData userData){
this.registrationServiceDao.completeRegistration(userData.getUser());
}
}
務(wù)實(shí)使用DAO模式
Data Access Object(DAO——數(shù)據(jù)訪問對(duì)象)在核心J2EE設(shè)計(jì)模式一書中被編目為一個(gè)集成層設(shè)計(jì)模式。它把持久庫存取和操作代碼封裝到了一個(gè)單獨(dú)的層次。本文中所指的持久庫就是RDBMS。
該模式在業(yè)務(wù)邏輯層和持久存儲(chǔ)層之間引入了一個(gè)抽象層。業(yè)務(wù)對(duì)象通過數(shù)據(jù)訪問對(duì)象訪問RDBMS(數(shù)據(jù)源)。該抽象層簡化了應(yīng)用代碼并引入了靈活性。理想地,對(duì)數(shù)據(jù)源所做的變動(dòng)(比如變換數(shù)據(jù)庫廠商或類型),僅僅需要改變數(shù)據(jù)訪問對(duì)象,因而對(duì)業(yè)務(wù)對(duì)象影響最小。本例中,我們使用Hibernate實(shí)現(xiàn)數(shù)據(jù)訪問策略。
DAO設(shè)計(jì)模式所提供的的靈活性主要被歸因于對(duì)象設(shè)計(jì)的最佳實(shí)踐:用接口編程。該原則規(guī)定了具體對(duì)象必須實(shí)現(xiàn)一個(gè)接口,在調(diào)用程序中使用該接口而非具體對(duì)象本身。因此,你可以容易地替換一個(gè)不同的實(shí)現(xiàn),而對(duì)客戶端代碼沖擊很小。
遵循上面所說的原則,我們將定義注冊(cè)服務(wù)DAO接口——IRegistrationServiceDao.java,它有一個(gè)completeRegistraion()行為。業(yè)務(wù)組件將通過這個(gè)接口與DAO交互。
Example: IRegistrationServiceDao.java
/**
*A POJO data access object interface for the CRS services business layer.
*The API's defined in this interface are all transactional APIs within the
*business services layer
*@author - Vigil Bose
*/
public interface IRegistrationServiceDao{
/**
* Data Access API to create the new user in the system
* @param user - The composite user domain object
*/
public void completeRegistration(User user) throws DataAccessException;
}
在定義了數(shù)據(jù)訪問接口之后,我們必須提供一個(gè)IRegistrationServiceDao的具體實(shí)現(xiàn)——RegistrationServiceDaoImpl。
本例中該實(shí)現(xiàn)中使用了Hibernate。這里所使用的模式是策略模式,可以用任何對(duì)象關(guān)系映射(Object Relation Mapping)產(chǎn)品或JDBC來替換。該類的職責(zé)是將客戶注冊(cè)交易的狀態(tài)記錄在數(shù)據(jù)庫表中。
Example: RegistrationServiceDaoImpl.java
/**
*The Registration Services Data Access Strategy implementation
*using Hibernate persistence mechanism that support various
*registration related business transactions.
*@author - Vigil Bose
*/
public class RegistrationServiceDaoImpl extends HibernateDaoSupport
implements IRegistrationServiceDao{
/**
* Data Access API to create the new user in the system
* @param users - The composite users domain object
*/
public void completeRegistration(Users users) throws DataAccessException {
getHibernateTemplate().save(users);
}
}
任何應(yīng)用程序中,訪問只讀數(shù)據(jù)都是重要的。讓我們看一個(gè)普通java接口——ILookUpServiceDao的例子,它在CRS中被用到,其暴露了finder和getter方法以訪問只讀數(shù)據(jù)。
Example: ILookUpServiceDao.java
/**
*A POJO data access object interface that exposes the lookup API's in Customer
*Registration System.
*The API's defined in this interface can be used with or without any other
*transactional APIs within the business services layer
*@author - Vigil Bose
*/
public interface ILookUpServiceDao{
/**
* Data Access API to find the event instance based on its primary key
* @param eventId - The event tables primary key identifier
*/
public Event findEventById(Integer eventId) throws DataAccessException;
}
下例是Hibernate實(shí)現(xiàn)策略。ILookUpServiceDao接口中的API定義在具體類LookUpServiceDaoImpl中被實(shí)現(xiàn)。為了簡單,這里只顯示了一個(gè)API實(shí)現(xiàn)。
Example: LookUpServiceDaoImpl.java
/**
*A POJO data access implementation that implements the lookup API's in Customer
*Registration System.
*The API's defined in this interface can be used with any other
*transactional APIs within the business services layer
*@author - Vigil Bose
*/
public classe LookUpServiceDaoImpl extends HibernateDaoSupport
implements ILookUpServiceDao {
/**
* Data Access API to find the event instance based on its primary key
* @param eventId - The event tables primary key identifier
* @return an instance of Event domain object
* @throws DataAccessException
*/
public Event findEventById(Integer eventId) throws DataAccessException{
return (Event)getHibernateTemplate().get(Event.class, eventId);
}
}
Spring框架提供的HibernateDaoSupport類是一個(gè)模板模式實(shí)現(xiàn),其抽象了Hibernate相關(guān)API和與Hibernate Session相關(guān)的資源管理。
有了數(shù)據(jù)訪問實(shí)現(xiàn),我們對(duì)業(yè)務(wù)需求的一個(gè)方面——客戶注冊(cè)過程——感到滿意了。現(xiàn)在我們將考慮需求的第二個(gè)方面,它是在設(shè)計(jì)期間所確定的子系統(tǒng)功能。該需求是這樣的:當(dāng)注冊(cè)完成時(shí),系統(tǒng)將發(fā)送郵件通知給客戶并傳送客戶地址數(shù)據(jù)給發(fā)票系統(tǒng)以產(chǎn)生發(fā)票。我們將通過著名的命令模式來實(shí)現(xiàn)子系統(tǒng)處理過程。
命令模式
命令模式用來提供一個(gè)公共接口以執(zhí)行不同的命令。任何實(shí)現(xiàn)了該命令接口的類可以在其execute()方法中提供特定任務(wù)的實(shí)現(xiàn)。
在設(shè)計(jì)期間,我們已經(jīng)確定了針對(duì)同一接口的兩個(gè)不同命令實(shí)現(xiàn)。就業(yè)務(wù)服務(wù)層來說,子系統(tǒng)功能是一個(gè)單獨(dú)的關(guān)注點(diǎn)。因此我使用AOP技術(shù)將這個(gè)單獨(dú)的關(guān)注點(diǎn)從主要業(yè)務(wù)服務(wù)層實(shí)現(xiàn)(RegistrationServiceImpl)中解耦。
想更多了解AOP概念的人,請(qǐng)點(diǎn)擊這里。你還可以從下面看到一些關(guān)于AOP的介紹。現(xiàn)在,讓我們?cè)O(shè)計(jì)命令接口。
Example: ICommand.java
/**
*ICommand.java - The famous command interface that exposes the execute API.
*@author - Vigil Bose
*/
public interface ICommand{
/**
*The Command design pattern encapsulates the concept of the
*command into an object. The issuer holds a reference to the
*command object rather than to the recipient.The issuer sends
*the command to the command object by executing a specific
*method on it. The command object is then responsible for
*dispatching the command to a specific recipient to get the
*job done.
*@param data - The data transfer object
*/
public void execute(Object data);
}
典型的命令實(shí)現(xiàn)提供了打包一組計(jì)算的方法(一個(gè)接收者和一組動(dòng)作)并把它作為第一級(jí)別對(duì)象向周圍傳遞。該命令對(duì)象調(diào)用請(qǐng)求命令接收者(receiver)的方法以真正處理請(qǐng)求。通常也能發(fā)現(xiàn)一個(gè)命令實(shí)現(xiàn)自己處理特定任務(wù),無須委派請(qǐng)求給receiver。在本例中,MailingCommandImpl通過調(diào)用EmailService實(shí)現(xiàn)了發(fā)送郵件通知的任務(wù)。為簡單起見,EmailService實(shí)現(xiàn)沒有在本例中展現(xiàn)。畢竟,我們的意圖是展現(xiàn)業(yè)務(wù)事件是如何借助于AOP和Spring2.0被路由到子系統(tǒng)處理器的。
Example: MailingCommandImpl.java
/**
*MailingCommandImpl.java - A command implementation that implements
*the task of sending mail notification to the customer who completed
*the registration process.
*@author - Vigil Bose
*/
public class MailingCommandImpl implements ICommand{
private IEmailService emailService;
/**
*A setter method of dependency injection
*@param emailService - The emailService instance to set.
*/
public void setEmailService(IEmailService emailService){
this.emailService = emailService;
}
/**
*API execute is used to execute the mailing tasks implemented
*@param args - An instance of AbstractData used in business service layer
*/
public void execute(Object args){
//get the reference of user object
User user = (User)args;
//get the reference of address object via its parent object User.
Address address = user.getAddress()
//Invoke the EmailService API here to send out the notifications....
}
}
現(xiàn)在,我將設(shè)計(jì)第二個(gè)命令實(shí)現(xiàn),它將幫助我們實(shí)現(xiàn)關(guān)于傳送客戶地址數(shù)據(jù)到發(fā)票應(yīng)用的業(yè)務(wù)需求。在這個(gè)特定實(shí)現(xiàn)中,我們可以選擇任何協(xié)議(如Web服務(wù)、消息傳遞,或 XML over Http等等)來發(fā)送客戶信息到發(fā)票應(yīng)用(假設(shè)發(fā)票應(yīng)用可以使用上面提到的任何協(xié)議用作應(yīng)用集成)。為了簡單,下面給出的是使用JMS消息傳遞的例子。本例并沒有展示JMS消息傳遞的內(nèi)部結(jié)構(gòu)。
Example: SendCustomerInfoCommandImpl.java
/**
*SendCustomerInfoCommandImpl.java - A command implementation that implements
*the task of transmiting the customer's address data to the invoice system.
*@author - Vigil Bose
*/
public class SendCustomerInfoCommandImpl implements ICommand{
private IMessagingService messagingService;
/**
* A setter method of dependency injection
*/
public void setMessagingService(IMessagingService messagingService){
this.messagingService = messagingService;
}
/**
*API execute is used to execute the messaging task implemented.
*@param args - An instance of AbstractData used in the business service layer
*/
public void execute(Object args){
User user = (User)args;
//Invoke the appropriate messagingService API
//to send the customer information here....
}
}
AOP的基本概念
AOP也被稱為Aspect Oriented Programming(面向方面編程)試圖幫助程序員分離關(guān)注點(diǎn),尤其是橫向切面關(guān)注點(diǎn)(cross-cutting concerns)。過程、包、類及方法都是幫助程序員把關(guān)注點(diǎn)封裝到單一實(shí)體內(nèi)。但是有些關(guān)注點(diǎn)不適合這種形式的封裝。我們稱之為橫向切面關(guān)注點(diǎn),是因?yàn)樗鼈儥M跨了程序中許多模塊。它可能使代碼分散或纏結(jié)(scattered or tangled),使人們更難理解和維護(hù)。當(dāng)一個(gè)關(guān)注點(diǎn)(例如本利的事件路由)蔓延到許多模塊(類和方法)時(shí),代碼被分散了。這意味著修改事件分發(fā)功能可能需要修改所有受影響的模塊。
代碼失去了典雅和簡單,因?yàn)楦鞣N新的關(guān)注點(diǎn)已經(jīng)與基本功能(有時(shí)稱為業(yè)務(wù)邏輯關(guān)注點(diǎn))纏結(jié)在一起。事務(wù)、消息傳遞、安全以及日志都是橫向切面關(guān)注點(diǎn)的例子。
AOP試圖通過讓程序員在一個(gè)單獨(dú)的稱之為aspect的模塊中表達(dá)橫向切面關(guān)注點(diǎn)來解決這些問題。Aspect可以包含advice(加入到程序指定點(diǎn)的代碼)和inter-type聲明(增加到其他類的結(jié)構(gòu)成員)。例如,一個(gè)安全模塊可以包含在訪問一個(gè)銀行賬戶前執(zhí)行安全檢查的advice。pointcut定義了一個(gè)銀行賬戶能被訪問的時(shí)機(jī)(加入點(diǎn),join points),而在advice體內(nèi)的代碼定義了安全檢查是如何實(shí)現(xiàn)的。使用這種方式,檢查代碼和位置點(diǎn)可以在一個(gè)地方維護(hù)。更進(jìn)一步,好的pointcut可以預(yù)見后期程序變動(dòng),因此如果另一個(gè)開發(fā)者創(chuàng)建了一個(gè)新的方法來訪問銀行賬戶,在其執(zhí)行時(shí)advice將應(yīng)用到該新方法上。占據(jù)領(lǐng)導(dǎo)地位的AOP實(shí)現(xiàn)是AspectJ、AspectWorkz、Spring AOP等等。
Spring AOP用純Java實(shí)現(xiàn)。不需要特殊編譯處理。AspectJ需要特殊編譯處理。Spring AOP不需要控制各層級(jí)類裝載器,因而適合用在J2EE web容器或應(yīng)用服務(wù)器中。Spring 2.0還提供了與AspectJ的緊密集成。
事件路由
為了滿足我們的業(yè)務(wù)需求,我以及確定了兩個(gè)AOP組件。它們是RegistrationBeforeAdvice和RegistrationAfterAdvice。請(qǐng)參考Spring參考文檔以獲得更多關(guān)于各種AOP advice和其他概念。
識(shí)別出兩個(gè)AOP組件背后的基本原理是支持事件路由和在代碼中最小化交叉依賴。RegistrationBeforeAdvice的職責(zé)被限制在識(shí)別和收集適當(dāng)?shù)臉I(yè)務(wù)事件。該before advice的Spring AOP實(shí)現(xiàn)可以被配置在Spring應(yīng)用上下文配置文件(Spring application context file)中,以截獲業(yè)務(wù)服務(wù)接口API來注入定制行為——識(shí)別并增加正確的業(yè)務(wù)事件到事件集合中。
本例中,RegistrationBeforAdvice截獲業(yè)務(wù)服務(wù)接口的doRegister(AbstractData data)API。該advice訪問該服務(wù)接口API的入?yún)ⅲˋbstractData)。早期在AbstractData層實(shí)現(xiàn)的事件集合在這里也變得垂手可得。RegistrationBeforeAdvice識(shí)別恰當(dāng)?shù)臉I(yè)務(wù)事件并把它增加到event集合中。
Spring應(yīng)用上下文中的eventMap配置是一個(gè)全局事件映射(global event map)。eventKey將適當(dāng)?shù)臉I(yè)務(wù)服務(wù)接口API名稱映射到事件標(biāo)識(shí)符。這讓我們可以在全局事件映射配置中無縫地將一個(gè)定義在業(yè)務(wù)服務(wù)接口的新的業(yè)務(wù)API映射到一個(gè)事件id,而無需修改RegistrationBeforeAdvice AOP組件的任何代碼。然而,對(duì)這種方法有一個(gè)警告。當(dāng)程序員犯了錯(cuò)誤,在全局事件映射配置中配置了錯(cuò)誤的方法名到eventId,這種錯(cuò)誤在編譯期間并不容易發(fā)現(xiàn)。但是一個(gè)簡單的Junit測試即可發(fā)現(xiàn)這種用詞不當(dāng)?shù)腻e(cuò)誤。
業(yè)務(wù)API名稱:doRegister
Event Id: 1
映射另一個(gè)業(yè)務(wù)API名,如doUpdate(),到另一個(gè)值為2的事件id現(xiàn)在變得非常容易了。我們所要改變的只是在接口中定義新的API之后,在Spring應(yīng)用上下文配置文件的事件映射中增加一個(gè)映射即可。下例給出了配置片斷。
<!-- creates a java.util.Map instance with values loaded from
the supplied 'sourceMap'.Global Event Map. The values are mapped
in event table. The keys are matched with the business API name-->
<util:map id="eventMap">
<entry key="doRegister">
<value type="java.lang.Integer">1</value></entry>
<entry key="doUpdate">
<value type="java.lang.Integer">2</value></entry>
</util:map>
在某些情況下,單個(gè)業(yè)務(wù)處理可能導(dǎo)致多個(gè)事件。這仍只需輕微修改RegistrationAfterAdvice(譯注:疑為RegistrationBeforeAdvice )的代碼及事件映射配置即可。這種情況下,我們需要說明每個(gè)業(yè)務(wù)交易的事件列表。為簡單起見,本文例中只限于展示一個(gè)業(yè)務(wù)交易僅有一個(gè)事件的情況。
請(qǐng)參考下面的代碼樣例,實(shí)際看看before advice。
Example: RegistrationBeforeAdvice.java
/**
*RegistrationBeforeAdvice.java - This advise acts before the doRegister() API in
*the business service interface executes and sets the appropriate event in the
*eventSet collection implemented in the AbstractData layer. The event is Customer
*Registered Event. This advice inserts custom behavior in IRegistrationService API
*doRegister() before it is executed and identifies the correct business event add
*it to the event collection
*@author - Vigil Bose
*/
public class RegistrationBeforeAdvice implements MethodBeforeAdvice{
private Map eventMap;
private ILookUpServiceDao lookUpServiceDao;
/**
* A setter method of dependency injection
* @param Map - The eventMap instance to set.
*/
public void setEventMap(Map eventMap){
this.eventMap = eventMap;
}
/**
* A setter method of dependency injection
* @param lookUpServiceDao - The lookUpServiceDao instance to set.
*/
public void setLookUpServiceDao(ILookUpServiceDao lookUpServiceDao){
this.lookUpServiceDao = lookUpServiceDao;
}
/**
*Before advice can insert custom behavior before the join point
*executes, but cannot change the return value.If a before advice
*throws an exception, this will abort further execution of the
*interceptor chain. The exception will propagate back up the
*interceptor chain. If it is unchecked, or on the signature of the
*invoked method, it will be passed directly to the client; otherwise
*it will be wrapped in an unchecked exception by the AOP proxy.
*@param method - method being invoked
*@param args - arguments to the method
*@param target - target of the method invocation. May be null
*@throws Throwable - if this object wishes to abort the call.
*Any exception thrown will be returned to the caller if it's allowed
*by the method signature. Otherwise the exception will be wrapped
*as a runtime exception
*@see org.springframework.aop.MethodBeforeAdvice#before(java.lang.reflect.Method
*java.lang.Object[], java.lang.Object)
*/
public void before(Method method, Object[] args, Object target) throws Throwable {
AbstractData data = (AbstractData)args[0];
Set keySet = this.eventMap.keySet();
Integer eventId;
Event event = null;
String eventKey = null;
//Iterate through the key set and extract event identifier and
//retrieve the event from the database and add it to the event
//collection.
for (Iterator iter = keySet.iterator(); iter.hasNext();) {
eventKey = (String) iter.next();
//Check whether the eventKey is matched with the business
//service interface API name. If it does, extract the eventId
//and retrieve the event instance from the datastore and add it
//to the event collection.
if (eventKey.equalsIgnoreCase(method.getName()){
eventId = (Integer)eventMap.get(eventKey);
event = this.lookupService.findEventById(Integer eventId);
data.addToEventSet(event);
}
}
}
}
本例中,一個(gè)需考慮的設(shè)計(jì)限制是Before advice或After advice組件拋出的異常應(yīng)該不影響在線業(yè)務(wù)交易。在線客戶不應(yīng)受到事件路由錯(cuò)誤的懲罰。為簡化起見,我沒有在該例中展示如何處理異常。
RegistrationAfterAdvice負(fù)責(zé)迭代事件集合、動(dòng)作代碼以及初始化路由過程。本例中使用的動(dòng)作代碼是M和T。在Spring應(yīng)用上下文配置文件中每一個(gè)動(dòng)作代碼都有命令與之映射。RegistrationAfterAdvice通過每個(gè)與事件(客戶注冊(cè)事件)相關(guān)聯(lián)的動(dòng)作代碼及獲得映射的命令對(duì)象實(shí)例對(duì)事件集合進(jìn)行迭代。一旦命令對(duì)象引用被獲得,路由自動(dòng)地發(fā)生,通過傳遞客戶數(shù)據(jù)給每個(gè)命令實(shí)現(xiàn)以執(zhí)行適當(dāng)?shù)娜蝿?wù)。
Example: RegistrationAfterAdvice.java
/**
*RegistrationAfterAdvice.java - This advise acts after when the doRegister()
*API of the business service implementation is executed. This advise will
*actually delegate the event actions associated with the Customer Registered
*Event to the appropriate command. This advice inserts custom behavior to
*IRegistrationService interface API's after the API is executed.
*@author - Vigil Bose
*/
public class RegistrationAfterAdvice implements AfterReturningAdvice {
/**
*After returning advice is invoked only on normal method return,
*not if an exception is thrown. Such advice can see the return
*value, but cannot change it.
*
*@param returnValue - the value returned by the method, if any
*@param method - method being invoked
*@param args - arguments to the method
*@param target - target of the method invocation. May be null
*@throws Throwable - if this object wishes to abort the call.
*Any exception thrown will be returned to the caller if it's allowed
*by the method signature. Otherwise the exception will be wrapped as a runtime
*exception
*@see org.springframework.aop.AfterReturningAdvice#afterReturning
*(java.lang.Object, java.lang.reflect.Method, java.lang.Object[],
*java.lang.Object)
*/
public void afterReturning(Object returnValue, Method method, Object[] args,
Object target) throws Throwable {
AbstractData data = (AbstractData)args[0];
User userInfo = (User)data.getUser();
Set eventSet = data.eventSet();
Set keySet = this.commandMap.keySet();
Event event = null;
String actionCodes = null;
String actionCode = null;
//Iterate through the event set
for (Iterator iter = eventSet.iterator(); iter.hasNext();) {
event = (Event) iter.next();
actionCodes = event.getEventActionCodes();
//Loop through the action codes and extract each command mapped to
//the action code in spring application context file.
for (int i=0; i < actionCodes.length();i++){
actionCode = new Character(eventActionCodes.charAt(i)).toString();
command = (ICommand)commandMap.get(actionCode);
if (command != null){
command.execute(userInfo);
}
}
}
}
}
在上面例子中可以看到,我本可以在RegistrationAfterAdvice本身實(shí)現(xiàn)事件集合和事件路由機(jī)制。但為了保持事件集合和事件路由的責(zé)任分離,我決定使用兩個(gè)AOP advice組件處理子系統(tǒng)路由功能。
完整配置
現(xiàn)在,在Spring應(yīng)用上下文xml文件中將所有配置都串起來。在下面的例子中,Hibernate實(shí)現(xiàn)被用作數(shù)據(jù)訪問層的數(shù)據(jù)關(guān)系映射(OR映射)。當(dāng)應(yīng)對(duì)多個(gè)資源提供者(如數(shù)據(jù)庫和JMS)時(shí),推薦使用JtaTransation策略。為簡單起見,下例中只展示了本地事務(wù)策略。
Example: applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-2.0.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-2.0.xsd
http://www.springframework.org/schema/jee
http://www.springframework.org/schema/jee/spring-jee-2.0.xsd">
<bean id="registrationService" class="RegistrationServiceImpl">
<property name="registrationServiceDao" ref="registrationServiceDao"/>
</beans>
<bean id="registrationServiceDao" class="RegistrationServiceDaoImpl">
<property name="sessionFactory" ref="sessionFactory"/>
</beans>
<bean id="lookUpServiceDao" class="LookUpServiceDaoImpl">
<property name="sessionFactory" ref="sessionFactory"/>
</beans>
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"/>
<property> name="dataSource" ref="dataSource"/>
<!-- Use the following property jtaTransactionManager when dealing
with CMT transactions -->
<!-- <property name="jtaTransactionManager" ref="jtaTransactionManager"/>-->
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.Oracle9Dialect</prop>
<prop key="hibernate.connection.pool_size">3</prop>
<prop key="hibernate.show_sql">true</prop>
<prop key="hibernate.generate_statistics">true</prop>
<prop key="hibernate.cache.use_structured_entries">true</prop>
<prop key="hibernate.max_fetch_depth">3</prop>
<prop key="hibernate.cache.provider_class">
org.hibernate.cache.EhCacheProvider</prop>
<prop key="hibernate.cache.region_prefix">node1</prop>
</props>
</property>
<property name="mappingResources">
<list>
<value>Users.hbm.xml</value>
<value>Address.hbm.xml</value>
</list>
</property>
</bean>
<bean>id="transactionManager"
class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
<!-- <bean id="jtaTransactionManager"
class="org.springframework.jndi.JndiObjectFactoryBean"> -->
<!--The JNDI name of TransactionManager published in OC4J Container in 10g-->
<!-- <property name="jndiName"
value="java:comp/pm/TransactionManager"/>
</bean> -->
<!-- Transaction manager that delegates to JTA for ultimate coordinate of transactions -->
<!-- <bean id="transactionManager"
class="org.springframework.transaction.jta.JtaTransactionManager"/>-->
<aop:config>
<!--Format: execution(modifiers-pattern? ret-type-pattern
declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)-->
<!--The pointcut expression here is the execution of any public method
defined by the IRegistrationService interface-->
<aop:pointcut id="registrationServicePointcut"
expression="execution(public * *..IRegistrationService.*(..))"/>
<!--
Here: applying the advice named "registrationBeforeAdvice" to all methods on classes named RegistrationServiceImpl.
-->
<aop:advisor pointcut-ref="registrationServicePointcut"
advice-ref="registrationBeforeAdvice" order="1"/>
<!--
This definition creates auto-proxy infrastructure based on the given
pointcut, expressed in AspectJ pointcut language. Here: applying the
advice named "registrationServiceTransactionAdvice" to all methods
on classes named RegistrationServiceImpl.-->
<aop:advisor pointcut-ref="registrationServicePointcut"
advice-ref="registrationServiceTransactionAdvice" order="2"/>
<!--
This definition creates auto-proxy infrastructure based on the given
pointcut,expressed in AspectJ pointcut language. Here: applying the
advice named "registrationAfterAdvice" to all methods on
classes named RegistrationServiceImpl.
-->
<aop:advisor pointcut-ref="registrationServicePointcut"
advice-ref="registrationAfterAdvice" order="3"/>
</aop:config>
<!--
Transaction advice definition, based on method name patterns.
Defaults to PROPAGATION_REQUIRED for all methods whose name starts with
"do". By default, the transaction is rolled back for runtime
exceptions including DataAccessException.
-->
<tx:advice id="registrationServiceTransactionAdvice"
transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="do*"/>
</tx:attributes>
</tx:advice>
<bean id="registrationBeforeAdvice" class="RegistraionBeforeAdvice">
<property name="order" value="1"/>
<property name="eventMap" ref="eventMap"/>
<property name="lookUpServiceDao" ref="lookUpServiceDao"/>
</bean>
<bean id="registrationAfterAdvice"
class="RegistrationAfterAdvice">
<property name="order" value="3"/>
<property name="commandMap">
<map>
<entry key="M"><ref bean="mailingCommand"/></entry>
<entry key="T"><ref bean="sendCustomerInfoCommand"/> </entry>
</map>
</property>
</beans>
<bean id="mailingCommand" class="MailingCommandImpl">
<property name="emailService" ref="emailService"/>
</beans>
<bean id="sendCustomerInfoCommand" class="SendCustomerInfoCommandImpl">
<property name="messagingService" ref="messagingService"/>
</beans>
<bean id="messagingService" class="MessagingService">
<property name="jmsTemplate" ref="jmsTemplate"/>
</beans>
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory" ref="defaultQueueConnectionFactory" />
<property name="defaultDestination" ref="defaultQueueDestination" />
</bean>
<!-- JNDI Lookup configuration for event queue connection factory
used in messaging -->
<jee:jndi-lookup id="defaultQueueConnectionFactory" jndi-name="EVENT_QUEUE"/>
<!-- JNDI Lookup configuration for event queue destination used in messaging -->
<jee:jndi-lookup id="defaultQueueDestination" jndi-name="EVENTQueue"/>
<!-- JNDI Lookup configuration for DataSource in J2EE environments -->
<jee:jndi-lookup id="dataSource" jndi-name="jdbc/userDS"/>
<bean id="mailSender"
class="org.springframework.mail.javamail.JavaMailSenderImpl">
<property name="host" value="localhost"/>
</bean>
<bean id="emailService" class="EmailServicesImpl">
<property name="mailSender" ref="mailSender"/>
</bean>
<!-- creates a java.util.Map instance with values loaded from
the supplied 'sourceMap'.Global Event Map. The values are mapped
in event table. The keys are matched with the business API name-->
<util:map id="eventMap">
<entry key="doRegister">
<value type="java.lang.Integer">1</value></entry>
</util:map>
</beans>
AOP——Aspect Oriented Programming是程序設(shè)計(jì)中相對(duì)比較新的概念。該技術(shù)是對(duì)面向?qū)ο蠹夹g(shù)的補(bǔ)充,并帶來了更多強(qiáng)大能力和關(guān)注點(diǎn)的分離,而這些正是面向?qū)ο蠹夹g(shù)的弱項(xiàng)。
結(jié)論
關(guān)注點(diǎn)分離是開發(fā)面向服務(wù)架構(gòu)的關(guān)鍵原則。它需要被分別應(yīng)用到基礎(chǔ)架構(gòu)層和實(shí)現(xiàn)層。本文中,我們示范了如何使用Spring框架的依賴注入原則和AOP特性分離出橫向切面關(guān)注點(diǎn)。正如你已經(jīng)在例子代碼中看到的,使用這一方法能讓我們把處理服務(wù)每個(gè)關(guān)注點(diǎn)的代碼的交叉依賴減到最小。
參考
Spring參考文檔: http://www.springframework.org/docs/reference/
免責(zé)
請(qǐng)將所提供的代碼更多的視為示范使用的例子,而非被證明準(zhǔn)備就緒的產(chǎn)品。如果你使用了這些代碼并發(fā)現(xiàn)有任何問題,請(qǐng)通知我以便更新。
查看英文原文:Dynamic Routing Using Spring framework and AOP