Skip to content

4. 示例应用程序 – 02:rdvmedecins-jsf2-spring

现在,我们将尝试将前一个应用程序移植到 Spring / Tomcat 环境中:

这实际上是一个移植过程。我们将基于前一个应用程序,并将其适配到新环境中。本文仅对修改部分进行说明。这些修改主要分为三类:

  • 服务器不再是 Glassfish,而是 Tomcat——一个轻量级服务器,它不包含 EJB 容器,
  • 为替代 EJB,我们将采用 Spring——EJB 和 [http://www.springsource.com/] 的主要竞争对手,
  • 将采用Hibernate替代EclipseLink作为实现方案。

由于我们将大量在旧项目和新项目之间进行复制/粘贴操作,因此我们会在 NetBeans 中保持之前项目的打开状态:

  

使用 Spring 框架需要一定的相关知识,这些内容可在 [ref7] 中找到(参见第 166 页)。

4.1. [DAO] 和 [JPA] 层

4.1.1. NetBeans 项目

我们正在构建一个类型为 [Java Application] 的 Maven 项目:

  • 在 [1] 中,即创建的项目,
  • 在 [2] 中,该项目去除了 [Source Packages] 和 [Test Packages] 包以及 [junit-3.8.1] 依赖项。

Maven 项目中最困难的部分是找到正确的依赖项。对于这个 Spring / JPA / Hibernate 项目,依赖项如下:


<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-rdvmedecins-spring-dao-jpa</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>mv-rdvmedecins-spring-dao-jpa</name>
  <url>http://maven.apache.org</url>

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

    <dependencies>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-entitymanager</artifactId>
      <version>4.1.2</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.hibernate.java-persistence</groupId>
      <artifactId>jpa-api</artifactId>
      <version>2.0.Beta-20090815</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.6</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.10</version>
      <scope>test</scope>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>commons-dbcp</groupId>
      <artifactId>commons-dbcp</artifactId>
      <version>1.2.2</version>
    </dependency>
    <dependency>
      <groupId>commons-pool</groupId>
      <artifactId>commons-pool</artifactId>
      <version>1.6</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>3.1.1.RELEASE</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-beans</artifactId>
      <version>3.1.1.RELEASE</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>3.1.1.RELEASE</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-orm</artifactId>
      <version>3.1.1.RELEASE</version>
      <type>jar</type>
    </dependency>
  </dependencies>

</project>
  • 第 18-29 行:用于 Hibernate,
  • 第 30-34 行:用于 MySQL 的 JDBC 驱动程序,
  • 第 35-41 行:用于测试 JUnit,
  • 第 42-51 行:用于 Apache Commons 连接池 DBCP。连接池是一组已打开的连接。当应用程序需要连接时,它会向连接池请求;当不再需要时,则将其归还。 连接在应用程序启动时建立,并在应用程序生命周期内保持打开状态。这避免了反复建立/关闭连接带来的开销。Glassfish 中曾存在此类连接池,但其使用对我们而言是透明的。此处情况亦然,但我们需要自行安装并配置它,
  • 第52-75行:针对Spring。

添加这些依赖项并构建项目:

  • 在 [1] 中构建项目,这将迫使 Maven 下载依赖项,
  • 在 [2] 中,这些依赖项随后会出现在 [Dependencies] 分支中。它们数量众多,因为 Hibernate 和 Spring 框架本身就包含大量依赖项。不过,得益于 Maven,我们无需为此操心。它们会自动下载。

现在我们已获取了依赖项,将 [dao] 层中的 EJB 项目代码粘贴到 [dao] 层的 Spring 项目中:

  • 在 [1] 中,复制到源项目,
  • 在 [2] 中,将其粘贴到目标项目中,
  • 在 [3] 中,显示结果。

复制完成后,需要修正错误。

4.1.2. [exceptions]

类 [RdvMedecinsExceptions] [1] 出现错误,原因是包 [javax] 的第 4 行已不存在。这是 EJB 专属的包。 第6行的错误源于第4行的错误。删除这两行。这将消除[2]的错误。

4.1.3. [jpa] 包

  • 在[1]中,由于缺少[5]行的验证包,导致[Creneau]类出现错误。 本可以将该包添加到项目的依赖中。但在测试时,Hibernate 因该包而抛出异常。由于它对我们的应用程序并非必需,因此我们将其移除。要修正该类,只需删除所有错误的 [2] 行。对所有出错的类都进行此操作。

4.1.4. [dao] 包

目前进展到以下步骤:

  • 在 [1] 中,这两个已修正的包,
  • 在 [2] 中,即 [dao] 包。 由于不再有 EJB,因此 EJB 中的远程和本地接口概念也不复存在。我们将它们删除 [3]。
  • 在 [1] 中,[DaoJpa] 类的错误有两个原因:
  • 导入了一个与 EJB 相关的包(第 6-8 行);
  • 使用了我们刚刚删除的本地和远程接口。

我们删除错误的行,并使用接口 [IDao] 代替本地和远程接口 [2]。

在 EJB 项目中,[DaoJpa] 类是一个单例,其方法在事务内执行。 我们将看到,类 [DaoJpa] 将成为一个由 Spring 管理的 Bean。默认情况下,所有 Spring Bean 都是单例。以上是第一个特性。第二个特性是通过 Spring 的 @Transactional 注解在 [3] 中实现的:

完成上述操作后,项目中不再出现错误 [4]。

4.1.5. [JPA] 层的配置

在项目 EJB 中,我们曾通过文件 [persistence.xml] 配置了 [JPA] 层。此处我们有一个 [JPA] 层,因此需要创建该文件。 在项目 EJB 中,我们曾使用 Glassfish 生成该文件。在此,我们将手动构建它。主要原因是 [persistence.xml] 文件中的一部分配置迁移到了 Spring 配置文件本身中。

我们创建文件 [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="spring-dao-jpa-hibernate-mysqlPU" transaction-type="RESOURCE_LOCAL">
    <class>rdvmedecins.jpa.Client</class>
    <class>rdvmedecins.jpa.Creneau</class>
    <class>rdvmedecins.jpa.Medecin</class>
    <class>rdvmedecins.jpa.Rv</class>
  </persistence-unit>
</persistence>
  • 第 3 行:为持久化单元命名,
  • 第 3 行:事务类型为 RESOURCE_LOCAL。在项目 EJB 中,该值是 JTA,表示事务由容器 EJB 管理。 值 RESOURCE_LOCAL 表示应用程序自行管理事务。此处将通过 Spring 实现,
  • 第4-7行:四个实体的完整名称为JPA。此项为可选,因为Hibernate会自动在项目的ClassPath中查找它们。

就这样。JPA 提供程序的名称、其属性以及数据源的特征 JDBC 现已写入 Spring 配置文件。

4.1.6. Spring配置文件

我们提到 [DaoJpa] 类是 Spring 管理的 Bean。这是通过配置文件实现的。该文件还将包含数据库访问配置以及事务管理配置。它应位于项目的 ClassPath 中。 我们将它放在 [Other sources] 分支中:

[spring-config-dao.xml]文件内容如下:


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">

  <!-- 应用层 -->
  <bean id="dao" class="    " />
  
  <!-- EntityManagerFactory -->
  <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="jpaVendorAdapter">
      <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
        <property name="databasePlatform" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
        <!--
        <property name="showSql" value="true" />
        <property name="generateDdl" value="true" />
        -->
      </bean>
    </property>
  </bean>

  <!-- 数据源DBCP -->
  <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://localhost:3306/dbrdvmedecins2" />
    <property name="username" value="root" />
    <property name="password" value="" />
  </bean>

  <!-- 事务管理器 -->
  <tx:annotation-driven transaction-manager="txManager" />
  <bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory" />
  </bean>

  <!-- 异常处理 -->
  <bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />

  <!-- 持久化 -->
  <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />

</beans>

这是一个兼容 Spring 2.x 的文件。我们并未尝试使用 3.x 版本的新特性。

  • 第 2-4 行:配置文件的根标签 <beans>。我们不对此标签的各项属性进行详细说明。请务必进行复制/粘贴操作,因为这些属性中的任何一个出现错误都可能导致难以理解的错误,
  • 第7行:"dao" Bean 指向 [rdvmedecins.dao.DaoJpa] 类的实例。将创建一个唯一的实例(单例),并实现应用程序的 [dao] 层,
  • 第24-29行:定义了一个数据源。它提供了我们之前提到的“连接池”服务。这里使用的是Apache Commons项目中的[DBCP](该项目包含DBCP和[http://jakarta.apache.org/commons/dbcp/]),
  • 第25-28行:为了与目标数据库建立连接,数据源需要知道所使用的驱动程序(第25行)、数据库的URL(第26行), 连接用户及其密码(第27-28行),
  • 第10-21行:配置JPA层,
  • 第 10 行:定义了一个 [EntityManagerFactory] 类型的 Bean,该 Bean 能够创建 [EntityManager] 类型的对象来管理持久化上下文。 实例化的 [LocalContainerEntityManagerFactoryBean] 类由 Spring 提供。它需要若干参数才能实例化,这些参数在第 11-20 行中定义,
  • 第 11 行:用于获取 SGBD 连接的数据源。即第 24-29 行定义的 [DBCP] 数据源,
  • 第12至20行:要使用的JPA实现,
  • 第13行:将Hibernate定义为要使用的JPA实现,
  • 第 14 行:Hibernate 需配合目标 SGBD(此处为 MySQL5)使用的 SQL 方言,
  • 第 16 行(已注释):要求将 Hibernate 执行的 SQL 命令记录到控制台,
  • 第 17 行(已注释):要求在应用程序启动时生成数据库(drop 和 create),
  • 第 32 行:表明事务通过 Java 注解进行管理(这些注解本也可在 spring-config.xml 中声明)。特别是 [DaoJpa] 类中出现的 @Transactional 注解,
  • 第33-35行:定义了要使用的事务管理器,
  • 第 33 行:事务管理器是 Spring 提供的类,
  • 第 34 行:Spring 的事务处理器需要知道管理 JPA 层的 EntityManagerFactory。该类定义在第 10-21 行,
  • 第 41 行:定义了管理 Spring 持久化注解的类,
  • 第38行:定义了Spring类,该类主要管理@Repository注解,使带有该注解的类能够将JDBC驱动程序中的原生异常(SGBD)转换为[DataAccessException]类型的Spring泛型异常。 此转换将原生异常 JDBC 封装为类型 [DataAccessException],该类型包含多个子类:

Image

此转换使客户端程序能够以通用方式处理异常,无论目标 SGBD 为何。我们在 Java 代码中未使用 @Repository 注解,因此第 38 行是多余的。我们保留该行仅供参考。

至此,Spring 配置文件的介绍已全部完成。该配置文件摘自 Spring 文档。将其适配于各种场景通常只需进行两处修改:

  • 目标数据库的配置:第24-29行,
  • JPA 实现的调整:第 12-20 行。

在代码执行时,配置文件中的所有 Bean 都会被实例化。我们将看到具体实现方式。

4.1.7. 测试类 JUnit

此前我们曾使用测试用例 JUnit 对项目 EJB 的 [DAO] 层进行了测试。对于 Spring 项目的 [DAO] 层,

  • 在 [1] 和 [2] 项目中,将测试 JUnit 复制粘贴到这两个项目之间,
  • 在 [3] 中,导入的测试在新的环境中出现了错误。

报告的错误[1]是EJB远程接口的错误,而该接口已不存在。 此外,第19行中字段[dao]的初始化代码,原本是针对EJB(第25-28行)的专属调用JNDI。 要实例化第19行的字段[dao],我们需要利用Spring的配置文件。具体操作如下:

  • 第 21 行:接口类型已变为 [IDao],
  • 第28行:实例化[spring-config-dao.xml]文件中声明的所有Bean,特别是以下这个:

  <bean id="dao" class="rdvmedecins.dao.DaoJpa" />
  • 第29行向第28行的Spring上下文请求一个id="dao"的Bean引用。于是我们获得了一个对Spring已实例化的单例[DaoJpa](即上文的class)的引用。

第28-29行构建了以下代码块(粉色虚线部分):

当客户端 JUnit 的测试运行时,[DAO] 层已被实例化。因此可以测试其方法。 需要注意的是,与需要 Glassfish 服务器的 EJB [DAO] 测试不同,此测试无需服务器。在此,所有操作均在同一个 JVM 中执行。

现在可以运行 JUnit 测试了。必须先启动 MySQL 服务器。结果如下:

测试 JUnit 已成功。让我们像测试 EJB 时那样查看测试日志:

mai 24, 2012 5:10:29 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
Infos: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@67291453: startup date [Thu May 24 17:10:29 CEST 2012]; root of context hierarchy
mai 24, 2012 5:10:29 PM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
...
mai 24, 2012 5:10:30 PM org.hibernate.annotations.common.Version <clinit>
INFO: HCANN000001: Hibernate Commons Annotations {4.0.1.Final}
mai 24, 2012 5:10:30 PM org.hibernate.Version logVersion
INFO: HHH000412: Hibernate Core {4.1.2}
mai 24, 2012 5:10:30 PM org.hibernate.cfg.Environment <clinit>
...
Infos: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@6affe94b: defining beans [dao,entityManagerFactory,dataSource,org.springframework.aop.config.internalAutoProxyCreator,org.springframework.transaction.annotation.AnnotationTransactionAttributeSource#0,org.springframework.transaction.interceptor.TransactionInterceptor#0,org.springframework.transaction.config.internalTransactionAdvisor,txManager,org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor#0,org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor#0]; 工厂层级根节点
Liste des clients :
Client[1,Mr,Jules,MARTIN]
Client[2,Mme,Christine,GERMAN]
Client[3,Mr,Jules,JACQUARD]
Client[4,Melle,Brigitte,BISTROU]
Liste des médecins :
Médecin[1,Mme,Marie,PELISSIER]
Médecin[2,Mr,Jacques,BROMARD]
Médecin[3,Mr,Philippe,JANDOT]
Médecin[4,Melle,Justine,JACQUEMOT]
Liste des créneaux du médecin Médecin[1,Mme,Marie,PELISSIER]
Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]]
Liste des créneaux du médecin Médecin[1,Mme,Marie,PELISSIER], le [Thu May 24 17:10:30 CEST 2012]
Rv[211, Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]], Client[4,Melle,Brigitte,BISTROU]]
Ajout d'un Rv le [Thu May 24 17:10:30 CEST 2012] dans le créneau Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] pour le client Client[1,Mr,Jules,MARTIN]
avant persist : Rv[null, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
après persist : Rv[216, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Rv ajouté
mai 24, 2012 5:10:31 PM org.hibernate.engine.jdbc.spi.SqlExceptionHelper logExceptions
Liste des Rv du médecin Médecin[1,Mme,Marie,PELISSIER], le [Thu May 24 17:10:30 CEST 2012]
WARN: SQL Error: 1062, SQLState: 23000
Rv[211, Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]], Client[4,Melle,Brigitte,BISTROU]]
Rv[216, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
mai 24, 2012 5:10:31 PM org.hibernate.engine.jdbc.spi.SqlExceptionHelper logExceptions
Ajout d'un Rv le [Thu May 24 17:10:30 CEST 2012] dans le créneau Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] pour le client Client[1,Mr,Jules,MARTIN]
ERROR: Duplicate entry '2012-05-24-3' for key 'UNQ1_RV'
avant persist : Rv[null, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
javax.persistence.PersistenceException: org.hibernate.exception.ConstraintViolationException: Duplicate entry '2012-05-24-3' for key 'UNQ1_RV'
javax.persistence.PersistenceException: org.hibernate.exception.ConstraintViolationException: Duplicate entry '2012-05-24-3' for key 'UNQ1_RV'
javax.persistence.PersistenceException: org.hibernate.exception.ConstraintViolationException: Duplicate entry '2012-05-24-3' for key 'UNQ1_RV'
javax.persistence.PersistenceException: org.hibernate.exception.ConstraintViolationException: Duplicate entry '2012-05-24-3' for key 'UNQ1_RV'
Liste des Rv du médecin Médecin[1,Mme,Marie,PELISSIER], le [Thu May 24 17:10:30 CEST 2012]
Rv[211, Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]], Client[4,Melle,Brigitte,BISTROU]]
Rv[216, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Suppression du Rv ajouté
Rv supprimé
Liste des Rv du médecin Médecin[1,Mme,Marie,PELISSIER], le [Thu May 24 17:10:30 CEST 2012]
Rv[211, Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]], Client[4,Melle,Brigitte,BISTROU]]
  • 第 1-4 行:Spring 日志,
  • 第 5-10 行:Hibernate 日志,
  • 第 11 行:Spring 报告其实例化的所有 Bean。其中首先出现的是 Bean [dao],
  • 第12行及后续:JUnit测试日志,
  • 第60-65行:清晰可见因向数据库中添加已存在的预约而引发的异常。需要指出的是,在EJB中,由于序列化问题,并未出现此异常。

