编程 提要 本文将与你一同探讨怎样把注解和方面的威力联合起来,以与EJB 3.0兼容的方式为企业实现提供声明性服务,而在同时仍然提供容器的独立性。
一、 引言
在我们共同寻求进一步提高软件开发生产性能的方法的过程中,我们-作为Java社团成员-一般都转向J2EE来提供针对企业开发中更具挑战性的技术问题如分布式事务管理、并发性和对象分布等的解决方案。其背后的指导思想-这些复杂的企业服务能被应用程序服务器供应商所实现并能为商业开发者所平衡-的确是一种很好的思想。J2EE,具体地说是EJB,已成功地提供了一个平台-在其上构建企业Java应用程序。
这其中部分的成功是由于能够进行声明性编程-一种程序开发方式-用这种方式,你可以声明基础结构服务而不是用商业逻辑明确地编码从而使代码散布于各处。EJB已经证明了这种编程方式的价值-通过允许企业问题例如事务和安全被用一种发布描述符所声明并为容器所处理。
然而,在过去的岁月中,越来越多的开发者认识到EJB在团队的生产效率方面给它自己带来新的大量的挑战-每个EJB必须伴随多个接口,以一种发布描述符描述,经由JNDI被存取,等等。而在容器外EJB上进行单元测试也带来另外的困难,如今EJB已不再把重点放在单纯的面向对象开发上。
请注意,为阅读本文您需具备如下工具:
·Java 2 SDK 1.5
·Maven 2.0 Beta 2
EJB 3.0的目标在于从以下几个方面使企业开发更为容易:
·通过引入元数据注解来实现声明性请求企业服务
·经由注解实现依赖性/资源注入
·实现企业beans与EJB特定接口的解耦
·经由轻量级的对象关系映射实现持续性存储的简化
这对于EJB开发者来说尤如一股春风-一直以来,他们竭力地从事开发、测试和维护EJB。利用EJB 3.0写一个企业bean现在变得很容易,就如用特定的注解创建一个POJO(传统的Java对象)以把它标明为一个EJB并请求企业服务。下面是一个来自于EJB 3.0 Public Draft中EJB的例子:
Stateful
public class CartBean implements ShoppingCart
{
private float total;
private Vector productCodes;
public int someShoppingMethod(){...};
...
}
EJB 3.0声明中实质上指明开发者需要的不是一重量级的、"一次发布满足所有"的解决方案,而是一个轻量级的、容易使用的解决方案-为开发者提供一定范围的企业服务。为此,EJB 3.0所提供的最重要的方法之一就是实现企业beans与EJB API的解耦。并且,此解决方案还带来令人感兴趣的衍生-EJB现在不仅能够运行在不同的EJB容器上,而且还能运行于任何应用程序框架内部-这些框架必须能够识别EJB 3.0(JSR 220)和用于声明企业服务的普通注解(JSR 250)。
本文没有提供关于声明性编程、EJBs、方面或注解的深度探索。相反,而只是分析一下这些技术之间的相互关系并讨论如何把它们用一种新的方式结合起来以简化应用程序开发。
在本文中,你将会学习到如何编写一个EJB 3.0兼容的bean并且通过创建几个简单的方面使其具有声明性事务管理、安全和资源注入等功能。我希望您能从这个练习中得到以下的受益:
·学习方面的三个实际应用(依赖性注入、安全和事务)。
·熟悉EJB 3.0及其背后的思想。
·认识到怎样实现EJB与特定API的解耦以允许EJB 3.0兼容的服务能够以轻量级实现而不是仅由EJB来提供。
二、 实例应用程序-航班订购
在整个后面的讨论中,你将学习到一个航班订购系统的实现-它使用方面和注解来实现依赖性注入、安全和事务管理。该应用程序仅执行两项功能:它允许用户搜索航班(图1),然后订购一次旅行(图2)。这两个操作都将被进行安全处理以仅允许能被识别的用户来执行它们。另外,既然"订购旅行"操作包含订购两个航班(外出和返回航班),那么需要把该操作创建为事务性的-如,两个订购将作为一个工作单元要么都成功要么都失败。
图1.航班查询:首先,用户查找满足他们的指定标准的航班。
图2.航班订购:接下来,用户订购一个外出航班和一个返回航班。两个订购要么都成功要么都失败。
这个简单的Web应用程序包含几个servlet、一个服务外观和一个DAO层(见图3)。
资源配置、安全性和事务管理等横切关注点将由方面(用AspectJ 1.5 M3实现)所提供以实现在Java 5注解中所声明的注入行为。
图3.航班订购系统架构:这个航班订购系统包括三个主要组成组件-它们联合起来共同完成用户请求。 三、 资源注入
EJB 3.0草案声明中允许资源经由Resource注解来声明(这一决定定义在草案普通注解声明中)并且被容器注入进你的EJB。依赖性注入是一项技术-使用这种技术,一个对象外部的实体而不是显式地为该对象所创建的实体能够提供(注入)一个对象的依赖性。它有时被描述为好莱坞原则-这开玩笑似地意味着"不要给我们打电话,我们会给你打电话的"。
以TravelAgencyServiceImpl类为例-这个类为了持续性存储一些数据需要找到一个IFlightDAO接口的实现。传统地,这是经由一个工厂、singleton、服务定位器或一些另外的定制解决方案来实现的。其中,一个可能的解决方案看上去如下所示:
public class TravelAgencyServiceImpl implements ITravelAgencyService
{
public IFlightDAO flightDAO;
public TravelAgencyServiceImpl()
{ flightDAO = FlightDAOFactory.getInstance().getFlightDAO(); }
public void bookTrip(long outboundFlightID, long returnFlightID, int seats)
throws InsufficientSeatsException
{
reserveSeats(outboundFlightID, seats);
reserveSeats(returnFlightID, seats);
}
}
你已看到,这个实现包含创建一个特定的工厂类-它很可能读取存储在某处的配置信息以了解要创建IFlightDAO的实现方式。如果不是让服务显式地创建它的由容器所注入的依赖性,那么配置细节和对象创建将被代理到容器上。这允许一个应用程序中的组件能够被容易地连接到一起-用不同的配置并且消除大量老式的singleton和工厂代码。
该类的一个实现-它依赖于一个用JSR 250资源注解所声明的IFlightDAO的实现-可能看上去如下所示:
public class TravelAgencyServiceImpl implements ITravelAgencyService
{
Resource(name = "flightDAO")
public IFlightDAO flightDAO;
public void bookTrip(long outboundFlightID, long returnFlightID, int seats)
throws InsufficientSeatsException
{
reserveSeats(outboundFlightID, seats);
reserveSeats(returnFlightID, seats);
}
}
在这种情况下,容器将把一个命名为"flightDAO"的资源的正确实现提供给服务类。但是,如果你现在就想利用资源注入,而不是等待EJB 3.0发行版,又该如何呢?好,你可以采用一种轻量级的容器-它能够提供例如Spring或Pico Container的依赖性注入。然而,当前我还不了解存在一个轻量级的容器-它能够使用JSR 250资源注解以指定注入要求(尽管我非常盼望在这一方面出现一些)。
一种解决方案是使用方面来实现依赖性注入。如果你为此使用Resource注解,那么你的实现将与EJB 3.0方式一致并且向前兼容EJB 3.0实现-而实现这并不是很困难的事情。下列列表显示用AspectJ创建的一个方面-它注入用Resource注解所注解的字段:
Aspect
public class InjectionAspect
{
private DependencyManager manager = new DependencyManager();
Before("get(Resource * *.*)")
public void beforeFieldAccesses(JoinPoint thisJoinPoint)
throws IllegalArgumentException, IllegalAccessException
{
FieldSignature signature = (FieldSignature) thisJoinPoint.getSignature();
Resource injectAnnotation = signature.getField().getAnnotation(Resource.class);
Object dependency = manager.resolveDependency(signature.getFieldType(),injectAnnotation.name());
signature.getField().set(thisJoinPoint.getThis(), dependency);
}
}
这个简单方面所做的全部是,从一个属性文件(这个逻辑被封装在DependencyManager对象中)查询实现类并且在存取字段之前把它注入到用Resource注解所注解的字段中。显然,这种实现不是完整的,但是它确实说明了你可以怎样以一种JSR 250兼容方式且不需采用EJB来提供资源注入。
四、 安全性
除了资源注入外,JSR 250和EJB 3.0还提供经由注解的元数据安全表示。javax.annotation.security包定义了五个注解-RunAs,RolesAllowed,PermitAll,DenyAll和RolesReferenced-所有这些都能应用到方法上来定义安全要求。例如,如果你想要声明上面列出的bookFlight方法仅能为具有"user"角色的调用者所执行,那么你可以用如下的安全约束来注解这个方法:
public class TravelAgencyServiceImpl implements ITravelAgencyService
{
Resource(name = "flightDAO")
public IFlightDAO flightDAO;
RolesAllowed("user")
public void bookTrip(long outboundFlightID, long returnFlightID, int seats)
throws InsufficientSeatsException
{
reserveSeats(outboundFlightID, seats);
reserveSeats(returnFlightID, seats);
}
}
这个注解将指出由容器来负责保证只有指定角色的调用者才能执行这个方法。因此现在我将展示另一个简单的方面-它将进一步加强在该应用程序上的安全约束:
Aspect
public class SecurityAspect
{
Around("execution(javax.annotation.security.RolesAllowed * *.*(..))")
public Object aroundSecuredMethods(ProceedingJoinPoint thisJoinPoint)
throws Throwable
{
boolean callerAuthorized = false;
RolesAllowed rolesAllowed = rolesAllowedForJoinPoint(thisJoinPoint);
for (String role : rolesAllowed.value())
{
if (callerInRole(role))
{ callerAuthorized = true; }
}
if (callerAuthorized)
{ return thisJoinPoint.proceed(); }
else
{
throw new RuntimeException("Caller not authorized to perform specified function");
}
}
private RolesAllowed rolesAllowedForJoinPoint(ProceedingJoinPoint thisJoinPoint)
{
MethodSignature methodSignature = (MethodSignature) thisJoinPoint.getSignature();
Method targetMethod = methodSignature.getMethod();
return targetMethod.getAnnotation(RolesAllowed.class);
}
private boolean callerInRole(String role)
{ ... }
}
这个方面包含了所有方法的执行-通过核实该调用者是在注解中所指定的角色之一,用RolesAllowed注解来注解并且保证调用者被授权调用该方法。当然你还能代之以任何你喜欢的算法来授权用户并且检索他/她的角色,例如JAAS或一个定制的解决方案。在本示例程序中,为方便起见,我选择代理到servlet容器。
五、 事务
事务成为企业开发的一个重要部分-因为它们有助于在一个并发的环境中的数据集成。从一个高层次上看,事务可以通过多种或者是完整的或者是都不完整的操作来保证这一点。
不象针对资源注入和安全的注解,针对事务的注解是特定于EJB 3.0的并且没有在JSR 250普通注解中定义。EJB 3.0定义了两个与事务相联系的注解:TransactionManagement和TransactionAttribute。该TransactionManager注解指定事务是由容器所管理还是为bean所管理的。在EJB 3中,如果这个注解没被指定,那么将使用容器所管理的事务。TransactionAttribute注解用于指定方法的事务传播级别。有效值-包括强制的、要求的、要求新的、支持的、不支持的和从不支持的-用来定义是否要求一个已有事务或启动一个新的事务,等等。
因为bookFlight操作包含两步-订购一个外出航班和一个返回航班,所以,通过把它包装成一个事务,你能保证这项操作的一致性。通过使用EJB 3.0事务注解,这将看上去如下所示:
public class TravelAgencyServiceImpl implements ITravelAgencyService
{
Resource(name = "flightDAO")
public IFlightDAO flightDAO;
RolesAllowed("user")
TransactionAttribute(TransactionAttributeType.REQUIRED)
public void bookTrip(long outboundFlightID, long returnFlightID, int seats)
throws InsufficientSeatsException
{
reserveSeats(outboundFlightID, seats);
reserveSeats(returnFlightID, seats);
}
}
并且你可以应用一个简单的方面来自动地界定事务边界:
Aspect
public class TransactionAspect
{
Pointcut("execution(javax.ejb.TransactionAttribute * *.*(..))")
public void transactionalMethods(){}
Before("transactionalMethods()")
public void beforeTransactionalMethods()
{ HibernateUtil.beginTransaction(); }
AfterReturning("transactionalMethods()")
public void afterReturningTransactionalMethods()
{ HibernateUtil.commitTransaction(); }
AfterThrowing("transactionalMethods()")
public void afterThrowingTransactionalMethods()
{ HibernateUtil.rollbackTransaction(); }
}
这个实现基于这样的假设-Hibernate和无所不在的线程本地模式被用于管理Hibernate会话和事务对象;但是,任何其它适当的实现,例如基于JTA的实现,都能被代替使用。
六、 小结
通过使用EJB 3.0和JSR 250注解集,本文已经展示了例如资源管理、安全和事务等横切关注点是怎样被实现为方面的。当然,还有许多内容我们需进一步学习。首先要学的就是通过使用AspectJ的实现这些示例方面为模块化横切关注点所提供的蓝图。其次,我们已经看到了在如今正浮出水面的EJB 3.0声明背后的一些新思想和新概念。最后,我们还以戏剧性的方式看到了从EJB API中解耦我们的商业对象所必须要提供的自由。在这一点上,所有你想使TravelAgencyServiceImpl成为一个无状态会话要做的仅是添加一条最后的注解:
Stateful
public class TravelAgencyServiceImpl implements ITravelAgencyService
{ ... }
最后,我非常希望这种自由地提供企业服务的方式会带来框架/容器工业界的竞争和革新。