Proyecto de Integración
 

Integración con Spring

Introducción

Nuestro objetivo en esta sesión es hacer que el proyecto pueda ejecutarse en un servidor web ligero como Tomcat pero conservando algunas ventajas de las aplicaciones enterprise como la transaccionalidad declarativa o la gestión automática de los Entity manager en JPA. Es decir, vamos a usar una arquitectura de contenedor ligero. Los proyectos van a pasar de usar los APIs JavaEE (que requieren un servidor de aplicaciones) a usar Spring. Tendremos que crear tres nuevos proyectos, uno para cada capa: acceso a datos, negocio y presentación. Intentaremos reutilizar la mayor cantidad de código posible de los proyectos que ya tenemos creados.

Capa de acceso a datos: "jbib-persist-jpa-spring"

De las dos tecnologías de acceso a datos que hemos usado en el curso, JDBC y JPA, vamos a usar en Spring esta última, porque simplifica mucho el código. Vamos a tomar como base el proyecto "jbib-persist-jpa". De hecho el código va a ser el mismo con la única diferencia de que tendremos que anotar los EAOs para que se conviertan en beans de Spring.

Lo primero es crear el proyecto Maven "jbib-persist-jpa-spring". Va a ser muy parecido a "jbib-persist-jpa", por lo que podéis o bien crear un proyecto nuevo e ir copiando el contenido del original o bien directamente hacer una copia (Botón derecho > Copy... y cambiar el nombre del proyecto). Cuidado, si hacéis una copia de este modo Netbeans lo seguirá mostrando con el nombre original, ya que el nombre visualizado lo toma del pom.xml. Tendréis que cambiar las etiquetas "artifactId" y "name".

Configuración del proyecto (pom.xml)

  1. Si no lo habéis hecho ya, cambiad las etiquetas "artifactId" y "name" por el nuevo nombre
  2. Aseguráos de incluir una dependencia de Junit 4 (si no está ya), ya que es la versión que se recomienda en Spring 3 para las pruebas unitarias
    
    <dependency>
       <groupId>junit</groupId>
       <artifactId>junit</artifactId>
       <version>4.8.1</version>
       <scope>test</scope>
    </dependency>
    
    
  3. Incluir las dependencias de Spring. La parte básica es "context", que incluirá automáticamente otras varias. Además necesitamos "orm" para JPA y "test" para las pruebas (como es lógico :)).
    
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>3.0.5.RELEASE</version>
    </dependency>
    
    <!-- JPA con Spring -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-orm</artifactId>
        <version>3.0.5.RELEASE</version>
    </dependency>
    
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>3.0.5.RELEASE</version>
        <scope>test</scope>
    </dependency>
    
    
  4. El resto de dependencias serán las mismas que tenía el proyecto "jbib-persist-jpa".

Cambios en el código fuente

Partiremos de las clases LibroEao, OperacionEao y UsuarioEao para crear las correspondientes "LibroEaoSpring", "OperacionEaoSpring" y "UsuarioEaoSpring". En realidad, la única diferencia entre los dos grupos es que estas últimas estarán anotadas con @Repository. Tenéis dos posibilidades, elegid una de ellas:

  • Copiar los fuentes originales en el nuevo proyecto, cambiarles el nombre a las clases y anotarlas con @Repository.
  • Hacer que las nuevas clases hereden de las anteriores y anotarlas con @Repository. Las nuevas clases no tendrán ni más datos ni más métodos que las anteriores, ya que ofrecen la misma funcionalidad. En este caso tendríais que añadir en el pom.xml la dependencia del proyecto "jbib-persist-jpa".

Los Entity serán exactamente los mismos que en el proyecto antiguo. Si habéis usado la herencia para los EaoSpring y por tanto incluido en el pom.xml la dependencia del proyecto antiguo, no tendréis que hacer nada. En otro caso tendréis que copiar los fuentes originales en el nuevo proyecto.

La clase PersistenceManager no es necesaria en el nuevo proyecto, ya que los Entity Managers se pueden obtener con inyección de dependencias.

Pruebas unitarias

