实战如何使用 WebSphere Application Server 提供的 Embeddable EJB Container 开发并测试 EJB 应用

EJB 3.1 规范中提出 Embeddable EJB Container,旨在方便开发人员对 EJB 的应用程序进行开发和测试,改进了传统意义上需要运行时环境才能进行 EJB 程序测试的不利因素。使用 Embeddable EJB Container 程序开发人员只需要在相对方便简洁的 J2SE 环境中便可对 EJB 的业务逻辑进行测试。WebSphere Application Server V8 支持 EJB 3.1 的这一新特性。本文将详细介绍如何使用 WebSphere Application Server V8 中提供的 Embeddable EJB Container 来进行 EJB 3.1 实例程序的开发和基于 Junit 框架的单元测试。

张 冠楠, 软件工程师, IBM

张冠楠,2010 年加入 IBM,在 IBM 中国软件开发中心从事 WebSphere 应用服务器系统测试工作。感兴趣的技术领域包括 Java EE 6 , BIRT 等开源项目。



2012 年 2 月 16 日

免费下载:IBM® WebSphere® Application Server 试用版
下载更多的 IBM 软件试用版,并加入 IBM 软件下载与技术交流群组,参与在线交流。

引言

Embeddable EJB Container 是 EJB 3.1 规范中提出的新特性,需要各大厂商的应用服务器支持轻量级的 EJB 运行时环境,方便开发人员在 J2SE 的环境中进行 EJB 应用的开发、调试和测试。WebSphere Application Server V8(以下简称 WAS V8) 提供的 Embeddable EJB Container 除了支持 EJB 3.1 规范中的新特性 EJB Lite 外,还在此基础上支持 JDBC 数据源的配置和使用、依赖注入以及 Bean Validation。Embeddable EJB Container 的引入带来了很大便利,如不再需要应用服务器的安装、内存和硬盘消耗较小、启动较快等。更多关于 EJB Lite 所包含的 EJB 特性以及 WebSphere Application Server V8 所支持的 EJB 特性请参考参考资源 [1] 获得更多内容。


实例化 Embeddable EJB Container

在使用 Embeddable EJB Container 之前,需要在您的应用环境中正确实例化,这要求您的开发环境必须满足如下的前提条件并做好相应的准备:

  • JDK 或者 JRE 的版本必须是 6.0 或者以上。
  • 在您的开发环境的类路径中包括文件 com.ibm.ws.ejb.embeddableContainer_8.0.0.jar。该 JAR 文件在您 WAS 的安装路径下。如您的安装路径变量为 app_server_root , 则该文件在 <app_server_root>\runtimes\ 下。
  • 如果在您的应用程序当中使用了注解 @Resource,则需要在您开发环境的类路径下包含文件 <app_server_root>\runtimes\endorsed\endorsed_apis_8.0.0.jar。
  • 如果您的应用程序使用了 JPA,则需要在您的开发环境的类路径下面包含文件 <app_server_root>\runtimes\com.ibm.ws.jpa.thinclient_8.0.0.jar。
  • 您的开发工程中至少包含一个 EJB 模块,或者是以 EJB JAR 文件的形式放到类路径下面,或者是编译后的 class 文件放到 \class 的目录下。
  • 需要提供一个包含 main 方法的类来创建 embeddable container,或者是在 Junit 的环境中作为测试组件进行创建。

做好了上述准备后,便可以在您的开发环境中使用 embeddable container。本文使用 eclipse 作为 IDE 进行示例,创建了工程 EmbeddableEJBContainerDemo,作为对 EJB 进行单元测试的工程,本文主要使用 Junit4 框架对 EJB 的业务逻辑进行测试,因此该工程同样需要包含 Junit4 的库在类路径当中。创建好的工程如图 1 所示,为方便后续特性的引入,这里便将所有可能用到的 JAR 包都包含进工程来。本文所有的示例代码均可以在附件中获得。

图 1. 用于初始化 Embeddable EJB Container 的工程示例
图 1. 用于初始化 Embeddable EJB Container 的工程示例

使用 Junit4 框架创建测试类

在工程 EmbeddableEJBContainerDemo 中创建 Junit Test Case 名为 EmbeddableEJBContainerTest.java 的类,此类用来进行 EJB 相关特性的单元测试工作,为方便读者查看,所有的单元测试用例方法都放到此类中。代码清单 1 所示为该类目前的代码片段,即初始化 embeddable container 并判断其是否为空。后续此类将会不断的增加内容。在此类中,基于 Junit 的特性,将初始化的工作放到有注解 @Before 的 setUp() 方法中,并将资源回收工作放到有注解 @After 的 tearDown() 方法中,核心的单元测试工作在有注解 @Test 的 testContainerNotNull 的方法当中。

