La solución implica usar el mecanismo de sincronización de transacciones de Spring para ejecutar código después del commmit de la transacción, combinado con un enfoque asíncrono como un pool de hilos o una cola de mensajes (MQ).
Ejecución post-commit con TransactionSynchronizationManager
Spring proporciona la clase TransactionSynchronizationManager para registrar callbacks que se ejecutan en eventos del ciclo de vida de la transacción. Un enfoque básico es implementar TransactionSynchronizationAdapter y sobrescribir el método afterCommit().
public void registrarLibro(LibroTecnico libro) {
repositorioLibros.insertar(libro);
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
// Lógica post-commit, como enviar un correo electrónico.
servicioNotificaciones.enviarCorreo(libro);
}
});
// Simulación de error aleatorio para probar la transacción.
GeneradorAleatorio generador = new GeneradorAleatorio();
if (generador.siguienteEntero() % 2 == 0) {
throw new ExcepcionAplicacion("Error en la transacción de prueba");
}
}
Asincronización del proceso post-commit
Para evitar bloqueos, la operación post-commit debe ejecutarse de forma asíncrona. Esto se puede lograr mediante un pool de hilos o integrando con un sistema de mensajería como RabbitMQ o Kafka.
private final ExecutorService poolHilos = Executors.newFixedThreadPool(5);
public void registrarLibro(LibroTecnico libro) {
repositorioLibros.insertar(libro);
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
poolHilos.submit(() -> {
// Tarea asíncrona, por ejemplo, enviar notificación.
servicioNotificaciones.enviarCorreo(libro);
// Simular trabajo prolongado.
Thread.sleep(5000);
});
}
});
GeneradorAleatorio generador = new GeneradorAleatorio();
if (generador.siguienteEntero() % 2 == 0) {
throw new ExcepcionAplicacion("Error en la transacción de prueba");
}
}
Encapsulamiento de la lógica post-commit
Para reducir la duplicación de código, se puede crear un componente reutilizable que ejecute tareas después del commit de la transacción. Esto involve registrar sincronizaciones de forma centralizada.
public interface EjecutorPostCommit extends Executor {}
@Component
public class EjecutorPostCommitImpl extends TransactionSynchronizationAdapter implements EjecutorPostCommit {
private static final Logger REGISTRO = LoggerFactory.getLogger(EjecutorPostCommitImpl.class);
private static final ThreadLocal<list :="" activa.="" aftercommit="" aftercompletion="" arraylist="" ejecutar="" estado="" execute="" executorservice="" for="" hay="" if="" inmediatamente="" list="" listatareas="TAREAS.get();" listatareas.add="" no="" null="" poolhilos="Executors.newFixedThreadPool(5);" poolhilos.execute="" private="" public="" return="" si="" tarea="" tarea.run="" tareas="new" tareas.remove="" tareas.set="" threadlocal="" transacci="" transactionsynchronizationmanager.registersynchronization="" void=""></list>
Uso del ejecutor encapsulado:
@Autowired
private EjecutorPostCommit ejecutorPostCommit;
public void registrarLibro(LibroTecnico libro) {
repositorioLibros.insertar(libro);
ejecutorPostCommit.execute(() -> servicioNotificaciones.enviarCorreo(libro));
GeneradorAleatorio generador = new GeneradorAleatorio();
if (generador.siguienteEntero() % 2 == 0) {
throw new ExcepcionAplicacion("Error en la transacción de prueba");
}
}
Consideraciones sobre la anotación @Async de Spring
Spring ofrece la anotación @Async para simplificar la ejecución asíncrona. Sin embargo, tiene limitaciones en el contexto de transacciones:
- La configuración debe estar en el contexto raíz de la aplicación para evitar conflictos con el contexto web.
- Los métodos
@Asyncno pueden ser invocados desde la misma clase, ya que la proxy de Spring no interceptará la llamada. - Dentro de un método
@Async, el uso deTransactionSynchronizationManagerpuede no funcionar correctamente, ya que se ejecuta en un hilo diferente con su propio contexto.
Ejemplo de configuración XML para @Async:
<context:component-scan base-package="com.ejemplo" />
<task:executor id="ejecutorTareas" pool-size="5"/>
<task:annotation-driven executor="ejecutorTareas" />
Es crucial asegurar que el escaneo de componentes en el contexto web no sobrescriba esta configuración.