[dao]层已正常运行。我们现在开始构建[métier]层。

4.2. [métier]层

我们采用与 [DAO] 层相同的方法,将 EJB 项目的内容复制并粘贴到 Spring 项目中。

4.2.1. NetBeans 项目

我们构建一个新的 [Java Application] 类型的 Maven 项目,并从 [1] 中移除所有不需要保留的内容:

4.2.2. 项目的依赖项

在架构中:

[métier] 层依赖于 [dao] 层。因此,我们需要添加对前一个项目的依赖:

  • 在 [1] 和 [2] 中,我们添加了对 [dao] 层项目的依赖,
  • 在 [3] 中,该依赖关系又引入了其他依赖关系,即 [dao] 层项目的依赖关系。
  • 在 [1] 和 [2] 中,将项目 EJB 的 Java 源代码复制到 Spring 项目中,
  • 在 [3] 中,导入的源代码在新环境中出现了错误。

首先删除 [métier] 层中已不存在的远程和本地接口([4]):

  • 在 [5] 中,[Metier] 类的错误有多种原因:
    • 使用了已不存在的 [javax.ejb] 包;
    • 使用了已不存在的接口 [IDaoLocal];
    • 使用了已不存在的接口 [IMetierRemote] 和 [IMetierLocal]。

我们将

  • 删除所有与软件包 [javax.ejb] 相关的错误行,
  • 将接口 [IDaoLocal] 替换为接口 [IDao],
  • 并将接口 [IMetierRemote] 和 [IMetierLocal] 替换为接口 [IMetier]。
  • 在 [6] 中,经此修正的类,
  • 为 [7],错误已消除。