El estado de la base de datos
Para hacer las pruebas, necesitaréis que al comienzo la BD esté en un estado conocido. La solución del proyecto de integración de JPA incluía unos archivos "src/main/sql/biblioteca.sql" y "src/test/sql/datos.sql con la estructura de la BD y los datos de ejemplo, respectivamente. Además el pom.xml debe incluir una sección (dentro de "plugins") con un plugin de maven para ejecutar estos .sql antes de hacer las pruebas. Aseguráos de que tenéis todo esto, en caso contrario las pruebas no funcionarán.
Bases de datos en memoria
También podríamos crear y rellenar una base de datos embebida (como HSQLDB o Derby) configurándola adecuadamente en el XML de Spring. Así el responsable de rellenar los datos sería Spring en lugar de Maven. Con esto conseguiríamos que las pruebas se ejecutarán más rápido al estar los datos en memoria. No obstante vamos a seguir haciéndolo con Maven ya que lo tenéis configurado así. Pero podéis ver cómo trabajar con bases de datos embebidas en la documentación de Spring.

Tenéis que probar el funcionamiento de los nuevos EAOs. Para ello partiremos del código de OperacionEaoTest que teníamos del proyecto JPA antiguo. Pero le vamos a añadir una serie de elementos de configuración propios de Spring. El nuevo código quedaría del siguiente modo (los añadidos se muestran en negrita):

package org.especialistajee.jbib.eao;

import static org.junit.Assert.*;

import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.especialistajee.jbib.entity.OperacionEntity;
import org.especialistajee.jbib.model.EstadoOperacion;
import org.especialistajee.jbib.model.TipoOperacion;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.transaction.annotation.Transactional;


@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "file:src/test/resources/eaos-test.xml" })
@TransactionConfiguration(transactionManager = "miTransactionManager",
                        	 defaultRollback = true)
@Transactional
public class OperacionEaoTest {
    @PersistenceContext
    EntityManager em;

    @Autowired
    OperacionEaoSpring operacionEao;

	@Test
	public void testFindAllOperaciones() {
		operacionEao.setEntityManager(em);
		List<OperacionEntity> lista = 
		  operacionEao.findAllOperaciones();
		assertEquals(18, lista.size());
		em.close();
	}

        
	@Test
	public void testFindAllOperacionesTipo() {
		operacionEao.setEntityManager(em);
		List<OperacionEntity> lista =
		   operacionEao.findAllOperaciones(TipoOperacion.PRESTAMO);
		assertEquals(15, lista.size());
		em.close();
	}

	@Test
	public void testFindAllOperacionesUsuarioEstado() {
		operacionEao.setEntityManager(em);
		List<OperacionEntity> lista = 
		  operacionEao.findOperacionesUsuario("aitor",EstadoOperacion.HISTORICO);
		assertEquals(4, lista.size());
		em.close();
	}
         
}

A destacar del código anterior:

  • La anotación @RunWith le indica a Spring qué herramienta vamos a usar para las pruebas, en nuestro caso JUnit 4.
  • Con @ContextConfiguration le indicamos a Spring dónde está el fichero de configuración de beans que vamos a usar para hacer las pruebas. Nótese que puede ser distinto del que usemos "en producción" (y de hecho lo habitual es que lo sea). Ahora veremos su contenido.
  • Con @TransactionConfiguration configuramos un gestor de transacciones que nos servirá para hacer un rollback automático ("defaultRollback=true") de todas las operaciones que hagamos en pruebas. Así podemos modificar la BD sin problemas en los test, y automáticamente después de cada uno de ellos se desharán los cambios.
  • Con @Transactional en la clase indicamos que queremos transaccionalidad automática para todos los test. Podríamos usarla solo en los estrictamente necesarios.
  • El Entity Manager se puede obtener con anotaciones aunque estemos fuera de un servidor de aplicaciones, ya que lo inyectará el propio Spring.
  • El EAO se obtiene con anotaciones, de este modo podemos comprobar que Spring lo reconoce como bean.

Hay que tener en cuenta que dada la naturaleza de las operaciones que estamos probando en realidad no hace falta usar transaccionalidad en estas pruebas, pero mostramos aquí cómo se configura porque no lo habíamos visto en las otras sesiones del módulo de Spring. Podéis no usar @TransactionConfiguration y @Transactional si lo deseáis.

Como ya se ha visto, las pruebas referencian el fichero de configuración de beans de spring. Debéis crearlo en la carpeta src/test/resources con el nombre "eaos-test.xml". Su contenido será el siguiente:


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:jee="http://www.springframework.org/schema/jee"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:p="http://www.springframework.org/schema/p"

       xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
          http://www.springframework.org/schema/context 
          http://www.springframework.org/schema/context/spring-context-3.0.xsd
          http://www.springframework.org/schema/jee 
          http://www.springframework.org/schema/jee/spring-jee-3.0.xsd
          http://www.springframework.org/schema/tx 
          http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">

    <!-- para que Spring le haga caso a las anotaciones @Repository -->
    <context:component-scan base-package="org.especialistajee.jbib.eao"/>

    <!-- para obtener el Entity Manager por inyección de dependencias -->
    <bean id="miEMF" class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean">
        <property name="persistenceUnitName" value="proyint"/>
    </bean>

    <!-- soporte de transacciones para poder anular las operaciones hechas en las pruebas -->
    <tx:annotation-driven transaction-manager="miTransactionManager" />

    <bean id="miTransactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
		<property name="entityManagerFactory" ref="miEMF" />
    </bean>

