JEE6, haciéndolo fácil.
Los fuentes
Lo primero es el enlace a los fuentes de este tutorial
Introducción
Ya estamos en el mes de julio y en Madrid hace un calor espantoso. A pesar de las inclemencias del tiempo y de la dificultad
que supone poner en marcha las neuronas en este momento, es una ocasión ideal para ponerse a cacharrear con
los nuevos juguetes que aparecen en escena en el mundo Java.
La nueva versión Enterprise de Java ya lleva unos cuantos meses con nosotros y es el momentos de empezar
a tantear un poco que nos trae o que se lleva. La versión anterior ya fué todo un cambio en su planteamiento
buscando sobre todo la sencillez en el desarrollo.
Ya hablé algo en tutoriales anteriores acerca de
la versión de EJB 3.0
que me sorprendió gratamente en su momento, y tengo que decir, que si bien la mayor parte de las nuevas incorporaciones
de esta versión y de la anterior no son ninguna novedad (ya que la mayor parte de ellas son incorporaciones de ideas de otros frameworks),
me satisface comprobar que los creadores de esta nueva versión han sido lo bastante inteligentes y humildes para incorporar las mejores
cosillas que ofrecen los demás frameworks que hoy día chapotean en nuestro mundillo.
Tras cacharrear, leer algún libro y mirar mucho por la red, se puede resumir que:
Cosillas nuevas en JEE6
- Búsqueda de un framework más sencillo. Casi todo se puede hacer con anotaciones y desaparece la necesidad de usar descriptores.
- Búsqueda de un framework más portable. Se incluyen especificaciones para normalizar los nombres JNDI de los EJBs (lo que era siempre un problema entre servidores) y se incluye el concepto de EmbeddedContainer para poder probar unitariamente los EJBs.
- Búsqueda de un framework más ligero. Debido a la gran cantidad de especificaciones que se han de cumplir,
la nueva versión ha incorporado algunos conceptos nuevos para tratar de minimizar este impacto:- Pruning: Esto sería algo parecido al deprecated. Han decidido incluir el concepto de Especificaciones que serán eliminadas (EJB 2.X, JAX-RPC, JAXR …)
- Profile (perfiles): Los servidores JEE incorporarán perfiles diferentes en función de la naturaleza de las aplicaciones que corran en nuestro servidor.
Este concepto aparecía en el servidor JBoss desde sus inicios (minimal, all, default …). Por ahora, la nueva versión únicamente ha definido el Web Profile (Perfil Web) que incluye JSF, JSP, JSTL, Servlet, EL, EJB Lite, JPA, JTA y Commons Annotations - EJB Lite: es un subconjunto de las especificaciones más importantes de EJB para poder ser incluído por ejemplo en el Web Profile.
- Búsqueda de un framework más completo. Se incluyen nuevas especificaciones como por ejemplo RESTFul Webservices (JAX-RS).
Sin más, y como el movimiento se demuestra andando, probemos algunas cosas nuevas. Lo primero que necesitamos es alguien que implemente todo esto. Tenemos varias elecciones, glassfish 3, JBoss 6 …
Esta vez toca glassfish:
Descargando Glassfish v3
Como siempre hay versiones de todos los colores. Yo he escogido la versión OpenSource sin instalador (el zip)
El enlace está aquí.
Lo único que hecho es descomprimirlo.
Configurando el entorno.
Para el tutorial, he aprovechado para instalar el Eclipse Helios (última versión de eclipse), pensando que incluiría
el glassfish v3 en la pestaña de Servers. Pero no lo incluye. Así que finalmente, lo he instalado buscando en el market place:
Lo configuramos en la pestaña de servers:
Cuidado porque nos pedirá usar la JDK, no vale con la JRE…
Arrancamos y comprobamos que va bien la cosa…
Vamos a comenzar con el modelo y el DAO:
JPA 2.0
La nueva versión de JPA no supone un impacto con respecto a la anterior. Básicamente las incorporaciones más importantes:
- API de generación de Queries dinámicas mediante POO. Yo prefiero usar JPQL, pero…es cuestión de gustos
- Tatatachán….delete orphans is here….¿ al estilo hibernate ? pues no. No es igual. Aquí es algo más parecido a un borrado en cascada.
En hibernate significaba que si sacas un elemento de la colección, se borraba automáticamente.
No entiendo entonces su aportación, ya que esto se podía hacer con operaciones en cascada en el otro lado de la relación. - Se incluye el bloqueo pesimista (el select … for update)
- Se aumenta la sintaxis de JPQL
- API de caché de segundo nivel
- La anotación @OrderColumn para mantener el orden de colecciones
Vamos a crearnos un proyecto maven (webapp) que nos va a servir para los ejemplos usando el plugin IAM de eclipse:
Debéis cambiar el web.xml para que utilice los namespaces de la nueva versión:
1 2 3 4 5 6 7 |
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <display-name>Aplicación Web JEE6 de Citas de Paco</display-name> </web-app> |
Ahora es momento de seleccionar una implementación de JPA…Os dejo un enlace muy interesante con comparaciones entre
las implementaciones más comunes. Yo iba a elegir la implementación de Hibernate antes de ver esto, pero ahora con más razón:
Ver comparaciones JPA
Configuremos el pom.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
<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/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>citas</groupId> <artifactId>citas</artifactId> <packaging>war</packaging> <version>1.0-SNAPSHOT</version> <name>citas Maven Webapp</name> <url>http://maven.apache.org</url> <properties> <hibernate-version>3.5.1-Final</hibernate-version> <hibernate-validator-version>4.0.2.GA</hibernate-validator-version> </properties> <repositories> <repository> <id>repository.jboss.org</id> <name>JBoss Repository</name> <url>http://repository.jboss.org/maven2</url> </repository> <!-- Repositorio donde encontrar el contenedor embebido de glassfish --> <repository> <id>java.net</id> <name>Glassfish</name> <url>http://download.java.net/maven/glassfish</url> </repository> <repository> <id>maven2-repository.dev.java.net</id> <name>Java.net Repository for Maven 2</name> <url>http://download.java.net/maven/2 </url> <layout>default</layout> </repository> </repositories> <dependencies> <!-- Test Unitarios --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.8.1</version> <scope>test</scope> </dependency> <!-- Implementación de JPA 2.0 de Hibernate --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>${hibernate-version}</version> </dependency> <!-- Implementación de la especificación de Validaciones de Hibernate --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>${hibernate-validator-version}</version> </dependency> <!-- Necesarias para Hibernate --> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>1.5.10</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.5.10</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.5.10</version> <scope>runtime</scope> </dependency> <!-- Contenedor embebido de glassfish --> <dependency> <groupId>org.glassfish.extras</groupId> <artifactId>glassfish-embedded-all</artifactId> <version>3.0.1</version> <scope>test</scope> </dependency> <!-- API de EJB 3.1 --> <dependency> <groupId>org.glassfish</groupId> <artifactId>javax.ejb</artifactId> <version>3.1-b11</version> <scope>provided</scope> </dependency> <!-- API de JPA 2 --> <dependency> <groupId>org.glassfish</groupId> <artifactId>javax.persistence</artifactId> <version>3.0-b29</version> <scope>provided</scope> </dependency> <!-- Commons de apache para los equals y hashCode --> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.5</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.0.2</version> <configuration> <source>1.6</source> <target>1.6</target> </configuration> </plugin> <!-- Configuramos el clean plugin para borrar los temporales del Embedded Container --> <plugin> <artifactId>maven-clean-plugin</artifactId> <version>2.4.1</version> <configuration> <filesets> <fileset> <directory>${basedir}</directory> <includes> <include>gfembed*/**</include> </includes> <followSymlinks>false</followSymlinks> </fileset> </filesets> </configuration> </plugin> </plugins> </build> </project> |
Ahora configuraremos el fichero persistence.xml en: src/main/resources/META-INF
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<persistence 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" version="2.0"> <persistence-unit name="citas" transaction-type="JTA"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <jta-data-source>jdbc/citasDatabase</jta-data-source> <class>com.autentia.citas.model.Cita</class> <class>com.autentia.citas.model.Contacto</class> <properties> <property name="hibernate.dialect" value="org.hibernate.dialect.DerbyDialect" /> <property name="hibernate.show_sql" value="true" /> <property name="hibernate.hbm2ddl.auto" value="create-drop" /> </properties> </persistence-unit> </persistence> |
Nos creamos nuestras clases del modelo. Como en sus sistema de citas:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
package com.autentia.citas.model; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import javax.persistence.CascadeType; import javax.persistence.ElementCollection; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.OneToMany; @Entity public class Contacto implements Serializable{ @Id @GeneratedValue private Long id; private String nombre; private String apellidos; @ElementCollection(fetch=FetchType.LAZY) private List<String> telefonos = new ArrayList<String>(0); @ElementCollection(fetch=FetchType.LAZY) private List<String> emails =new ArrayList<String>(0); @OneToMany(mappedBy="contacto",orphanRemoval=true,cascade=CascadeType.PERSIST,fetch=FetchType.EAGER) private List<Cita> citas =new ArrayList<Cita>(0); public String getNombre() { return nombre; } public void setNombre(String nombre) { this.nombre = nombre; } public String getApellidos() { return apellidos; } public void setApellidos(String apellidos) { this.apellidos = apellidos; } public List<String> getTelefonos() { return telefonos; } public List<String> getEmails() { return emails; } public List<Cita> getCitas() { return citas; } public Long getId() { return id; } public void addCita(Cita cita) { citas.add(cita); cita.setContacto(this); } public void addTelefono(String telefono) { if(!telefonos.contains(telefono)) { telefonos.add(telefono); } } public void addEmail(String email) { if(!emails.contains(email)) { emails.add(email); } } public void removeCita(Cita cita) { if(citas.contains(cita)) { citas.remove(cita); cita.setContacto(null); } } public void removeTelefono(String telefono) { if(telefonos.contains(telefono)) { telefonos.remove(telefono); } } public void removeEmails(String email) { if(emails.contains(email)) { emails.remove(email); } } @Override public int hashCode() { return new HashCodeBuilder().append(nombre).append(apellidos).toHashCode(); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; final Contacto other = (Contacto)obj; return new EqualsBuilder().append(nombre, other.nombre).append(apellidos, other.apellidos).isEquals(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
package com.autentia.citas.model; import java.io.Serializable; import java.util.Date; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.ManyToOne; @Entity public class Cita implements Serializable { @Id @GeneratedValue private Long id; private String asunto; private Date fecha; @ManyToOne private Contacto contacto; public String getAsunto() { return asunto; } public void setAsunto(String asunto) { this.asunto = asunto; } public Date getFecha() { return fecha; } public void setFecha(Date fecha) { this.fecha = fecha; } public Contacto getContacto() { return contacto; } void setContacto(Contacto contacto) { this.contacto = contacto; } public Long getId() { return id; } } |
Una vez creado nuestro modelo, empezaremos con el DAO, pero para eso nos apoyaremos en EJB 3.1:
EJB 3.1
La nueva versión de EJB da un salto más hacia la simplicidad, es decir, mucho más con mucho menos.
Entre las cosas más interesantes:
- Se marcan para eliminar: Entity Bean 2.X, EJB QL, JAX-RPC.
- No son obligatorios los interfaces locales y remotos en Session Beans
- Podemos desplegar EJBs en un war.
- API Estandar para contenedores embebidos. Lo que simplificará bastante nuestras pruebas unitarias de EJBs
- Por fin…el Singleton EJB
- Se amplía bastante el Timer Service, sobre todo la capacidad de scheduling…
- Llamadas asíncronas a métodos de EJBs por fin. Antes teníamos que montar un circo a través de colas, MDBs etc… para una simple llamada no bloqueante.
- Estandarización de los nombres JNDI de los EJBs… Esto definitivamente está muy bien.
- AOP…Interceptores y cosillas así.
Probaremos algunas de estas cosas a lo largo del tutorial.
Vamos ahora a crearnos un simple Dao para nuestro proyecto de citas con EJBs.
Primero definimos el interfaz del Dao:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
package com.autentia.citas.dao; import java.io.Serializable; import java.util.List; public interface Dao<T> { public List<T> getAll(Class<T> entityClass); public T findById(Class<T> entityClass, Serializable id); public List findByQuery(String query); public void create(T entity); public void delete(T entity); public T update(T entity); } |
A continuación, lo implementamos con JPA en un EJB:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
package com.autentia.citas.dao; import java.io.Serializable; import java.util.List; import javax.ejb.Stateless; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; @Stateless public class JPADaoImpl<T> implements Dao<T>{ @PersistenceContext(unitName="citas") private EntityManager manager; public List<T> getAll(Class<T> entityClass) { return manager.createQuery("select e from "+entityClass.getName()+" as e",entityClass).getResultList(); } public T findById(Class<T> entityClass, Serializable id) { return manager.find(entityClass, id); } public List findByQuery(String query) { return manager.createQuery(query).getResultList(); } public void create(T entity) { manager.persist(entity); } public void delete(T entity) { manager.remove(manager.merge(entity)); } public T update(T entity) { return manager.merge(entity); } } |
Fijáos que no hemos marcado en ningún momento, ni el interfaz ni en la clase @Remote ni @Local
ni nada similar. Tampoco hemos creado ningún proyecto diferente de tipo ejb o jar. Es decir, el EJB
va a estar en el proyecto web.
Ahora, todo código que se precie debería se acompañado con una o varias pruebas unitarias que certifiquen
que la cosa funciona. Creo el Test y una clase de Utilidades para levantar el contenedor:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
package com.autentia.citas.dao; import java.util.Calendar; import javax.naming.NamingException; import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; import com.autentia.citas.model.Cita; import com.autentia.citas.model.Contacto; import com.autentia.citas.utils.Utils; public class DaoTest { @BeforeClass public static void initTests() throws Exception { if(Utils.container==null) { Utils.startContainer(); } } @Test public void createCita() throws NamingException { Contacto contacto = new Contacto(); contacto.setNombre("Francisco Javier"); contacto.setApellidos("Martínez Páez"); contacto.addEmail("fjmpaez@autentia.com"); contacto.addEmail("fjmpaez@acme.com"); contacto.addTelefono("915551111"); contacto.addTelefono("620999999"); Cita cita = new Cita(); cita.setAsunto("Quedada en el parque con los niños"); Calendar cal = Calendar.getInstance(); cal.set(Calendar.YEAR, 2014); cal.set(Calendar.DAY_OF_MONTH, 21); cal.set(Calendar.MONTH, 7); cita.setFecha(cal.getTime()); cal.set(Calendar.YEAR, 2012); cal.set(Calendar.DAY_OF_MONTH, 21); cal.set(Calendar.MONTH, 7); Cita cita2 = new Cita(); cita2.setAsunto("Reunión de seguimiento proyecto ACME LA CAMA"); cita2.setFecha(cal.getTime()); // Usamos el contexto JNDI para buscar el EJB...Y usando el nombre estandarizado... Dao dao =(Dao)Utils.ctx.lookup("java:global/citas/JPADaoImpl"); contacto.addCita(cita); contacto.addCita(cita2); // Persistimos contacto y en cascada se persistirán las citas dao.create(contacto); Assert.assertTrue(dao.getAll(Cita.class).size()==2); Assert.assertTrue(dao.getAll(Contacto.class).size()==1); Assert.assertTrue(dao.findByQuery("select cita from Cita cita inner join cita.contacto contacto where contacto.nombre like 'Francisco Javier'").size()==2); } @Test public void deletingAll() throws NamingException { Dao dao =(Dao)Utils.ctx.lookup("java:global/citas/JPADaoImpl"); Contacto contacto = (Contacto) dao.getAll(Contacto.class).get(0); // El orphans delete entra en juego eliminado las citas dao.delete(contacto); Assert.assertTrue(dao.getAll(Cita.class).size()==0); Assert.assertTrue(dao.getAll(Contacto.class).size()==0); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
package com.autentia.citas.utils; import java.io.File; import java.util.HashMap; import java.util.Map; import javax.ejb.embeddable.EJBContainer; import javax.naming.Context; public class Utils { public static EJBContainer container; public static Context ctx; public static void startContainer() throws Exception { // Inicializamos el EJB Container... Map<String, Object> properties = new HashMap<String, Object>(); // Le decimos donde están los EJBs properties.put(EJBContainer.MODULES, new File("target/classes")); // Le damos un nombre a la aplicación properties.put(EJBContainer.APP_NAME, "citas"); container = EJBContainer.createEJBContainer(properties); ctx=container.getContext(); } public static void closeContainer() { container.close(); } } |
Esperad… no le déis todavía al test que todavía hemos de configurar unas cosillas.
Para que el embedded container funcione correctamente necesitamos un fichero de configuración
llamado domain.xml, y para evitar una excepción, el fichero server.policy en las rutas de la imágen:
Esos ficheros los podéis obtener de la instalación del glassfish que os habéis descargado o en el
fichero: glassfish-embedded-all-3.0.1.jar (lo tendréis en el repositorio local de maven en [RUTA_REPO]\.m2\org\glassfish\extras\)
Eso es lo que hice yo… Vosotros tenéis los del ejemplo.
Ahora debemos configurar en el fichero domain.xml el datasource de nuestras citas. Os pongo únicamente lo que hay que incluir:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
... <resources> <jdbc-resource pool-name="__TimerPool" jndi-name="jdbc/__TimerPool" object-type="system-admin" /> <jdbc-resource pool-name="DerbyPool" jndi-name="jdbc/__default" /> <!-- Nuestro datasource. Incluye esto --> <jdbc-resource pool-name="CitasPool" jndi-name="jdbc/citasDatabase" /> <!-- Nuestro datasource. Incluye esto --> <jdbc-connection-pool name="__TimerPool" datasource-classname="org.apache.derby.jdbc.EmbeddedXADataSource" res-type="javax.sql.XADataSource"> <property value="{com.sun.aas.instanceRoot}/lib/databases/ejbtimer" name="databaseName" /> <property value=";create=true" name="connectionAttributes" /> </jdbc-connection-pool> <jdbc-connection-pool is-isolation-level-guaranteed="false" name="DerbyPool" datasource-classname="org.apache.derby.jdbc.ClientDataSource" res-type="javax.sql.DataSource"> <property value="1527" name="PortNumber" /> <property value="APP" name="Password" /> <property value="APP" name="User" /> <property value="localhost" name="serverName" /> <property value="sun-appserv-samples" name="DatabaseName" /> <property value=";create=true" name="connectionAttributes" /> </jdbc-connection-pool> <!-- Nuestro connection pool. Incluye esto --> <jdbc-connection-pool datasource-classname="org.apache.derby.jdbc.EmbeddedXADataSource" res-type="javax.sql.XADataSource" name="CitasPool"> <property name="databaseName" value="${com.sun.aas.instanceRoot}/lib/databases/default" /> <property name="connectionAttributes" value=";create=true" /> </jdbc-connection-pool> <!-- Nuestro connection pool. Incluye esto --> </resources> ... |
Ahora sí…dale al botón de probar el DaoTest (con JUnit) …o bien desde la consola. Veréis como se levanta
el contenedor embebido, se despliegan los ejbs, el datasource y se cierra al final del los tests:
Y lo mejor es que funciona.
Vamos a continuar haciendo un poco de negocio.
Vamos a crearnos un Stateless y le llamaremos CitasManager.
Éste nos servirá para probar algunas cosas nuevas:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
package com.autentia.citas.managers; import java.util.Calendar; import java.util.List; import java.util.concurrent.Future; import javax.annotation.Resource; import javax.ejb.AsyncResult; import javax.ejb.Asynchronous; import javax.ejb.EJB; import javax.ejb.SessionContext; import javax.ejb.Stateless; import javax.ejb.Timeout; import javax.ejb.Timer; import javax.ejb.TimerConfig; import javax.ejb.TimerService; import javax.interceptor.AroundInvoke; import javax.interceptor.InvocationContext; import com.autentia.citas.dao.Dao; import com.autentia.citas.model.Cita; import com.autentia.citas.model.Contacto; @Stateless public class CitasManager { @EJB private Dao dao; @Resource private SessionContext ctx; @SuppressWarnings("unchecked") public List<Cita> getAllCitas() { return dao.getAll(Cita.class); } @SuppressWarnings("unchecked") public Cita getCita(Long id) { return (Cita) dao.findById(Cita.class, id); } @SuppressWarnings("unchecked") public Contacto getContacto(Long id) { return (Contacto) dao.findById(Contacto.class, id); } public void citarme(Contacto contacto, Cita cita) { if(contacto.getId()!=null) { dao.create(cita); contacto.addCita(cita); dao.update(contacto); } else { contacto.addCita(cita); dao.create(contacto); } crearRecordatorio(cita); } /** * Método que crea un Timer de recordatorio de la cita una hora antes de la misma * @param cita */ private void crearRecordatorio(Cita cita) { // No uso DI para buscar el TimerService por problemas en el contenedor embebido. TimerService timerService = null; try { timerService = (TimerService) ctx.getTimerService(); } catch (Exception e) { } if(timerService!=null) { Calendar cal = Calendar.getInstance(); cal.setTime(cita.getFecha()); cal.add(Calendar.HOUR, -1); timerService.createSingleActionTimer(cal.getTime(), new TimerConfig(cita, true)); } } /** * Método de callback del timer. * @param timer */ @Timeout private void sendRecordatorio(Timer timer) { Cita c = (Cita) timer.getInfo(); // Ahora enviamos el correo al contacto de la cita y a mi...pero esto no lo voy a hacer } @SuppressWarnings("unchecked") public List<Contacto> getAllContactos() { return dao.getAll(Contacto.class); } public List<Cita> getCitasWith(Contacto contacto) { return dao.findByQuery("select cita from Cita cita inner join cita.contacto contacto where contacto.id = ?1" ,contacto.getId()); } /** * Método asíncrono sin respuesta * @param c */ @Asynchronous public void recuerdaleLaCita(Cita c) { // No lo hagáis en casa... ni en producción, es para probar esto... try { Thread.currentThread().sleep(10000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } // Aquí le mandamos un mail... // Lo siento pero paso de hacer esto. } /** * Método asíncrono con respuesta * @param c */ @Asynchronous public Future<Boolean> recuerdaleLaCitaAck(Cita c) { // No lo hagáis en casa... ni en producción, es para probar esto... try { Thread.currentThread().sleep(10000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } // Aquí le mandamos un mail... // Y le decimos que OK // Lo siento pero paso de hacer esto. return new AsyncResult<Boolean>(true); } /** * Este método nos contará lo que tardan las cosas. * @param inv * @return */ @AroundInvoke private Object cronometro(InvocationContext inv) { long time1 = System.currentTimeMillis(); try { return inv.proceed(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); return null; } finally { long total = System.currentTimeMillis() - time1; System.out.println("La invocación a:"+inv.getTarget()+"->"+inv.getMethod()+" ha tardado: "+total+" ms"); } } } |
Ahora haremos un test para probarlo…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
package com.autentia.citas.managers; import java.util.Calendar; import java.util.List; import java.util.concurrent.Future; import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; import com.autentia.citas.dao.Dao; import com.autentia.citas.model.Cita; import com.autentia.citas.model.Contacto; import com.autentia.citas.utils.Utils; public class CitasManagerTest { @BeforeClass public static void initTests() throws Exception { if(Utils.container==null) { Utils.startContainer(); } } @SuppressWarnings("unchecked") @AfterClass public static void releaseTests() throws Exception { // Limpiamos la Base de datos para no interferir en otros tests Dao dao =(Dao)Utils.ctx.lookup("java:global/citas/JPADaoImpl"); List<Contacto> contactos = dao.getAll(Contacto.class); for(Contacto c:contactos) { dao.delete(c); } } @Test public void createCitas() throws Exception { // // Usamos el contexto JNDI para buscar el EJB...Y usando el nombre estandarizado... CitasManager manager =(CitasManager)Utils.ctx.lookup("java:global/citas/CitasManager"); Cita cita = new Cita(); cita.setAsunto("Quedada en el parque con los niños"); Calendar cal = Calendar.getInstance(); cal.set(Calendar.YEAR, 2014); cal.set(Calendar.DAY_OF_MONTH, 21); cal.set(Calendar.MONTH, 7); cita.setFecha(cal.getTime()); Contacto contacto = new Contacto(); contacto.setNombre("Francisco Javier"); contacto.setApellidos("Martínez Páez"); contacto.addEmail("fjmpaez@autentia.com"); contacto.addEmail("fjmpaez@acme.com"); contacto.addTelefono("915551111"); contacto.addTelefono("620999999"); manager.citarme(contacto, cita); Assert.assertTrue(manager.getAllCitas().size()==1); Assert.assertTrue(manager.getAllContactos().size()==1); Assert.assertTrue(manager.getCitasWith(contacto).size()==1); // Probamos cita con contacto ya existente cal.set(Calendar.YEAR, 2012); cal.set(Calendar.DAY_OF_MONTH, 21); cal.set(Calendar.MONTH, 7); Cita cita2 = new Cita(); cita2.setAsunto("Reunión de seguimiento proyecto ACME LA CAMA"); cita2.setFecha(cal.getTime()); manager.citarme(contacto, cita2); Assert.assertTrue(manager.getAllCitas().size()==2); Assert.assertTrue(manager.getAllContactos().size()==1); Assert.assertTrue(manager.getCitasWith(contacto).size()==2); } @Test(timeout=9000) public void recuerda1() throws Exception { CitasManager manager =(CitasManager)Utils.ctx.lookup("java:global/citas/CitasManager"); manager.recuerdaleLaCita(null); } @Test public void recuerda2() throws Exception { CitasManager manager =(CitasManager)Utils.ctx.lookup("java:global/citas/CitasManager"); // A mi esto del futuro me da miedo. LO ha debido diseñar Doc el de Regreso Al futuro long time1 = System.currentTimeMillis(); Future<Boolean> futuro = manager.recuerdaleLaCitaAck(null); long time2 = System.currentTimeMillis(); Assert.assertTrue((time2-time1)< 9000 ); // Ahora se bloquea esperando respuesta. Boolean result = futuro.get(); long time3 = System.currentTimeMillis(); Assert.assertTrue((time3-time1)>= 10000 ); Assert.assertTrue(result); // Hay también una versión con espera activa: futuro.get(timeout, unit) } } |
Ejecutadlo y veréis como todo esto funciona. AOP con el @AroundInvoke (mirad las trazas), los métodos asíncronos,
la inyección de dependencias con @EJB, etc. También he incluido un ejemplo de crear un recordatorio de citas usando Timers.
Ahora lo ejecuto desde la consola de maven:
Bueno, ahora tenemos que empezar con la vista. En esta primera parte únicamente desplegaremos la aplicación
en el glassfish y comprobaremos que se despliega bien.
Antes de eso, debemos configurar también el datasource en el fichero domain.xml de vuestra instalación
tal y cómo lo hemos hecho en el de los tests. La ruta (por si no lo encontráis es [RUTA_GLASSFISH]\domains\domain1\config)
Una vez hecho esto, basta con agregar nuestra aplicación en la pestaña servers.
Seleccionad el servidor glassfish, pulsad el botón derecho y ejecutad Add and Remove…:
Ahora arrancar el servidor pulsando en start:
Vamos a comprobar que se ha desplegado bien. Desde la consola usaremos las herramientas de administración de glassfish: [RUTA_GLASSFISH]\bin]
Ejecutamos:
Y comprobamos que nuestra aplicación es: ejb y web.
Podemos entrar ahora en http://localhost:8080/citas/ y veréis la página que nos creó el arquetipo de maven: Hello World
Vamos con la vista
Para la vista usaremos evidentemente JSF 2.0 + Facelets. Entre las cosas nuevas de JSF 2:
- Facelets ya forma parte de JSF. No sólo plantillas sino además componentes por composición.
- Ãmbitos nuevos para los controladores (View y Component).
- Hola a nuevas anotaciones, adios a los descriptores (o hasta luego, es decir, no se eliminan).
- Soporte nativo para Ajax.
- Se incluye un nuevo mecanismo para recuperar recursos (imágenes, css, js …) desde el classpath (En wuija hacíamos lo mismo, aunque supongo que se habrán inspirado en RichFaces)
Os recomiendo el tutorial de Alex antes de empezar, porque yo no me voy a entretener tanto a explicar las cosas: JSF 2
Vamos primero a incluir en el pom.xml la dependencia a JSF 2 y alguna más:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<!-- API de JSF 2 --> <dependency> <groupId>javax.faces</groupId> <artifactId>jsf-api</artifactId> <version>2.0</version> <scope>provided</scope> </dependency> <!-- Para el Listener --> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <scope>provided</scope> </dependency> |
Ahora configuraremos JSF en el web.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <display-name>Aplicación Web JEE6 de Citas de Paco</display-name> <!-- Estamos en desarrollo --> <context-param> <description> Define the value returned by Application.getProjectStage(). Allowed values: Production, Development, UnitTest, SystemTest, Extension. Default value is Production. </description> <param-name>javax.faces.PROJECT_STAGE</param-name> <param-value>Development</param-value> </context-param> <!-- El servlet front controller de JSF --> <servlet> <servlet-name>Faces Servlet</servlet-name> <servlet-class>javax.faces.webapp.FacesServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <!-- El mapping... todo lo que acabe en .faces pasará por él --> <servlet-mapping> <servlet-name>Faces Servlet</servlet-name> <url-pattern>*.faces</url-pattern> </servlet-mapping> <!-- Nos creamos un Listener que rellene de datos la agenda --> <listener> <listener-class>com.autentia.citas.utils.PopulatorListener</listener-class> </listener> </web-app> |
En el código del listener podréis comprobar que rellena datos si no hay. Recordad que cuando despleguéis la aplicación
debemos tener configurado en el servidor el datasource que estamos usando, tal y como lo hicimos para el contenedor embebido.
Ahora necesitamos el faces-config.xml donde configuraremos el fichero de mensajes i18n. Este fichero está en resources
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<faces-config xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd" version="2.0"> <application> <!-- Configuramos la i18n --> <locale-config> <default-locale>es</default-locale> </locale-config> <resource-bundle> <base-name>messages</base-name> <var>msg</var> </resource-bundle> </application> </faces-config> |
Ahora nos vamos a crear un view helper en formato ManagedBean (que es el apropiado en estos charcos). Le llamamos CitasController y le decimos que tiene ámbito de session:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
package com.autentia.citas.view; import java.util.List; import javax.ejb.EJB; import javax.faces.bean.ManagedBean; import javax.faces.bean.SessionScoped; import com.autentia.citas.managers.CitasManager; import com.autentia.citas.model.Cita; import com.autentia.citas.model.Contacto; @ManagedBean @SessionScoped public class CitasController { private Cita cita; private Contacto contacto; @EJB private CitasManager mgr; public List<Cita> getAllCitas() { return mgr.getAllCitas(); } public List<Contacto> getAllContactos() { return mgr.getAllContactos(); } public Cita getCita() { return cita; } public Contacto getContacto() { return contacto; } public void setContacto(Contacto contacto) { this.contacto = contacto; } public void setCita(Cita cita) { this.cita = cita; } public String newCita() { cita = new Cita(); return "newCita.xhtml"; } public String saveCita() { mgr.citarme(contacto, cita); return "home.xhtml"; } } |
Ahora nos creamos nuestras primeras páginas:
- /WEB-INF/templates/defaultLayout.xhtml. Plantilla maestra que dará estructura a nuestras páginas. Creamos una forma sencilla con cabecera, cuerpo y pie.
- /home.xhtml . Será nuestra página de inicio y usará la plantilla anterior.
- Cambiamos index.jsp para apuntar a la home.
Os muestro nuestra home:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core"> <ui:composition template="/WEB-INF/templates/defaultLayout.xhtml"> <ui:define name="content"> <h:form> <h:dataTable id="citas" value="#{citasController.allCitas}" var="cita" border="1"> <h:column> <f:facet name="header"> #{msg['cita.asunto']} </f:facet> <h:outputText value="#{cita.asunto}" /> </h:column> <h:column> <f:facet name="header"> #{msg['cita.fecha']} </f:facet> <h:outputText value="#{cita.fecha}" > <f:convertDateTime dateStyle="full" /> </h:outputText> </h:column> <h:column> <f:facet name="header"> #{msg['cita.contacto']} </f:facet> <h:outputText value="#{cita.contacto.nombre} #{cita.contacto.apellidos}" /> </h:column> </h:dataTable> <h:commandButton action="#{citasController.newCita}" value="#{msg['cita.new']}" /> </h:form> </ui:define> </ui:composition> </html> |
Si desplegamos la aplicación y navegamos a ella, mostrará: (si, es que desde que España ha ganado el mundial,
por estos lares nos sentimos especialmente españoles):
Vamos a continuar creando la página de entrada de una cita:
También hemos usado el converter «selectItemsConverter». Este converter lo he cogido del tutorial de JSF de Alex que os indicaba arriba.
Si pulsamos sobre «Crear una nueva Cita»:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core"> <ui:composition template="/WEB-INF/templates/defaultLayout.xhtml"> <ui:define name="content"> <h:form> <h:panelGrid columns="2"> <h:outputLabel value="#{msg['cita.asunto']}"/> <h:inputText value="#{citasController.cita.asunto}" required="true"/> <h:outputLabel value="#{msg['cita.fecha']} #{msg['cita.pattern']}"/> <h:inputText value="#{citasController.cita.fecha}" required="true"> <f:convertDateTime pattern="dd/MM/yyyy HH:mm" /> </h:inputText> <h:outputLabel value="#{msg['cita.contacto']}"/> <h:selectOneListbox required="true" value="#{citasController.contacto}" converter="selectItemsConverter"> <f:selectItems value="#{citasController.allContactos}" itemLabel="#{contacto.nombre} #{contacto.apellidos}" var="contacto"/> </h:selectOneListbox> </h:panelGrid> <h:commandButton action="#{citasController.saveCita}" value="#{msg['cita.save']}" /> </h:form> </ui:define> </ui:composition> </html> |
Si guardamos:
Conclusiones
En cuanto a lo visto a JEE6 me gusta lo que va apareciendo. Todavía sigo pensando que prefiero
usar Hibernate + Spring para el Dao y el negocio que me aportan lo que realmente se necesita para hacer la mayor parte de
las aplicaciones Web y me basta un tomcat para desplegarlo, ya que se agiliza bastante el desarrollo.
Aunque la cosa es que ya me empieza resultar tentador…
He encontrado algunos problemas a la hora de usar el contenedor embebido y el plugin de glassfish para eclipse. En cuanto al primero he encontrado
el problema de la falta de documentación. En cuanto a lo segundo, supongo que se irá afinando en futuras versiones del mismo.
Joer que mezcla de gustazo, pique sano y agradecimiento siento al leeros, de verdaz que si el mundo entendiera de informática, que se quitaran los Piques, Casillas y demás… chavales 😉 Vaya equipazo tenéis por Autentia…
Hola Javier gracias por este tutorial esta muy bueno, queria pedirte colaboracion es que estoy tratando de pintar un combo de paises y pues como tu sabes sin necesidad de que haya una accion el debe consultar inicialmente la lista, entonces en el constructor de me Managedbean llamo a un metodo del EJB que me trae la lista pero me sale que esta nulo la instancia remota del ejb, pero cuando lo llamo desde una accion de la vista funciona perfectamente… agradezco si me podes colaborar
Hola Javier,
Me he bajado del código y al ejecutar el test, produce el siguiente error:
java.lang.IllegalStateException: Unable to retrieve EntityManagerFactory for unitName citas.
Creo que el problema es que no encuentra el fichero persistence.xml.