Skip to content

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

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

这确实是一个移植项目。我们将以之前的应用程序为基础,对其进行调整以适应新环境。本文仅对所做的修改进行说明。主要有三处改动:

  • 服务器不再是 GlassFish,而是 Tomcat——一款不带 EJB 容器的轻量级服务器,
  • 为替代 EJB,我们将采用 Spring——EJB 的主要竞争对手 [http://www.springsource.com/],
  • 所使用的JPA实现将采用Hibernate,而非EclipseLink。

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

  

使用 Spring 框架需要具备一定的知识,相关内容可参见 [ref7](见第 166 页)。

4.1. [DAO] 和 [JPA] 层

4.1.1. NetBeans 项目

我们正在构建一个类型为 [Java 应用程序] 的 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 项目代码粘贴到 Spring 项目的 [dao] 层中:

  • 在 [1] 中,我们将代码复制到源项目,
  • 在 [2] 中,我们将内容粘贴到目标项目中,
  • 在 [3] 中,显示结果。

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

4.1.2. [exceptions] 包

[RdvMedecinsExceptions] 类 [1] 存在错误,因为第 4 行中的 [javax] 包已不存在。这是一个 EJB 专用的包。第 6 行的错误源于第 4 行的错误。我们删除这两行代码。这样就解决了这些错误 [2]。

4.1.3. [jpa] 包

  • 在 [1] 中,由于第 [5] 行缺少 validation 包,[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">
 
  <!-- application layers -->
  <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>
 
  <!-- data source 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>
 
  <!-- transaction manager -->
  <tx:annotation-driven transaction-manager="txManager" />
  <bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory" />
  </bean>

  <!-- translation of exceptions -->
  <bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
 
  <!-- persistence -->
  <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
 
</beans>

这是一个与 Spring 2.x 兼容的文件。我们尚未尝试使用 3.x 版本的新功能。

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

Image

这种转换使客户端程序能够以通用方式处理异常,而无需考虑目标数据库管理系统。我们在 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 专用的 JNDI 调用(第 25–28 行)。要实例化第 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] 单例(即上文所述的)的引用。

第 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]; root of factory hierarchy
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。首先,我们看到 [dao] Bean,
  • 第 12 行及后续:JUnit 测试日志,
  • 第 60–65 行:我们清楚地看到因向数据库添加已存在的预约而引发的异常。回想一下,在 EJB 中,由于序列化问题,我们并未遇到此异常。

[dao] 层已正常运行。接下来我们将构建 [business] 层。

4.2. [业务]层

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

4.2.1. NetBeans 项目

我们正在创建一个类型为 [Java 应用程序] 的新 Maven 项目,并删除了所有不需要保留的内容 [1]:

4.2.2. 项目依赖项

在架构中:

[业务]层依赖于[DAO]层。因此,我们需要在当前项目中添加对前一个项目的依赖:

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

首先,我们将 [4] 中已不存在的远程和本地接口从 [业务] 层中移除:

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

我们将

  • 删除所有与 [javax.ejb] 包相关的错误行,
  • 将 [IDaoLocal] 接口替换为 [IDao] 接口,
  • 将 [IMetierRemote] 和 [IMetierLocal] 接口替换为 [IMetier] 接口。
  • 在 [6] 中,按照上述方法修正类后,
  • 在[7]中,不再出现错误。

我们已移除了对 EJB 的引用,但现在需要获取它们的属性:

 
  • 第 22 行:我们原本使用了一个单例。通过将该类设为 Spring 管理的 Bean,即可实现此行为,
  • 第 23 行:每个方法都在事务中运行。这将通过使用 Spring 的 @Transactional 注解来实现,
  • 第 27–28 行:通过 EJB 容器注入获取了对 [DAO] 层的引用。我们将使用 Spring 注入。

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

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

4.2.3. Spring 配置文件

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

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

