viernes, 8 de noviembre de 2013

Integración y transacciones con Spring en Apache Tapestry

Apache Tapestry
Spring
En otra entrada comentaba como hacer transacciones en una base de datos relacional con Apache Tapestry y como mejorar el soporte que ofrece de por si con la anotación CommitAfter mediante con una solución propia que proporciona la anotación Transactional. La solución propia mejora la anotación CommitAfter y es usable en más casos como cuando dos servicios distintos necesitan colaborar en una transaccion y compartirla. Sin embargo, si el correcto funcionamiento de las transacciones es una parte importante de la aplicación (y en una aplicación grande lo será) podemos evaluar si optar por Spring o los EJB en vez de la solución propia o la anotación CommitAfter.

Unos buenos motivos para optar tanto por Spring como por los EJB es que son soluciones ya desarrolladas con lo que solo tendremos que integrarlo en nuestros proyectos y no tendremos que preocuparnos de mantener nuestra solución en caso de que tenga errores, además ambas son ampliamente usadas incluso en proyectos grandes y complejos y están ya probadas lo que es una garantía. Entre optar por Spring o los EJB depende de varios factores como puede ser si la aplicación va ha ser desplegada en un servidor de aplicaciones con soporte para EJB (como JBoss/Wildfly, Geronimo, ...) o no (Tomcat, Jetty) o de nuestras preferencias entre ambas opciones. En esta entrada explicaré como integrar Spring con el framework Apache Tapestry y como hacer uso de las transacciones de Spring en los servicios que contienen la lógica de la aplicación.

Primeramente, decir que en cierta medida la funcionalidad proporcionada por el contenedor de dependencias de Tapestry y el contenedor de dependencias de Spring se solapan, ambos proporcionan Inversion of Control (IoC). Pero el contenedor de dependencias de Tapestry tiene algunas ventajas como permitir configuración distribuida, esto hace referencia a que cada librería jar puede contribuir con su configuración al contenedor de dependencias y que la configuración se hace mediante código Java en vez de xml como en Spring con la ventaja de que es más rápido, tenemos la ayuda del compilador para detectar errores y el lenguaje Java es más adecuado para expresar la construcción de objetos. De modo que si podemos es mejor usar el contenedor de Tapestry que el de Spring, sin embargo, Spring ofrece un montón de funcionalidades muy útiles y esto nos puede obligar a usar el contenedor de Spring para ciertos objetos. Una de ellas son las transacciones para cumplir con las reglas ACID de las bases de datos relacionales, para ello deberemos definir en el contenedor de Spring (y no en el de Tapestry) los servicios con la lógica de negocio con necesidades transaccionales y las dependencias referidas por esos servicios en la configuración del contexto de Spring. A pesar de todo en los demás casos podemos optar por la opción que prefiramos ya que tanto a los servicios de Spring se les pueden inyectar dependencias del contenedor de Tapestry y, el caso contrario, a los servicios de Tapestry se les pueden inyectar servicios de Spring.

Veamos en código un ejemplo de como conseguir integración entre Tapestry y Spring. La primera cosa que cambia es que hay que usar un filtro de Tapestry especial para integrarse con Spring con lo que deberemos modificarlo en el archivo web.xml. Si normalmente usamos el filtro org.apache.tapestry5.TapestryFilter para que Tapestry procese las peticiones que llegan a la aplicación, integrándonos con Spring usaremos un filtro especial, org.apache.tapestry5.spring.TapestrySpringFilter. También mediante una propiedad de contexto indicaremos el (o los) xml con la definición de los beans de Spring.

<?xml version="1.0" encoding="UTF-8"?>
<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_3_0.xsd"
version="3.0">
<display-name>PlugInTapestry Tapestry 5 Application</display-name>
<context-param>
<!-- The only significant configuration for Tapestry 5, this informs Tapestry of where to look for pages, components and mixins. -->
<param-name>tapestry.app-package</param-name>
<param-value>es.com.blogspot.elblogdepicodev.plugintapestry</param-value>
</context-param>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:/applicationContext.xml</param-value>
</context-param>
<filter>
<filter-name>app</filter-name>
<filter-class>org.apache.tapestry5.spring.TapestrySpringFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>app</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>ERROR</dispatcher>
</filter-mapping>
<error-page>
<error-code>404</error-code>
<location>/error404</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/error500</location>
</error-page>
<session-config>
<tracking-mode>COOKIE</tracking-mode>
</session-config>
</web-app>
view raw web.xml hosted with ❤ by GitHub
En el xml del contexto para Spring definimos la configuración para que Hibernate se conecte a la base de datos, definimos el SesionFactory que creará la sesiones de Hibernate, el gestor de transacciones y los servicios con la lógica de negocio. En este ejemplo he optado por definir las transacciones mediante anotaciones en los servicios con la lógica de negocio. Spring también permite definir la transaccionalidad de forma declarativa en este xml.