</beans>

Todos los elementos de configuración se han visto en las sesiones del módulo. Lo único reseñable es que para inyectar los "Entity Manager" usamos la clase LocalEntityManagerFactoryBean, ligeramente distinta a la que usábamos en la sesión de acceso a datos para este propósito. Esta es una clase más sencilla y "ligera" indicada precisamente para testing.

Puedes escribir pruebas para los otros EAOs (deberías, aunque no se pedirá obligatoriamente dado el tiempo disponible). Puedes ejecutar las pruebas desde Netbeans con el botón derecho sobre el proyecto y la opción "Test". Ya sabes, "make it green!"

Capa de negocio: "jbib-negocio-spring"

Crear un nuevo proyecto Maven llamado "jbib-negocio-spring". Da igual el arquetipo, ya que vamos a sustituir el pom.xml. Este proyecto va a ser el equivalente a "jbib-negocio-ejb", pero usando Spring en lugar de java EE 6. Perderemos algunas funcionalidades como las colas de mensajes, los timer o los servicios web, que no nos da tiempo a implementar.

Ooooooh....
Aunque podríamos implementar el acceso remoto, las colas de mensajes, el servicio SOAP y el timer con APIs de Spring bajo Tomcat, no hemos visto la mayor parte de estos APIs en el módulo (salvo el acceso remoto) y de todos modos tampoco tenemos el tiempo necesario para la implementación. No obstante, a los interesados en estos temas se os recomienda que examinéis los sitios web de Spring AMQP (colas de mensajes), o Spring Web Services.

Configuración del proyecto (pom.xml)

Para ahorrar tiempo, podéis usar directamente este POM, que incluye las dependencias necesarias y el plugin de maven para crear y rellenar la base de datos.

Cambios en el código fuente

Hay que implementar las clases OperacionBoSpring, LibroBoSpring y UsuarioBoSpring, en el package org.especialistajee.jbib.springbean. Como punto de partida podéis tomar el código de los EJB, aunque habrá que hacer bastantes modificaciones puntuales. Antes de empezar la implementación leed los puntos siguientes:

  1. Hay que anotar las tres clases como @Service
  2. Solo tenéis que implementar los métodos que están en IUsuarioBo, ILibroBo e IOperacionBO. No hay que realizar reservas remotas, ni controlar las reservas caducadas con un timer ni usar colas de mensajes.
  3. Los EAOs no se deben obtener con new(), sino con inyección de dependencias. Es decir, en el código no debería usarse por ejemplo new UsuarioEaoSpring(). Los Entity manager también se obtendrán con inyección de dependencias. Así el código de UsuarioBoSpring podría quedar como sigue:
    @Service
    public class UsuarioBoSpring implements IUsuarioBo {
    
        @PersistenceContext
        EntityManager em;
        @Autowired
        UsuarioEaoSpring usuarioEao;
    
        @Override
        public UsuarioDomain recuperaUsuario(String login) throws UsuarioException {
            usuarioEao.setEntityManager(em);
            UsuarioEntity usuario = usuarioEao.find(login);
    
            UsuarioDomain usuarioDomain = null;
            if (usuario != null) {
                usuarioDomain = usuario.obtenerDomain();
            }
            return usuarioDomain;
        }
    }
    
  4. Aunque la clase BibliotecaBR, con las reglas de negocio, se podría convertir en un bean de Spring y gestionarla también con @Autowired, habría que modificar el proyecto "jbib-comun" y además ya es un singleton, como lo sería en Spring. Dejadla tal cual (aunque si os empeñais también podéis heredar de ella y anotarla como @Service).
  5. En cuanto a la transaccionalidad:
    • Recordad que el equivalente al @TransactionAttribute de EJB en Spring es la anotación @Transactional. Podéis indicar si la transacción es requerida, nueva, etc. con el atributo "propagation", por ejemplo @Transactional(propagation = Propagation.REQUIRED)
    • El equivalente al context.setRollbackOnly() de EJB en Spring es
      TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
      
      .