现在我们可以复制并粘贴 Spring 配置文件:

  • 在 [1] 中,将文件从 [DAO] 项目复制到 [business] 项目 [2] 中,
  • 在 [3] 中,即该已复制的文件。

已复制的配置文件用于配置 [DAO] 层。我们向其中添加一个 Bean 来配置 [business] 层:

1
2
3
4
5
  <!-- application layers -->
  <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 行:[业务] 层 Bean,
  • 第 3 行:该 Bean 名为 métierid 属性),是 [rdvmedecins.metier.service.Metier] 类的实例(class 属性)。该 Bean 将与其他 Bean 一样,在应用程序启动时被实例化。

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


package rdvmedecins.metier.service;
 
...
 
public class Metier implements IMetier, Serializable {
 
  // dao layer
  private IDao dao;
 
  public Metier() {
}
  • 第 8 行:[dao] 字段将由 Spring 在业务 Bean 实例化时一并实例化。让我们回到 Spring 配置文件中该 Bean 的定义:
1
2
3
4
5
  <!-- application layers -->
  <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 layer
  private IDao dao;
 
  public Metier() {
}

第 19 行中的 dao 字段将由 Spring 通过引用 [dao] 层进行初始化。这正是我们所期望的。dao 字段将由 Spring 通过一个我们需要添加的 setter 方法进行初始化:


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

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

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

4.2.4. 测试 [业务] 层

该测试将采用以下架构运行:

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

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

导入的代码包含两种类型的错误:

  • 第 13 行:[IMetierRemote] 接口已被 [IMetier] 接口取代,
  • 第 24–27 行:[business] 层不再通过 JNDI 调用进行实例化,而是通过从 Spring 配置文件中实例化 Bean 来实现。

我们修正了这两个问题:

  • 第 22 行:使用了 [spring-config-metier-dao.xml] 文件。该文件中的所有 Bean 随后都会被实例化。其中包括以下内容:

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

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

完成上述步骤后,即可运行测试:

  

测试日志如下:

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]; root of factory hierarchy
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 节)。

我们已经构建了[业务]层。现在我们将转向最后一个层,即[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] 层依赖于 [业务]、[DAO] 和 [JPA] 层。这些层属于我们刚刚构建的两个项目。因此,需要对这两个项目分别进行依赖声明:

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

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

