
Con la anotación CommitAfter si se produce una excepción no controlada («unchecked») se hará un rollback de la transacción y, esto es importante, aún produciendose una excepción controlada («checked») se hará el commit de la transacción y es responsabilidad del programador tratar la excepción adecuadamente. Se puede usar en los métodos de los servicios y en los métodos manejadores de eventos de los componentes.
Sabiendo como funciona la anotación se nos plantean preguntas:
- ¿Cuál es el comportamiento cuando un método del servicio anotado llame a otro también anotado del mismo servicio?
- ¿Que pasa si cada método está en un servicio diferente?
Si tenemos una aplicación compleja probablemente se nos planteará el caso de tener varios servicios que se llaman entre si y que ambos necesiten compartir la transacción, en esta situación la anotación CommitAfter probablemente no nos sirva por hacer un commit en la salida de cada método.
Tapestry no pretende proporcionar una solución propia que cubra todas las necesidades transaccionales que puedan tener todas las aplicaciones sino que con la anotación CommitAfter pretende soportar los casos simples, para casos más complejos ya existen otras opciones que están ampliamente probadas. Si necesitamos un mejor soporte para las transacciones que el que ofrece Tapestry debemos optar por Spring o por los EJB. Sin embargo, la solución de Spring nos obliga a definir los servicios transaccionales como servicios de Spring y los EJBs nos obligan a desplegar la aplicación en un servidor de aplicaciones que soporte un contenedor de EJB como JBoss/Wildfy, Gernimo, TomEE, ... Si nuestro caso no es tan complejo como para necesitar mucho de lo que ofrece Spring o no queremos o podemos usar un servidor que soporte EJB podemos aplicar el ejemplo ya comentado en Tapestry Magic #5: Advising Services.
En esta entrada pondré un ejemplo completo usando la solución de Tapestry Magic #5 pero con la adición de una anotación que permite definir más propiedades de las transacciones y la diferencia respecto a la anotación CommitAfter de que independientemente de si se produce una excepción checked o unchecked se hace un rollback de la transacción. La anotación Transactional permite definir si la transacción es de solo lectura, definir un timeout para completar la transacción o el nivel de aislamiento de la transacción además de la estrategia de propagación. Aunque no sea una solución tan buena como la de usar Spring o EJBs, puede ser suficiente para muchos más casos que la anotación CommitAfter.
La solución consiste en implementar una nueva anotación para los métodos transaccionales que he llamado Transactional, unos advices con las diferentes estrategias de transaccionalidad (REQUIRED, SUPPORTS, NEVER, NESTED, MANDATORY), un advisor que aplicará una estrategia transaccional en función de la anotación Transactional de los métodos y un poco de configuración para el contenedor IoC que define los servicios y aplica la decoración a los métodos anotados.
Hay que tener en cuenta que esta solución es una prueba de concepto que he probado en este ejemplo y puede presentar problemas que aún desconozco en una aplicación real. Una vez dicho esto veámos el código.
Primero la anotación, el enumerado de las estrategias de propagación de transacciones, el DTO (Data Transfer Object) con las propiedades de la anotación y la interfaz del servicio transaccional.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package es.com.blogspot.elblogdepicodev.plugintapestry.services.transaction; | |
import java.lang.annotation.ElementType; | |
import java.lang.annotation.Retention; | |
import java.lang.annotation.RetentionPolicy; | |
import java.lang.annotation.Target; | |
@Retention(RetentionPolicy.RUNTIME) | |
@Target({ ElementType.METHOD }) | |
public @interface Transactional { | |
Propagation propagation() default Propagation.REQUIRED; | |
int isolation() default -1; | |
boolean readonly() default false; | |
int timeout() default -1; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package es.com.blogspot.elblogdepicodev.plugintapestry.services.transaction; | |
public enum Propagation { | |
REQUIRED, SUPPORTS, NEVER, NESTED, MANDATORY | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package es.com.blogspot.elblogdepicodev.plugintapestry.services.transaction; | |
public class TransactionDefinition { | |
private Propagation propagation; | |
private Integer isolation; | |
private Boolean readOnly; | |
private Integer timeout; | |
public TransactionDefinition(Propagation propagation, Integer isolation, Boolean readOnly, Integer timeout) { | |
this.propagation = propagation; | |
this.isolation = isolation; | |
this.readOnly = readOnly; | |
this.timeout = timeout; | |
} | |
public Propagation getPropagation() { | |
return propagation; | |
} | |
public void setPropagation(Propagation propagation) { | |
this.propagation = propagation; | |
} | |
public Integer getIsolation() { | |
return isolation; | |
} | |
public void setIsolation(Integer isolation) { | |
this.isolation = isolation; | |
} | |
public Boolean getReadOnly() { | |
return readOnly; | |
} | |
public void setReadOnly(Boolean readOnly) { | |
this.readOnly = readOnly; | |
} | |
public Integer getTimeout() { | |
return timeout; | |
} | |
public void setTimeout(Integer timeout) { | |
this.timeout = timeout; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package es.com.blogspot.elblogdepicodev.plugintapestry.services.transaction; | |
public interface TransactionService { | |
boolean beginIfNoPresent(TransactionDefinition definition); | |
void begin(TransactionDefinition definition); | |
void commit(); | |
void rollback(); | |
boolean isWithinTransaction(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package es.com.blogspot.elblogdepicodev.plugintapestry.services.transaction; | |
import org.apache.tapestry5.ioc.MethodAdviceReceiver; | |
public interface TransactionAdvisor { | |
void addAdvice(MethodAdviceReceiver methodAdviceReceiver); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package es.com.blogspot.elblogdepicodev.plugintapestry.services.transaction; | |
import java.lang.reflect.Method; | |
import org.apache.tapestry5.ioc.MethodAdviceReceiver; | |
public class TransactionAdvisorImpl implements TransactionAdvisor { | |
private TransactionService service; | |
public TransactionAdvisorImpl(TransactionService service) { | |
this.service = service; | |
} | |
public void addAdvice(final MethodAdviceReceiver receiver) { | |
for (Method method : receiver.getInterface().getMethods()) { | |
Transactional transactional = method.getAnnotation(Transactional.class); | |
if (transactional != null) { | |
adviceMethod(buildTransactionDefinition(transactional), method, receiver); | |
} | |
} | |
} | |
private void adviceMethod(TransactionDefinition definition, Method method, MethodAdviceReceiver receiver) { | |
switch (definition.getPropagation()) { | |
case REQUIRED: | |
receiver.adviseMethod(method, new RequiredTransactionAdvice(definition, service)); | |
break; | |
case NESTED: | |
receiver.adviseMethod(method, new NestedTransactionAdvice(definition, service)); | |
break; | |
case MANDATORY: | |
receiver.adviseMethod(method, new MandatoryTransactionAdvice(service)); | |
break; | |
case NEVER: | |
receiver.adviseMethod(method, new NeverTransactionAdvice(service)); | |
break; | |
case SUPPORTS: | |
break; | |
} | |
} | |
private TransactionDefinition buildTransactionDefinition(Transactional transactional) { | |
return new TransactionDefinition(transactional.propagation(), (transactional.isolation() == -1) ? null : transactional.isolation(), transactional.readonly(), | |
(transactional.timeout() == -1) ? null : transactional.timeout()); | |
} | |
} |
- REQUIRED: si no hay una transaccion activa inicia una y hace el commit al finalizar. Si existe una al entrar en el método simplemente ejecuta la lógica usando la transacción actual
- MANDATORY: requiere que haya una transacción iniciada, en caso contrario produce una excepción.
- NESTED: inicia una nueva transacción siempre aún existiendo ya una, con lo que puede haber varias transacciones a la vez de forma anidada.
- NEVER: es el caso contrario de MANDATORY, si existe una transacción produce una excepción.
- SUPPORTS: puede ejecutarse tanto dentro como fuera de una transacción.
Y ahora las implementaciones de las estrategias de propagación que iniciarán, harán el rollbak y commit de forma adecuada a la estrategia usando el servicio transaccional.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package es.com.blogspot.elblogdepicodev.plugintapestry.services.transaction; | |
import org.apache.tapestry5.plastic.MethodAdvice; | |
import org.apache.tapestry5.plastic.MethodInvocation; | |
public class RequiredTransactionAdvice implements MethodAdvice { | |
private TransactionDefinition definition; | |
private TransactionService service; | |
public RequiredTransactionAdvice(TransactionDefinition definition, TransactionService service) { | |
this.definition = definition; | |
this.service = service; | |
} | |
public void advise(MethodInvocation invocation) { | |
boolean isNew = service.beginIfNoPresent(definition); | |
try { | |
invocation.proceed(); | |
if (isNew) { | |
service.commit(); | |
} | |
} catch (Exception e) { | |
if (isNew) { | |
service.rollback(); | |
} | |
throw e; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package es.com.blogspot.elblogdepicodev.plugintapestry.services.transaction; | |
import org.apache.tapestry5.plastic.MethodAdvice; | |
import org.apache.tapestry5.plastic.MethodInvocation; | |
public class NestedTransactionAdvice implements MethodAdvice { | |
private TransactionDefinition definition; | |
private TransactionService service; | |
public NestedTransactionAdvice(TransactionDefinition definition, TransactionService service) { | |
this.definition = definition; | |
this.service = service; | |
} | |
public void advise(MethodInvocation invocation) { | |
try { | |
service.begin(definition); | |
invocation.proceed(); | |
service.commit(); | |
} catch (Exception e) { | |
service.rollback(); | |
throw e; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package es.com.blogspot.elblogdepicodev.plugintapestry.services.transaction; | |
import org.apache.tapestry5.plastic.MethodAdvice; | |
import org.apache.tapestry5.plastic.MethodInvocation; | |
public class MandatoryTransactionAdvice implements MethodAdvice { | |
private TransactionService service; | |
public MandatoryTransactionAdvice(TransactionService service) { | |
this.service = service; | |
} | |
public void advise(MethodInvocation invocation) { | |
if (!service.isWithinTransaction()) { | |
throw new RuntimeException("Debe haber una transacción"); | |
} | |
invocation.proceed(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package es.com.blogspot.elblogdepicodev.plugintapestry.services.transaction; | |
import org.apache.tapestry5.plastic.MethodAdvice; | |
import org.apache.tapestry5.plastic.MethodInvocation; | |
public class NeverTransactionAdvice implements MethodAdvice { | |
private TransactionService service; | |
public NeverTransactionAdvice(TransactionService service) { | |
this.service = service; | |
} | |
public void advise(MethodInvocation invocation) { | |
if (service.isWithinTransaction()) { | |
throw new RuntimeException("Hay una transacción activa y se require ninguna"); | |
} | |
invocation.proceed(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package es.com.blogspot.elblogdepicodev.plugintapestry.services.transaction; | |
import java.sql.Connection; | |
import java.sql.SQLException; | |
import java.util.Stack; | |
import org.apache.tapestry5.ioc.services.PerthreadManager; | |
import org.hibernate.Session; | |
import org.hibernate.Transaction; | |
import org.hibernate.jdbc.Work; | |
public class HibernateTransactionServiceImpl implements TransactionService { | |
private Session session; | |
private Stack<Transaction> transactionStack; | |
public HibernateTransactionServiceImpl(Session session, PerthreadManager perthreadManager) { | |
this.session = session; | |
this.transactionStack = new Stack<Transaction>(); | |
perthreadManager.addThreadCleanupCallback(new Runnable() { | |
@Override | |
public void run() { | |
cleanup(); | |
} | |
}); | |
} | |
public boolean beginIfNoPresent(TransactionDefinition definition) { | |
if (isWithinTransaction()) { | |
return false; | |
} | |
begin(definition); | |
return true; | |
} | |
public void begin(TransactionDefinition definition) { | |
Transaction transaction = session.beginTransaction(); | |
configure(session, transaction, definition); | |
transactionStack.push(transaction); | |
} | |
public void commit() { | |
if (isWithinTransaction()) { | |
transactionStack.pop().commit(); | |
} | |
} | |
public void rollback() { | |
if (isWithinTransaction()) { | |
transactionStack.pop().rollback(); | |
} | |
} | |
public boolean isWithinTransaction() { | |
return !transactionStack.empty(); | |
} | |
private void cleanup() { | |
for (Transaction transaction : transactionStack) { | |
transaction.rollback(); | |
} | |
} | |
private void configure(Session session, Transaction transaction, final TransactionDefinition definition) { | |
if (definition.getReadOnly() != null) { | |
session.setDefaultReadOnly(definition.getReadOnly()); | |
} | |
if (definition.getTimeout() != null) { | |
transaction.setTimeout(definition.getTimeout()); | |
} | |
session.doWork(new Work() { | |
public void execute(Connection connection) throws SQLException { | |
if (definition.getReadOnly() != null) { | |
connection.setReadOnly(definition.getReadOnly()); | |
} | |
if (definition.getIsolation() != null) { | |
connection.setTransactionIsolation(definition.getIsolation().intValue()); | |
} | |
} | |
}); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package es.com.blogspot.elblogdepicodev.plugintapestry.services.transaction; | |
import org.apache.tapestry5.hibernate.HibernateSessionManager; | |
import org.apache.tapestry5.hibernate.HibernateSessionSource; | |
import org.apache.tapestry5.ioc.services.PerthreadManager; | |
import org.hibernate.Session; | |
public class HibernateSessionManagerImpl implements HibernateSessionManager { | |
private Session session; | |
public HibernateSessionManagerImpl(HibernateSessionSource source, PerthreadManager manager) { | |
this.session = source.create(); | |
manager.addThreadCleanupCallback(new Runnable() { | |
@Override | |
public void run() { | |
cleanup(); | |
} | |
}); | |
} | |
public void abort() { | |
session.getTransaction().rollback(); | |
} | |
public void commit() { | |
session.getTransaction().commit(); | |
} | |
public Session getSession() { | |
return session; | |
} | |
private void cleanup() { | |
session.close(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package es.com.blogspot.elblogdepicodev.plugintapestry.services; | |
... | |
public class AppModule { | |
public static void bind(ServiceBinder binder) { | |
... | |
// Servicios para la gestión de transacciones | |
binder.bind(HibernateSessionManager.class, HibernateSessionManagerImpl.class).scope(ScopeConstants.PERTHREAD).withId("AppHibernateSessionManager"); | |
binder.bind(TransactionAdvisor.class, TransactionAdvisorImpl.class); | |
binder.bind(TransactionService.class, HibernateTransactionServiceImpl.class).scope(ScopeConstants.PERTHREAD); | |
... | |
} | |
public static void contributeServiceOverride(MappedConfiguration<Class,Object> configuration, @Local HibernateSessionManager sessionManager) { | |
configuration.add(HibernateSessionManager.class, sessionManager); | |
} | |
... | |
/** | |
* Dar soporte transaccional a los servicios con una interfaz que cumplan el patrón (los advices se aplican a los métodos de una interfaz). | |
*/ | |
@Match({ "*DAO" }) | |
public static void adviseTransaction(TransactionAdvisor advisor, MethodAdviceReceiver receiver) { | |
advisor.addAdvice(receiver); | |
} | |
} |
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$ git clone git://github.com/picodotdev/elblogdepicodev.git | |
$ cd elblogdepicodev/PlugInTapestry | |
$ ./gradlew tomcatRun | |
# Abrir en el navegador http://localhost:8080/PlugInTapestry/ |
Integración y transacciones con Spring en Apache Tapestry