Skip to content

6. Versión 2: Arquitectura OpenEJB / JPA

6.1. Introducción a los principios de la migración

Aquí presentamos los principios que regirán la migración de una aplicación JPA / Spring / Hibernate a una aplicación JPA / OpenEJB / EclipseLink. Esperaremos hasta el párrafo 6.2 para crear los proyectos de Maven.

6.1.1. Las dos arquitecturas

La implementación actual con Spring / Hibernate

La implementación que se va a construir con OpenEJB / EclipseLink

6.1.2. Las bibliotecas de los proyectos

  • Las capas [DAO] y [metier] ya no son instanciadas por Spring. Son instanciadas por el contenedor OpenEJB.
  • Las bibliotecas del contenedor Spring y su configuración desaparecen y son reemplazadas por las bibliotecas del contenedor OpenEJB y su configuración.
  • Las bibliotecas de la capa JPA / Hibernate se reemplazan por las de la capa JPA / EclipseLink

  • El archivo [META-INF/persistence.xml], que configura la capa JPA, queda así:
<?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">
     <!-- entidades JPA -->
    <class>jpa.Cotisation</class>
    <class>jpa.Employe</class>
    <class>jpa.Indemnite</class>
     <!-- el proveedor JPA es EclipseLink -->
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
     <!-- propiedades del proveedor -->
    <properties>
      <property name="eclipselink.ddl-generation" value="create-tables"/>
    </properties>
  </persistence-unit>
</persistence>
  • línea 3: las transacciones en un contenedor EJB son de tipo JTA (transacción Java API). Con Spring, eran de tipo RESOURCE_LOCAL.
  • línea 9: la implementación JPA utilizada es EclipseLink
  • líneas 5-7: las entidades gestionadas por la capa JPA
  • líneas 11-13: propiedades del proveedor EclipseLink
  • línea 12: en cada ejecución, se crearán las tablas