清单 1. 初始化 Embeddable EJB Container
 package com.zgn.test; 

  import java.util.HashMap; 
  import java.util.Map; 
  import javax.ejb.embeddable.EJBContainer; 
  import org.junit.After; 
  import org.junit.Before; 
  import org.junit.Test; 
  import static org.junit.Assert.*; 

 public class EmbeddableEJBContainerTest { 
   Map<String,Object> properties = new HashMap<String,Object>(); 
   EJBContainer container; 
 @Before 
 public void setUp() throws Exception { 
   properties.put(EJBContainer.PROVIDER, 
  "com.ibm.websphere.ejbcontainer.EmbeddableContainerProvider"); // 设置属性值
 container = EJBContainer.createEJBContainer(properties); // 初始化 embeddable container 
 } 
 @After 
 public void tearDown() throws Exception { 
   container.close(); 
  } 	
 @Test 
 public void testContainerNotNull(){ 
   assertNotNull(container); 
  } 
 }

以 Junit 测试用例的方法来运行该类,因为变量 container 已经在 setUp() 方法中进行了初始化,因此该测试会通过。Junit 的运行效果如图 2 所示。Embeddable container 初始化成功后便可以对 EJB 的特性进行调用和测试,如果使用代码清单 1 的方式进行 EJB 的调用和测试,则相关 EJB 组件需要以 EJB JAR 的方式存在于本工程的类路径里面。本文后面章节将详细介绍如何在 Junit 的框架下使用该 Embeddable EJB Container 对其支持的 EJB 特性进行单元测试。

图 2. Junit 测试 embeddable container 初始化成功。
图 2. Junit 测试 embeddable container 初始化成功。

以属性值的形式引入 EJB 组件

在代码清单 1 中我们知道可以将需要使用的 EJB 组件放到工程的类路径下面,WAS V8 的 embeddable container 支持另外一种方式引入 EJB 组件,即使用文件数组作为属性参数的方式。参考代码清单 2 所示,如果想对 TestEJB1.jar 和 TestEJB2.jar 进行单元测试,则将这两个 JAR 文件以属性值的方式放到变量 EJBContainer.MODULES 中,embeddable container 同样会正确识别该 EJB 组件。示例中,TestEJB1.jar 和 TestEJB2.jar 都依赖于不在类路径下面的 SharedLib.jar 文件,如果需要正确加载该文件则需要改变当前线程的上下文类加载器,代码清单 2 中亦有体现。

清单 2. 以属性值的形式引入 EJB 组件示例
 @Before 
 public void setUp() throws Exception { 
   File[] ejbModules = new File[2]; 
   ejbModules[0] = new File("/Directory/TestEJB1.jar"); 
   ejbModules[1] = new File("/Directory/TestEJB2.jar"); 
   properties.put(EJBContainer.MODULES, ejbModules); // 以属性值方式引入 EJB 组件
   File sharedLibUtilityFile = new File("/Directory/SharedLib.jar"); 
   ClassLoader oldCL = Thread.currentThread().getContextClassLoader(); 
   ClassLoader newCL = new URLClassLoader(new URL[]{sharedLibUtilityFile.toURI().toURL()},
     oldCL); 
  Thread.currentThread().setContextClassLoader(newCL); // 改变上下文类加载器
   container = EJBContainer.createEJBContainer(properties); 
 }

Embeddable EJB Container 支持的特性

WebSphere Application Server V8 中的 Embeddable EJB Container 支持 EJB 3.1 规范中的本地 Session Bean,包括有状态的(stateful bean)、无状态的(stateless bean)、单例(singleton bean)以及无接口(No-Interface local bean),同步方法,事务,安全,拦截器,JPA 2.0,数据源的配置,依赖注入以及 Bean Validation。本章节将就上述各个特性进行详细的介绍。

创建 EJB 工程

首先需要创建一个简单的 EJB 工程,本示例中创建了 EJB 工程 SimpleEJBDemo,本文中所有关于 EJB 逻辑的代码都将放到该工程当中。为保证编译通过,同工程 EmbeddableEJBContainerDemo 一样,需要在类路径中加入相应的 JAR 包,如图 3 所示。

图 3. EJB 工程示例
图 3. EJB 工程示例

EJB Session Bean

Stateless Session Bean