Pruebas de integración

Necesitarás los archivos "src/main/sql/biblioteca.sql" y "src/test/sql/datos.sql" del proyecto "jbib-persist-jpa-spring" con los scripts para la creación y llenado de la base de datos.

Ahora las llamamos pruebas de integración porque vamos a probar el funcionamiento conjunto de los BOs y los EAOs. Aquí tienes el código de LibroBoSpringTest:

package org.especialistajee.jbib.springbean;

import java.util.List;
import javax.persistence.EntityNotFoundException;
import org.especialistajee.jbib.bo.libro.ILibroBo;
import org.especialistajee.jbib.bo.libro.LibroException;
import static org.junit.Assert.*;
import org.especialistajee.jbib.bo.usuario.IUsuarioBo;
import org.especialistajee.jbib.model.LibroDomain;
import org.especialistajee.jbib.model.UsuarioDomain;
import org.especialistajee.jbib.model.to.LibroTo;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.ExpectedException;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.transaction.annotation.Transactional;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"file:src/test/resources/bos-spring-test.xml"})
@TransactionConfiguration(transactionManager = "miTransactionManager", 
                          defaultRollback = true)
@Transactional
public class LibroBoSpringTest {
    @Autowired
    ILibroBo lbo;

    @Test
    public void testRecuperaLibro() {
        LibroDomain libro = lbo.recuperaLibro("0764558315");
        assertEquals("El nombre del autor no es correcto","Rod Johnson",libro.getAutor());
    }

    @Test
    public void testRecuperaLibroOperaciones() {
        LibroTo libro = lbo.recuperaLibroOperaciones("0321180860");
        assertEquals("El título del libro no es correcto",
            "Understanding SOA with Web Services", libro.getTitulo());
        assertNotNull("Debería haber una reserva activa sobre este libro", 
             libro.getReservaActiva());
        assertEquals("El usuario que ha reservado el libro no es correcto", 
             libro.getReservaActiva().getUsuario().getLogin(),"antonio");
    }

    @Test
    public void testListaLibros() {
        List<LibroDomain> lista = lbo.listaLibros();
        assertEquals("Debería haber un número distinto de libros", 12, lista.size());
    }

    @Test
    @ExpectedException(EntityNotFoundException.class)
    public void testEliminaLibro() {
        lbo.eliminaLibro("0764558315");
        lbo.recuperaLibro("0764558315");       
    }   
}

Para crear el fichero src/test/resources/bos-spring-test.xml puedes tomar como base el mismo que usamos para la capa de acceso a datos. Unicamente podría cambiar la etiqueta context:component-scan para indicar el paquete por debajo del que están los objetos de Spring. ¡¡Pon un paquete que englobe tanto a Bos como a Eaos!!.

Implementa tú al menos la clase UsuarioBoSpringTest, que pruebe el método "recuperaUsuario"

Capa de presentación: "jbib-web-jsf-spring"

Aunque podríamos crear una capa web partiendo de cero con Spring MVC, una solución alternativa es integrar las capas de negocio y acceso a datos Spring con la capa web en JSF que ya tenemos hecha. Esto es factible ya que JSF no requiere de un servidor de aplicaciones para funcionar, simplemente es un conjunto de JARs. Además Spring proporciona un "enganche" relativamente sencillo con JSF, como veremos.

Lo primero es crear el proyecto "jbib-web-jsf-spring". Puedes tomar como punto de partida el "jbib-web-jsf". Los XHTML van a ser exactamente iguales y solo vamos a modificar ligeramente los fuentes java de los "managed beans".

Configuración del proyecto

Para ahorrar tiempo, puedes tomar este POM

Para el descriptor de despliegue, WEB-INF/web.xml toma el de "jbib-web-jsf" y añádele la siguiente configuración, para arrancar Spring cuando arranque la aplicación

 
 <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>
<listener>
    <listener-class>
      org.springframework.web.context.ContextLoaderListener
    </listener-class>
</listener>
<listener>
    <listener-class>
      org.springframework.web.context.request.RequestContextListener
    </listener-class>
</listener>
 
 
No es bueno mezclar...
Como en el proyecto necesitamos una versión relativamente moderna de la librería de expresiones (EL) tendremos problemas con los JSP. En general no es recomendable mezclar JSPs y facelets. Para evitar nuestro "index.jsp", que simplemente nos redirige a "faces/login.xhtml" y que va a dar error en el nuevo proyecto, cambiaremos el <welcome-file> que tenemos en el web.xml por otro que apunte directamente a la página de login: <welcome-file>faces/login.xhtml</welcome-file>

