dev2dev 首页 > 资源中心 > 技术文章
未来的EJB外观
Enterprise Java Beans (EJB)是 Java 2 Enterprise Edition技术的重要组成部分,它是一种服务器端的组件架构,可以简化构建企业级分布式组件应用程序的过程。这篇文章依据EJB2.1规范,重点讨论如何以最好的方式将EJB组件公开为服务。此外,还阐述了在EJB层合理分层的重要性。
每当说到我们需要公开EJB服务时,实际上我们是在讨论EJB组件的一般特性和特殊接口。组件的特性包括类似组件的安全服务在运行时的情况和事务支持等。这些特性都很重要,因为在TX_REQUIRED上下文中调用的服务与在TX_REQUIRES_NEW上下文中调用的服务表现完全不同。组件接口包括参数和返回类型,然而需要指出的是,组件的可用性是依赖于参数和返回类型的。说得更明确些,参数和返回类型决定了所公开服务的粒度。
公开组件接口的方式有好几种。当需要通过IIOP协议(因特网ORB间协议)访问服务时,服务就作为兼容Java RMI的接口或者IDL接口公开。当需要通过HTTP协议来访问服务时,服务可以作为WSDL脚本公开。显而易见,以上介绍的这些特性(包括接口和运行时特性)组成了组件的部分公共契约。在本文中,我想表达的意思是,仅仅将方法签名作为组件接口公开出来是远远不够的。相反,服务调用者还需要知道服务的运行时属性(例如,安全、事务,等等)。这一点在以下环境中尤为重要:即当贸易合作伙伴通过执行UDDI查询来找到最有用的服务时,服务可以被发现和调用等。Java编程语言中的元数据工具允许类、接口、域和方法拥有特定的属性。在用于描述方法的事务属性等信息的EJB部署描述符中,EJB架构定义了大量有意义的额外数据。Java Specification Request(JSR)175是Java编程语言的一种元数据工具,它将会解决这些需求的主要部分,包括简化开发工具、部署工具和运行时库,这样就可以使用特殊的方式处理注释。当然,这只是提高带有事务、安全等(这些可能都不包括在目前的JSR范围中)附加特性的服务接口的第一步。
服务设计者、开发者和组装者仍然需要做大量的工作来定义一些将会经历时间和技术考验的稳定接口。我更加喜欢把“组件”称为“服务”(当特指那些现成的商业组件时的情况除外),因为服务代表了最终用户感兴趣的一面。最近,我负责检查一个已经投入生产的航空货运应用系统的架构。目前,用户是通过web界面来使用该应用系统的。而当前的需求是另一个“专有”系统需要以一种“专有格式”与该货运系统通信。这个新的交互式系统的请求与应答信息的格式是固定的,因此该货运系统需要合并新的交互方式(B2B),并且调整固定的请求和应答格式。新的请求和应答是通过HTTP协议以XML格式发送的,因此传输协议无需改变。然而,使用不同的传输协议(请求和/或应答通过SMTP协议以Email形式发送或接收)来访问该货运服务也是有可能的。这些因素揭示了系统成功发展时服务需求固有的不稳定性(当然,我们得承认对于那些不成功的系统或项目来说,这种问题是不存在的)。
现在我们集中精力来看EJB服务接口。无论什么时候,我一想到EJB服务接口(我更喜欢直接称之为“服务接口”,因为作为服务的调用者来说,我并不关心调用的到底是不是EJB服务接口),就会联想起那种不是从javax.ejb.EJBObject扩展而来的Java接口。这种接口的例子如下所示:
|
public interface CargoServiceBI{
/**
* Makes a best effort to do a space booking for the cargo
details
* given in the parameter cargo transfer object.
*
* @param cargoTO Cargo specification for which space has
to be
* booked
* @returns a booking ID, if space has been booked, -1L
otherwise
* @throws ServiceException if an application level exception
* occured while doing the booking
* @throws IOException if an exception occurred while communicating
* with the service
*/
public Long bookSpace(CargoTO cargoTO) throws ServiceException,
IOException;
/**
* Makes a best effort to do a space booking for all the
cargo details
* given in the parameter collection of cargo transfer object.
*
* @param cargoTOs contains a list of CargoTOs (<CargoTO>)
for
* which space has to be booked
* @param partBookingAllowed boolean flag indicating whether
booking
* can be continued (true) or not (false), in case space
cannot be
* allocated for some of the cargo
* @returns Map (<>), correlating
* the booking status of each cargo in param with its Booking
ID.
* If param partBookingAllowed is true and any BookingID
value is
* -1L implies, booking for corresponding CargoID is unsuccessful.
* Empty Map, if param partBookingAllowed is false and booking
* cannot be completed for at least one Cargo in param cargoTOs.
* @throws ServiceException if an application level exception
* occured while doing the booking
* @throws IOException if an exception occurred while communicating
* with the service
* @see CargoTO
* @see Long
*
*/
public Map bookSpace(Collection cargoTOs, Boolean partBookingAllowed)
throws ServiceException, IOException;
}
|
从客户或服务调用者的观点来看,这样的接口被称为“业务接口”。根据服务实现和提供的情况,我们可能还需要定义另外的接口。说的更清楚一点,如果我们准备把这种服务接口实现为可在本地(“本地”指同一个进程空间)访问的简单的Java类,那么我们可以忽略IOException。但是如果我们需要把服务接口实现为Java远程方法协议(JRMP)对象,那么我们还需要定义另一个符合JRMP的接口,如下所示:
|
public interface CargoServiceRMIIntf
extends CargoServiceBI, Remote{
}
|
类似地,对于EJB实现来说,我们还需要定义另一个接口,如下所示:
|
public interface CargoServiceEJBIntf
extends CargoServiceBI, EJBObject{
}
|
这种接口分层的优点在于,当服务实现发生改变的时候,客户端的代码几乎不用修改。
|
public class ServiceInvoker{
public void invoke(){
CargoServiceBI cargoServiceBI = (CargoServiceBI )
getService(CargoServiceBI.SERVICE_NAME);
Map result = cargoServiceBI.bookSpace(cargoTOs, true);
}
}
|
上面代码中的getService方法使用另一个称为ServiceLocator的helper类取得所有与协议相关的特定的详细信息。getService方法也可以使用一个ServiceContext对象作为参数。ServiceContext是根据一些JVM参数进行初始化的,从这些JVM参数可以看出被调用的服务是在RMI、EJB还是在RPC上下文中实现的。由于我们的业务接口符合JAX-RPC,所以我们甚至可以将服务实现为Web服务,并且客户在不改变任何代码的情况下仍然可以调用服务。
讨论完业务接口,我们再来看服务方法。服务不是免费的,一个服务提供商根据下面这些标准向服务调用者收费:
1.
服务连接被初始化的次数。
2.
每个连接保持激活的时间(服务时间)。
3.
每次服务在客户端和服务端之间传输的数据总量。
4.
其他。
设计良好的接口需要解决这些问题,这就是所提供的服务的粒度所要解决的问题。每个接口方法都应该在最合适的层次上实现粗粒度。这种粗粒度方法具有下列属性:
1.
一次完成一项任务。
2.
作为独立的服务(或方法)提供给客户。然而,在内部,它可能被分解为许多客户不可见的其他服务调用。
3.
接收和返回一批数据。
public Map bookSpace(Collection cargoTOs, Boolean partBookingAllowed) 方法就是这种粗粒度方法的一个例子。假设服务调用者需要为三个不同的货盘预订空间,他既可以调用三次public Long bookSpace(CargoTO cargoTO) 方法,也可以仅调用一次public Map bookSpace(Collection cargoTOs, Boolean partBookingAllowed) 方法来完成这一任务。如果使用了第二种方法,客户端仅需要初始化一次连接,并且交互的数据量也比调用第一种方法三次少得多。如果上面的方法执行的是采购任务并且涉及到电子货币的转帐,那么采购方法中可能会需要第三方服务的支持来完成货币的转帐。很显然,使用第三方服务需要花钱,但是如果所有采购订单的电子货币转移能一次完成,就会比为每一次采购服务执行货币转帐的费用之和少很多。
在EJB实现中,我们需要一个(或更多的)EJB实现类。但是如果仔细研究服务接口就会发现,在EJB层中进一步的层次划分有一个范围。首先,我们将会有一个会话外观,它将被部署为远程的无状态的会话EJB。服务外观,例如会话外观,提供了一种几乎不包含业务逻辑的简单的粗粒度接口。业务对象则封装了一组相关的业务操作。应用程序实现的是那些协调多个业务对象和服务的用例。然而,我们不应该实现下面这种用例,即在业务对象中协调行为的用例,因为这会增加这些业务对象间的耦合性,降低结合能力。同样地,我们也不想在服务外观中增加业务逻辑,因为业务逻辑在不同的外观中可能会有重复,这会降低公共代码的可重用性和可维护性。所以我们需要在服务外观中提供可进行大批量空间预订的方法,但是将实际的预订工作交给一个应用程序服务(可以执行事务组合的服务)来处理。在EJB2.1中,我们可以将应用程序服务实现为本地的无状态的会话EJB,这样做的优点是我们仍然可以控制方法的事务属性。请看下面的例子:
|
/**
* Remote EJB
Implementation class
*/
public class CargoRemoteFacadeEJB
extends EJBAdapter implements
CargoServiceBI, SessionBean{
private CargoServiceBI cargoServiceBI;
public Long bookSpace(CargoTO cargoTO) throws ServiceException{
return localCargoServiceBI.bookSpace(cargoTO);
}
public Map bookSpace(Collection cargoTOs, boolean partBookingAllowed)
throws ServiceException{
return localCargoServiceBI.bookSpace(cargoTOs, partBookingAllowed);
}
/**
* Hook method, overridden from super class, and called
from
* within setSessionContext() method.
*/
protected void init(){
String context = ic.lookup("java:comp/env/CargoServiceContext");
cargoServiceBI = = (CargoServiceBI )
getService(CargoServiceBI.SERVICE_NAME, ContextFactory.getContext(context));
}
}
/**
* Local EJB
Implementation class
*/
public class CargoLocalServiceEJB
extends EJBAdapter implements
CargoServiceBI, SessionBean{
/**
* @TX TX_REQUIRED
*/
public Long bookSpace(CargoTO cargoTO) throws ServiceException{
//Implementation goes here, and only here
}
/**
* @TX TX_REQUIRED
*/
public Map bookSpace(Collection cargoTOs, boolean partBookingAllowed)
throws ServiceException{
Map returnMap = new HashMap();
CargoTO cargoTO = null;
for(Iterator iterator = cargoTOs.iterator(); iterator.hasNext();){
cargoTO = (CargoTO) iterator.next();
try{
if(partBookingAllowed){
returnMap.put(cargoTO.getId(),
((CargoServiceBI) getSessionContext().getEJBLocalObject())
.bookSpaceNewTX(cargoTO));
}
else{
returnMap.put(cargoTO.getId(),
bookSpace(cargoTO));
}
}
catch(ServiceException serviceException){
if(!partBookingAllowed){
getSessionContext().setRollbackOnly();
throw serviceException;
}
else{
returnMap.put(cargoTO.getId(),
CargoServiceBI.ERROR_STATUS);
}
}
}
return returnMap;
}
/**
* @TX TX_REQUIRES_NEW
*/
public Long bookSpaceNewTX(CargoTO cargoTO) throws ServiceException{
return bookSpace(cargoTO);
}
}
|
通过上面的例子可以看到,外观层作为服务层的代理(当然,外观层会有许多的方法,为用例的交互建立模型)。在服务层的EJB类中,我使用了一些非标准的文档标记,它们使得这种代理概念更加清楚。如果partBookingAllowed标志为true,本例中的服务类就会为“大批空间预订”方法执行一种事务组合。如果partBookingAllowed标志为true,我们需要在单独的不交叉的事务中完成单个的空间预订。直接重复调用bookSpaceNewTX是不够的,因为所有方法的重复调用都在封闭的事务上下文中处理,任何一个方法调用的失败都会影响到封闭事务中的其他方法调用。这就是为什么我们要使用SessionContext.getEJBLocalObject()
方法来控制类似EJB容器中服务的句柄。如果有一种机制可以告诉运行时间我们需要在新事务中执行方法就好了(也许在将来的某一天,Java元数据工具可以帮助我们做到这一点)。public Long bookSpaceNewTX(CargoTO
cargoTO) 是在EJB的实现类和接口中引入的一个新方法,它可以满足上面提到的这种需求。
上面所讲的分层或组合需求在简单的系统中也许不是那么明显。但是无论系统是简单还是复杂,如果它是一个成功的系统,那么即使用以前没有考虑到的许多不同方法也应该可以使用(或访问)该系统。因此,成功的设计必定会使系统在未来的发展中受益。
结束语
在本文中,首先,我们通过了解JSR计划对当前的开发情况有了一个总体的了解。然后,我们看到了如何使用业务接口模式设计优良的接口。接着,我们讨论了如何在EJB层次架构中合理分层的需求,以使我们能够在进行事务组合的同时消除代码的重复。最后,我们希望随着技术的成熟,我们将具有针对安全性和事务性等组件组合的规则。
| 作者简介 |
|
Binildas Christudas是工程学学士和系统学硕士,他还是Sun公司认证的Java平台程序员、软件开发员和企业级的系统架构师。他有6年IT行业的从业经验,目前供职于一家印度IT公司 —— 塔塔咨询服务公司,为瑞士航空、Sabena航空、新加坡航空、荷兰皇家航空公司等提供咨询服务。对于许多新一代航空系统的设计和开发,他的经验非常丰富。 |
作者其它文章
|