Skip to content

6. 版本 2:OpenEJB / JPA 架构

6.1. 移植原则简介

本文将介绍将 JPA / Spring / Hibernate 应用程序移植到 JPA / OpenEJB / EclipseLink 应用程序时应遵循的原则。 关于创建Maven项目,请参阅第6.2节。

6.1.1. 两种架构

当前基于 Spring / Hibernate 的实现

待构建的基于 OpenEJB / EclipseLink 的实现

6.1.2. 项目库

  • [DAO] 和 [metier] 层不再由 Spring 实例化。它们由容器 OpenEJB 实例化。
  • Spring 容器的库及其配置将被移除,取而代之的是 OpenEJB 容器的库及其配置。
  • JPA / Hibernate 层的库被 JPA / EclipseLink 层的库所取代

  • 配置 JPA 层的 [META-INF/persistence.xml] 文件内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="pam-openejb-ui-metier-dao-jpa-eclipselinkPU" transaction-type="JTA">
     <!-- 实体 JPA -->
    <class>jpa.Cotisation</class>
    <class>jpa.Employe</class>
    <class>jpa.Indemnite</class>
     <!-- 提供者 JPA 是 EclipseLink -->
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
     <!-- 提供者属性 -->
    <properties>
      <property name="eclipselink.ddl-generation" value="create-tables"/>
    </properties>
  </persistence-unit>
</persistence>
  • 第 3 行:EJB 容器中的事务类型为 JTA(Java 事务 API)。在使用 Spring 时,其类型为 RESOURCE_LOCAL
  • 第 9 行:使用的 JPA 实现为 EclipseLink
  • 第 5-7 行:由 JPA 层管理的实体
  • 第11-13行:提供程序EclipseLink的属性
  • 第 12 行:每次执行时,都会创建这些表

容器 OpenEJB 所使用的数据源 JTA 的特征 JDBC 将由以下配置文件 [conf/openejb.conf] 指定:

1
2
3
4
5
6
7
8
<?xml version="1.0"?>
<openejb>
  <Resource id="Default JDBC Database">
    JdbcDriver com.mysql.jdbc.Driver
    JdbcUrl jdbc:mysql://localhost:3306/dbpam_eclipselink
    UserName root
  </Resource>
</openejb>
  • 第 3 行:当处理嵌入在应用程序本身中的 OpenEJB 容器时,使用默认 ID JDBC Database
  • 第 5 行:我们使用 MySQL [dbpam_eclipselink] 数据库

6.1.4. 通过 EJB 实现 [DAO] 层

  • 实现 [DAO] 层的类将变为 EJB。以 [CotisationDao] 类为例:

Spring 版本中的 [ICotisationDao] 接口如下:

package dao;

import java.util.List;
import jpa.Cotisation;

public interface ICotisationDao {
   // 创建新会费
  Cotisation create(Cotisation cotisation);
   // 修改现有会费
  Cotisation edit(Cotisation cotisation);
   // 删除现有会费
  void destroy(Cotisation cotisation);
   // 搜索特定缴费
  Cotisation find(Long id);
   // 获取所有“缴费”对象
  List<Cotisation> findAll();

}

EJB 将以两种不同形式实现该接口:本地形式和远程形式。 本地接口可供在同一 JVM 中运行的客户端使用,远程接口则可供在另一个 JVM 中运行的客户端使用。

本地接口:

1
2
3
4
5
6
7
package dao;

import javax.ejb.Local;

@Local
public interface ICotisationDaoLocal extends ICotisationDao{
}
  • 第 6 行:接口 [ICotisationDaoLocal] 继承自接口 [ICotisationDao],以继承其所有方法。它未添加新方法。
  • 第 5 行:@Local 注解将其定义为本地接口,供后续实现该接口的 EJB 使用。

远程接口:

1
2
3
4
5
6
7
package dao;

import javax.ejb.Remote;

@Remote
public interface ICotisationDaoRemote extends ICotisationDao{
}
  • 第 6 行:接口 [ICotisationDaoRemote] 继承自接口 [ICotisationDao],以继承其所有方法。它未添加新方法。
  • 第 5 行:@Remote 注解使其成为 EJB 的远程接口,后者将实现该接口。

[DAO] 层由 EJB 实现,该类实现了这两个接口(并非强制要求):