我们已删除对 EJB 的引用,但现在需要找回其属性:

 
  • 第 22 行:原为单例模式。通过将该类设为 Spring 管理的 Bean,即可实现此特性,
  • 第23行:每个方法都在事务中执行。这将通过Spring的@Transactional注解实现,
  • 第27-28行:对[DAO]层的引用是通过EJB容器的注入获得的。我们将使用Spring注入。

因此,Spring 项目中 [Metier] 类的代码如下所示:

Java 代码部分到此结束。其余内容将在 Spring 配置文件中完成。

4.2.3. Spring配置文件

我们将 [DAO] 层项目中的 Spring 配置文件复制到 [métier] 层项目中。 如果 [métier] 层项目中不存在 [Other Resources] 分支,则先创建该分支:

  • 在 [1] 中,于 [Files] 选项卡下,在 [main] 文件夹中创建一个子文件夹,
  • 在 [2] 中,其名称应改为 [resources],
  • 在 [3] 中,[Projects] 选项卡下已创建了 [Other Sources] 分支。

我们可以开始复制/粘贴 Spring 配置文件:

  • 在 [1] 中,将项目 [DAO] 的文件复制到项目 [métier] [2] 中,
  • 即 [3],该文件即为已复制的文件。

已复制的配置文件用于配置 [DAO] 层。我们为其添加一个 Bean 来配置 [métier] 层:

