Skip to content

6. Versión 2: Arquitectura OpenEJB / JPA

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

A continuación 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 apartado 6.2 para crear los proyectos Maven.

6.1.1. Las dos arquitecturas

La implementación actual con Spring / Hibernate

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

6.1.2. Las bibliotecas de los proyectos

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

  • El archivo [META-INF/persistence.xml], que configura la capa JPA, queda de la siguiente manera:
<?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] pasan a ser EJB. Tomemos como ejemplo la clase [CotisationDao]:

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

package dao;

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

public interface ICotisationDao {
   // crear una nueva cuota
  Cotisation create(Cotisation cotisation);
   // modificar una cuota existente
  Cotisation edit(Cotisation cotisation);
   // eliminar una cotización existente
  void destroy(Cotisation cotisation);
   // buscar una cotización concreta
  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 añade 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 añade ninguno nuevo.
  • línea 5: la anotación @Remote la convierte en una interfaz remota para EJB, que la implementará.

La capa [DAO] se implementa mediante un EJB que implementa ambas interfaces (aunque 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 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 dicha 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 dicho 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 este 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] se convierte también 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 cuyos métodos se ejecutan 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 instancie el EJB [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 parte de la hipótesis de que las capas [metier] y [DAO] se ejecutarán en la misma JVM.

6.1.6. Los clientes de EJB

En el esquema anterior, para comunicarse con la capa [metier], la capa [ui] debe obtener una referencia a la interfaz remota de la capa EJB desde 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. En el caso del contenedor OpenEJB, se puede proceder de la siguiente manera:

Referencia en la interfaz local:

1
2
3
4
5
6
7
8
9
     // Configuración del 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 dispone de un contexto JNDI (Java Naming and Directory Interface) que permite obtener referencias sobre los EJB. Cada EJB se designa con un nombre JNDI:
  • (continuación)
    • para la interfaz local, se añade «Local» al nombre del EJB (líneas 7-9)
    • para la interfaz remota, se añade «Remote» al nombre del EJB

Con Java EE 5, estas reglas varían en función del contenedor EJB. Esto supone 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 hemos mencionado que estas también se pueden obtener mediante la anotación @EJB. Por lo tanto, podríamos querer 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, crea la base de datos MySQL [dbpam_eclipselink]. Si existe, elimina todas sus tablas. Crea una conexión de NetBeans a esta base de datos tal y como se describe en el apartado 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], crear una carpeta [conf] [3] en la raíz del proyecto
  • colocar 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,
  • Añadir 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. Portabilidad 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{
}

A continuación, 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 framework 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 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 pequeño detalle:

package exception;

import javax.ejb.ApplicationException;

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

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

Se ha añadido la línea 5. Para obtener los import correctos, hay que crear [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] en cuanto surge algún problema

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], denominada aquí [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, a continuación, cede el control al método M de la capa [DAO], que se ejecuta entonces dentro de dicha 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, que 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 finaliza con una excepción, la ejecución vuelve al proxy EJB, que 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 llamante de la capa [metier], que recibe, por 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 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 mediante EJB, puede someterse a pruebas. Empezamos 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 ejecutado 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 dispone de 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 a 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 posibles tablas de 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 apartado 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 superado
  • 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 rellenarlas. Se visualiza el contenido de una de ellas
  • en [8], el contenido de la tabla [EMPLOYES]

El contenedor OpenEJB ha mostrado 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, utilizando esta vez la interfaz remota de EJB.

En [1], la clase [JUnitInitDBLocal] se ha duplicado (copiar/pegar) en [JUnitInitDBRemote]. En esta clase, sustituimos las interfaces locales por las 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 ello, con la conexión de NetBeans [dbpam_eclipselink], elimina las tablas de la base de datos [dbpam_eclipselink].

 

Con la conexión de NetBeans [dbpam_eclipselink], comprueba que la base de datos se ha rellenado.

6.2.4. Migración de la capa [metier]

Vamos a realizar 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 recuperan los datos relacionados con el empleado
...
  • línea 1: la anotación @Stateless convierte 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: EJB [Metier] utilizará EJB [CotisationDao] a través de la interfaz local de este último. 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 consiste en 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]

Ya podemos probar nuestra capa [metier], implementada mediante un EJB. 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 pasa a llamarse [JUnitMetierLocal]

La clase [JUnitMetierLocal] queda así:

public class JUnitMetierLocal {

// capa de negocio local
  static private IMetierLocal metier;

  @BeforeClass
  public static void init() throws NamingException {
     // configuración del 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 a los tres EJB de la capa [DAO] y al EJB de la capa [metier]. Los EJB de la capa [DAO] servirá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] da 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 rellena
    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 intercambiados 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, en las que 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] evoluciona 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 bien: 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-25. Se trata de la forma de hacer referencia a la capa [metier], que cambia (líneas 17-22). No explicamos el nuevo código, ya que ya 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 con normalidad, es necesario que haya 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. Es necesario recompilar el proyecto (Clean and Build) para que se aplique este cambio. Una vez hecho esto, se puede ejecutar el programa. Si todo va bien, aparecerá en la consola un mensaje similar al 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 ha duplicado en [MainRemote]. Se modifica el código de [MainRemote] para utilizar la interfaz remota de la capa [metier]:

// Todo en orden: ya se puede 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 han realizado modificaciones en las líneas 2 y 8. Se ha configurado el proyecto [2] para ejecutar la clase [MainRemote]. Su ejecución ofrece 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 llevar a cabo sin demasiadas dificultades porque la aplicación inicial se había estructurado en capas. Es importante comprender este punto.