Las características JDBC de la fuente de datos JTA utilizada por el contenedor OpenEJB se especificarán mediante el siguiente archivo de configuración [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>
  • línea 3: se utiliza el identificador «Default JDBC Database» cuando se trabaja con un contenedor OpenEJB integrado (embedded) en la propia aplicación.
  • línea 5: utilizamos una base de datos MySQL [dbpam_eclipselink]

6.1.4. Implementación de la capa [DAO] mediante EJB

  • Las clases que implementan la capa [DAO] se convierten en EJB. Tomemos como ejemplo la clase [CotisationDao]:

La interfaz [ICotisationDao] en la versión de Spring era la siguiente:

package dao;

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

public interface ICotisationDao {
   // crear una nueva cotización
  Cotisation create(Cotisation cotisation);
   // modificar una cotización existente
  Cotisation edit(Cotisation cotisation);
   // eliminar una cotización existente
  void destroy(Cotisation cotisation);
   // buscar una cotización específica
  Cotisation find(Long id);
   // obtener todos los objetos de cotización
  List<Cotisation> findAll();

}

La clase EJB implementará esta misma interfaz de dos formas diferentes: una local y otra remota. La interfaz local puede ser utilizada por un cliente que se ejecute en el mismo JVM, mientras que la interfaz remota puede ser utilizada por un cliente que se ejecute en otro JVM.

La interfaz local:

1
2
3
4
5
6
7
package dao;

import javax.ejb.Local;

@Local
public interface ICotisationDaoLocal extends ICotisationDao{
}
  • línea 6: la interfaz [ICotisationDaoLocal] hereda de la interfaz [ICotisationDao] para adoptar todos sus métodos. No agrega ninguno nuevo.
  • línea 5: la anotación @Local la convierte en una interfaz local para la interfaz EJB, que la implementará.

La interfaz remota:

1
2
3
4
5
6
7
package dao;

import javax.ejb.Remote;

@Remote
public interface ICotisationDaoRemote extends ICotisationDao{
}
  • línea 6: la interfaz [ICotisationDaoRemote] hereda de la interfaz [ICotisationDao] para adoptar todos sus métodos. No agrega ninguno nuevo.
  • línea 5: la anotación @Remote la convierte en una interfaz remota para EJB, que la implementará.

La capa [DAO] es implementada por un EJB que implementa ambas interfaces (esto no es obligatorio):

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

  @PersistenceContext
  private EntityManager em;
  • línea 1: la anotación @Stateless, que convierte a la clase en un EJB
  • línea 2: la anotación @TransactionAttribute, que hace que cada método de la clase se ejecute dentro de una transacción.
  • línea 5: la anotación @PersistenceContext, que inyecta en la clase [CotisationDao] el EntityManager de la capa JPA. Es idéntica a la que teníamos en la versión de Spring.

Cuando se utiliza la interfaz local de la capa [DAO], el cliente de esta interfaz se ejecuta en la misma JVM.

En el ejemplo anterior, las capas [metier] y [DAO] intercambian objetos por referencia. Cuando una capa modifica el objeto compartido, la otra capa detecta ese cambio.

Cuando se utiliza la interfaz remota de la capa [DAO], el cliente de dicha interfaz suele ejecutarse en otra JVM.

En el ejemplo anterior, las capas [metier] y [DAO] intercambian objetos por valor (serialización del objeto intercambiado). Cuando una capa modifica un objeto compartido, la otra capa solo detecta ese cambio si se le devuelve el objeto modificado.

6.1.5. Implementación de la capa [metier] mediante una EJB

  • La clase que implementa la capa [metier] también se convierte en un EJB que implementa una interfaz local y remota. La interfaz inicial [IMetier] era la siguiente:
package metier;

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

public interface IMetier {
   // obtener la nómina
  FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
   // lista de empleados
  List<Employe> findAllEmployes();
}

Se crean una interfaz local y una interfaz remota a partir de la interfaz anterior:

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{
}

La clase EJB de la capa [metier] implementa estas dos interfaces:

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

   // Referencia sobre las capas locales [DAO]
  @EJB
  private ICotisationDaoLocal cotisationDao = null;
  @EJB
  private IEmployeDaoLocal employeDao = null;
  @EJB
  private IIndemniteDaoLocal indemniteDao = null;
  • líneas 1-2: definen un EJB en el que cada método se ejecuta en una transacción.
  • línea 7: una referencia a la interfaz local de EJB [CotisationDao].
  • línea 6: la anotación @EJB indica que el contenedor EJB debe inyectar una referencia a la interfaz local de EJB [CotisationDao].
  • Líneas 8-11: se repite el mismo proceso para las interfaces locales de EJB, [EmployeDao] y [IndemniteDao].

Al final, cuando se instancien los EJB y [Metier], los campos de las líneas 7, 9 y 11 se inicializarán con referencias a las interfaces locales de los tres EJB de la capa [DAO]. Por lo tanto, aquí se supone que las capas [metier] y [DAO] se ejecutarán en la misma JVM.

6.1.6. Los clientes de EJB

En el diagrama anterior, para comunicarse con la capa [metier], la capa [ui] debe obtener una referencia a la interfaz remota de la capa EJB de la capa [metier].

En el esquema anterior, para comunicarse con la capa [metier], la capa [ui] debe obtener una referencia a la interfaz local de la capa EJB de la capa [metier]. El método para obtener estas referencias varía de un contenedor a otro. Para el contenedor OpenEJB, se puede proceder de la siguiente manera:

Referencia en la interfaz local:

1
2
3
4
5
6
7
8
9
     // Se configura el contenedor Open EJB integrado
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
      // inicialización del contexto JNDI con las propiedades anteriores
    InitialContext initialContext = new InitialContext(properties);
     // instanciación de las capas DAO
    employeDao = (IEmployeDaoLocal) initialContext.lookup("EmployeDaoLocal");
    cotisationDao = (ICotisationDaoLocal) initialContext.lookup("CotisationDaoLocal");
indemniteDao = (IIndemniteDaoLocal) initialContext.lookup("IndemniteDaoLocal");
  • líneas 2-5: se inicializa el contenedor OpenEJB.
  • línea 5: se cuenta con un contexto JNDI (Java Naming and Directory Interface) que permite obtener referencias sobre los EJB. Cada EJB se identifica con un nombre JNDI:
  • (continuación)
    • para la interfaz local, se agrega «Local» al nombre del EJB (líneas 7-9)
    • para la interfaz remota, se agrega «Remote» al nombre del EJB

Con Java EE 5, estas reglas varían según el contenedor EJB. Esto representa una dificultad. Java EE 6 introdujo una notación JNDI compatible con todos los servidores de aplicaciones.

El código anterior obtiene referencias a las interfaces locales de EJB a través de sus nombres JNDI. Anteriormente mencionamos que estas también se pueden obtener mediante la anotación @EJB. Por lo tanto, tal vez se desee escribir:

@EJB
private IemployeDaoLocal employeDaoLocal ;

La anotación @EJB solo se respeta si pertenece a una clase cargada por el contenedor EJB. Este será el caso, por ejemplo, de la clase [Metier]. El código anterior, por su parte, pertenecerá a una clase de consola que no será cargada por el contenedor EJB. Por lo tanto, nos vemos obligados a utilizar los nombres JNDI o EJB.

A continuación, el código para obtener una referencia a la interfaz remota de EJB y [Metier]:

1
2
3
4
5
6
7
8
     // se configura el contenedor Open EJB integrado
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
     // Inicialización del contexto JNDI del contenedor EJB
    InitialContext initialContext = new InitialContext(properties);

     // instanciación de la capa de negocio remota
metier = (IMetierRemote) initialContext.lookup("MetierRemote");

6.2. Trabajo práctico

Nos proponemos migrar la aplicación NetBeans Spring / Hibernate a una arquitectura OpenEJB / EclipseLink.

La implementación actual con Spring / Hibernate

La implementación que se va a desarrollar con OpenEJB / EclipseLink

Si no existe, cree la base de datos MySQL [dbpam_eclipselink]. Si existe, elimine todas sus tablas. Cree una conexión de NetBeans a esta base de datos tal como se describe en el párrafo 6.2.1.

6.2.2. Configuración inicial del proyecto de NetBeans

  • Cargar el proyecto Maven [mv-pam-spring-hibernate]
  • Crea un nuevo proyecto Maven Java [mv-pam-openejb-eclipselink] [1]
  • En la pestaña [Files] [2], crea una carpeta [conf] [3] en la raíz del proyecto
  • coloca en esta carpeta el siguiente archivo [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>
  • crea la carpeta [src / main/ resources/ META-INF] [5]
  • colocar en ella el archivo [persistence.xml] [6] siguiente:

<?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">
    <!-- el proveedor JPA es EclipseLink -->
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <!-- entidades Jpa -->
    <class>jpa.Cotisation</class>
    <class>jpa.Employe</class>
    <class>jpa.Indemnite</class>
    <!-- propiedades del proveedor EclipseLink -->
    <properties>
      <property name="eclipselink.logging.level" value="FINE"/>
      <property name="eclipselink.ddl-generation" value="drop-and-create-tables"/>
    </properties>
  </persistence-unit>
</persistence>
  • línea 12: se solicitan registros detallados a EclipseLink,
  • línea 13: las tablas se crearán al instanciar la capa JPA,
  • Agrega las bibliotecas OpenEJB, EclipseLink y el controlador JDBC de MySQL al archivo [pom.xml] del proyecto:

<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>
  • líneas 18-22: la dependencia OpenEJB,
  • líneas 30-39: las dependencias EclipseLink,
  • líneas 41-44: la dependencia del controlador JDBC de MySQL

6.2.3. Portación de la capa [DAO]

Vamos a realizar la migración de la capa [DAO] copiando los paquetes del proyecto [mv-pam-spring-hibernate] al proyecto [mv-pam-openejb-eclipselink].

  • Copiar los paquetes [dao, exception, jpa]
 

Los errores señalados anteriormente se deben a que la capa [DAO] copiada utiliza Spring y las bibliotecas de Spring ya no forman parte del proyecto.

6.2.3.1. El EJB [CotisationDao]

Creamos las interfaces local y remota del futuro EJB [CotisationDao]:

La interfaz local ICotisationDaoLocal:

1
2
3
4
5
6
7
package dao;

import javax.ejb.Local;

@Local
public interface ICotisationDaoLocal extends ICotisationDao{
}

Para obtener los paquetes correctos import, ejecute [clic droit sur le code / Fix Imports].

La interfaz remota ICotisationDaoRemote:

1
2
3
4
5
6
7
package dao;

import javax.ejb.Remote;

@Remote
public interface ICotisationDaoRemote extends ICotisationDao{
}

Luego modificamos la clase [CotisationDao] para convertirla en una EJB:

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

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

  @PersistenceContext
  private EntityManager em;
...  

El import que generaba esta clase en el marco Spring desaparece. Generar un [Clean and Build] del proyecto:

En el [1], ya no hay errores en la clase [CotisationDao].

6.2.3.2. Los archivos EJB, [EmployeDao] y [IndemniteDao]

Se repite el mismo procedimiento para los demás elementos de la capa [DAO]:

  • las interfaces IEmployeDaoLocal y IEmployeDaoRemote, derivadas de IEmployeDao
  • EJB y EmployeDao, que implementan estas dos interfaces
  • interfaces IIndemniteDaoLocal y IIndemniteDaoRemote, derivadas de IIndemniteDao
  • EJB y IndemniteDao, que implementan estas dos interfaces

Una vez hecho esto, ya no hay errores en el proyecto [2].

6.2.3.3. La clase [PamException]

La clase [PamException] sigue siendo la misma, salvo por un detalle:

package exception;

import javax.ejb.ApplicationException;

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

   // código de error
  private int code;
...

Se ha agregado la línea 5. Para obtener los import correctos, cambie a [Fix imports].

Para entender la anotación de la línea 5, hay que recordar que cada método de EJB de nuestra capa [DAO]:

  • se ejecuta en una transacción iniciada y finalizada por el contenedor EJB
  • lanza una excepción de tipo [PamException] tan pronto como algo sale mal

Cuando la capa [metier] llama a un método M de la capa [DAO], esta llamada es interceptada por el contenedor EJB. Todo ocurre como si hubiera una clase intermedia entre la capa [metier] y la capa [DAO], aquí denominada [Proxy EJB], que intercepta todas las llamadas a la capa [DAO]. Cuando se intercepta la llamada al método M de la capa [DAO], el proxy EJB inicia una transacción y luego le cede el control al método M de la capa [DAO], el cual se ejecuta entonces dentro de esa transacción. El método M finaliza con o sin excepción.

  • Si el método M finaliza sin excepción, la ejecución vuelve al proxy EJB, el cual finaliza la transacción validándola mediante un commit. A continuación, el flujo de ejecución se devuelve al método llamante de la capa [metier]
  • Si el método M termina con una excepción, la ejecución vuelve al proxy EJB, el cual finaliza la transacción invalidándola mediante un rollback. Además, encapsula esta excepción en un tipo EJBException. A continuación, el flujo de ejecución se devuelve al método que lo invocó en la capa [metier], el cual recibe, por lo tanto, un EJBException. La anotación de la línea 5 anterior impide esta encapsulación. Por lo tanto, la capa [metier] recibirá un PamException. Además, el atributo rollback=true le indica al proxy EJB que, cuando reciba un PamException, debe invalidar la transacción.

6.2.3.4. Prueba de la capa [DAO]

Nuestra capa [DAO], implementada por EJB, puede someterse a pruebas. Comenzamos copiando el paquete [dao] de [Test Packages] del proyecto [mv-pam-springhibernate] al proyecto en desarrollo [1]:

Solo conservamos la prueba [JUnitInitDB], que inicializa la base de datos con algunos datos de [2]. Renombramos la clase [ JUnitInitDbLocal] como [3]. La clase [JUnitInitDBLocal] utilizará la interfaz local de EJB de la capa [DAO].

En primer lugar, modificamos la clase [JUnitInitDBLocal] de la siguiente manera:

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 {
     // se configura el contenedor Open EJB integrado
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
      // inicialización del contexto JNDI con las propiedades anteriores
    InitialContext initialContext = new InitialContext(properties);
     // instanciación de capas locales DAO
    employeDao = (IEmployeDaoLocal) initialContext.lookup("EmployeDaoLocal");
    cotisationDao = (ICotisationDaoLocal) initialContext.lookup("CotisationDaoLocal");
    indemniteDao = (IIndemniteDaoLocal) initialContext.lookup("IndemniteDaoLocal");
}

...
  • líneas 3-5: referencias a las interfaces locales de EJB de la capa [DAO]
  • línea 7: @BeforeClass anota el método que se ejecuta al iniciar la prueba JUnit
  • líneas 10-13: inicialización del contenedor OpenEJB. Esta inicialización es específica y varía con cada contenedor EJB.
  • línea 13: se cuenta con un contexto JNDI (Java Naming and Directory Interface) que permite acceder a los EJB mediante nombres. Con OpenEJB, la interfaz local de un EJB E se designa como ELocal y la interfaz remota como ERemote.
  • líneas 15-17: se solicita al contexto JNDI una referencia sobre las interfaces locales de los EJB y [EmployeDao, CotisationDao, IndemniteDao].

Se compila el proyecto (Build), se inicia el servidor MySQL si es necesario y se ejecuta la prueba JUnitInitDBLocal. Cabe recordar que el archivo [persistence.xml] se ha configurado para recrear las tablas en cada ejecución. Antes de ejecutar la prueba, es recomendable eliminar las tablas que puedan existir en las bases de datos MySQL y [dbpam_eclipselink].

  • en [1], en la pestaña [Services], se eliminan las tablas de la conexión de NetBeans establecida en el párrafo 6.2.1.
  • En [2], la base de datos [dbpam_eclipselink] ya no tiene tablas
  • En [3], se compila el proyecto
  • en [4], se ejecuta la prueba JUnitInitDBLocal
  • en [5], la prueba se ha realizado con éxito
  • en [6], se actualiza la conexión de NetBeans
  • en [7], se ven las 4 tablas creadas por la capa JPA. El objetivo de la prueba era llenarlas. Se visualiza el contenido de una de ellas
  • en [8], el contenido de la tabla [EMPLOYES]

El contenedor OpenEJB mostró registros en la consola:

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)
  • líneas 2-3: los dos nombres JNDI de EJB y [CotisationDaoLocal],
  • líneas 4-5: los dos nombres JNDI de EJB y [CotisationDaoRemote],
  • líneas 7-8: los dos nombres JNDI de EJB y [EmployeDaoLocal],
  • líneas 9-10: los dos nombres JNDI de EJB y [EmployeDaoRemote],
  • líneas 12-13: los dos nombres JNDI de EJB y [IndemniteDaoLocal],
  • líneas 14-15: los dos nombres JNDI de EJB y [EmployeDaoRemote].

Repetimos la misma prueba, esta vez utilizando la interfaz remota de EJB.

En [1], la clase [JUnitInitDBLocal] se duplicó (copiar/pegar) en [JUnitInitDBRemote]. En esta clase, reemplazamos las interfaces locales por las interfaces remotas:

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 {
     // se configura el contenedor Open EJB integrado
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
      // inicialización del contexto JNDI con las propiedades anteriores
    InitialContext initialContext = new InitialContext(properties);
     // instanciación de las capas remotas DAO
    employeDao = (IEmployeDaoRemote) initialContext.lookup("EmployeDaoRemote");
    cotisationDao = (ICotisationDaoRemote) initialContext.lookup("CotisationDaoRemote");
    indemniteDao = (IIndemniteDaoRemote) initialContext.lookup("IndemniteDaoRemote");
}

Una vez hecho esto, se puede ejecutar la nueva clase de prueba. Antes de eso, con la conexión de NetBeans [dbpam_eclipselink], elimine las tablas de la base de datos [dbpam_eclipselink].

 

Con la conexión de NetBeans [dbpam_eclipselink], verifique que la base de datos se haya llenado.

6.2.4. Portación de la capa [metier]

Realizaremos la migración de la capa [metier] copiando los paquetes del proyecto [mv-pam-spring-hibernate] al proyecto [mv-pam-openejb-eclipselink].

Los errores señalados anteriormente ([1]) se deben a que la capa [metier] copiada utiliza Spring y las bibliotecas de Spring ya no forman parte del proyecto.

6.2.4.1. El EJB [Metier]

Seguimos el mismo procedimiento que el descrito para EJB y [CotisationDao]. En primer lugar, creamos en [2] las interfaces local y remota del futuro EJB [Metier]. Ambas derivan de la interfaz inicial [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{

}

Una vez hecho esto, en [3] modificamos la clase [Metier] para que se convierta en una EJB:

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

   // referencias a la capa local [DAO]
  @EJB
  private ICotisationDaoLocal cotisationDao = null;
  @EJB
  private IEmployeDaoLocal employeDao = null;
  @EJB
  private IIndemniteDaoLocal indemniteDao = null;

   // obtener la nómina
  public FeuilleSalaire calculerFeuilleSalaire(String SS,
          double nbHeuresTravaillées, int nbJoursTravaillés) {
     // se recupera la información relacionada con el empleado
...
  • línea 1: la anotación @Stateless convierte a la clase en una EJB
  • línea 2: cada método de la clase se ejecutará en una transacción
  • línea 3: el EJB [Metier] implementa las dos interfaces, la local y la remota, que acabamos de definir
  • línea 7: el EJB [Metier] utilizará el EJB [CotisationDao] a través de su interfaz local. Esto significa que las capas [metier] y [DAO] deben ejecutarse en el mismo JVM.
  • línea 6: la anotación @EJB hace que el contenedor EJB inyecte él mismo la referencia en la interfaz local de EJB [CotisationDao]. La otra forma que hemos encontrado es utilizar un contexto JNDI.
  • Líneas 8-11: se utiliza el mismo mecanismo para los otros dos EJB de la capa [DAO].

6.2.4.2. Prueba de la capa [metier]

Nuestra capa [metier], implementada por un EJB, puede someterse a prueba. Comenzamos copiando el paquete [metier] de [Test Packages] del proyecto [mv-pam-spring-hibernate] al proyecto en construcción [1]:

  • en [1], el resultado de la copia
  • en [2], se elimina la primera prueba
  • en [3], la prueba restante se renombra como [JUnitMetierLocal]

La clase [JUnitMetierLocal] queda así:

public class JUnitMetierLocal {

// capa de negocio local
  static private IMetierLocal metier;

  @BeforeClass
  public static void init() throws NamingException {
     // se configura el contenedor Open EJB integrado
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
     // inicialización del contexto JNDI con las propiedades anteriores
    InitialContext initialContext = new InitialContext(properties);

     // instanciación de las capas locales DAO
    IEmployeDaoLocal employeDao = (IEmployeDaoLocal) initialContext.lookup("EmployeDaoLocal");
    ICotisationDaoLocal cotisationDao = (ICotisationDaoLocal) initialContext.lookup("CotisationDaoLocal");
    IIndemniteDaoLocal indemniteDao = (IIndemniteDaoLocal) initialContext.lookup("IndemniteDaoLocal");
     // instanciación de la capa de negocio local
    metier = (IMetierLocal) initialContext.lookup("MetierLocal");

     // se vacía la base de datos
...
}
  • línea 4: una referencia a la interfaz local de EJB [Metier]
  • líneas 8-12: configuración del contenedor OpenEJB idéntica a la realizada en la prueba de la capa [DAO]
  • líneas 15-19: se solicitan al contexto JNDI de la línea 12, referencias sobre los 3 EJB de la capa [DAO] y sobre el EJB de la capa [metier]. Los EJB de la capa [DAO] se utilizarán para inicializar la base de datos, y el EJB de la capa [metier] para realizar pruebas de cálculo de salarios.

La ejecución de la prueba [JUnitMetierLocal] arroja el siguiente resultado [1]:

En [2], se duplica [JUnitMetierLocal] en [JUnitMetierRemote] para probar, en esta ocasión, la interfaz remota de EJB y [Metier]. Se modifica el código de [JUnitMetierRemote] para utilizar esta interfaz remota. El resto no cambia.

public class JUnitMetierRemote {

   // capa de negocio remota
  static private IMetierRemote metier;

  @BeforeClass
  public static void init() throws NamingException {
     // se configura el contenedor Open EJB integrado
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
     // inicialización del contexto JNDI con las propiedades anteriores
    InitialContext initialContext = new InitialContext(properties);

     // instanciación de las capas remotas DAO
    IEmployeDaoRemote employeDao = (IEmployeDaoRemote) initialContext.lookup("EmployeDaoRemote");
    ICotisationDaoRemote cotisationDao = (ICotisationDaoRemote) initialContext.lookup("CotisationDaoRemote");
    IIndemniteDaoRemote indemniteDao = (IIndemniteDaoRemote) initialContext.lookup("IndemniteDaoRemote");
     // instanciación de la capa de negocio remota
    metier = (IMetierRemote) initialContext.lookup("MetierRemote");

     // se vacía la base de datos
    for(Employe employe:employeDao.findAll()){
      employeDao.destroy(employe);
    }
    for(Cotisation cotisation:cotisationDao.findAll()){
      cotisationDao.destroy(cotisation);
    }
    for(Indemnite indemnite : indemniteDao.findAll()){
      indemniteDao.destroy(indemnite);
    }
     // se llena
    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));
  }
}
  • líneas 4 y 19: se utiliza la interfaz remota de EJB [Metier].
  • líneas 15-17: se utilizan las interfaces remotas de la capa [DAO]
  • líneas 34-35: dado que con las interfaces remotas los objetos que se intercambian entre el cliente y el servidor se pasan por valor, es necesario recuperar el resultado devuelto por el método create(Indemnite i). Esto no era obligatorio con las interfaces locales, donde los objetos se pasan por referencia.

Una vez hecho esto, se puede compilar el proyecto y ejecutar la prueba [JUnitMetierRemote]:

  

6.2.5. Portabilidad de la capa [console]

Vamos a realizar la migración de la capa [console] copiando los paquetes del proyecto [mv-pam-spring-hibernate] al proyecto [mv-pam-openejb-eclipselink].

Los errores señalados anteriormente ([1]) se deben a que la capa [metier] copiada utiliza Spring y las bibliotecas de Spring ya no forman parte del proyecto. En [2], la clase [Main] pasa a llamarse [MainLocal]. Utilizará la interfaz local de EJB [Metier].

El código de la clase [MainLocal] cambia de la siguiente manera:

  public static void main(String[] args) {
     // datos locales
    final String syntaxe = "pg num_securite_sociale nb_heures_travaillées nb_jours_travaillés";
...
     // ¿Hay errores?
    if (erreurs.size() != 0) {
      for (int i = 0; i < erreurs.size(); i++) {
        System.err.println(erreurs.get(i));
      }
      return;
    }
     // Todo listo: se puede solicitar la nómina a la capa [métier]
    IMetierLocal metier = null;
    FeuilleSalaire feuilleSalaire = null;
    try {
       // configuramos el contenedor Open EJB integrado
      Properties properties = new Properties();
      properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
       // inicialización del contexto JNDI con las propiedades anteriores
      InitialContext initialContext = new InitialContext(properties);
       // instanciación de la capa de negocio local
      metier = (IMetierLocal) initialContext.lookup("MetierLocal");
       // cálculo de la nómina
      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;
    }
     // visualización detallada
    String output = "Valeurs saisies :\n";
    output += ajouteInfo("N° de sécurité sociale de l'employé", args[0]);
....

Los cambios se encuentran en las líneas 13 a 25. Se trata de la forma de hacer referencia a la capa [metier], que cambia (líneas 17 a 22). No explicamos el nuevo código, ya que se ha visto en ejemplos anteriores. Una vez realizadas estas modificaciones, el proyecto ya no presenta errores (véase [3]).

Configuramos el proyecto para que se ejecute con los argumentos [1]:

Para que la aplicación de consola se ejecute normalmente, debe haber datos en la base de datos. Para ello, hay que modificar el archivo [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">
    <!-- el proveedor JPA es EclipseLink -->
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <!-- entidades Jpa -->
    <class>jpa.Cotisation</class>
    <class>jpa.Employe</class>
    <class>jpa.Indemnite</class>
    <!-- propiedades del proveedor EclipseLink -->
    <properties>
      <property name="eclipselink.logging.level" value="FINE"/>
      <!--
      <property name="eclipselink.ddl-generation" value="drop-and-create-tables"/>
      -->
    </properties>
  </persistence-unit>
</persistence>

La línea 14, que provocaba que las tablas de la base de datos se volvieran a crear en cada ejecución, se ha comentado. Se debe reconstruir el proyecto (Clean and Build) para que se aplique este cambio. Una vez hecho esto, se puede ejecutar el programa. Si todo sale bien, se obtendrá una salida en la consola similar a la siguiente:

.......
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)

Aquí hemos utilizado la interfaz local de la capa [metier]. Ahora utilizamos su interfaz remota en una segunda clase de consola:

En [1], la clase [MainLocal] se duplicó en [MainRemote]. Se modifica el código de [MainRemote] para utilizar la interfaz remota de la capa [metier]:

// Todo está bien; podemos solicitar la nómina a la capa [metier]
    IMetierRemote metier = null;
    FeuilleSalaire feuilleSalaire = null;
    try {
       // configuramos el contenedor Open EJB integrado
...
       // instanciación de la capa de negocio remota
      metier = (IMetierRemote) initialContext.lookup("MetierRemote");
       // cálculo de la nómina
      feuilleSalaire = metier.calculerFeuilleSalaire(args[0], nbHeuresTravaillées, nbJoursTravaillés);
    } catch (PamException ex) {
...
    } catch (Exception ex) {
...
    }

Se realizan modificaciones en las líneas 2 y 8. Se configura el proyecto [2] para ejecutar la clase [MainRemote]. Su ejecución arroja los mismos resultados que anteriormente.

6.3. Conclusion

Hemos mostrado cómo migrar una arquitectura Spring / Hibernate a una arquitectura OpenEJB / EclipseLink.

La arquitectura Spring/Hibernate

La arquitectura OpenEJB / EclipseLink

La migración se pudo realizar sin demasiadas dificultades porque la aplicación inicial se había estructurado en capas. Es importante comprender este punto.