<?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:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
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/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="org.h2.Driver" />
<property name="url" value="jdbc:h2:mem:test" />
<property name="username" value="sa" />
<property name="password" value="sa" />
</bean>
<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="packagesToScan" value="es.com.blogspot.elblogdepicodev.plugintapestry.entities" />
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.HSQLDialect</prop>
<prop key="hibernate.hbm2ddl.auto">create</prop>
<!-- Debug -->
<prop key="hibernate.generate_statistics">true</prop>
<prop key="hibernate.show_sql">true</prop>
</props>
</property>
</bean>
<bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<bean id="productoDAO" class="es.com.blogspot.elblogdepicodev.plugintapestry.services.dao.ProductoDAOImpl" />
<context:annotation-config />
<tx:annotation-driven transaction-manager="transactionManager" />
</beans>
Como Spring se encargará de la configuración de Hibernate si incluimos la dependencia tapestry-hibernate tendremos un problema ya que este módulo de Tapestry también intenta inicializar Hibernate. Para evitarlo y disponer de toda la funcionalidad que ofrece este módulo como encoders para las entidades de dominio, la página de estadísticas de Hibernate o el objeto Session como un servicio inyectable en páginas o componentes hay que redefinir el servicio HibernateSessionSource. El nuevo servicio es muy sencillo, básicamente obtiene el la configuración de Hibernate mediante el bean SessionFactory definido en Spring y además mediante el mismo bean se crea el objeto Session que podrá inyectarse en los componentes y páginas de Tapestry en los que lo necesitemos. También deberemos añadir un poco de configuración en el módulo de la aplicación para redefinir este servicio.

package es.com.blogspot.elblogdepicodev.plugintapestry.services.hibernate;
import org.apache.tapestry5.hibernate.HibernateSessionSource;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import org.springframework.context.ApplicationContext;
import org.springframework.orm.hibernate4.LocalSessionFactoryBean;
public class HibernateSessionSourceImpl implements HibernateSessionSource {
private SessionFactory sessionFactory;
private Configuration configuration;
public HibernateSessionSourceImpl(ApplicationContext context) {
this.sessionFactory = (SessionFactory) context.getBean("sessionFactory");
// http://stackoverflow.com/questions/2736100/how-can-i-get-the-hibernate-configuration-object-from-spring
LocalSessionFactoryBean localSessionFactoryBean = (LocalSessionFactoryBean) context.getBean("&sessionFactory");
this.configuration = localSessionFactoryBean.getConfiguration();
}
@Override
public Session create() {
return sessionFactory.openSession();
}
@Override
public SessionFactory getSessionFactory() {
return sessionFactory;
}
@Override
public Configuration getConfiguration() {
return configuration;
}
}
package es.com.blogspot.elblogdepicodev.plugintapestry.services;
...
public class AppModule {
public static void bind(ServiceBinder binder) {
// Añadir al contenedor de dependencias nuestros servicios, se proporciona la interfaz y la
// implementación. Si tuviera un constructor con parámetros se inyectarían como
// dependencias.
// binder.bind(Sevicio.class, ServicioImpl.class);
// Servicios de persistencia (definidos en Spring por la necesidad de que Spring gestione las transacciones)
// binder.bind(ProductoDAO.class, ProductoDAOImpl.class);
}
// Servicio que delega en Spring la inicialización de Hibernate, solo obtiene la configuración de Hibernate creada por Spring
public static HibernateSessionSource buildAppHibernateSessionSource(ApplicationContext context) {
return new HibernateSessionSourceImpl(context);
}
public static void contributeServiceOverride(MappedConfiguration<Class, Object> configuration, @Local HibernateSessionSource hibernateSessionSource) {
configuration.add(HibernateSessionSource.class, hibernateSessionSource);
}
...
public static void contributeBeanValidatorSource(OrderedConfiguration<BeanValidatorConfigurer> configuration) {
configuration.add("AppConfigurer", new BeanValidatorConfigurer() {
public void configure(javax.validation.Configuration<?> configuration) {
configuration.ignoreXmlConfiguration();
}
});
}
...
}
view raw AppModule.java hosted with ❤ by GitHub
Para definir la transaccionalidad de una operación debemos usar la anotación Transactional usando los valores por defecto o indicando la propagación, el aislamiento, si es de solo lecura, timeout, etc, ... según consideremos. Debido a lo simple de la lógica de negocio de la aplicación de este ejemplo la anotación se aplica al DAO, sin embargo, en una aplicación más compleja y con mas clases sería mejor definirlo a nivel de servicio de lógica de negocio o punto de entrada a la lógica de negocio y no al nivel de los DAO que están en una capa de la aplicación más baja.