1
2
3
4
5
   <!-- 应用层 -->
  <bean id="dao" class="rdvmedecins.dao.DaoJpa" />
  <bean id="metier" class="rdvmedecins.metier.service.Metier">
    <property name="dao" ref="dao"/>
</bean>
  • 第 2 行:[DAO] 层的 Bean,
  • 第 3-5 行:[métier] 层的 Bean,
  • 第 3 行:该 Bean 名为 métier(属性 id),是类 [rdvmedecins.metier.service.Metier] 的实例(属性 class)。 该 Bean 将与其他 Bean 一样在应用程序启动时被实例化。

回顾一下 Bean [rdvmedecins.metier.service.Metier] 的代码:


package rdvmedecins.metier.service;

...

public class Metier implements IMetier, Serializable {

  // DAO层
  private IDao dao;

  public Metier() {
}
  • 第 8 行:字段 [dao] 将由 Spring 与 Bean métier 同时实例化。让我们回到 Spring 配置文件中该 Bean 的定义:
1
2
3
4
5
   <!-- 应用层 -->
  <bean id="dao" class="rdvmedecins.dao.DaoJpa" />
  <bean id="metier" class="rdvmedecins.metier.service.Metier">
    <property name="dao" ref="dao"/>
</bean>
  • 第 4 行:<property> 标签用于初始化已实例化 Bean 的字段。字段名称由 name 属性指定。 因此,将被实例化的将是类 [rdvmedecins.metier.service.Metier] 中的 dao 字段。它将通过一个必须存在的 setDao 方法进行实例化。分配给它的值是 ref 属性的值。此处的值即为第 2 行中 dao Bean 的引用。

简而言之,在代码中:


package rdvmedecins.metier.service;

...

public class Metier implements IMetier, Serializable {