Hay que crear también el WEB-INF/applicationContext.xml, fichero de configuración de Spring, que se referencia en el fragmento anterior. Su contenido será el siguiente:


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:jee="http://www.springframework.org/schema/jee"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:p="http://www.springframework.org/schema/p"

       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
          http://www.springframework.org/schema/context 
          http://www.springframework.org/schema/context/spring-context-3.0.xsd
          http://www.springframework.org/schema/jee 
          http://www.springframework.org/schema/jee/spring-jee-3.0.xsd
          http://www.springframework.org/schema/tx 
          http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">

    <context:component-scan 
       base-package="org.especialistajee.jbib.springbean, org.especialistajee.jbib.eao"/>

    <bean id="miEMF" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="persistenceUnitName" value="proyint"/>
    </bean>

    <!-- soporte de transacciones para poder anular las operaciones hechas en las pruebas -->
    <tx:annotation-driven transaction-manager="miTransactionManager" />

    <bean id="miTransactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="miEMF" />
    </bean>
</beans>
 

El archivo WEB-INF/sun-web.xml puedes borrarlo porque es propio de glassfish.

Para terminar la configuración básica nos falta crear el archivo Web Pages/META-INF/context.xml , ya que es la forma en Tomcat de asociarle un nombre JNDI a la base de datos y crear un pool de conexiones, mientras que en glassfish se hace desde la consola.

 
<?xml version="1.0" encoding="UTF-8"?>
<Context antiJARLocking="true" path="/jbib-web-jsf-spring">
  <Resource auth="Container" driverClassName="com.mysql.jdbc.Driver" 
  maxActive="8" maxIdle="4" name="jdbc/jbib" 
  password="especialista" type="javax.sql.DataSource" 
  url="jdbc:mysql://localhost/biblioteca" username="root"/>
</Context>
 
 

Cambios en el código fuente

Nuestro objetivo es que desde JSF se pueda acceder a los beans de Spring pero usando inyección de dependencias para simplificar el código en lo posible. Como visteis en el módulo de JSF, los managed beans permiten inyección de dependencias, las managed properties, que se expresan usando EL. Lo único que nos falta es que los nombres de los beans de Spring se resuelvan correctamente en EL. Afortunadamente, JSF permite "enganchar" con una implementación alternativa del "resolver" de EL. Usaremos uno de Spring, que primero buscará los objetos en Spring y si no los encuentra los buscará en JSF. Por ello, en el faces-config.xml tenemos que definir la siguiente etiqueta (en negrita):

<faces-config version="2.0" xmlns="http://java.sun.com/xml/ns/javaee"
 xmlns:xi="http://www.w3.org/2001/XInclude"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
 xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
    http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd">

  <application>
    <el-resolver>
       org.springframework.web.jsf.el.SpringBeanFacesELResolver
    </el-resolver>
  </application>
  
  <navigation-rule>
      <from-view-id>/login.xhtml</from-view-id>
      ...

Ahora en los managed beans tenemos que sustituir todas las referencias a FactoriaBos por inyección de dependencias con @ManagedProperty. Por ejemplo, en "LoginBean" necesitamos de un Bo para recuperar un usuario por login. Podemos dejar el código como sigue (solo se muestran los fragmentos relevantes):

@ManagedBean
@RequestScoped
public class LoginBean implements Serializable {

  ...
  
  
  //Inyectamos el bean de Spring de la capa de negocio
  //lo referenciamos por nombre, que recordemos que en Spring por defecto
  //era el de la clase con la inicial en minúscula
  @ManagedProperty(value = "#{usuarioBoSpring}")
  IUsuarioBo usuarioBo;
  
  
  //necesitamos el setter para que JSF nos inyecte el bean
  //Spring puede inyectar directamente en el campo, pero JSF necesita el setter
  public void setUsuarioBo(IUsuarioBo usuarioBo) {
     this.usuarioBo = usuarioBo;
  }
  
  //finalmente, usamos el bean
  public String doLogin() {
     usuario = usuarioBo.recuperaUsuario(login);
     if (usuario != null) {
         ...
     }
     ...
  }
 
}

Resumen

Capa de acceso a datos Capa de negocio

Se deben crear y entregar los tres proyectos:

  • "jbib-persist-jpa-spring"
  • "jbib-negocio-spring"
  • "jbib-web-jsf-spring". No se muestra este proyecto porque no hay ningún fichero Java nuevo, todos son modificaciones de los antiguos