package es.com.blogspot.elblogdepicodev.plugintapestry.services.dao;
import java.io.Serializable;
import java.util.List;
import es.com.blogspot.elblogdepicodev.plugintapestry.misc.Pagination;
public interface GenericDAO<T> {
T findById(Serializable id);
List<T> findAll();
List<T> findAll(Pagination paginacion);
long countAll();
void persist(T entity);
void remove(T entity);
void removeAll();
}
view raw GenericDAO.java hosted with ❤ by GitHub
package es.com.blogspot.elblogdepicodev.plugintapestry.services.dao;
import java.io.Serializable;
import java.util.List;
import org.hibernate.Criteria;
import org.hibernate.Query;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projections;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import es.com.blogspot.elblogdepicodev.plugintapestry.misc.Pagination;
@SuppressWarnings({ "rawtypes", "unchecked" })
public class GenericDAOImpl<T> implements GenericDAO<T> {
private Class clazz;
protected SessionFactory sessionFactory;
public GenericDAOImpl(Class<T> clazz, SessionFactory sessionFactory) {
this.clazz = clazz;
this.sessionFactory = sessionFactory;
}
@Override
@Transactional(readOnly = true)
public T findById(Serializable id) {
return (T) sessionFactory.getCurrentSession().get(clazz, id);
}
@Override
@Transactional(readOnly = true)
public List<T> findAll() {
return findAll(null);
}
@Override
@Transactional(readOnly = true)
public List<T> findAll(Pagination paginacion) {
Criteria criteria = sessionFactory.getCurrentSession().createCriteria(clazz);
if (paginacion != null) {
List<Order> orders = paginacion.getOrders();
for (Order order : orders) {
criteria.addOrder(order);
}
}
if (paginacion != null) {
criteria.setFirstResult(paginacion.getStart());
criteria.setFetchSize(paginacion.getEnd() - paginacion.getStart() + 1);
}
return criteria.list();
}
@Override
@Transactional(readOnly = true)
public long countAll() {
Criteria criteria = sessionFactory.getCurrentSession().createCriteria(clazz);
criteria.setProjection(Projections.rowCount());
return (long) criteria.uniqueResult();
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void persist(T object) {
sessionFactory.getCurrentSession().persist(object);
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void remove(T object) {
sessionFactory.getCurrentSession().delete(object);
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void removeAll() {
String hql = String.format("delete from %s", clazz.getName());
Query query = sessionFactory.getCurrentSession().createQuery(hql);
query.executeUpdate();
}
}
Finalmente, debemos añadir o modificar las dependencias de nuestra aplicación. La dependencia tapestry-spring usa por defecto la versión 3.1.0 de Spring, en el ejemplo la sustituyo por la versión 3.2.4 más reciente. A continuación incluyo la parte relevante.

description = 'PlugInTapestry application'
apply plugin: 'eclipse'
apply plugin: 'java'
apply plugin: 'groovy'
apply plugin: 'war'
apply plugin: 'jetty'
apply plugin: 'tomcat'
group = 'es.com.blogspot.elblogdepicodev.plugintapestry'
version = '1.1'
...
dependencies {
// Tapestry
compile 'org.apache.tapestry:tapestry-core:5.4-alpha-23'
compile 'org.apache.tapestry:tapestry-hibernate:5.4-alpha-23'
compile 'org.apache.tapestry:tapestry-beanvalidator:5.4-alpha-23'
// Compresión automática de javascript y css en el modo producción
compile 'org.apache.tapestry:tapestry-webresources:5.4-alpha-23'
appJavadoc 'org.apache.tapestry:tapestry-javadoc:5.4-alpha-23'
// Spring
compile ('org.apache.tapestry:tapestry-spring:5.4-alpha-23') {
exclude(group: 'org.springframework')
}
compile 'org.springframework:spring-web:3.2.4.RELEASE'
compile 'org.springframework:spring-orm:3.2.4.RELEASE'
compile 'org.springframework:spring-tx:3.2.4.RELEASE'
compile 'commons-dbcp:commons-dbcp:1.4'
// Hibernate
compile 'org.hibernate:hibernate-core:4.2.7.SP1'
compile 'org.hibernate:hibernate-validator:5.0.1.Final'
compile 'com.h2database:h2:1.3.173'
...
}
...
view raw build.gradle hosted with ❤ by GitHub
Si te ha parecido interesante esta entrada puedes descargar el libro PlugIn Tapestry en el que explico más en detalle como desarrollar aplicaciones web en Tapestry y en el que descubrirás como resolver problemas comunes en las aplicaciones web de una forma tan buena como esta.

Si quieres probarlo en tu equipo lo puedes hacer de forma muy sencilla con los siguientes comandos y sin instalar nada. Si no dispones de git para clonar mi repositorio de GitHub puedes obtener el código fuente del repositorio en un archivo zip.

$ git clone git://github.com/picodotdev/elblogdepicodev.git
$ cd elblogdepicodev/PlugInTapestry
$ ./gradlew tomcatRun
# Abrir en el navegador http://localhost:8080/PlugInTapestry/
view raw git.sh hosted with ❤ by GitHub
Referencia:
Transacciones en Apache Tapestry
Persistencia con JPA y Apache Tapestry
Acceso a base de datos con Hibernate y JPA
Transaction Management (Spring)