  // DAO层
  private IDao dao;

  public Metier() {
}

第 19 行的 dao 字段将由 Spring 通过 [dao] 层的引用进行初始化。这正是我们想要的效果。dao 字段将由 Spring 通过 setter 进行初始化,我们需要添加该方法:


  // 设置器

  public void setDao(IDao dao) {
    this.dao = dao;
}

我们将 Spring 配置文件重命名为以反映这些更改:

现在我们可以进行测试了。我们将使用之前用于测试 EJB 和 [Metier] 的控制台测试脚本。

4.2.4. [métier] 层的测试

测试将采用以下架构进行:

我们将 EJB 项目的控制台测试复制到 Spring 项目中:

  • 在 [1] 和 [2] 项目之间进行复制/粘贴,
  • 在 [3] 中,导入的代码出现错误。
 

导入的代码存在两种类型的错误:

  • 第 13 行:接口 [IMetierRemote] 已被接口 [IMetier] 替换,
  • 第24-27行:[métier]层的实例化不再通过调用JNDI实现,而是通过实例化Spring配置文件中的Bean来实现。

我们修正了以下两点:

  • 第 22 行:调用文件 [spring-config-metier-dao.xml]。该文件中的所有 Bean 均被实例化。其中包括以下这些:

  <!-- 应用层 -->
  <bean id="dao" class="rdvmedecins.dao.DaoJpa" />
  <bean id="metier" class="rdvmedecins.metier.service.Metier">
    <property name="dao" ref="dao"/>
</bean>

这两个 Bean 分别代表测试架构中的 [DAO] 和 [métier] 层:

完成上述操作后,测试即可开始:

  

测试日志如下:

mai 25, 2012 9:45:07 AM org.springframework.context.support.AbstractApplicationContext prepareRefresh
Infos: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@22a92801: startup date [Fri May 25 09:45:07 CEST 2012]; root of context hierarchy
mai 25, 2012 9:45:07 AM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
....
Infos: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@38a0a058: defining beans [dao,metier,entityManagerFactory,dataSource,org.springframework.aop.config.internalAutoProxyCreator,org.springframework.transaction.annotation.AnnotationTransactionAttributeSource#0,org.springframework.transaction.interceptor.TransactionInterceptor#0,org.springframework.transaction.config.internalTransactionAdvisor,txManager,org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor#0,org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor#0]; 工厂层级根节点
Liste des clients :
Client[1,Mr,Jules,MARTIN]
Client[2,Mme,Christine,GERMAN]
Client[3,Mr,Jules,JACQUARD]
Client[4,Melle,Brigitte,BISTROU]
Liste des médecins :
Médecin[1,Mme,Marie,PELISSIER]
Médecin[2,Mr,Jacques,BROMARD]
Médecin[3,Mr,Philippe,JANDOT]
Médecin[4,Melle,Justine,JACQUEMOT]
Liste des créneaux du médecin Médecin[1,Mme,Marie,PELISSIER]
Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]]
Liste des rendez-vous du médecin Médecin[1,Mme,Marie,PELISSIER], le [Fri May 25 09:45:07 CEST 2012]
Agenda[Médecin[1,Mme,Marie,PELISSIER],25/05/2012, [Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]] null]]
Ajout d'un Rv le [Fri May 25 09:45:07 CEST 2012] dans le créneau Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] pour le client Client[1,Mr,Jules,MARTIN]
avant persist : Rv[null, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
après persist : Rv[220, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Rv ajouté
Liste des Rv du médecin Médecin[1,Mme,Marie,PELISSIER], le [Fri May 25 09:45:07 CEST 2012]
Rv[220, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Agenda[Médecin[1,Mme,Marie,PELISSIER],25/05/2012, [Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] Rv[220, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]] [Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]] null]]
Suppression du Rv ajouté
Rv supprimé
Liste des Rv du médecin Médecin[1,Mme,Marie,PELISSIER], le [Fri May 25 09:45:07 CEST 2012]
Agenda[Médecin[1,Mme,Marie,PELISSIER],25/05/2012, [Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]] null]]
  • 第1-4行:Spring和Hibernate的日志,
  • 第5行:Spring实例化的Bean。请注意DAO业务Bean,
  • 第6-53行:测试日志。这些日志与项目EJB的测试结果一致。请参阅该测试的注释(第3.5.3节)。