1
2
3
4
5
6
@Stateless()
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class CotisationDao implements ICotisationDaoLocal, ICotisationDaoRemote {

  @PersistenceContext
  private EntityManager em;
  • 第 1 行:@Stateless 注解,将该类定义为 EJB
  • 第 2 行:@TransactionAttribute 注解,确保该类的每个方法都在事务中执行。
  • 第 5 行:注解 @PersistenceContext,用于将 JPA 层中的 EntityManager 注入 [CotisationDao] 类。 这与 Spring 版本中的实现完全相同。

当使用 [DAO] 层的本地接口时,该接口的客户端将在同一个 JVM 中运行。

在上文中,[metier] 和 [DAO] 层通过引用交换对象。当一个层更改共享对象时,另一个层会看到此更改。

当使用 [DAO] 层的远程接口时,该接口的客户端通常在另一个 JVM 中运行。

在上文中,[metier] 和 [DAO] 层通过值传递对象(对交换的对象进行序列化)。当一个层更改共享对象时,另一个层只有在收到修改后的对象时才能察觉到该更改。

6.1.5. 通过 EJB 实现 [metier] 层

  • 实现 [metier] 层的类也变成了一个实现本地远程接口的 EJB。最初的 [IMetier] 接口如下:
package metier;

import java.util.List;
import jpa.Employe;

public interface IMetier {
   // 获取工资单
  FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
   // 员工列表
  List<Employe> findAllEmployes();
}

基于之前的接口,创建一个本地接口和一个远程接口:

1
2
3
4
5
6
7
package metier;

import javax.ejb.Local;

@Local
public interface IMetierLocal extends IMetier{
}
1
2
3
4
5
6
7
package metier;

import javax.ejb.Remote;

@Remote
public interface IMetierRemote extends IMetier{
}

[metier] 层中的 EJB 实现了这两个接口:

@Stateless()
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Metier implements IMetierLocal, IMetierRemote {

   // 本地层参考 [DAO]
  @EJB
  private ICotisationDaoLocal cotisationDao = null;
  @EJB
  private IEmployeDaoLocal employeDao = null;
  @EJB
  private IIndemniteDaoLocal indemniteDao = null;
  • 第 1-2 行:定义了一个 EJB,其每个方法都在事务中执行。
  • 第 7 行:对 EJB 的本地接口 [CotisationDao] 进行引用。
  • 第 6 行:注解 @EJB 要求容器 EJB 注入对 EJB [CotisationDao] 本地接口的引用。
  • 第8-11行:对EJB、[EmployeDao]和[IndemniteDao]的本地接口重复上述操作。

最终,当 EJB [Metier] 被实例化时, 第7、9和11行的字段将初始化为[DAO]层中三个EJB实例的本地接口引用。 因此,这里假设 [metier] 和 [DAO] 层将在同一个 JVM 中运行。

6.1.6. EJB 的客户端

在上图中,为了与 [metier] 层通信,[ui] 层必须从 [metier] 层获取 EJB 的远程接口引用

在上图中,为了与 [metier] 层通信,[ui] 层必须获取 [metier] 层中 EJB 的本地接口引用。 获取这些引用方法因容器而异。对于容器 OpenEJB,可按以下步骤操作:

本地接口上的引用:

1
2
3
4
5
6
7
8
9
     // 配置嵌入式 Open EJB 容器
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
      // 使用上述属性初始化上下文 JNDI
    InitialContext initialContext = new InitialContext(properties);
     // 实例化 DAO 层
    employeDao = (IEmployeDaoLocal) initialContext.lookup("EmployeDaoLocal");
    cotisationDao = (ICotisationDaoLocal) initialContext.lookup("CotisationDaoLocal");
indemniteDao = (IIndemniteDaoLocal) initialContext.lookup("IndemniteDaoLocal");
  • 第2-5行:初始化容器OpenEJB。
  • 第 5 行:存在一个 JNDI 上下文(Java 命名和目录接口),可用于获取 EJB 的引用。每个 EJB 都由一个 JNDI 名称标识:
  • (续)
    • 对于本地接口,在 EJB 的名称后添加 Local(第 7-9 行)
    • 对于远程接口,在 EJB 的名称后添加 Remote

在 Java EE 5 中,这些规则会根据容器 EJB 而变化。 这带来了一定困难。Java EE 6 引入了一种可在所有应用服务器上通用的 JNDI 表示法。

前面的代码通过 JNDI 的名称获取 EJB 本地接口的引用。我们之前提到,这些引用也可以通过 @EJB 注解获得。因此,我们可能希望这样写:

@EJB
private IemployeDaoLocal employeDaoLocal ;

注解 @EJB 仅在属于由容器 EJB 加载的类时才生效。例如,类 [Metier] 即属于这种情况。 而上面的代码将属于一个控制台类,该类不会由容器 EJB 加载。因此,我们必须使用 JNDI 或 EJB 这些名称。

以下是获取 EJB 和 [Metier] 远程接口引用所需的代码:

1
2
3
4
5
6
7
8
     // 配置嵌入式 Open EJB 容器
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
     // 初始化容器 EJB 的上下文 JNDI
    InitialContext initialContext = new InitialContext(properties);

     // 实例化远程业务层
metier = (IMetierRemote) initialContext.lookup("MetierRemote");

6.2. 实践操作

我们计划将 NetBeans Spring / Hibernate 应用程序迁移至 OpenEJB / EclipseLink 架构。

当前基于 Spring / Hibernate 的实现

计划基于 OpenEJB / EclipseLink 构建的实现

如果该数据库不存在,请创建 MySQL [dbpam_eclipselink]。如果已存在,请删除其中的所有表。按照第 6.2.1 节所述,在 NetBeans 中建立与该数据库的连接。

6.2.2. NetBeans 项目的初始配置

  • 加载 Maven 项目 [mv-pam-spring-hibernate]
  • 创建新的 Maven Java 项目 [mv-pam-openejb-eclipselink] [1]
  • 在 [Files] [2] 选项卡中,在项目根目录下创建一个文件夹 [conf] [3]
  • 将以下文件 [openejb.conf] [4] 放入该文件夹中:
1
2
3
4
5
6
7
8
<?xml version="1.0"?>
<openejb>
  <Resource id="Default JDBC Database">
    JdbcDriver com.mysql.jdbc.Driver
    JdbcUrl jdbc:mysql://localhost:3306/dbpam_eclipselink
    UserName root
  </Resource>
</openejb>
  • 创建文件夹 [src / main/ resources/ META-INF] [5]
  • 将以下文件放入其中:[persistence.xml] [6]

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence              http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
  <persistence-unit name="dbpam_eclipselinkPU" transaction-type="JTA">
    <!-- 提供者 JPA 为 EclipseLink -->
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <!-- Jpa 实体 -->
    <class>jpa.Cotisation</class>
    <class>jpa.Employe</class>
    <class>jpa.Indemnite</class>
    <!-- 属性提供者 EclipseLink -->
    <properties>
      <property name="eclipselink.logging.level" value="FINE"/>
      <property name="eclipselink.ddl-generation" value="drop-and-create-tables"/>
    </properties>
  </persistence-unit>
</persistence>
  • 第 12 行:向 EclipseLink 请求详细日志,
  • 第 13 行:在实例化 JPA 层时将创建表,
  • 将库文件 OpenEJBEclipseLink 以及驱动程序 JDBC 从 MySQL 添加到项目中的文件 [pom.xml] 中:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>istia.st</groupId>
  <artifactId>mv-pam-openejb-eclipselink</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>mv-pam-openejb-eclipselink</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.apache.openejb</groupId>
      <artifactId>openejb-core</artifactId>
      <version>4.0.0</version>
    </dependency>                
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.10</version>
      <scope>test</scope>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.eclipse.persistence</groupId>
      <artifactId>eclipselink</artifactId>
      <version>2.3.0</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.persistence</groupId>
      <artifactId>javax.persistence</artifactId>
      <version>2.0.3</version>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.6</version>
    </dependency>
    <dependency>
      <groupId>org.swinglabs</groupId>
      <artifactId>swing-layout</artifactId>
      <version>1.0.3</version>
    </dependency>
  </dependencies>
  
  <repositories>
    <repository>
      <url>http://download.eclipse.org/rt/eclipselink/maven.repo/</url>
      <id>eclipselink</id>
      <layout>default</layout>
      <name>Repository for library Library[eclipselink]</name>
    </repository>
  </repositories>
  
</project>
  • 第 18-22 行:依赖项 OpenEJB,
  • 第 30-39 行:EclipseLink 的依赖关系,
  • 第 41-44 行:JDBC 驱动程序对 MySQL 的依赖

6.2.3. [DAO] 层的移植

我们将通过将 [mv-pam-spring-hibernate] 项目的软件包复制到 [mv-pam-openejb-eclipselink] 项目中,来移植 [DAO] 层。

  • 复制 [dao, exception, jpa] 包
 

上述错误是由于复制的 [DAO] 层使用了 Spring,而 Spring 库已不再属于该项目所致。

6.2.3.1. EJB [CotisationDao]

我们创建未来 EJB [CotisationDao] 的本地远程接口:

本地接口 ICotisationDaoLocal

1
2
3
4
5
6
7
package dao;

import javax.ejb.Local;

@Local
public interface ICotisationDaoLocal extends ICotisationDao{
}

要获取正确的 import 软件包,请执行 [clic droit sur le code / Fix Imports]。

远程接口 ICotisationDaoRemote

1
2
3
4
5
6
7
package dao;

import javax.ejb.Remote;

@Remote
public interface ICotisationDaoRemote extends ICotisationDao{
}

然后我们将类 [CotisationDao] 修改为 EJB:

...
import javax.persistence.PersistenceContext;
import jpa.Cotisation;

@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class CotisationDao implements ICotisationDaoLocal, ICotisationDaoRemote {

  @PersistenceContext
  private EntityManager em;
...  

该类在 Spring 框架中生成的 import 文件将消失。请为该项目生成 [Clean and Build] 文件:

在[1]中,[CotisationDao]类不再报错。

6.2.3.2. EJB、[EmployeDao] 和 [IndemniteDao]

对 [DAO] 层的其他元素重复相同的步骤:

  • IEmployeDao 派生的接口 IEmployeDaoLocalIEmployeDaoRemote
  • EJB、EmployeDao 实现了这两个接口
  • 接口 IIndemniteDaoLocalIIndemniteDaoRemote 继承自 IIndemniteDao
  • EJB IndemniteDao 实现了这两个接口

完成上述操作后,[2] 项目中不再存在错误。

6.2.3.3. 类 [PamException]

[PamException] 类除了一个细节外,其余部分保持不变:

package exception;

import javax.ejb.ApplicationException;

@ApplicationException(rollback=true)
public class PamException extends RuntimeException {

   // 错误代码
  private int code;
...

第 5 行已被添加。要获得正确的 import,请将其改为 [Fix imports]。

要理解第5行的注释,需注意我们[DAO]层中的每个EJB方法:

  • 都在由容器 EJB 启动并结束的事务中执行
  • 一旦发生异常,会立即抛出类型为 [PamException] 的异常

当 [metier] 层调用 [DAO] 层的 M 方法时,该调用会被 EJB 容器拦截。 这就像是在 [metier] 层和 [DAO] 层之间存在一个中间类(此处称为 [Proxy EJB]),拦截所有对 [DAO] 层的调用。 当对 [DAO] 层中方法 M 的调用被拦截时,代理 EJB 会启动一个事务,然后将控制权移交给 [DAO] 层中的方法 M,该方法随后将在该事务中执行。 方法 M 可能抛出异常,也可能不抛出异常。

  • 如果方法 M 正常结束(未抛出异常),执行流程将返回至代理 EJB,该代理通过调用 commit 来提交事务并结束该事务。随后,执行流程将返回至 [metier] 层的调用方法
  • 如果方法 M 因异常而终止,执行将返回至代理 EJB,该代理通过调用 rollback 方法使事务失效,从而结束事务。此外,它将该异常封装在 EJBException 类型中。 随后,执行流将返回给 [metier] 层的调用方法,该方法因此会收到一个 EJBException。上文第 5 行中的注解会阻止这种封装。 因此,[metier]层将收到一个PamException此外,rollback=true属性指示EJB代理,当其收到一个PamException时,必须回滚事务。

6.2.3.4. 对 [DAO] 层的测试

由 EJB 实现的 [DAO] 层现已可供测试。 首先,我们将 [mv-pam-springhibernate] 项目中的 [Test Packages] 包复制到正在构建的 [1] 项目中:

我们仅保留测试类 [JUnitInitDB],该类使用 [2] 中的部分数据初始化数据库。我们将类 [ JUnitInitDbLocal] 重命名为 [3]。 类 [JUnitInitDBLocal] 将使用 [DAO] 层中 EJB 的本地接口。

首先,我们将类 [JUnitInitDBLocal] 修改如下:

public class JUnitInitDBLocal {

  static private IEmployeDaoLocal employeDao = null;
  static private ICotisationDaoLocal cotisationDao = null;
  static private IIndemniteDaoLocal indemniteDao = null;

  @BeforeClass
  public static void init() throws Exception {
     // 正在配置嵌入式 Open EJB 容器
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
      // 使用上述属性初始化上下文 JNDI
    InitialContext initialContext = new InitialContext(properties);
     // 实例化本地 DAO 层
    employeDao = (IEmployeDaoLocal) initialContext.lookup("EmployeDaoLocal");
    cotisationDao = (ICotisationDaoLocal) initialContext.lookup("CotisationDaoLocal");
    indemniteDao = (IIndemniteDaoLocal) initialContext.lookup("IndemniteDaoLocal");
}

...
  • 第 3-5 行:对 [DAO] 层中 EJB 的本地接口的引用
  • 第 7 行:@BeforeClass 标注了在 JUnit 测试启动时执行的方法
  • 第10-13行:初始化容器OpenEJB。此初始化为专有操作,且随每个EJB容器而变化。
  • 第 13 行:存在一个 JNDI 上下文(Java 命名和目录接口),允许通过名称访问 EJB。 在 OpenEJB 中,EJB E 的本地接口由 ELocal 表示,远程接口由 ERemote 表示。
  • 第 15-17 行:向上下文 JNDI 请求 EJB 和 [EmployeDao, CotisationDao, IndemniteDao] 的本地接口引用。

构建项目(Build),如有需要则启动服务器 MySQL,随后执行测试 JUnitInitDBLocal。 请注意,文件 [persistence.xml] 已配置为在每次执行时重新创建表。在执行测试之前,最好先删除数据库 MySQL 和 [dbpam_eclipselink] 中可能存在的表。

  • 在 [1] 的 [Services] 选项卡中,删除第 6.2.1 节中建立的 NetBeans 连接中的表。
  • 在 [2] 中,数据库 [dbpam_eclipselink] 已无表
  • 在 [3] 中,构建了该项目
  • 在 [4] 中,执行测试 JUnitInitDBLocal
  • 在 [5] 中,测试已通过
  • 在 [6] 中,刷新 NetBeans 连接
  • 在 [7] 中,可以看到由 JPA 层创建的 4 张表。该测试的目的是填充这些表。我们查看其中一张表的内容
  • 在 [8] 中,[EMPLOYES] 表的内容

容器 OpenEJB 在控制台中显示了日志:

Infos - PersistenceUnit(name=dbpam_eclipselinkPU, provider=org.eclipse.persistence.jpa.PersistenceProvider) - provider time 396ms
Infos - Jndi(name=CotisationDaoLocal) --> Ejb(deployment-id=CotisationDao)
Infos - Jndi(name=global/classpath.ear/mv-pam-openejb-eclipselink/CotisationDao!dao.ICotisationDaoLocal) --> Ejb(deployment-id=CotisationDao)
Infos - Jndi(name=CotisationDaoRemote) --> Ejb(deployment-id=CotisationDao)
Infos - Jndi(name=global/classpath.ear/mv-pam-openejb-eclipselink/CotisationDao!dao.ICotisationDaoRemote) --> Ejb(deployment-id=CotisationDao)
Infos - Jndi(name=global/classpath.ear/mv-pam-openejb-eclipselink/CotisationDao) --> Ejb(deployment-id=CotisationDao)
Infos - Jndi(name=EmployeDaoLocal) --> Ejb(deployment-id=EmployeDao)
Infos - Jndi(name=global/classpath.ear/mv-pam-openejb-eclipselink/EmployeDao!dao.IEmployeDaoLocal) --> Ejb(deployment-id=EmployeDao)
Infos - Jndi(name=EmployeDaoRemote) --> Ejb(deployment-id=EmployeDao)
Infos - Jndi(name=global/classpath.ear/mv-pam-openejb-eclipselink/EmployeDao!dao.IEmployeDaoRemote) --> Ejb(deployment-id=EmployeDao)
Infos - Jndi(name=global/classpath.ear/mv-pam-openejb-eclipselink/EmployeDao) --> Ejb(deployment-id=EmployeDao)
Infos - Jndi(name=IndemniteDaoLocal) --> Ejb(deployment-id=IndemniteDao)
Infos - Jndi(name=global/classpath.ear/mv-pam-openejb-eclipselink/IndemniteDao!dao.IIndemniteDaoLocal) --> Ejb(deployment-id=IndemniteDao)
Infos - Jndi(name=IndemniteDaoRemote) --> Ejb(deployment-id=IndemniteDao)
Infos - Jndi(name=global/classpath.ear/mv-pam-openejb-eclipselink/IndemniteDao!dao.IIndemniteDaoRemote) --> Ejb(deployment-id=IndemniteDao)
Infos - Jndi(name=global/classpath.ear/mv-pam-openejb-eclipselink/IndemniteDao) --> Ejb(deployment-id=IndemniteDao)
Infos - existing thread singleton service in SystemInstance() org.apache.openejb.cdi.ThreadSingletonServiceImpl@624a240d
Infos - OpenWebBeans Container is starting...
Infos - Adding OpenWebBeansPlugin : [CdiPlugin]
Infos - All injection points were validated successfully.
Infos - OpenWebBeans Container has started, it took [70] ms.
Infos - Created Ejb(deployment-id=IndemniteDao, ejb-name=IndemniteDao, container=Default Stateless Container)
Infos - Created Ejb(deployment-id=EmployeDao, ejb-name=EmployeDao, container=Default Stateless Container)
Infos - Created Ejb(deployment-id=CotisationDao, ejb-name=CotisationDao, container=Default Stateless Container)
Infos - Started Ejb(deployment-id=IndemniteDao, ejb-name=IndemniteDao, container=Default Stateless Container)
Infos - Started Ejb(deployment-id=EmployeDao, ejb-name=EmployeDao, container=Default Stateless Container)
Infos - Started Ejb(deployment-id=CotisationDao, ejb-name=CotisationDao, container=Default Stateless Container)
Infos - Deployed Application(path=D:\data\istia-1112\netbeans\glassfish\mv-pam\tmp\mv-pam-openejb-eclipselink\classpath.ear)
  • 第2-3行:EJB和[CotisationDaoLocal]的两个名称JNDI,
  • 第 4-5 行:JNDI 包含 EJB 和 [CotisationDaoRemote] 这两个名称,
  • 第7-8行:EJB和[EmployeDaoLocal]中的两个名称JNDI,
  • 第9-10行:EJB和[EmployeDaoRemote]中的两个名称JNDI,
  • 第12-13行:EJB和[IndemniteDaoLocal]中的两个名称JNDI,
  • 第14-15行:JNDI的两个名称来自EJB和[EmployeDaoRemote]。

我们再次进行相同的测试,这次使用 EJB 的远程接口。

在 [1] 中,[JUnitInitDBLocal] 类已被复制(复制/粘贴)到 [JUnitInitDBRemote] 中。在此类中,我们将本地接口替换为远程接口:

public class JUnitInitDBRemote {

  static private IEmployeDaoRemote employeDao = null;
  static private ICotisationDaoRemote cotisationDao = null;
  static private IIndemniteDaoRemote indemniteDao = null;

  @BeforeClass
  public static void init() throws Exception {
     // 配置嵌入式 Open EJB 容器
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
      // 使用上述属性初始化 JNDI 上下文
    InitialContext initialContext = new InitialContext(properties);
     // 实例化远程层 DAO
    employeDao = (IEmployeDaoRemote) initialContext.lookup("EmployeDaoRemote");
    cotisationDao = (ICotisationDaoRemote) initialContext.lookup("CotisationDaoRemote");
    indemniteDao = (IIndemniteDaoRemote) initialContext.lookup("IndemniteDaoRemote");
}

完成上述操作后,即可运行新的测试类。在此之前,请通过 NetBeans 连接 [dbpam_eclipselink],删除数据库 [dbpam_eclipselink] 中的表。

 

使用 NetBeans 连接 [dbpam_eclipselink],验证数据库是否已填充数据。

6.2.4. [metier] 层的移植

我们将通过将 [mv-pam-spring-hibernate] 项目的包复制到 [mv-pam-openejb-eclipselink] 项目中,来移植 [metier] 层。

上述报告的 [1] 错误是由于复制的 [metier] 层使用了 Spring,而 Spring 库已不再属于该项目所致。

6.2.4.1. EJB [Metier]

我们遵循与 EJB [CotisationDao] 描述相同的步骤。 首先,我们在 [2] 中创建未来 EJB [Metier] 的本地远程接口。它们都继承自初始接口 [IMetier]。

1
2
3
4
5
6
7
8
package metier;

import javax.ejb.Local;

@Local
public interface IMetierLocal extends IMetier{

}
1
2
3
4
5
6
7
8
package metier;

import javax.ejb.Remote;

@Remote
public interface IMetierRemote extends IMetier{

}

完成上述操作后,在 [3] 中,我们将类 [Metier] 修改为 EJB:

@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Metier implements IMetierLocal, IMetierRemote {

   // 在本地层 [DAO] 上建立引用
  @EJB
  private ICotisationDaoLocal cotisationDao = null;
  @EJB
  private IEmployeDaoLocal employeDao = null;
  @EJB
  private IIndemniteDaoLocal indemniteDao = null;

   // 获取工资单
  public FeuilleSalaire calculerFeuilleSalaire(String SS,
          double nbHeuresTravaillées, int nbJoursTravaillés) {
     // 获取员工相关信息
...
  • 第 1 行:@Stateless 注解将该类转换为 EJB
  • 第 2 行:该类的每个方法都将在事务中执行
  • 第 3 行:EJB 实现了我们刚刚定义的本地远程这两个接口
  • 第 7 行:EJB 将通过 [Metier] 的本地接口调用 EJB 这意味着 [metier] 和 [DAO] 层必须在同一个 JVM 中运行。
  • 第6行:注解 @EJB 确保容器 EJB 会自行将引用注入到 EJB 的本地接口上。 我们遇到的另一种方法是使用 JNDI 上下文。
  • 第 8-11 行:[DAO] 层中的另外两个 EJB 也采用了相同的机制。

6.2.4.2. [metier] 层的测试

由 EJB 实现的 [metier] 层现已可以进行测试。 首先,我们将项目 [mv-pam-spring-hibernate] 中的 [Test Packages] 包复制到正在构建的项目 [1] 中:

  • 复制到 [1],复制结果
  • 为 [2],删除第一个测试
  • 在 [3] 中,剩余的测试重命名为 [JUnitMetierLocal]

类 [JUnitMetierLocal] 变为如下:

public class JUnitMetierLocal {

// 本地业务层
  static private IMetierLocal metier;

  @BeforeClass
  public static void init() throws NamingException {
     // 配置嵌入式 Open EJB 容器
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
     // 使用上述属性初始化 JNDI 上下文
    InitialContext initialContext = new InitialContext(properties);

     // 实例化本地 DAO 层
    IEmployeDaoLocal employeDao = (IEmployeDaoLocal) initialContext.lookup("EmployeDaoLocal");
    ICotisationDaoLocal cotisationDao = (ICotisationDaoLocal) initialContext.lookup("CotisationDaoLocal");
    IIndemniteDaoLocal indemniteDao = (IIndemniteDaoLocal) initialContext.lookup("IndemniteDaoLocal");
     // 实例化本地业务层
    metier = (IMetierLocal) initialContext.lookup("MetierLocal");

     // 清空数据库
...
}
  • 第 4 行:对 EJB 的本地接口引用 [Metier]
  • 第 8-12 行:OpenEJB 容器的配置与 [DAO] 层测试中的配置相同
  • 第15-19行:请求第12行中的JNDI上下文, 获取[DAO]层中3个EJB以及[metier]层中EJB的引用。 [DAO]层中的EJB将用于初始化数据库,[metier]层中的EJB将用于进行工资计算测试。

执行测试 [JUnitMetierLocal] 得到以下结果 [1]:

在 [2] 中, 将 [JUnitMetierLocal] 复制为 [JUnitMetierRemote],以便此次测试 EJB 和 [Metier] 的远程接口。 修改了 [JUnitMetierRemote] 的代码以使用该远程接口。其余部分保持不变。

public class JUnitMetierRemote {

   // 远程业务层
  static private IMetierRemote metier;

  @BeforeClass
  public static void init() throws NamingException {
     // 配置嵌入式 Open EJB 容器
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
     // 使用上述属性初始化 JNDI 上下文
    InitialContext initialContext = new InitialContext(properties);

     // 实例化远程层 DAO
    IEmployeDaoRemote employeDao = (IEmployeDaoRemote) initialContext.lookup("EmployeDaoRemote");
    ICotisationDaoRemote cotisationDao = (ICotisationDaoRemote) initialContext.lookup("CotisationDaoRemote");
    IIndemniteDaoRemote indemniteDao = (IIndemniteDaoRemote) initialContext.lookup("IndemniteDaoRemote");
     // 实例化远程业务层
    metier = (IMetierRemote) initialContext.lookup("MetierRemote");

     // 清空数据库
    for(Employe employe:employeDao.findAll()){
      employeDao.destroy(employe);
    }
    for(Cotisation cotisation:cotisationDao.findAll()){
      cotisationDao.destroy(cotisation);
    }
    for(Indemnite indemnite : indemniteDao.findAll()){
      indemniteDao.destroy(indemnite);
    }
     // 填充数据库
    Indemnite indemnite1=new Indemnite(1,1.93,2,3,12);
    Indemnite indemnite2=new Indemnite(2,2.1,2.1,3.1,15);
    indemnite1=indemniteDao.create(indemnite1);
    indemnite2=indemniteDao.create(indemnite2);
    employeDao.create(new Employe("254104940426058","Jouveinal","Marie","5 rue des oiseaux","St Corentin","49203",indemnite2));
    employeDao.create(new Employe("260124402111742","Laverti","Justine","La brûlerie","St Marcel","49014",indemnite1));
    cotisationDao.create(new Cotisation(3.49,6.15,9.39,7.88));
  }
}
  • 第 4 行和第 19 行:使用 EJB 和 [Metier] 的远程接口。
  • 第 15-17 行:使用 [DAO] 层的远程接口
  • 第34-35行:由于使用远程接口时,客户端与服务器之间交换的对象是按值传递的,因此需要获取create(Indemnite i)方法返回的结果。而在使用本地接口时,对象是按引用传递的,因此无需此操作。

完成上述操作后,即可构建项目并运行测试 [JUnitMetierRemote]:

  

6.2.5. [console] 层的移植

我们将通过将 [console] 项目的包复制到 [mv-pam-openejb-eclipselink] 项目中,来移植 [console] 层。

上述报告的 [1] 错误是由于复制的 [metier] 层使用了 Spring,而 Spring 库已不再属于该项目所致。 在 [2] 中,类 [Main] 已重命名为 [MainLocal]。它将使用 EJB 的本地接口 [Metier]。

类 [MainLocal] 的代码如下所示:

  public static void main(String[] args) {
     // 本地数据
    final String syntaxe = "pg num_securite_sociale nb_heures_travaillées nb_jours_travaillés";
...
     // 有错误吗?
    if (erreurs.size() != 0) {
      for (int i = 0; i < erreurs.size(); i++) {
        System.err.println(erreurs.get(i));
      }
      return;
    }
     // 完成 - 可向 [métier] 层请求工资单
    IMetierLocal metier = null;
    FeuilleSalaire feuilleSalaire = null;
    try {
       // 配置嵌入式 Open EJB 容器
      Properties properties = new Properties();
      properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
       // 使用上述属性初始化上下文 JNDI
      InitialContext initialContext = new InitialContext(properties);
       // 实例化本地业务层
      metier = (IMetierLocal) initialContext.lookup("MetierLocal");
       // 计算工资单
      feuilleSalaire = metier.calculerFeuilleSalaire(args[0], nbHeuresTravaillées, nbJoursTravaillés);
    } catch (PamException ex) {
      System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
      return;
    } catch (Exception ex) {
      System.err.println("L'erreur suivante s'est produite : " + ex.toString());
      return;
    }
     // 显示详细信息
    String output = "Valeurs saisies :\n";
    output += ajouteInfo("N° de sécurité sociale de l'employé", args[0]);
....

修改位于第13至25行。这是为了更改对[metier]层的引用方式(第17至22行)。 由于新代码已在之前的示例中出现过,此处不再赘述。完成这些修改后,项目不再出现错误(参见 [3])。

我们将项目配置为使用参数 [1] 运行:

为了使控制台应用程序正常运行,数据库中必须有数据。为此,需要修改文件 [META-INF/persistence.xml]:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence              http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
  <persistence-unit name="dbpam_eclipselinkPU" transaction-type="JTA">
    <!-- 提供者 JPA 为 EclipseLink -->
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <!-- Jpa 实体 -->
    <class>jpa.Cotisation</class>
    <class>jpa.Employe</class>
    <class>jpa.Indemnite</class>
    <!-- 提供商属性 EclipseLink -->
    <properties>
      <property name="eclipselink.logging.level" value="FINE"/>
      <!--
      <property name="eclipselink.ddl-generation" value="drop-and-create-tables"/>
      -->
    </properties>
  </persistence-unit>
</persistence>

将第14行(该行导致每次运行时都会重新创建数据库表)注释掉。必须重建项目(Clean and Build)才能使此修改生效。完成上述操作后,即可运行程序。如果一切正常,控制台将显示类似以下内容:

.......
INFO - Created EJB(deployment-id=IndemniteDao, ejb-name=IndemniteDao, container=Default Stateless Container)
INFO - Deployed Application(path=classpath.ear)
[EL Info]: 2009-09-30 15:09:21.109--ServerSession(16658781)--EclipseLink, version: Eclipse Persistence Services - 1.1.2.v20090612-r4475
[EL Info]: 2009-09-30 15:09:21.937--ServerSession(16658781)--file:/C:/temp/09-09-28/pam-console-metier-dao-openejb-eclipselink-0910/build/classes/-jpa login successful
Valeurs saisies :
N° de sécurité sociale de l'employé : 254104940426058
Nombre d'heures travaillées : 150
Nombre de jours travaillés : 20

Informations Employé : 
Nom : Jouveinal
Prénom : Marie
Adresse : 5 rue des oiseaux
Ville : St Corentin
Code Postal : 49203
Indice : 2

Informations Cotisations : 
CSGRDS : 3.49 %
CSGD : 6.15 %
Retraite : 7.88 %
Sécurité sociale : 9.39 %

Informations Indemnités : 
Salaire horaire : 2.1 euro
Entretien/jour : 2.1 euro
Repas/jour : 3.1 euro
Congés Payés : 15.0 %

Informations Salaire : 
Salaire de base : 362.25 euro
Cotisations sociales : 97.48 euro
Indemnités d'entretien : 42.0 euro
Indemnités de repas : 62.0 euro
Salaire net : 368.77 euro

BUILD SUCCESSFUL (total time: 4 seconds)

此处我们使用了 [metier] 层的本地接口。现在,我们将在第二个控制台类中使用其远程接口:

在 [1] 中,类 [MainLocal] 已被复制为 [MainRemote]。 [MainRemote] 的代码已修改,以使用 [metier] 层的远程接口:

// 没问题——我们可以向 [metier] 层请求工资单
    IMetierRemote metier = null;
    FeuilleSalaire feuilleSalaire = null;
    try {
       // 配置嵌入式 Open EJB 容器
...
       // 实例化远程业务层
      metier = (IMetierRemote) initialContext.lookup("MetierRemote");
       // 计算工资单
      feuilleSalaire = metier.calculerFeuilleSalaire(args[0], nbHeuresTravaillées, nbJoursTravaillés);
    } catch (PamException ex) {
...
    } catch (Exception ex) {
...
    }

修改内容位于第 2 行和第 8 行。项目 [2] 已配置为执行类 [MainRemote]。其执行结果与之前相同。

6.3. Conclusion

我们演示了如何将 Spring / Hibernate 架构迁移至 OpenEJB / EclipseLink 架构。

Spring/Hibernate架构

OpenEJB / EclipseLink架构

由于初始应用程序采用了分层结构,因此移植过程得以顺利进行。这一点非常重要,需要理解。