Web 层是 JSF 层。因此,我们需要 Java Server Faces 库。Tomcat 服务器中没有这些库。因此,该依赖项不会像在 GlassFish 服务器中那样具有 [provided] 作用域,而是具有 [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>
  • 已在 [pom.xml] 文件中添加了第 7–16 行。这些是 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 的 [文件] 选项卡下,在 [main] 文件夹内创建一个 [resources] 子文件夹,
  • 这将在 [项目] 选项卡 [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 应用程序。服务器只需提供 [javaee-web-api-6.0] 依赖项即可。我们可以看到,该依赖项具有 [provided] 作用域 [3]。

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

这会导致新的错误[2]。我们先从[Form] Bean中的错误开始:

  • 在 [1] 中,出错的代码行与丢失 [javax] 包有关。 我们将它们全部删除 [2]。这些错误行使得 [Form] 类成为了一个会话范围的 Bean([1] 的第 18–20 行)。此外,[Application] Bean 是在第 25 行注入的。这些信息将被移至 JSF 配置文件 [faces-config.xml] 中。

接下来我们来看 [Application] Bean:

我们从 [1] 中删除了所有错误的代码行,并将第 13 行和第 21 行中的 [IMetierLocal] 接口改为 [IMetier]。在 [2] 中,已无错误在 [1] 中,我们删除了第 15–16 行,这些代码曾将 [Application] 类定义为应用程序作用域的 Bean 这些信息将被移至 JSF 配置文件 [faces-config.xml] 中。我们 还删除了第 20 行,该行曾将 [business] 层的引用注入到 Bean 中。现在,该 Bean 将由 Spring 进行初始化。我们已经拥有必要的配置文件,即来自 Spring / Business 项目的那个。我们将它复制过来:

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

在 [3] 中,结果如下。

在 [Application] Bean 中,我们需要使用此配置文件来获取 [business] 层的引用。这在它的 [init] 方法中完成:


package beans;
 
...
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
 
public class Application {
 
  // business layer
  private IMetier metier;
...
 
  public Application() {
  }
 
  @PostConstruct
  public void init() {
    try {
      // instantiation layer [business]
      ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-metier-dao.xml");
      metier = (IMetier) ctx.getBean("metier");
      // caching doctors and customers
...
    } catch (Throwable th) {
...
    }
...
  }
  • 第 20 行:实例化来自 Spring 配置文件的 Bean,
  • 第 21 行:请求业务 Bean 的引用,即位于 [business] 层中的 Bean。

一般而言,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>
    <!-- message file -->
    <resource-bundle>
      <base-name>
        messages
      </base-name>
      <var>msg</var>
    </resource-bundle>
    <message-bundle>messages</message-bundle>
  </application>
    <!-- the applicationBean bean -->
    <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>
    <!-- the bean form -->
    <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,
  • 第 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 行定义的 applicationBean 的引用。因此,我们将应用范围的 Bean 注入会话范围的 Bean 中,以便后者能够访问应用范围的数据。

我们之前提到,[beans.Form] Bean 的 [application] 字段将通过设置器进行初始化。因此,如果 [beans.Form] 类中尚未存在该字段,我们必须将其添加进去:


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

4.3.5. 测试应用程序

我们的应用程序现已无错误,可以进行测试了:

  • 在 [1] 中,修正后的项目,
  • 在 [2] 中,我们进行构建,
  • 在 [3] 中,我们运行它。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 space 错误。如果错误源于第三方库(JAR),就像本例中一样,似乎没有办法预防。此时必须重启 Tomcat 才能解决。

不过,您可以延缓该错误的发生。首先,增加已溢出的内存空间。

  • 在 [1] 中,进入 Tomcat 服务器属性,
  • 在 [2] 中,进入 [Platform] 选项卡,设置内存溢出阈值。这里我们将其设为 1 GB,因为我们总共拥有 8 GB 内存。如果内存较少,您可以将其设为 512 MB(512 兆字节)。

接下来,将 MySQL JDBC 驱动程序放置在 <tomcat>/lib 目录下,其中 <tomcat> 是 Tomcat 的安装目录。

  • 在 [1] 的 Tomcat 属性中,记下其安装目录 <tomcat>,
  • 在 <tomcat>/lib [2] 目录中,放置一个最新的 MySQL JDBC 驱动程序 [3]。

接下来,移除项目对 MySQL JDBC 驱动程序的依赖 [4]。

完成此操作后,请测试应用程序。你会发现可以反复加载和卸载应用程序。然而,内存泄漏问题并未解决,只是推迟到了稍后才出现。

4.4. 结论

我们将 JSF/EJB/GlassFish 应用程序迁移到了 JSF/Spring/Tomcat 环境中。迁移主要通过在两个项目之间进行复制粘贴来完成。之所以能够实现这一点,是因为 Spring 和 EJB3 具有许多相似之处。事实上,EJB3 的诞生正是因为 Spring 被证明比 EJB2 更高效,随后 EJB3 便融合了 Spring 的最佳特性。

4.5. 使用 Eclipse 进行测试

  • 在 [1] 中,导入三个 Spring 项目,
  • 在 [2] 中,从 [DAO] 层选择 JUnit 测试,并在 [3] 中运行它,
  • 在 [4] 中,测试通过,
  • 在 [5] 中,控制台会输出日志。
  • 在 [6A] [6B] 中,运行 [业务] 层的控制台客户端,
  • 在 [7] 中,显示生成的控制台输出,
  • 在[8]和[9]中,我们在Tomcat 7服务器[10]上运行了该Web项目,
  • 在[11]中,应用程序的首页在Eclipse的内置浏览器中显示。