我们已构建了 [métier] 层。接下来我们将进入最后一层,即 [web] 层。

4.3. [web]层

要构建 [web] 层,我们将采用与前两个层相同的方法,即从项目 EJB 的 [web] 层进行复制/粘贴。

4.3.1. NetBeans 项目

首先,我们构建一个 Web 项目:

  • 在 [1] 中,创建一个新项目,
  • 在 [2] 中,创建一个类型为 [Web Application] 的 Maven 项目,
  • 在 [3] 中,为其命名,
  • 在 [4] 中,这次选择 Tomcat 服务器,而不是 EJB 项目中使用的 Glassfish,
  • 在 [5] 中,生成的项目,
  • 在 [6] 中,这是在删除 [index.jsp] 以及 [Source Packages] 的包之后的项目。

4.3.2. 项目的依赖关系

让我们看看项目的架构:

[web]层需要[métier]、[DAO]和[JPA]层。这些层属于我们刚刚构建的两个项目。因此,它依赖于这两个项目:

  • 在 [1] 中,我们添加了对 Spring / 业务项目的依赖,
  • 在 [2] 中,已添加 Spring / 业务项目。 由于该项目本身依赖于 Spring / DAO / JPA 项目,因此该依赖项已自动添加到 [3] 的依赖列表中。

让我们回到应用程序的结构:

Web层是JSF层。因此我们需要Java Server Faces库。而Tomcat服务器并不包含这些库。 因此,该依赖项的范围(scope)将不再是 [provided](如在 Glassfish 服务器中那样),而是 [compile]——这是未指定范围时的默认范围。

我们将这些依赖项直接添加到 [pom.xml] 的代码中:


<dependencies>
    <dependency>
      <groupId>${project.groupId}</groupId>
      <artifactId>mv-rdvmedecins-spring-metier</artifactId>
      <version>${project.version}</version>
    </dependency>
    <dependency>
      <groupId>com.sun.faces</groupId>
      <artifactId>jsf-api</artifactId>
      <version>2.1.7</version>
    </dependency>
    <dependency>
      <groupId>com.sun.faces</groupId>
      <artifactId>jsf-impl</artifactId>
      <version>2.1.7</version>
    </dependency>
    <dependency>
      <groupId>javax</groupId>
      <artifactId>javaee-web-api</artifactId>
      <version>6.0</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>
  • 第 7-16 行已添加到文件 [pom.xml] 中。这些是针对 JSF 的依赖项,也是 EJB / Glassfish 项目中使用的依赖项。 需要注意的是,这些依赖项没有 <scope> 标签。因此,它们默认的范围是 [compile]。因此,JSF 库将被打包到 Web 项目的 [war] 归档中。

在将这些依赖项添加到 [pom.xml] 文件后,我们编译该项目以便下载这些依赖项。

4.3.3. 将 JSF / Glassfish 项目迁移至 JSF / Tomcat 项目

我们将 JSF / Glassfish 项目的全部代码复制到 JSF / Tomcat 项目中:

  • [1, 2, 3]:将旧项目的网页复制到新项目中,
  • [1, 2, 3]:将旧项目的Java代码复制到新项目。存在一些错误,这是正常的。我们将予以修正,
  • 在 [1] 中,于 NetBeans 的 [Files] 标签页中,在 [main] 文件夹下创建子文件夹 [resources],
  • 这将在[Projects]选项卡中创建分支[Other Sources] [3],
  • [1, 2, 3]:将旧项目中的消息文件复制到新项目中。

4.3.4. 导入项目的修改