Embeddable EJB Container 只支持 Local 的 Session Bean,因此本文示例均使用 Local 的接口。在工程 SimpleEJBDemo 的源代码中创建包 com.zgn.ejb.test 并在包中创建接口 TestStatelessSessionLocal.java,同时创建类 TestStatelessSessionBean.java 实现该接口,如代码清单 3 所示。

清单 3. Stateless Session Bean 示例
 TestStatelessSessionLocal.java:
 package com.zgn.ejb.test; 

 import javax.ejb.Local; 
 @Local 
 public interface TestStatelessSessionLocal { 
 public String getInfo(); 
 } 

 TestStatelessSessionBean.java:
 package com.zgn.ejb.test; 

 import javax.ejb.Stateless; 
 @Stateless 
 public class TestStatelessSessionBean implements TestStatelessSessionLocal { 
 @Override 
 public String getInfo() { 
 System.out.println("This is a simple EJB Embedded Container test for session bean"); 
 return "Good Test For Stateless Session Bean"; // 返回字符串
 } 
 }

完成上述类的编写后将工程 SimpleEJBDemo 打包成 SimpleEJBDemo.jar 并放到工程 EmbeddableEJBContainerDemo 的类路径下面,这样 embeddable container 便会搜索其类路径下面的 EJB 组件并加载到容器中。同时为完成 Junit 单元测试,我们需要在类 EmbeddableEJBContainerTest.java 中加入方法用以完成对该 stateless session bean 的测试,本文后续章节所有的 Junit 测试代码均放到该类中,如代码清单 4 所示。

