本文的大体思路是展示了一次业务交易如何动态地为子系统处理过程触发业务事件。本文所示的例子使用 Spring 框架和 Spring AOP 有效地解耦业务服务和子系统处理功能。现在让我们仔细看看业务需求。
一个事件可以被认为是在特定时间间隔发生的一些事情。本例中,它是客户注册过程。典型地,当事件发生时,一个单一事件可能包含了一个或多个需要发生的动作。按照业务需求,我们已经确定了如下两个动作:
- 发送邮件通知客户。
- 传送客户地址数据到发票系统。
我们现在将设计事件数据结构以持有存储在事件数据库表中的信息。以下事件属性被确定下来。
- 事件标识符:1
- 事件描述:客户注册事件
- 动作代码:MT
该事件标识符是映射到数据库中的主键。事件描述定义了关于事件的描述信息。最后一个是动作代码,当事件发生时,它代表了要发生的不同动作。该动作代码定义在动作代码对照表中。
上面提到的事件,其动作代码标识为 M 和 T。M 代表发送一个邮件通知到客户,T 代表传送客户地址数据到发票系统。
Example: Event.java
/**<br></br> *Event.java - The event domain object<br></br> *@author - Vigil Bose<br></br> */<br></br> public class Event implements Serializable {<p> private Integer eventId;</p><br></br> private String eventDesc;<br></br> private String eventActionCodes;<br></br> private static final long serialVersionUID = 1L;<p> /** The cached hash code value for this instance. Settting to 0 triggers</p><br></br> re-calculation. */<br></br> private int hashValue = 0;<p> /**</p><br></br> *@return the eventActionCodes<br></br> */<br></br> public String getEventActionCodes(){<br></br> return eventActionCodes;<br></br> }<br></br> /**<br></br> * @param eventActionCodes the eventActionCodes to set<br></br> */<br></br> public void setEventActionCodes(String eventActionCodes) {<br></br> this.eventActionCodes = eventActionCodes;<br></br> }<br></br> /**<br></br> * @return the eventDesc<br></br> */<br></br> public String getEventDesc() {<br></br> return eventDesc;<br></br> }<br></br> /**<br></br> * @param eventDesc the eventDesc to set<br></br> */<br></br> public void setEventDesc(String eventDesc) {<br></br> this.eventDesc = eventDesc;<br></br> }<br></br> /**<br></br> * Return the simple primary key value that identifies this object.<br></br> * @return the eventId<br></br> */<br></br> public Integer getEventId() {<br></br> return eventId;<br></br> }<br></br> /**<br></br> * Set the simple primary key value that identifies this object.<br></br> * @param eventId the eventId to set<br></br> */<br></br> public void setEventId(Integer eventId) {<br></br> this.hashValue = 0;<br></br> this.eventId = eventId;<br></br> }<br></br> /**<br></br> *Implementation of the equals comparison on the basis of equality<br></br> *of the primary key values.<br></br> * @param rhs<br></br> * @return boolean<br></br> */<br></br> public boolean equals(Object rhs){<br></br> if (rhs == null)<br></br> return false;<br></br> if (! (rhs instanceof Event))<br></br> return false;<br></br> Event that = (Event) rhs;<br></br> if (this.getEventId() == null || that.getEventId() == null)<br></br> return false;<br></br> return (this.getEventId().equals(that.getEventId()));<br></br> }<p> /**</p><br></br> * Implementation of the hashCode method conforming to the Bloch pattern with<br></br> * the exception of array properties (these are very unlikely primary key types).<br></br> * @return int<br></br> */<br></br> public int hashCode(){<br></br> if (this.hashValue == 0){<br></br> int result = 17;<br></br> int eventIdValue = this.getEventId() == null ? 0 :<br></br> this.getEventId().hashCode();<br></br> result = result * 37 + eventIdValue;<br></br> this.hashValue = result;<br></br> }<br></br> return this.hashValue;<br></br> }<br></br> }
现在我们已经设计了事件领域对象以代表客户注册事件。现在我们转而设计 Web 层与业务服务层之间的 API 契约。设计的约束之一是改变领域模型不能破坏 Web 层与业务服务层之间的 API 契约。为了满足该设计约束,确定了两个数据包装类:AbstractData 和 UserData。AbstractData 本质上定义了一些行为的抽象。UserData 是 AbstractData 的子类,并提供了其他行为。比如,如果你有一个应用框架,抽象类可以提供诸如事件和消息处理的默认服务。
在本例中,AbstractData 负责收集各种业务事件。AbstractData 的子类是 UserData,用来持有我们的主要领域对象(用户对象)。
用户领域对象由不同属性组成,这些属性是用来识别用户的,如 userId、firstName、lastName 和加过密的 password。它还包含了地址领域对象,该对象有多个地址属性,如 address line 1、address line 2、city、state 等等。
Example: AbstractData.java
/**<br></br> *AbstractData.java - A template pattern like class that implments<br></br> *the event collection behavior. This class is used by all data<br></br> *transfer wrapper objects between UI Layer and Server side Layer<br></br> *@author - Vigil Bose<br></br> */<br></br>public abstract class AbstractData{<p> /**</p><br></br> *Stores all the events identified during the transaction<br></br> *Processing.<br></br> */<br></br> private Set eventSet = Collections.synchronizedSet(new HashSet());<p> /**</p><br></br> * @return Returns the eventSet.<br></br> */<br></br> public Set getEventSet() {<br></br> return eventSet;<br></br> }<p> /**</p><br></br> *@param event - An instance of a business event resulted from a particular<br></br> *business transaction<br></br> */<br></br> public void addToEventSet(Event event) {<br></br> this.eventSet.add(event);<br></br> }<p>}</p>
在 AbstractData 中声明一个集合(Set)的原因,是为了避免在给定时间点集合中有同一重复事件。让我们看看 UserData 长什么样吧。UserData 包含了实际 User 领域对象。因此针对 User 领域对象的任何改变都被限制在这个包装类中,不会破坏客户端和业务服务层之间的接口契约。
Example: UserData.java
/**<br></br> *UserData.java - A concrete POJO data wrapper whose responsibility is to<br></br> *holds the main domain object reference and used between client and business<br></br> *service layers.<br></br> *@author - Vigil Bose<br></br> */<br></br>public class UserData extends AbstractData{<p> private User user;</p><p> /**</p><br></br> * @return The user domain object instance<br></br> */<br></br> public Users getUsers(){<br></br> return this.users;<br></br> }<br></br> /**<br></br> *@param The user instance to set.<br></br> */<br></br> public void setUser(User user){<br></br> this.user = user;<br></br> }<p> }</p>
让我们看看业务服务接口。本例中,我们将在 IRegistrationService 接口中定义一个叫做 doRegister() 的 API 契约,以完成用户注册业务过程。该 API 本质上是事务型的,因为它要向多个数据库表插入记录。客户层和业务层通过该接口进行交互。
Example: IRegistrationService.java
/**<br></br> *IRegistrationService.java - A classic example of EJB's business<br></br> *methods interface pattern that exposes all the Registration<br></br> *related business methods that can be implemented by both the<br></br> *enterprise session bean as well as the Pojo service. Moreover<br></br> *this pattern allows us to switch to POJO implementation later<br></br> *if it makes sense to do so.<br></br> *@author - Vigil Bose<br></br> */<br></br>public interface IRegistrationService{<p> /**</p><br></br> *API to complete the registration business process<br></br> *@param userData - The data wrapper object<br></br> */<br></br> public void doRegister(AbstractData userData);<p>}</p>
为了简单起见,本例中我们只用了 POJO(Plain Old Java Object)服务。业务服务实现则实现了完成客户注册过程的业务逻辑。本例中,服务实现的唯一职责是将调用委派给数据访问层 (Data Access Layer),以在适当的数据库表中记录客户注册交易的状态。
Example: RegistrationServiceImpl.java
/**<br></br> *The primary business method implementation of Customer Registration Service.<br></br> *This is a POJO. It does not depend on any Spring APIs. It's usable outside a<br></br> *Spring container, and can be instantiated using new in a JUnit test. However,<br></br> *we can still apply declarative transaction management to it using Spring AOP.<br></br> *@author - Vigil Bose<br></br> */<br></br>public class RegistrationServiceImpl implements IRegistrationService{<p> private IRegistrationServiceDao registrationServiceDao;</p><p> /**</p><br></br> * A setter method of dependency injection<br></br> * @param registrationServiceDao - The registrationServiceDao to set.<br></br> */<br></br> public setRegistrationServiceDao(IRegistrationServiceDao<br></br> registrationServiceDao){<br></br> this.registrationServiceDao = registrationServiceDao;<br></br> }<br></br> /**<br></br> * API to register the user<br></br> * @param user - The user domain object<br></br> */<br></br> public void doRegister(AbstractData userData){<br></br> this.registrationServiceDao.completeRegistration(userData.getUser());<br></br> }<br></br>}
## 务实使用 DAO 模式
Data Access Object(DAO——数据访问对象)在核心 J2EE 设计模式一书中被编目为一个集成层设计模式。它把持久库存取和操作代码封装到了一个单独的层次。本文中所指的持久库就是 RDBMS。
该模式在业务逻辑层和持久存储层之间引入了一个抽象层。业务对象通过数据访问对象访问 RDBMS(数据源)。该抽象层简化了应用代码并引入了灵活性。理想地,对数据源所做的变动(比如变换数据库厂商或类型),仅仅需要改变数据访问对象,因而对业务对象影响最小。本例中,我们使用 Hibernate 实现数据访问策略。
DAO 设计模式所提供的的灵活性主要被归因于对象设计的最佳实践:用接口编程。该原则规定了具体对象必须实现一个接口,在调用程序中使用该接口而非具体对象本身。因此,你可以容易地替换一个不同的实现,而对客户端代码冲击很小。
遵循上面所说的原则,我们将定义注册服务 DAO 接口——IRegistrationServiceDao.java,它有一个 completeRegistraion() 行为。业务组件将通过这个接口与 DAO 交互。
Example: IRegistrationServiceDao.java
/**<br></br> *A POJO data access object interface for the CRS services business layer.<br></br> *The API's defined in this interface are all transactional APIs within the<br></br> *business services layer<br></br> *@author - Vigil Bose<br></br> */<br></br>public interface IRegistrationServiceDao{<br></br> /**<br></br> * Data Access API to create the new user in the system<br></br> * @param user - The composite user domain object<br></br> */<br></br> public void completeRegistration(User user) throws DataAccessException;<br></br>}
在定义了数据访问接口之后,我们必须提供一个 IRegistrationServiceDao 的具体实现——RegistrationServiceDaoImpl。
本例中该实现中使用了 Hibernate。这里所使用的模式是策略模式,可以用任何对象关系映射(Object Relation Mapping)产品或 JDBC 来替换。该类的职责是将客户注册交易的状态记录在数据库表中。
Example: RegistrationServiceDaoImpl.java
/**<br></br> *The Registration Services Data Access Strategy implementation<br></br> *using Hibernate persistence mechanism that support various<br></br> *registration related business transactions.<br></br> *@author - Vigil Bose<br></br> */<br></br>public class RegistrationServiceDaoImpl extends HibernateDaoSupport<br></br> implements IRegistrationServiceDao{<p> /**</p><br></br> * Data Access API to create the new user in the system<br></br> * @param users - The composite users domain object<br></br> */<br></br> public void completeRegistration(Users users) throws DataAccessException {<p> getHibernateTemplate().save(users);</p><br></br> }<p>}</p>
任何应用程序中,访问只读数据都是重要的。让我们看一个普通 java 接口——ILookUpServiceDao 的例子,它在 CRS 中被用到,其暴露了 finder 和 getter 方法以访问只读数据。
Example: ILookUpServiceDao.java
/**<br></br> *A POJO data access object interface that exposes the lookup API's in Customer<br></br> *Registration System.<br></br> *The API's defined in this interface can be used with or without any other<br></br> *transactional APIs within the business services layer<br></br> *@author - Vigil Bose<br></br> */<br></br>public interface ILookUpServiceDao{<br></br> /**<br></br> * Data Access API to find the event instance based on its primary key<br></br> * @param eventId - The event tables primary key identifier<br></br> */<br></br> public Event findEventById(Integer eventId) throws DataAccessException;<p>}</p>
下例是 Hibernate 实现策略。ILookUpServiceDao 接口中的 API 定义在具体类 LookUpServiceDaoImpl 中被实现。为了简单,这里只显示了一个 API 实现。
Example: LookUpServiceDaoImpl.java
/**<br></br> *A POJO data access implementation that implements the lookup API's in Customer<br></br> *Registration System.<br></br> *The API's defined in this interface can be used with any other<br></br> *transactional APIs within the business services layer<br></br> *@author - Vigil Bose<br></br> */<br></br>public classe LookUpServiceDaoImpl extends HibernateDaoSupport<br></br> implements ILookUpServiceDao {<br></br> /**<br></br> * Data Access API to find the event instance based on its primary key<br></br> * @param eventId - The event tables primary key identifier<br></br> * @return an instance of Event domain object<br></br> * @throws DataAccessException<br></br> */<br></br> public Event findEventById(Integer eventId) throws DataAccessException{<br></br> return (Event)getHibernateTemplate().get(Event.class, eventId);<br></br> }<p>}</p>
Spring 框架提供的 HibernateDaoSupport 类是一个模板模式实现,其抽象了 Hibernate 相关 API 和与 Hibernate Session 相关的资源管理。
有了数据访问实现,我们对业务需求的一个方面——客户注册过程——感到满意了。现在我们将考虑需求的第二个方面,它是在设计期间所确定的子系统功能。该需求是这样的:当注册完成时,系统将发送邮件通知给客户并传送客户地址数据给发票系统以产生发票。我们将通过著名的命令模式来实现子系统处理过程。
命令模式
命令模式用来提供一个公共接口以执行不同的命令。任何实现了该命令接口的类可以在其 execute() 方法中提供特定任务的实现。
在设计期间,我们已经确定了针对同一接口的两个不同命令实现。就业务服务层来说,子系统功能是一个单独的关注点。因此我使用 AOP 技术将这个单独的关注点从主要业务服务层实现(RegistrationServiceImpl)中解耦。
想更多了解 AOP 概念的人,请点击这里。你还可以从下面看到一些关于AOP 的介绍。现在,让我们设计命令接口。
Example: ICommand.java
/**<br></br> *ICommand.java - The famous command interface that exposes the execute API.<br></br> *@author - Vigil Bose<br></br> */<br></br>public interface ICommand{<p> /**</p><br></br> *The Command design pattern encapsulates the concept of the<br></br> *command into an object. The issuer holds a reference to the<br></br> *command object rather than to the recipient.The issuer sends<br></br> *the command to the command object by executing a specific<br></br> *method on it. The command object is then responsible for<br></br> *dispatching the command to a specific recipient to get the<br></br> *job done.<br></br> *@param data - The data transfer object<br></br> */<br></br> public void execute(Object data);<p>}</p>
典型的命令实现提供了打包一组计算的方法(一个接收者和一组动作)并把它作为第一级别对象向周围传递。该命令对象调用请求命令接收者(receiver)的方法以真正处理请求。通常也能发现一个命令实现自己处理特定任务,无须委派请求给 receiver。在本例中,MailingCommandImpl 通过调用 EmailService 实现了发送邮件通知的任务。为简单起见,EmailService 实现没有在本例中展现。毕竟,我们的意图是展现业务事件是如何借助于 AOP 和 Spring2.0 被路由到子系统处理器的。
Example: MailingCommandImpl.java
/**<br></br> *MailingCommandImpl.java - A command implementation that implements<br></br> *the task of sending mail notification to the customer who completed<br></br> *the registration process.<br></br> *@author - Vigil Bose<br></br> */<br></br>public class MailingCommandImpl implements ICommand{<p> private IEmailService emailService;</p><p> /**</p><br></br> *A setter method of dependency injection<br></br> *@param emailService - The emailService instance to set.<br></br> */<br></br> public void setEmailService(IEmailService emailService){<br></br> this.emailService = emailService;<br></br> }<br></br> /**<br></br> *API execute is used to execute the mailing tasks implemented<br></br> *@param args - An instance of AbstractData used in business service layer<br></br> */<br></br> public void execute(Object args){<p> //get the reference of user object</p><br></br> User user = (User)args;<p> //get the reference of address object via its parent object User.</p><br></br> Address address = user.getAddress()<p> //Invoke the EmailService API here to send out the notifications....</p><p> }</p><br></br>}
现在,我将设计第二个命令实现,它将帮助我们实现关于传送客户地址数据到发票应用的业务需求。在这个特定实现中,我们可以选择任何协议(如 Web 服务、消息传递,或 XML over Http 等等)来发送客户信息到发票应用(假设发票应用可以使用上面提到的任何协议用作应用集成)。为了简单,下面给出的是使用 JMS 消息传递的例子。本例并没有展示 JMS 消息传递的内部结构。
Example: SendCustomerInfoCommandImpl.java
/**<br></br> *SendCustomerInfoCommandImpl.java - A command implementation that implements<br></br> *the task of transmiting the customer's address data to the invoice system.<br></br> *@author - Vigil Bose<br></br> */<br></br> public class SendCustomerInfoCommandImpl implements ICommand{<p> private IMessagingService messagingService;</p><p> /**</p><br></br> * A setter method of dependency injection<br></br> */<br></br> public void setMessagingService(IMessagingService messagingService){<br></br> this.messagingService = messagingService;<br></br> }<p> /**</p><br></br> *API execute is used to execute the messaging task implemented.<br></br> *@param args - An instance of AbstractData used in the business service layer<br></br> */<br></br> public void execute(Object args){<p> User user = (User)args;</p><p> //Invoke the appropriate messagingService API</p><br></br> //to send the customer information here....<br></br> }<br></br>}
## AOP 的基本概念
AOP 也被称为 Aspect Oriented Programming(面向方面编程)试图帮助程序员分离关注点,尤其是横向切面关注点(cross-cutting concerns)。过程、包、类及方法都是帮助程序员把关注点封装到单一实体内。但是有些关注点不适合这种形式的封装。我们称之为横向切面关注点,是因为它们横跨了程序中许多模块。它可能使代码分散或缠结(scattered or tangled),使人们更难理解和维护。当一个关注点(例如本利的事件路由)蔓延到许多模块(类和方法)时,代码被分散了。这意味着修改事件分发功能可能需要修改所有受影响的模块。
代码失去了典雅和简单,因为各种新的关注点已经与基本功能(有时称为业务逻辑关注点)缠结在一起。事务、消息传递、安全以及日志都是横向切面关注点的例子。
AOP 试图通过让程序员在一个单独的称之为 aspect 的模块中表达横向切面关注点来解决这些问题。Aspect 可以包含 advice(加入到程序指定点的代码)和 inter-type 声明(增加到其他类的结构成员)。例如,一个安全模块可以包含在访问一个银行账户前执行安全检查的 advice。pointcut 定义了一个银行账户能被访问的时机(加入点,join points),而在 advice 体内的代码定义了安全检查是如何实现的。使用这种方式,检查代码和位置点可以在一个地方维护。更进一步,好的 pointcut 可以预见后期程序变动,因此如果另一个开发者创建了一个新的方法来访问银行账户,在其执行时 advice 将应用到该新方法上。占据领导地位的 AOP 实现是 AspectJ、AspectWorkz、Spring AOP 等等。
Spring AOP 用纯 Java 实现。不需要特殊编译处理。AspectJ 需要特殊编译处理。Spring AOP 不需要控制各层级类装载器,因而适合用在 J2EE web 容器或应用服务器中。Spring 2.0 还提供了与 AspectJ 的紧密集成。
事件路由
为了满足我们的业务需求,我以及确定了两个 AOP 组件。它们是 RegistrationBeforeAdvice 和 RegistrationAfterAdvice。请参考 Spring 参考文档以获得更多关于各种 AOP advice 和其他概念。
识别出两个 AOP 组件背后的基本原理是支持事件路由和在代码中最小化交叉依赖。RegistrationBeforeAdvice 的职责被限制在识别和收集适当的业务事件。该 before advice 的 Spring AOP 实现可以被配置在 Spring 应用上下文配置文件(Spring application context file)中,以截获业务服务接口 API 来注入定制行为——识别并增加正确的业务事件到事件集合中。
本例中,RegistrationBeforAdvice 截获业务服务接口的 doRegister(AbstractData data)API。该 advice 访问该服务接口 API 的入参(AbstractData)。早期在 AbstractData 层实现的事件集合在这里也变得垂手可得。RegistrationBeforeAdvice 识别恰当的业务事件并把它增加到 event 集合中。
Spring 应用上下文中的 eventMap 配置是一个全局事件映射(global event map)。eventKey 将适当的业务服务接口 API 名称映射到事件标识符。这让我们可以在全局事件映射配置中无缝地将一个定义在业务服务接口的新的业务 API 映射到一个事件 id,而无需修改 RegistrationBeforeAdvice AOP 组件的任何代码。然而,对这种方法有一个警告。当程序员犯了错误,在全局事件映射配置中配置了错误的方法名到 eventId,这种错误在编译期间并不容易发现。但是一个简单的 Junit 测试即可发现这种用词不当的错误。
业务 API 名称:doRegister
Event Id: 1
映射另一个业务 API 名,如 doUpdate(),到另一个值为 2 的事件 id 现在变得非常容易了。我们所要改变的只是在接口中定义新的 API 之后,在 Spring 应用上下文配置文件的事件映射中增加一个映射即可。下例给出了配置片断。
<!-- creates a java.util.Map instance with values loaded from<br></br> the supplied 'sourceMap'.Global Event Map. The values are mapped<br></br> in event table. The keys are matched with the business API name--><br></br> <util:map id="eventMap"><br></br> <entry key="doRegister"><br></br> <value type="java.lang.Integer">1</value></entry><br></br> <entry key="doUpdate"><br></br> <value type="java.lang.Integer">2</value></entry><br></br> </util:map>
在某些情况下,单个业务处理可能导致多个事件。这仍只需轻微修改 RegistrationAfterAdvice(译注:疑为 RegistrationBeforeAdvice )的代码及事件映射配置即可。这种情况下,我们需要说明每个业务交易的事件列表。为简单起见,本文例中只限于展示一个业务交易仅有一个事件的情况。
请参考下面的代码样例,实际看看 before advice。
Example: RegistrationBeforeAdvice.java
/**<br></br> *RegistrationBeforeAdvice.java - This advise acts before the doRegister() API in<br></br> *the business service interface executes and sets the appropriate event in the<br></br> *eventSet collection implemented in the AbstractData layer. The event is Customer<br></br> *Registered Event. This advice inserts custom behavior in IRegistrationService API<br></br> *doRegister() before it is executed and identifies the correct business event add<br></br> *it to the event collection<br></br> *@author - Vigil Bose<br></br> */<br></br>public class RegistrationBeforeAdvice implements MethodBeforeAdvice{<p> private Map eventMap;</p><br></br> private ILookUpServiceDao lookUpServiceDao;<p> /**</p><br></br> * A setter method of dependency injection<br></br> * @param Map - The eventMap instance to set.<br></br> */<br></br> public void setEventMap(Map eventMap){<br></br> this.eventMap = eventMap;<br></br> }<br></br> /**<br></br> * A setter method of dependency injection<br></br> * @param lookUpServiceDao - The lookUpServiceDao instance to set.<br></br> */<br></br> public void setLookUpServiceDao(ILookUpServiceDao lookUpServiceDao){<br></br> this.lookUpServiceDao = lookUpServiceDao;<br></br> }<br></br> /**<br></br> *Before advice can insert custom behavior before the join point<br></br> *executes, but cannot change the return value.If a before advice<br></br> *throws an exception, this will abort further execution of the<br></br> *interceptor chain. The exception will propagate back up the<br></br> *interceptor chain. If it is unchecked, or on the signature of the<br></br> *invoked method, it will be passed directly to the client; otherwise<br></br> *it will be wrapped in an unchecked exception by the AOP proxy.<br></br> *@param method - method being invoked<br></br> *@param args - arguments to the method<br></br> *@param target - target of the method invocation. May be null<br></br> *@throws Throwable - if this object wishes to abort the call.<br></br> *Any exception thrown will be returned to the caller if it's allowed<br></br> *by the method signature. Otherwise the exception will be wrapped<br></br> *as a runtime exception<br></br> *@see org.springframework.aop.MethodBeforeAdvice#before(java.lang.reflect.Method<br></br> *java.lang.Object[], java.lang.Object)<br></br> */<br></br> public void before(Method method, Object[] args, Object target) throws Throwable {<p> AbstractData data = (AbstractData)args[0];</p><p> Set keySet = this.eventMap.keySet();</p><br></br> Integer eventId;<p> Event event = null;</p><br></br> String eventKey = null;<p> //Iterate through the key set and extract event identifier and</p><br></br> //retrieve the event from the database and add it to the event<br></br> //collection.<p> for (Iterator iter = keySet.iterator(); iter.hasNext();) {</p><br></br> eventKey = (String) iter.next();<p> //Check whether the eventKey is matched with the business</p><br></br> //service interface API name. If it does, extract the eventId<br></br> //and retrieve the event instance from the datastore and add it<br></br> //to the event collection.<p> if (eventKey.equalsIgnoreCase(method.getName()){</p><br></br> eventId = (Integer)eventMap.get(eventKey);<p> event = this.lookupService.findEventById(Integer eventId);</p><br></br> data.addToEventSet(event);<br></br> }<br></br> }<br></br> }<br></br>}
本例中,一个需考虑的设计限制是 Before advice 或 After advice 组件抛出的异常应该不影响在线业务交易。在线客户不应受到事件路由错误的惩罚。为简化起见,我没有在该例中展示如何处理异常。
RegistrationAfterAdvice 负责迭代事件集合、动作代码以及初始化路由过程。本例中使用的动作代码是 M 和 T。在 Spring 应用上下文配置文件中每一个动作代码都有命令与之映射。RegistrationAfterAdvice 通过每个与事件(客户注册事件)相关联的动作代码及获得映射的命令对象实例对事件集合进行迭代。一旦命令对象引用被获得,路由自动地发生,通过传递客户数据给每个命令实现以执行适当的任务。
Example: RegistrationAfterAdvice.java
/**<br></br> *RegistrationAfterAdvice.java - This advise acts after when the doRegister()<br></br> *API of the business service implementation is executed. This advise will<br></br> *actually delegate the event actions associated with the Customer Registered<br></br> *Event to the appropriate command. This advice inserts custom behavior to<br></br> *IRegistrationService interface API's after the API is executed. <br></br> *@author - Vigil Bose<br></br> */<br></br>public class RegistrationAfterAdvice implements AfterReturningAdvice {<br></br> /**<br></br> *After returning advice is invoked only on normal method return,<br></br> *not if an exception is thrown. Such advice can see the return<br></br> *value, but cannot change it.<br></br> *<br></br> *@param returnValue - the value returned by the method, if any<br></br> *@param method - method being invoked<br></br> *@param args - arguments to the method<br></br> *@param target - target of the method invocation. May be null<br></br> *@throws Throwable - if this object wishes to abort the call.<br></br> *Any exception thrown will be returned to the caller if it's allowed<br></br> *by the method signature. Otherwise the exception will be wrapped as a runtime<br></br> *exception<br></br> *@see org.springframework.aop.AfterReturningAdvice#afterReturning<br></br> *(java.lang.Object, java.lang.reflect.Method, java.lang.Object[],<br></br> *java.lang.Object)<br></br> */<br></br> public void afterReturning(Object returnValue, Method method, Object[] args,<br></br> Object target) throws Throwable {<p> AbstractData data = (AbstractData)args[0];</p><br></br> User userInfo = (User)data.getUser();<br></br> Set eventSet = data.eventSet();<p> Set keySet = this.commandMap.keySet();</p><br></br> Event event = null;<br></br> String actionCodes = null;<br></br> String actionCode = null;<p> //Iterate through the event set</p><br></br> for (Iterator iter = eventSet.iterator(); iter.hasNext();) {<p> event = (Event) iter.next();</p><br></br> actionCodes = event.getEventActionCodes();<p> //Loop through the action codes and extract each command mapped to</p><br></br> //the action code in spring application context file.<p> for (int i=0; i < actionCodes.length();i++){</p><br></br> actionCode = new Character(eventActionCodes.charAt(i)).toString();<br></br> command = (ICommand)commandMap.get(actionCode);<br></br> if (command != null){<br></br> command.execute(userInfo);<br></br> }<br></br> }<br></br> }<br></br> }<br></br>}
在上面例子中可以看到,我本可以在 RegistrationAfterAdvice 本身实现事件集合和事件路由机制。但为了保持事件集合和事件路由的责任分离,我决定使用两个 AOP advice 组件处理子系统路由功能。
完整配置
现在,在 Spring 应用上下文 xml 文件中将所有配置都串起来。在下面的例子中,Hibernate 实现被用作数据访问层的数据关系映射(OR 映射)。当应对多个资源提供者(如数据库和 JMS)时,推荐使用 JtaTransation 策略。为简单起见,下例中只展示了本地事务策略。
Example: applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?><br></br> <beans xmlns="http://www.springframework.org/schema/beans"<br></br> xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"<br></br> xmlns:aop="http://www.springframework.org/schema/aop"<br></br> xmlns:tx="http://www.springframework.org/schema/tx"<br></br> xmlns:util="http://www.springframework.org/schema/util"<br></br> xmlns:jee="http://www.springframework.org/schema/jee"<br></br> xsi:schemaLocation="http://www.springframework.org/schema/beans<br></br> http://www.springframework.org/schema/beans/spring-beans-2.0.xsd<br></br> http://www.springframework.org/schema/aop<br></br> http://www.springframework.org/schema/aop/spring-aop-2.0.xsd<br></br> http://www.springframework.org/schema/tx<br></br> http://www.springframework.org/schema/tx/spring-tx-2.0.xsd<br></br> http://www.springframework.org/schema/util<br></br> http://www.springframework.org/schema/util/spring-util-2.0.xsd<br></br> http://www.springframework.org/schema/jee<br></br> http://www.springframework.org/schema/jee/spring-jee-2.0.xsd"><p> <bean id="registrationService" class="RegistrationServiceImpl"></p><br></br> <property name="registrationServiceDao" ref="registrationServiceDao"/><br></br> </beans><p> <bean id="registrationServiceDao" class="RegistrationServiceDaoImpl"></p><br></br> <property name="sessionFactory" ref="sessionFactory"/><br></br> </beans><p> <bean id="lookUpServiceDao" class="LookUpServiceDaoImpl"></p><br></br> <property name="sessionFactory" ref="sessionFactory"/><br></br> </beans><p> <bean id="sessionFactory"</p><br></br> class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"/><br></br> <property> name="dataSource" ref="dataSource"/><br></br> <!-- Use the following property jtaTransactionManager when dealing<br></br> with CMT transactions --><br></br> <!-- <property name="jtaTransactionManager" ref="jtaTransactionManager"/>--><br></br> <property name="hibernateProperties"><br></br> <props><br></br> <prop key="hibernate.dialect">org.hibernate.dialect.Oracle9Dialect</prop><br></br> <prop key="hibernate.connection.pool_size">3</prop><br></br> <prop key="hibernate.show_sql">true</prop><br></br> <prop key="hibernate.generate_statistics">true</prop><br></br> <prop key="hibernate.cache.use_structured_entries">true</prop><br></br> <prop key="hibernate.max_fetch_depth">3</prop><br></br> <prop key="hibernate.cache.provider_class"><br></br> org.hibernate.cache.EhCacheProvider</prop><br></br> <prop key="hibernate.cache.region_prefix">node1</prop><br></br> </props><br></br> </property><br></br> <property name="mappingResources"><br></br> <list><br></br> <value>Users.hbm.xml</value><br></br> <value>Address.hbm.xml</value><br></br> </list><br></br> </property><br></br> </bean><p> <bean>id="transactionManager"</p><br></br> class="org.springframework.orm.hibernate3.HibernateTransactionManager"><br></br> <property name="sessionFactory" ref="sessionFactory"/><br></br> </bean><p> <!-- <bean id="jtaTransactionManager"</p><br></br> class="org.springframework.jndi.JndiObjectFactoryBean"> --><br></br> <!--The JNDI name of TransactionManager published in OC4J Container in 10g--> <br></br> <!-- <property name="jndiName"<br></br> value="java:comp/pm/TransactionManager"/><br></br> </bean> --><p> <!-- Transaction manager that delegates to JTA for ultimate coordinate of transactions --></p><br></br> <!-- <bean id="transactionManager" <br></br> class="org.springframework.transaction.jta.JtaTransactionManager"/>--><p> <aop:config></p><br></br> <!--Format: execution(modifiers-pattern? ret-type-pattern<br></br> declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)--><p> <!--The pointcut expression here is the execution of any public method</p><br></br> defined by the IRegistrationService interface--><p> <aop:pointcut id="registrationServicePointcut" </p><br></br> expression="execution(public * *..IRegistrationService.*(..))"/><br></br> <!--<br></br> Here: applying the advice named "registrationBeforeAdvice" to all methods on classes named RegistrationServiceImpl.<br></br> --><br></br> <aop:advisor pointcut-ref="registrationServicePointcut"<br></br> advice-ref="registrationBeforeAdvice" order="1"/> <p> <!--</p><br></br> This definition creates auto-proxy infrastructure based on the given<br></br> pointcut, expressed in AspectJ pointcut language. Here: applying the<br></br> advice named "registrationServiceTransactionAdvice" to all methods<br></br> on classes named RegistrationServiceImpl.--><br></br> <aop:advisor pointcut-ref="registrationServicePointcut" <br></br> advice-ref="registrationServiceTransactionAdvice" order="2"/> <p> <!--</p><br></br> This definition creates auto-proxy infrastructure based on the given <br></br> pointcut,expressed in AspectJ pointcut language. Here: applying the<br></br> advice named "registrationAfterAdvice" to all methods on <br></br> classes named RegistrationServiceImpl.<br></br> --><br></br> <aop:advisor pointcut-ref="registrationServicePointcut" <br></br> advice-ref="registrationAfterAdvice" order="3"/><p> </aop:config></p><br></br> <!--<br></br> Transaction advice definition, based on method name patterns.<br></br> Defaults to PROPAGATION_REQUIRED for all methods whose name starts with<br></br> "do". By default, the transaction is rolled back for runtime<br></br> exceptions including DataAccessException.<br></br> --><br></br> <tx:advice id="registrationServiceTransactionAdvice" <br></br> transaction-manager="transactionManager"><br></br> <tx:attributes><br></br> <tx:method name="do*"/><br></br> </tx:attributes><br></br> </tx:advice><p> <bean id="registrationBeforeAdvice" class="RegistraionBeforeAdvice"></p><br></br> <property name="order" value="1"/><br></br> <property name="eventMap" ref="eventMap"/><br></br> <property name="lookUpServiceDao" ref="lookUpServiceDao"/><br></br> </bean><p> <bean id="registrationAfterAdvice"</p><br></br> class="RegistrationAfterAdvice"><br></br> <property name="order" value="3"/><br></br> <property name="commandMap"><br></br> <map><br></br> <entry key="M"><ref bean="mailingCommand"/></entry><br></br> <entry key="T"><ref bean="sendCustomerInfoCommand"/> </entry><br></br> </map><br></br> </property><br></br> </beans><p> <bean id="mailingCommand" class="MailingCommandImpl"></p><br></br> <property name="emailService" ref="emailService"/><br></br> </beans><p> <bean id="sendCustomerInfoCommand" class="SendCustomerInfoCommandImpl"></p><br></br> <property name="messagingService" ref="messagingService"/><br></br> </beans><p> <bean id="messagingService" class="MessagingService"></p><br></br> <property name="jmsTemplate" ref="jmsTemplate"/><br></br> </beans><p> <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate"></p><br></br> <property name="connectionFactory" ref="defaultQueueConnectionFactory" /><br></br> <property name="defaultDestination" ref="defaultQueueDestination" /><br></br> </bean><p> <!-- JNDI Lookup configuration for event queue connection factory</p><br></br> used in messaging --><br></br> <jee:jndi-lookup id="defaultQueueConnectionFactory" jndi-name="EVENT_QUEUE"/><br></br> <!-- JNDI Lookup configuration for event queue destination used in messaging --> <br></br> <jee:jndi-lookup id="defaultQueueDestination" jndi-name="EVENTQueue"/><p> <!-- JNDI Lookup configuration for DataSource in J2EE environments --></p><br></br> <jee:jndi-lookup id="dataSource" jndi-name="jdbc/userDS"/><p> <bean id="mailSender"</p><br></br> class="org.springframework.mail.javamail.JavaMailSenderImpl"><br></br> <property name="host" value="localhost"/><br></br> </bean> <p> <bean id="emailService" class="EmailServicesImpl"> </p><br></br> <property name="mailSender" ref="mailSender"/><br></br> </bean><p> <!-- creates a java.util.Map instance with values loaded from</p><br></br> the supplied 'sourceMap'.Global Event Map. The values are mapped<br></br> in event table. The keys are matched with the business API name--><br></br> <util:map id="eventMap"><br></br> <entry key="doRegister"><br></br> <value type="java.lang.Integer">1</value></entry><br></br> </util:map><br></br> </beans>
AOP——Aspect Oriented Programming 是程序设计中相对比较新的概念。该技术是对面向对象技术的补充,并带来了更多强大能力和关注点的分离,而这些正是面向对象技术的弱项。
结论
关注点分离是开发面向服务架构的关键原则。它需要被分别应用到基础架构层和实现层。本文中,我们示范了如何使用 Spring 框架的依赖注入原则和 AOP 特性分离出横向切面关注点。正如你已经在例子代码中看到的,使用这一方法能让我们把处理服务每个关注点的代码的交叉依赖减到最小。
参考
Spring 参考文档: http://www.springframework.org/docs/reference/
免责
请将所提供的代码更多的视为示范使用的例子,而非被证明准备就绪的产品。如果你使用了这些代码并发现有任何问题,请通知我以便更新。
评论 1 条评论