我们已指出导入的 Java 代码中存在错误。让我们来查看这些错误:

  • 在 [1] 中,仅 [Application] Bean 存在错误,
  • 在 [2] 中,错误仅因接口 [IMetierLocal] 已不存在而引发。这里令人惊讶的是,第 20 行并未报错。 注解 @EJB 明确引用了 EJB,且在此处被识别。这是因为存在依赖关系 [javaee-web-api-6.0] [3]。 Java EE 6 引入了一种架构,允许在没有 EJB 远程接口的服务器上,部署基于 EJB 的 Web 应用程序,即使这些服务器没有 EJB 容器。 服务器只需提供 [javaee-web-api-6.0] 依赖项即可。实际上,我们可以看到该依赖项的作用域为 [provided] [3]。

在此我们不会使用依赖项 [javaee-web-api-6.0]。我们将删除 [1]:

这导致了新的错误 [2]。我们将从 [Form] bean 的错误开始处理:

  • 在 [1] 中,出错的行与 [javax] 包的缺失有关。 我们将 [2] 中的所有相关行删除。这些错误行将类 [Form] 定义为会话作用域的 Bean(见 [1] 的第 18-20 行)。 此外,bean [Application] 是在第 25 行被注入的。这些信息将迁移到 JSF 和 [faces-config.xml] 的配置文件中。

接下来看 [Application] Bean:

删除 [1] 中所有错误的行,并将第 13 行和第 21 行中的接口 [IMetierLocal] 更改为 [IMetier]。 在 [2] 中,已无错误。在 [1] 中, 我们删除了第15-16行,这些代码原本将类[Application]定义为作用域为application.的Bean。该信息将迁移至JSF和[faces-config.xml]的配置文件中。 我们还删除了第 20 行,该行曾向 Bean 中注入 [métier] 层的引用。现在,该引用将由 Spring 进行初始化。我们已经拥有所需的配置文件,即 Spring / 业务项目中的配置文件。我们将它复制如下:

  • 复制为 [1, 2],我们将 Spring / 业务 项目中的 Spring 配置文件复制到 Spring / JSF 项目中,

在 [3] 中,即为结果。

在 Bean [Application] 中,需利用该配置文件以获取 [métier] 层的引用。此操作在方法 [init] 中完成:


package beans;