清单 4. 测试 Stateless Session Bean 的 Junit 代码示例
 @Test 
 public void testEJBStatelessSessionBean() throws Exception{ 
  Context ctx = container.getContext(); 
  // 使用 JNDI 查找 TestStatelessSessionBean 的实例
 TestStatelessSessionLocal testSessionLocalBean 
   = (TestStatelessSessionLocal)ctx.lookup(
    "java:global/SimpleEJBDemo/
    TestStatelessSessionBean!com.zgn.ejb.test.TestStatelessSessionLocal"); 
 // 通过方法 assertEquals() 判断该 bean 的方法 getInfo() 是否正确返回字符串
 assertEquals("Good Test For Stateless Session Bean", testSessionLocalBean.getInfo()); 
 }

同代码清单 1,以 Junit 测试用例的方法来运行类 EmbeddableEJBContainerTest.java 会得到如图 2 相似的测试通过界面,这里为防止赘述,将所有 Junit 的测试结果放到本章的最后统一给出。

Stateful Session Bean

在工程 SimpleEJBDemo 中创建 TestStatefulSessionLocal.java 接口和 TestStatefulSessionBean.java 类。该 session bean 中用一个变量 resultString 存放用户每次输入的字符串追加的最终结果,每个用户会维护自身的一个 resultString,它会将该用户输入的字符串进行追加并通过方法 getResult() 方法给出一个最终结果。

清单 5. Stateful Session Bean 示例
 TestStatefulSessionLocal.java:
 package com.zgn.ejb.test; 
 import java.io.Serializable; 
 public interface TestStatefulSessionLocal extends Serializable{ 
  public String append(String str); 
  public String getResult(); 
 } 

 TestStatefulSessionBean.java:
  package com.zgn.ejb.test; 
  import javax.ejb.Stateful; 
  @Stateful 
 public class TestStatefulSessionBean implements TestStatefulSessionLocal { 
   private static final long serialVersionUID = 1L; 
   private String tempString = ""; 
   private String resultString = "Stateful Session Bean "; 
   // 该方法将其参数 str 追加到变量 tempString 上
  @Override 
   public String append(String str) { 
   tempString += str; 
   return tempString; 
 } 
 // 该方法将 append() 方法赋值完毕的 tempString 追加到变量 resultString 上并返回
 @Override 
 public String getResult() { 
   resultString += tempString; 
   return resultString; 
   } 
 }

在代码清单 6 中可以看出,从 TestStatefulSessionBean 的两个实例 testSessionLocalBean1 和 testSessionLocalBean2 的方法 getResult() 得到的字符串都是自己输入的,其中 testSessionLocalBean1 最终得到的字符串为“Stateful Session Bean Test First”,而 testSessionLocalBean2 得到的则为“Stateful Session Bean Test Second”,从而验证了 stateful session bean 的特性。

清单 6. 测试 Stateful Session Bean 示例
 @Test 
 public void testEJBStatefulSessionBean() throws Exception{ 
   Context ctx = container.getContext(); 
   TestStatefulSessionLocal testSessionLocalBean1 = (TestStatefulSessionLocal)ctx. 
   lookup("java:global/SimpleEJBDemo/TestStatefulSessionBean"); 
   testSessionLocalBean1.append("Test First"); 
 // 判断 TestStatefulSessionBean 的实例 1 testSessionLocalBean1 是否正确进行了字符串的累加
   assertEquals("Stateful Session Bean Test First",testSessionLocalBean1.getResult()); 
   TestStatefulSessionLocal testSessionLocalBean2 = (TestStatefulSessionLocal)ctx. 
   lookup("java:global/SimpleEJBDemo/TestStatefulSessionBean"); 
   testSessionLocalBean2.append("Test Second"); 
 // 判断 TestStatefulSessionBean 的实例 2 testSessionLocalBean2 是否正确进行了字符串的累加
   assertEquals("Stateful Session Bean Test Second",testSessionLocalBean2.getResult()); 
 }

No-Interface View 的 Singleton Bean

EJB 3.1 支持 No-Interface View 使开发者不需要开发接口类,直接将实现类暴露给客户端,本节将该特征与 EJB 的 Singleton Bean 结合起来使用。参考代码清单 7,直接创建实现类 TestSingletonNoInterfaceBean.java,该类包含了生命周期的管理,当实例化的时候通过方法 @PostConstruct 和销毁的时候通过方法 @PreDestroy 分别输出字符串方便我们查看。

清单 7. No-Interface View 的 Singleton Bean 示例
 package com.zgn.ejb.test; 

  import javax.annotation.PostConstruct; 
  import javax.annotation.PreDestroy; 
  import javax.ejb.LocalBean; 
  import javax.ejb.Singleton; 
 @Singleton 
 @LocalBean 
 public class TestSingletonNoInterfaceBean { 
   public TestSingletonNoInterfaceBean() { 
  } 
 @PostConstruct 
 public void initBean(){ 
  System.out.println("TestSingletonNoInterfaceBean is being initialized."); 
 } 
 public String getInfo(){ 
   return "Good Test For TestSingletonNoInterfaceBean"; 
 } 
 @PreDestroy 
 public void destoryBean(){ 
   System.out.println("TestSingletonNoInterfaceBean is being destroyed."); 
   } 
 }

使用代码清单 8 中的测试代码对该 No-Interface View 的 Singleton Bean 进行 Junit 测试的时候会在控制台输出如下的信息:

 TestSingletonNoInterfaceBean is being initialized. 
 TestSingletonNoInterfaceBean is being destroyed.
清单 8. 测试 No-Interface View 的 Singleton Bean 示例
 @Test 
 public void testEJBSingletonNoInterfaceBean() throws Exception{ 
   Context ctx = container.getContext(); 
   TestSingletonNoInterfaceBean testSingletonBean = (TestSingletonNoInterfaceBean)ctx. 
   lookup("java:global/SimpleEJBDemo/TestSingletonNoInterfaceBean"); 
 // 判断 TestSingletonNoInterfaceBean 的方法 getInfo() 是否正确返回值
   assertEquals("Good Test For TestSingletonNoInterfaceBean",
   testSingletonBean.getInfo()); 
 }

拦截器 (Interceptor)

某些业务逻辑需要在调用 EJB 方法的同时自动执行一些方法,这些方法可统称为拦截器(Interceptor),用来完成特定的功能。Embeddable Container 也支持拦截器。在代码清单 9 中, TestInterceptorBean.java 类声明了一个类级别的 Interceptor,当调用该类中的方法 getInfo() 时会自动执行类 Interceptor.java 中的以 @AroundInvoke 注解的方法 print()。

清单 9. Interceptor 示例
 TestInterceptorBean.java:
 package com.zgn.ejb.test; 

  import javax.ejb.LocalBean; 
  import javax.ejb.Stateless; 
  import javax.interceptor.Interceptors; 
  @Stateless 
  @LocalBean 
   @Interceptors({Interceptor.class}) 
 // 此处定义 TestInterceptorBean 的所有方法在执行时需要使用 Interceptor.java 类作为拦截器做相应处理
  public class TestInterceptorBean { 
  public String getInfo(){ 
  return "Good Test For Interceptor Bean"; 
  } 
 } 

 Interceptor.java:
 package com.zgn.ejb.test; 

 import javax.interceptor.AroundInvoke; 
 import javax.interceptor.InvocationContext; 
 public class Interceptor { 
 @AroundInvoke // 该注释用以说明该类在作为拦截器时会被自动调用的方法
 public Object print(InvocationContext ctx) throws Exception{ 
 System.out.println("Simple EJB Embedded Container Test for Interceptor"); 
 return ctx.proceed(); 
  } 
 }

使用代码清单 10 中的测试代码对该 Interceptor 进行测试,在 Junit 框架进行验证的同时会在控制台输出如下的信息用以验证该 Interceptor 是否正确工作:

Simple EJB Embedded Container Test for Interceptor

清单 10. 测试 Interceptor 示例
 @Test 
 public void testEJBInterceptor() throws Exception{ 
  Context ctx = container.getContext(); 
  TestInterceptorBean testInterceptor = (TestInterceptorBean)ctx. 
  lookup("java:global/SimpleEJBDemo/TestInterceptorBean"); 
  // 判断 TestInterceptorBean 的方法 getInfo() 的返回值
  assertEquals("Good Test For Interceptor Bean",testInterceptor.getInfo()); 
 }

数据源的配置使用和依赖注入

Embeddable EJB Container 支持依赖注入的使用,包括获得资源以及对其他 EJB 对象的引用。同时,WAS V8 所提供的 embeddable container 在 EJB Lite 的基础上同时支持对数据源的配置和使用,因此本文将如何使用数据源以及依赖注入合并到一起。参考代码清单 11 所示,TestDSBean.java 以 @Resource 的形式依赖注入了数据源“jdbc/AccountDS”和“jdbc/AccountNoTxDs”。该 Session Bean 主要提供了两个方法,saveAccount() 用来向数据库写入数据,getUsername() 用来以给定的 id 查询表中的另一个字段,其操作的实体 bean 为 Account.java,参见代码清单 12 所示。Account.java 对应的数据库表名称为 accountable,包括两个字段:主键 id 以及 usename 列。

清单 11. 测试数据源的 Session Bean 示例
 package com.zgn.ejb.test; 
  import javax.ejb.LocalBean; 
  import javax.ejb.Stateless; 
  import javax.persistence.EntityManager; 
  import javax.persistence.PersistenceContext; 
  import javax.sql.DataSource; 
  import javax.annotation.Resource; 
  import javax.annotation.Resources; 
  import com.zgn.ejb.test.entity.Account; 
 @LocalBean 
 @Stateless 
 @Resources({ // 此处引入该 session bean 所需要的数据源
 @Resource (name="jdbc/AccountDS", type=DataSource.class), 
 @Resource (name="jdbc/AccountNoTxDS", type=DataSource.class) 
  }) 
 public class TestDSBean { 
  @PersistenceContext (unitName="AccountPU") 
  private EntityManager em; 	
  // 该方法通过参数 id 返回实体 bean Account.java 的另一个字段
  public String getUsername(Long id)throws Exception{ 		
   Account account = em.find(Account.class, id); 
   return account.getUserName(); 
   } 
   // 该方法将实体 bean 的某一实例持久化
   public Account saveAccount(Account account){ 
  return em.merge(account); 
  } 
 }
清单 12. 实体 Bean 示例
 package com.zgn.ejb.test.entity; 
 import javax.persistence.Entity; 
 import javax.persistence.Id; 
 import javax.persistence.Table; 
 @Entity 
 @Table(name = "accountable") // 该 bean 对应的数据库中的表名称
 public class Account { 
   @Id 
 // 该 bean 中的字段 id 和 userName,其中 id 为表 accountable 的主键
 private Long id; 
 private String userName; 
 public Long getId() { 
   return id; 
 } 
 public void setId(Long id) { 
    this.id = id; 
 } 
 public String getUserName() { 
   return userName; 
 } 
 public void setUserName(String userName) { 
   this.userName = userName; 
 } 
 }

WAS V8 提供的 Embeddable EJB Container 在使用数据源操作数据库时,需要以属性的方式显示定义数据源等相关配置信息,并支持两种方法来定义属性值。用户可以在自己的工程根目录下面定义文件 embeddable.properties,并将属性值定义在其中,如代码清单 13 所示。由此文件可以看出本文使用了 DB2 数据库,因此在工程中需要将 DB2 的驱动放到类路径下面。您可以从图 4 中看到此时的工程缩略。同时,用户也可以编程的方式定义这些属性值,参见代码清单 1 中的变量 properties,使用 properties.put("DataSource.ds1.name", "jdbc/AccountDS") 也可以达到同 embeddable.properties 同样的效果。Embeddable EJB Container 提供了许多属性用以配置数据源,如数据连接池的大小、操作语句的最大值以及是否嵌入到事务中等等。关于更多关于数据源配置以及其它的配置信息,详见参考资源 [2]。

清单 13. embeddable.properties 示例
 DataSource.ds1.name=jdbc/AccountDS # 指定数据源的 JNDI 名称
 DataSource.ds1.className=com.ibm.db2.jcc.DB2DataSource # 指定数据源的类
 DataSource.ds1.databaseName=EJBTEST # 指定数据源对应的数据库的名称
 DataSource.ds1.transactional=true # 指定以该数据源进行数据库操作时是否基于事务
 DataSource.ds1.driverType=4 # 指定数据源的连接类型
 DataSource.ds1.serverName=localhost # 指定数据库所在的主机名称
 DataSource.ds1.portNumber=50000 # 指定数据库的端口号
 DataSource.ds1.user=administrator # 指定数据库的用户名
 DataSource.ds1.password=password # 指定数据库的密码

 DataSource.ds2.name=jdbc/AccountNoTxDS 
 DataSource.ds2.className=com.ibm.db2.jcc.DB2DataSource 
 DataSource.ds2.databaseName=EJBTEST 
 DataSource.ds2.transactional=false 
 DataSource.ds2.driverType=4 
 DataSource.ds2.serverName=localhost 
 DataSource.ds2.portNumber=50000 
 DataSource.ds2.user=administrator 
 DataSource.ds2.password=password 

 # 以下属性指定了 @Resource 所定义的数据源名称和所定义的全局 JNDI 名称进行绑定
 Bean.#SimpleEJBDemo#TestDSBean.ResourceRef.BindingName.jdbc/AccountDS=jdbc/AccountDS 
 Bean.#SimpleEJBDemo#TestDSBean.ResourceRef.BindingName.jdbc/AccountNoTxDS
 =jdbc/AccountNoTxDS
图 4. 配置了数据源的工程缩略图
图 4. 配置了数据源的工程缩略图

代码清单 14 给出了测试该数据源的代码,该段代码首先向数据库中存入一条数据,然后以该条数据的 id 字段查询出其 usename 字段并判断其正确性。

清单 14. 测试数据源示例
 @Test 
 public void testEJBDataSource() throws Exception{ 
 Account account = new Account(); 
 // 如下实例化 Account 并设置 id 为 1,userName 为 administrator 
 account.setId(1L); 
 account.setUserName("administrator"); 
 Context ctx = container.getContext(); 
 TestDSBean testDS = (TestDSBean)ctx.lookup("java:global/SimpleEJBDemo/TestDSBean"); 
 testDS.saveAccount(account);// 将实例化的 Account 进行持久化存储到数据库中
 // 通过上面持久化的 Account 实例,以参数 1 获得其 userName 字段并判断是否为 administrator 
 assertEquals("administrator",testDS.getUsername(1L)); 
 }

JPA2.0 的支持

前面的章节介绍了 Embeddable EJB Container 对数据源配置的支持,其中实体 Bean 中操作数据库的部分使用的便是 JPA 规范,embeddable container 支持 JPA 2.0 规范,您可以从代码清单 15 中查看上述 EJB 的 persistence.xml 文件,其中只定义了属性 “openjpa.jdbc.SynchronizeMappings” 用来创建实体 Bean 对应的表 accountable,用户也可以定义更多的属性。

清单 15. Persistence.xml 文件示例
 <?xml version="1.0"?> 
 <persistence xmlns="http://java.sun.com/xml/ns/persistence"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.0"
 xsi:schemaLocation="http://java.sun.com/xml/ns/persistence 
 http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"> 
 <persistence-unit name="AccountPU" transaction-type="JTA"> 
  <!-- 定义了数据源 --> 
 <jta-data-source>java:comp/env/jdbc/AccountDS</jta-data-source> 
 <non-jta-data-source>java:comp/env/jdbc/AccountNoTxDS</non-jta-data-source> 
 <!-- 指定了实体 bean--> 
 <class>com.zgn.ejb.test.entity.Account</class> 
 <exclude-unlisted-classes>true</exclude-unlisted-classes> 
 <properties> 
  <!-- 指定在持久化时创建实体 bean 对应的表 --> 
 <property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema"/> 
 </properties> 
 </persistence-unit> 
 </persistence>

编程方式和声明方式的事务

Embeddable EJB Container 支持编程方式的事务,也支持声明方式的事务。编程方式下,您可以在代码清单中 11 中的方法 getUsername() 或者 saveAccount() 前加注解 @TransactionAttribute(REQUIRED),则将该方法定义到事务的管理当中。同时您也可以部署描述符的形式定义事务管理,如代码清单 16 所示,embeddable container 会对部署描述符 ejb-jar.xml 文件进行扫描并正确解析,完成同编程方式同样的功能。

清单 16. ejb-jar 文件示例
 <?xml version="1.0" encoding="UTF-8"?> 
 <ejb-jar version="3.1" xmlns="http://java.sun.com/xml/ns/javaee" 
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
 xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
 http://java.sun.com/xml/ns/javaee/ejb-jar_3_1.xsd"> 
 <assembly-descriptor> 
 <container-transaction> 
 <method> 
    <!-- 指定对 TestDSBean 的所有方法进行事务管理 --> 
 <ejb-name>TestDSBean</ejb-name> 
 <method-name>*</method-name> 
 </method> 
 <!-- 指定事务管理的属性 --> 
 <trans-attribute>Required</trans-attribute> 
 </container-transaction> 	
 </assembly-descriptor> 
 </ejb-jar>

编程方式和声明方式的安全

Embeddable EJB Container 支持 EJB 中的安全机制,用户同样可以使用编程方式或者声明方式进行安全相关的定义,只需要在初始化 embeddable container 之前以属性的方式给角色赋予用户并且指定调用相应 bean 的用户即可。鉴于本文前面举例数据源时使用的是配置文件的方式,这里使用编程的方式给属性值赋值。在工程 SimpleEJBDemo 中创建 TestSecurityBean.java 用来进行安全测试,详见代码清单 17 所示。在该 session bean 中,声明了角色 role1,并且方法 getRole1Name() 只允许具有 role1 角色的用户进行访问。

清单 17. 安全测试 Session Bean 示例
 package com.zgn.ejb.test; 
 import javax.annotation.Resource; 
 import javax.annotation.security.DeclareRoles; 
 import javax.annotation.security.RolesAllowed; 
 import javax.ejb.LocalBean; 
 import javax.ejb.SessionContext; 
 import javax.ejb.Stateless; 
 @Stateless 
 @LocalBean 
 @DeclareRoles({"role1"}) // 声明了角色 roles1 
 public class TestSecurityBean { 
 @Resource SessionContext ctx; 
 @RolesAllowed("role1")// 指定该方法只能具有 role1 的角色访问
 public String getRole1Name(){ 
   return ctx.getCallerPrincipal().getName(); 
 } 
 public String getRole2Name(){ 
   return ctx.getCallerPrincipal().getName(); 
  } 
 }

代码清单 17 中的方法 getRole2Name() 也在部署描述符 ejb-jar.xml 文件中定义了访问权限,如代码清单 18 中,定义了角色 role2,并且只有具有 role2 角色的用户才能访问方法 getRole2Name()。同以注解方式编程实现的方式类似,当使用不具有其规定角色的用户访问这些方法时,均会抛出错误。

清单 18. 定义了安全的 ejb-jar.xml 文件
 <?xml version="1.0" encoding="UTF-8"?> 
 <ejb-jar xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
 xmlns="http://java.sun.com/xml/ns/javaee" 
 xmlns:ejb="http://java.sun.com/xml/ns/javaee/ejb-jar_3_0.xsd" 
 xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
 http://java.sun.com/xml/ns/javaee/ejb-jar_3_1.xsd" version="3.1"> 
 <assembly-descriptor>    
 <security-role> 
 <!-- 声明了角色 role2 --> 
 <role-name>role2</role-name> 
 </security-role>     
 <method-permission> 
 <!-- 定义方法 getRole2Name 只能由具有 role2 的角色访问 - – > 
 <role-name>role2</role-name> 
 <method> 
 <ejb-name>TestSecurityBean</ejb-name> 
 <method-name>getRole2Name</method-name> 
 </method> 
 </method-permission>   
 </assembly-descriptor> 
 </ejb-jar>

在类 EmbeddableEJBContainerTest.java 中加入对安全的测试代码,如代码清单 19 所示。因为安全测试此处需要以编程的方式加入一些属性值,如将属性 securityEnabled 设置为 true,并且赋予角色 role1 用户 user1,而且最后将以 user1 来调用相应的 session bean,因此这里会给属性变量 properties 赋值并且自行创建 EJBContainer, 这需要读者将之前在 setUp() 方法中创建 EJBContainer 的部分注释掉,并以单独的测试套件进行测试。

清单 19. 安全测试示例
 @Test 
 public void testSecurityProgram() throws Exception{ 
 Map<String,Object> properties = new HashMap<String,Object>(); 
 // 以编程的方式将安全相关的角色放到 properties 中
 properties.put("com.ibm.websphere.securityEnabled","true" ); 
 properties.put("role.role1","user1" ); // 给 user1 用户赋予角色 role1 
 properties.put("user.invocation","user1" ); // 指定使用 user1 进行 EJB 方法的调用
 container = EJBContainer.createEJBContainer(properties); 
 TestSecurityBean securityBean1 = (TestSecurityBean) 
 container.getContext().lookup("java:global/SimpleEJBDemo/TestSecurityBean"); 
 assertEquals("user1",securityBean1.getRole1Name()); 
 container.close(); 
 }

对 TestSecurityBean.java 中的方法 getRole2Name() 的测试代码基本同代码清单 19,这里不再赘述,读者可从本文的附件源代码中获取此部分的代码。

Bean Validation 的支持

Java EE 6 提出了 Bean Validation 规范,旨在对 Java Bean 进行约束验证。WAS V8 中实现了该规范,并且与 JPA 规范进行了兼容。同样,WAS V8 中的 embeddable container 也同样支持 Bean Validation。读者可以在代码清单 12 的基础上对字段 userName 进行非空约束,修改后的代码如清单 20 所示。当操作该实体 Bean 时,如果字段 userName 在实际中为空,则会违反注解 @NotNull 的约束并抛出异常。您可以从参考资源 [3] 中获得更多关于 Bean Validation 相关的介绍。

清单 20. 声明了约束的实体 Bean
 package com.zgn.ejb.test.entity; 
 import javax.persistence.Entity; 
 import javax.persistence.Id; 
 import javax.persistence.Table; 
 import javax.validation.constraints.NotNull; 
 @Entity 
 @Table(name = "accountable") 
  public class Account { 
 @Id 
 private Long id; 
 @NotNull ( message = "The username can not be null") 
 private String userName; 
  public Long getId() { 
 return id; 
 } 
 public void setId(Long id) { 
  this.id = id; 
 } 
 public String getUserName() { 
   return userName; 
 } 
 public void setUserName(String userName) { 
   this.userName = userName; 
  } 
 }

同时,为了便于读者更好的理解,笔者对本文之前调用该实体 Bean 的 session bean 的方法 做了相应的修改,参考代码清单 21 所示。在捕获了 ConstraintViolationException 之后将该 ConstraintViolation(即此次验证失败的约束实例)的约束失败消息进行打印。

清单 21. 测试 Bean Validation 的 session bean 示例
 public Account saveAccount(Account account){ 
 try{ 
  account = em.merge(account); 
  }catch (ConstraintViolationException cve){ 
  // 从异常中获得 Bean Validation 中失败的实例并输出失败消息
   Set<ConstraintViolation<?>> set = cve.getConstraintViolations(); 
  for (ConstraintViolation<?> constraintViolation : set) 
  {System.out.println(constraintViolation.getMessage()); 
  } 
 } 
   return account; 
}

代码清单 22 为对 Bean Validation 规范进行测试的代码片段,其在实例化了实体 bean 后并没有给字段 userName 进行赋值,因此在实际使用中会约束验证失败。由代码清单 20 中可以看到 @NotNull 注解的 message 属性值,因此此处验证失败后便会由控制台打印出约束失败消息 “The username can not be null”。

清单 22. 验证 Bean Validation 代码片段
 @Test 
 public void testBeanValidation()throws Exception{ 
   Account account = new Account(); 
   account.setId(1L); 
   Context ctx = container.getContext(); 
   TestDSBean testDS = (TestDSBean)ctx.lookup("java:global/SimpleEJBDemo/TestDSBean"); 
   testDS.saveAccount(account); 
 }

到此,WAS V8 提供的 Embeddable EJB Container 所支持的功能便介绍完毕,图 5 给出了对上述所有功能的 Junit 测试结果(除了安全方面)。

图 5. Junit 框架测试结果示意图
图 5. Junit 框架测试结果示意图

结束语

本文只是一个简单的使用 Junit 测试框架对 EJB 的应用程序进行测试的示例,您可以在本文的基础上进行扩充,使用更多的 Junit 相关功能对 EJB 的各种特性进行测试。本文旨在为想使用 WAS V8 中提供的 Embeddable EJB Container 对 EJB 功能进行单元测试的开发者提供一个有益的参考。

参考资料

学习

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=WebSphere, Java technology
ArticleID=794009
ArticleTitle=实战如何使用 WebSphere Application Server 提供的 Embeddable EJB Container 开发并测试 EJB 应用
publish-date=02162012