EJB 3.0 y pruebas de persistencia con Maven, JUnit 4 y Embedded JBoss sobre Java 6.
0. Índice de contenidos.
- 1. Introducción.
- 2. Entorno.
- 3. La capa de persistencia.
- 4. Configuración de la unidad de persistencia.
- 5. Ejecución del test desde maven.
- 6. Conclusiones.
1. Introducción.
El hecho de que haya casi plagiado el título de este tutorial del de mi compi Carlos García sobre EJB 3.0
y pruebas unitarias con Maven, JUnit 4 y Embedded JBoss sobre Java 6 no es casual, puesto que lo que vamos a hacer es darle continuidad, probando ahora cómo llevar a cabo un test
de persistencia de un EJB de entidad, bajo el soporte de un EJB de servicio que implementa el patrón dao, en el mismo entorno.
La configuración en el entorno de Embedded Jboss 2 ya la vimos de la mano de Alejandro, ahora se trata de comprobar si la misma funciona en el entorno de la versión 3.
Vamos a ir, sobre la base del tutorial de Carlos, paso a paso añadiendo la configuración necesaria para JPA, comentando los errores que se pueden ir produciendo y cómo darles una solución.
2. Entorno.
El tutorial está escrito usando el siguiente entorno:
- Hardware: Portátil Asus G1 (Core 2 Duo a 2.1 GHz, 2048 MB RAM, 120 GB HD).
- Sistema operativo: Windows Vista Ultimate.
- JDK 1.6.0_14
- Maven 2.1.
- Embedded Jboss beta3.SP10
3. La capa de persistencia.
La capa de persistencia se va a componer de un EJB de entidad y uno de servicio sin estado que expone una interfaz local.
User.java: el EJB de entidad.
package com.autentia.training.ejb3.entities; import java.io.Serializable; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.HashCodeBuilder; /** * Entity linked to the table "User" of the data model. * * @author Autentia Real Business Solutions S.L. * @see http://www.autentia.com * */ @Entity public class User implements Serializable{ private static final long serialVersionUID = 5729361595476015876L; /** Id*/ private Integer id; /** The name */ private String name; /** The password */ private String password; /** * Gets the identifier of the user * Strategy : Auto increment the value of the identifier * @return Id */ @Id @GeneratedValue(strategy = GenerationType.AUTO) public Integer getId() { return (Integer) id; } protected void setId(Integer id){ this.id = id; } /** * Gets the name * @return name */ @Column(nullable=false) public String getName() { return name; } /** * Sets the name * @param name */ public void setName(String name) { this.name = name; } /** * Gets the password * @return password */ @Column(nullable=false) public String getPassword() { return password; } /** * Sets the password * @param password */ public void setPassword(String password) { this.password = password; } /** * Get the hashCode of the user. ** Uses all fields except those that are the subject of an inverse relationship *
* @return hashCode */ @Override public int hashCode() { final HashCodeBuilder hcb = new HashCodeBuilder(); hcb.append(super.hashCode()); hcb.append(getName()); hcb.append(getPassword()); return hcb.toHashCode(); } /** * Verifies that the object passed as parameter is equal to the current. ** Uses all fields except those that are the subject of an inverse relationship *
* @param an user object * @return the result of the equals method */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } try { final User other = (User) obj; final EqualsBuilder eqb = new EqualsBuilder(); eqb.append(getName(), other.getName()); eqb.append(getPassword(), other.getPassword()); return eqb.isEquals(); } catch (Exception e) { // if nay error returns false } return false; } }
Dao.java: el interfaz de EJB Local.
package com.autentia.training.ejb3.persistence; import java.io.Serializable; import javax.ejb.Local; /** * Base interface for all DAO classes in the application. * ** For more information on the DAO pattern, visit: http://java.sun.com/blueprints/corej2eepatterns/Patterns/ * DataAccessObject.html. * * @author Autentia Real Business Solutions S.L. * @see http://www.autentia.com * */ @Local public interface Dao { /** Persists the entity object into database * @param entity entity to persist */ public void persist(Object entity); /** Removes an object from persistent storage in the database * @param entity entity to remove */ public void remove(Object entity); /** Retrieves an object of the Class indicated, that was previously persisted to the database, * using the indicated id as primary key * @param entityClass * @param id * @return an entity */ public
T get(Class entityClass, Serializable id); ... }
GenericDao.java: el EJB de servicio que expone la interfaz Local.
package com.autentia.training.ejb3.persistence; import java.io.Serializable; import javax.ejb.Stateless; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; /** * Generic DAO, provides basic CRUD operations, based in JPA EntityManager. * * @author Autentia Real Business Solutions S.L. * @see http://www.autentia.com * */ @Stateless public class GenericDao implements Dao { @PersistenceContext private EntityManager em; /** @see Dao#persist(TransferObject) */ public void persist(Object entity) { em.persist(entity); } /** @see Dao#remove(TransferObject) */ public void remove(Object entity) { em.remove(em.merge(entity)); } /** @see Dao#get(Class, Serializable) */ publicT get(Class entityClass, Serializable id) { return (T) em.find(entityClass, id); } ... }
Estas clases se ubicarán bajo la carpeta src/main/java, puesto que son clases de aplicación y para que compilen necesitaremos añadir la dependencia al siguiente artefacto en el pom.xml:
javax.persistence persistence-api 1.0
El test es el siguiente. Muestro sólo el método, puesto que la clase que tiene el soporte para levantar el Embedded Jboss, la podéis encontrar
en el tutorial de Carlos.
Estará ubicado bajo la carpeta src/test/java:
@Test public void userCrudOperations() throws javax.naming.NamingException { InitialContext ctx = new InitialContext(); Dao dao = (GenericDao)ctx.lookup("GenericDao/local"); User user = new User(); user.setName("Jose Manuel Sánchez"); user.setPassword("password"); dao.persist(user); Assert.assertNotNull("User not found by id:" + user.getId(), dao.get(User.class, user.getId())); }
Si ejecutamos el test sin más, se producirá la siguiente excepción:
javax.naming.NameNotFoundException: GenericDao not bound
puesto que es necesario añadir, al jar que se despliega, las clases con las que estamos trabajando:
private static void deploy() throws DeploymentException { jar = AssembledContextFactory.getInstance().create("ejbTestCase.jar"); jar.addClass(User.class); jar.addClass(Dao.class); jar.addClass(GenericDao.class); ...
Si os parece tedioso añadir una a una las clases necesarias, existe, entre otros, un método como el que sigue, que añade todas las clases de un paquete
raíz a partir del package de la clase que se pasa como primer parámetro:
jar.addResources(User.class, new String[]{"**/entities/*.class"}, null);
4. Configuración de la unidad de persistencia.
En este punto el test aún no pasa, se debería producir la siguiente excepción:
java.lang.RuntimeException: Illegal @PersistenceUnit on private javax.persistence.EntityManager com.autentia.training.ejb3.persistence.GenericDao.em :There is no default persistence unit in this deployment.
Nos falta un fichero persistence.xml en el que definiremos la unidad de persistencia:
- src/main/resources/META-INF/persistence.xml: con la configuración para el entorno de producción, esto es, la de la aplicación en un entorno no de tests,
- src/test/resources/persistence-test.xml: con la configuración para el entorno de tests. El cambio de nomenclatura del fichero respecto al de producción, al anterior, viene dado porque ambos estarán
en el classpath de la ejecución de los tests, de ahí que no convenga tampoco incluirlo dentro de una carpeta META-INF dentro de resources, para que no colisionen.
El contenido del fichero persistence-test.xml, podría ser como sigue:
<?xml version="1.0" encoding="UTF-8"?>java:DefaultDS com.autentia.training.ejb3.entities.User
La fuente de datos DefaultDS se incluye por defecto en el entorno de Embedded Jboss, la podéis encontrar en \src\test\resources\deploy\hsqldb-ds.xml, y configura una base de datos hsqldb.
Simplemente con incluir el fichero persistence-test.xml al directorio de resources no funciona puesto que tenemos que añadirlo al jar que se despliega. Así creammos un directorio de ensamblado
que se llame META-INF dentro del jar y añadimos el fichero sin el path:
private static void deploy() throws DeploymentException { jar = AssembledContextFactory.getInstance().create("ejbTestCase.jar"); ... AssembledDirectory metainf = jar.mkdir("META-INF"); metainf.addResource("persistence-test.xml", "persistence.xml"); Bootstrap.getInstance().deploy(jar); ... }
En este punto, ejecutando el test desde Eclipse con la propiedad «-Dsun.lang.ClassLoader.allowArraySyntax=true» asignada en el arranque de la VM, debería funcionar correctamente, aunque suelta la siguiente traza a nivel de warning:
WARN [org.hibernate.ejb.packaging.InputStreamZippedJarVisitor] Unable to find file (ignored): vfs://12526767712961/ java.lang.RuntimeException: Cannot open stream at org.jboss.virtual.plugins.context.vfs.AssembledDirectoryHandler.openStream(AssembledDirectoryHandler.java:117)
Si bien, con maven desde línea de comandos, deberíamos tener esta maravillosa excepción:
ERROR [org.jboss.kernel.plugins.dependency.AbstractKernelController] Error installing to Parse: name=vfs://12526314481830/ state=Not Installed mode=Manual requiredState=Parse java.lang.AssertionError: expected a jar or file url, but was vfs://12526297739780/
vfs o Virtual File System es una abstracción de un sistema de ficheros, precisamente el que se crea para ensamblar el jar y el número es el nombre del mismo.
Pero… ¿cuál es el problema?
- parece que el encargado de desplegar la unidad de persistencia ahora no soporta este sistema de ficheros, puesto que en la versión 2 de Embedded Jboss no teníamos este problema,
- pero ¿será una problema de classpath?, puesto que desde el entorno de Eclipse no se reproduce.
- ¿habrá sido al subir de versión la JDK?, no, porque es la misma que la del Eclipse, la 1.6, y ahí sí funciona,
- …
Como hemos comentado en el resto de tutoriales sobre Embedded Jboss, está aún en versión beta, la documentación es escasa y las respuestas en los foros a las cuestiones que se plantean no son de mucha calidad.
La casuística de nuestro error es la misma que se describe en este post del foro de Jboss, y la solución que le dan pasa por
dejar de usar el directorio virtual y realizar un scan de un directorio físico para que monte todo lo que se encuentre dentro de él.
private static void deploy() throws DeploymentException { Bootstrap.getInstance().scanClasspath("embeddedJBoss3Java6\\target\\classes"); // donde embeddedJBoss3Java6 es el nombre del proyecto }
También se podría incluir algo como lo que sigue, que permite desplegar las librerías que contegan el recurso que recibe como parámetro.
private static void deploy() throws DeploymentException { Bootstrap.getInstance().deployResourceBases("META-INF/persistence.xml"); }
Y el caso es que ambas soluciones funcionan, pero no es lo que queremos puesto que está levantando la unidad de persistencia que se encuentra en el directorio src/main/resources/META-INF, se podría parchear en base a
mover ficheros de un sitio a otro con la ayuda del plugin de resources de maven, pero estaríamos complicandolo más aún. Hay que poner especial cuidado en revisar la siguiente traza para comprobar qué unidad de
persistencia está levantando:
INFO [org.hibernate.ejb.Ejb3Configuration] Processing PersistenceUnitInfo [ name: testPersistenceUnit ...]
5. Ejecución del test desde maven.
Entonces… ¿qué diferencia existe entre la ejecución desde Eclipse y con maven desde línea de comandos?, la respuesta es sencilla, aunque ha costado algo más de lo deseado encontrarla.
El plugin de surefire es el encargado de ejecutar los tests en el entorno de maven y por defecto tiene activa la propiedad enableAssertions que habilita las aserciones de la JVM en la ejecución de los tests,
modificando el comportamiento por defecto de la JVM, que lo tiene desactivado y que es como se ecuentra en el entorno de ejecución desde Eclipse.
Las asercciones o asertos son instrucciones del propio lenguaje Java,
que permiten poner a prueba suposiciones dentro de la lógica de un programa. La lógica es la misma de los Assert.assert de los tests de Junit,
pero forman parte del propio lenguaje, con lo que los evalúa la propia JVM en tiempo de ejecución.
void foo() { for (...) { if (...) return; } assert false; // Execution should never reach this point! } // Confirm adherence to precondition in nonpublic method assert interval > 0 && interval <= 1000/MAX_REFRESH_RATE : interval; // org.jboss.ejb3.deployers.PersistenceUnitParsingDeployer.java:110 assert persistenceUnitRootUrl.getProtocol().equals("jar") || persistenceUnitRootUrl.getProtocol().equals("file") : "expected a jar or file url, but was " + persistenceUnitRootUrl;
El último de los tres ejemplos anteriores es precisamente el código que está lanzando la excepción en la ejecución del test que nos ocupa, ejecutando maven por línea
de comandos, y el caso es que la aserción no debe ser tal, o no tan importante, puesto que desde Eclipse el test funciona y deshabilitando los assert en el plugin de surefire también.
org.apache.maven.plugins maven-surefire-plugin 2.4 false -Dsun.lang.ClassLoader.allowArraySyntax=true
Deshabilitando la propiedad enableAssertions, el test funciona, aunque seguimos teniendo la traza a nivel de warning que comentábamos más arriba. Existe un
hilo en el foro de Jboss donde tratan el tema, pero no parece que sea
demasiado relevante puesto que se trata de un error al abrir el directorio virtual, seguramente producido por no estar activo el assert. El caso es que se realiza un
scan en busca de clases anotadas, y se produce esa excepción. Como nosotros las incluimos en el persistence.xml no pasa de ser un warning.
Por último, hemos comprobado que bajo una versión 1.5 de Java los tests también pasan, con el mismo warning.
9. Conclusiones.
Recordar que Embedded Jboss sigue en versión beta y ya lleva unos años así, y este tipo de problemas con la subida de versiones los tenemos que ir asumiendo.
Al subir de versión una aplicación, tendremos que adaptar el entorno de pruebas si es necesario, pero puede ocurrirnos lo contrario, que no podamos subir de
versión la tecnología de una aplicación porque no está soportada en el entorno de pruebas.
Por otro lado, si cuando comenzamos con Embeded Jboss era la única solución disponible para hacer uso de un micro-contenedor de EJBs en el entorno de nuestros tests,
hoy día existen más y alguna bastante interesante como Apache Open EJB, que iremos evaluando, en breve.
Un saludo.
Jose