...
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Application {

  // 业务层
  private IMetier metier;
...

  public Application() {
  }

  @PostConstruct
  public void init() {
    try {
      // [métier] 层的实例化
      ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-metier-dao.xml");
      metier = (IMetier) ctx.getBean("metier");
      // 将医生和客户缓存
...
    } catch (Throwable th) {
...
    }
...
  }
  • 第 20 行:实例化 Spring 配置文件中的 Bean,
  • 第21行:请求业务Bean的引用,即[métier]层的引用。

通常情况下,Spring Bean 的实例化应在应用范围 Bean 的 init 方法中进行。还有另一种方法,即通过 Spring Servlet 来实例化 Bean。这需要修改文件 [web.xml],并添加对工件 [spring-web] 的依赖。 出于与先前代码保持一致的考虑,我们在此未采用该方法。

我们已移除了 [Application] 和 [Form] 类中的注解,这些注解原本将它们定义为 JSF Bean。这些类必须保持为 JSF Bean。 因此,我们不再使用注解,而是通过 JSF 和 [WEB-INF / faces.config.xml] 的配置文件来声明 Bean。

该文件现内容如下:


<?xml version='1.0' encoding='UTF-8'?>

<!-- =========== FULL CONFIGURATION FILE ================================== -->

<faces-config version="2.0"
              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/web-facesconfig_2_0.xsd">

  <application>
    <!-- 消息文件 -->
    <resource-bundle>
      <base-name>
        messages
      </base-name>
      <var>msg</var>
    </resource-bundle>
    <message-bundle>messages</message-bundle>
  </application>
    <!-- Bean applicationBean -->
    <managed-bean>
      <managed-bean-name>applicationBean</managed-bean-name>
      <managed-bean-class>beans.Application</managed-bean-class>
      <managed-bean-scope>application</managed-bean-scope>
    </managed-bean>
    <!-- 表单 Bean -->
    <managed-bean>
      <managed-bean-name>form</managed-bean-name>
      <managed-bean-class>beans.Form</managed-bean-class>
      <managed-bean-scope>session</managed-bean-scope>
      <managed-property>
        <property-name>application</property-name>
        <value>#{applicationBean}</value>
      </managed-property>
    </managed-bean>
</faces-config>
  • 第 10-19 行配置了消息文件。这是 JSF / EJB 项目中唯一的配置,
  • 第21至35行声明了应用程序JSF的Bean。 这是 JSF 1.x 版本的标准方法。JSF 2 引入了注解,但 JSF 和 1.x 的方法仍然受支持,
  • 第 21-25 行:声明 applicationBean Bean,
  • 第 22 行:Bean 的名称。可能会有人想使用“application”作为名称,但应避免,因为这是 JSF 中一个预定义 Bean 的名称,
  • 第 23 行:Bean 类的完整名称,
  • 第 24 行:其作用域,
  • 第 27-35 行:定义表单 Bean,
  • 第 28 行:Bean 的名称,
  • 第 29 行:Bean 类的完整名称,
  • 第 30 行:其作用域,
  • 第 31-34 行:定义 [beans.Form] 类的属性,
  • 第 32 行:属性名称。[beans.Form] 类必须包含一个同名字段及其对应的 setter 方法,
  • 第 33 行:字段的值。此处是第 21 行定义的 Bean applicationBean 的引用。 因此,此处将作用域为 application 的 Bean 注入到作用域为 session 的 Bean 中,以便后者能够访问作用域为 application 的数据。

我们前面提到,bean [beans.Form] 的字段 [application] 将通过 setter 进行初始化。因此,如果该字段尚未存在,则需将其添加到类 [beans.Form] 中:


public void setApplication(Application application) {
    this.application = application;
  }

4.3.5. 应用程序测试

我们的应用程序现已无错误,并已准备好进行测试:

  • 在 [1] 中,将修正后的项目
  • 在 [2] 中进行构建,
  • 在 [3] 中,我们运行它。必须启动 SGBD 和 MySQL。 如果 Tomcat 服务器尚未启动,则会启动 [4],随后应用程序的首页将显示 [5]:

至此,我们已进入所研究的应用程序。请读者自行验证其是否正常运行。现在,让我们停止该应用程序:

  • 在 [1] 状态下,应用程序被卸载,
  • 在[2]中,该应用程序已不复存在。

现在我们来看Tomcat的 日志:

1
2
3
4
5
6
mai 25, 2012 2:15:57 PM org.apache.catalina.loader.WebappClassLoader clearReferencesJdbc
Grave: The web application [/mv-rdvmedecins-spring-jsf2] registered the JDBC driver [com.mysql.jdbc.Driver] but failed to unregister it when the web application was stopped. To prevent a memory leak, the JDBC Driver has been forcibly unregistered.
mai 25, 2012 2:15:57 PM org.apache.catalina.loader.WebappClassLoader clearReferencesThreads
Grave: The web application [/mv-rdvmedecins-spring-jsf2] appears to have started a thread named [MySQL Statement Cancellation Timer] but has failed to stop it. This is very likely to create a memory leak.
mai 25, 2012 2:15:59 PM org.apache.catalina.startup.HostConfig checkResources
Infos: Repli (undeploy) de l'application web ayant pour chemin de contexte /mv-rdvmedecins-spring-jsf2

第2行和第4行提示应用程序停止时出现故障。第4行指出可能存在内存泄漏的风险。 实际上,内存泄漏确实发生了,过了一会儿 NetBeans 就无法使用了。这个问题特别令人恼火,因为每次重新运行项目时都必须重新启动 NetBeans。在文档《通过实例了解 Struts 2》[http://tahe.developpez.com/java/struts2] 中已经遇到过这个问题。

网上关于此错误的信息很多。当反复在 Tomcat 上加载/卸载应用程序时,该错误就会出现。过了一段时间后,会出现错误 java.lang.OutOfMemoryError: PermGen 空间。 如果该错误源于第三方归档文件(jar),如本例所示,似乎没有办法避免。此时必须重启 Tomcat 才能消除该错误。

不过,我们可以延缓该错误的出现。首先,增加溢出的内存空间。

  • 在 [1] 中,进入 Tomcat 服务器的属性,
  • 在[2]中,进入[Platform]选项卡,设置内存溢出的阈值。 此处设置为 1 GB,因为总内存为 8 GB。若内存较小,可设置为 512M(512 MB)。

随后,将 JDBC 驱动程序从 MySQL 移动到 <tomcat>/lib,其中 <tomcat> 是 Tomcat 的安装目录。

  • 在 [1] 中,于 Tomcat 的属性中,记录其安装目录 <tomcat>,
  • 在 <tomcat>/lib 目录下,将 [2] 驱动程序更新为 JDBC(最新版本),替换原有的 MySQL 和 [3]。

随后,移除项目对驱动程序 JDBC(版本号为 MySQL、[4])的依赖。

完成上述操作后,对应用程序进行测试。发现可以反复加载/卸载应用程序。然而,内存泄漏问题并未解决,只是推迟到了稍后出现。

4.4. 结论

我们将 JSF / EJB / Glassfish 应用程序迁移到了 JSF / Spring / Tomcat 环境。这主要通过在两个项目之间进行复制粘贴来实现。 之所以能够实现,是因为 Spring 和 EJB3 技术具有高度相似性。事实上,EJB3 的创建,正是源于 Spring 展现出的性能优于 EJB2。 随后,EJB3 便借鉴了 Spring 的优秀设计理念。

4.5. 使用Eclipse进行的测试

  • 在 [1] 中,导入三个 Spring 项目,
  • 在 [2] 中,选择 [DAO] 层中的 JUnit 测试并执行,
  • 在 [4] 中,测试成功,
  • 在 [5] 中,控制台日志。
  • 在 [6A] [6B] 中,执行 [métier] 层的控制台客户端,
  • 在 [7] 中,显示的控制台界面,
  • 在 [8] [9] 中,在 Tomcat 7 服务器上运行 Web 项目 [10],
  • 在 [11] 中,应用程序的首页将在 Eclipse 的内置浏览器中显示。