Cuando rediseñamos las distintas capas de logica de negocio y de prentacion, a menudo nos encontramos con la problematica de manejar tareas automatizadas o asincronas. Por ejemplo, el borrado de antiguas conversaciones de un foro de mensajes o tener que enviar e-mails de “newletter” a usuarios. Como vemos, la invocacion de estos servicios no depende directamente de la interacción de un usuario a través de un controlador. Asi que, como podemos realizar esta llamada? En el caso de envio masivo de e-mails. Vemos, que no es una buena practica enviar un solo e-mail con todos los destinatarios, ya que revelariamos las direcciones de correo al resto, si por otro lado cargasemos el campo BCC, esto suele marcar el correo como Spam o incluso peor, poner el servidor de correo dentro de una lista negra. Por lo que lo mas recomendable en un servicio de “newsletter”, es enviar un correo a cada destinatario, lo cual puede tener una duracion más larga de lo que pensamos. Para poder realizar esto, necesitamos ejecutar una tarea asincrona en segundo plano.
Spring ofrece mecanismos para planificar una tarea o bien ejecutar codigo de manera asincrona pero uno de los mayores cuellos de botella, reside en el manejo de los threads. En una aplicacion que se ejecuta dentro de un contenedor de Servlets, no se pueden lanzar uno o mas Threads cada vez que se necesite, sin que esto impacte el resto. Por ello, es importante gestionar el uso de Threads de manera que no castigue el rendimiento global. La parte que mas recursos consume es la creacion y abandono de threads, ya que puede causar problemas de falta de memoria, lo cual suele terminar bastante mal. Uno de los problemas que se suelen obviar es que la creacion y destruccion de threads implica un gran consumo de recursos, que afecta al rendimiento. Asi que una buena practica consiste en el uso de un Pool de Threads que reutilize dichos threads en lugar de crearlos y destruirlos y que ademas los planifica, poniendo en cola procesos, cuando no se dispone de suficiente CPU.
Spring, por su parte, incluye dicha gestion de Threads para ello aporta las anotaciones:
@org.springframework.scheduling.annotation.Async y @org.springframework.scheduling.annotation.Scheduled para indicar que un metodo a de ejecutarse de manera asincrona y de forma planificada respectivamente.
Esto no es tan sencillo como aplicar dichas anotaciones a los metodos que deseemos ejecutar de esta manera, sino que es necesaria alguna configuracion previa. Lo primero que necesitamos es activar estas caracteristicas. Por defecto los metodos anotados como @Async o como @Scheduled, no usan el mismo Pool de threads, pero para este caso, si es lo deseado, ya que de esta manera, podemos usar los recursos de nuestra aplicacion de la forma mas eficiente (estamos dentro del mismo contenedor).
Para hacer esto, vamos a explicar de forma breve, que hay debajo de todo esto:
En el Framework Spring existen dos conceptos parecidos “executors” y “schedulers”. Una clase “Executor” hace lo que intuitivamente se entiende, ejecutar una tarea. En sus requerimientos no se especifica si esto ha de ser de forma sincrona o asincrona, esto depende de sus diferentes implementaciones, que especifican como ha de ejecutarse.
La interfaz java.util.concurrent.Executor especifica que ha de ejecutar un Runnable.
Spring extiende este interfaz con org.springframework.core.task.TaskExecutor
Spring aporta la interfaz org.springframework.scheduling.TaskScheduler que incluye distintos metodos para planificar una tarea una o mas veces y en un punto del futuro
Existen varias implementaciones de estos interfaces, y la mayoria incluyen ambos. El mas usado es “org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler”, que comprende ambas caracteristicas de “executor” y de “scheduler” y un pool de threads, para la ejecucion de manera ordenada y eficiente. Cuando nuestra aplicacion se cierra, esta clase asegura de que todos los threads que se crearon se cierran de forma eficiente, para evitar problemas de memoria y otros. Esta clase implementa ademas la interfaz “java.util.concurrent.ThreadFactory”. Gracias a esto, podemos declarar un bean “ThreadPoolTaskScheduler”, y complementar cualquier dependencia con las interfaces “Executor” , “TaskExecutor” , “TaskScheduler” , o “ThreadFactory” . La configuracion se vuelve mas facil de configurar como veremos a continuacion.
Para poder anotar metodos que se ejecutaran de forma asincrona con @Async, es necesario que en @Configuration indiquemos @EnableAsync. De la misma manera para poder ejecutar tareas planificadas con @Scheduled, debemos indicar @EnableScheduling. Podemos hacerlo en nuestra clase principal, para que dicha configuracion este disponible a todos los Beans de la aplicacion. Sin embargo, estas anotaciones, tienen una configuracion por defecto, que es posible que necesitemos adaptar. Para poder hacer esto, debemos implementar la interfaz AsyncConfigurer que devuelva el async Executor deseado e implementar la clase “SchedulingConfigurer” para asignar el dicho Executor al objeto de la clase Scheduler.
@Configuration @EnableAsync(proxyTargetClass = true) @EnableScheduling ... public class RootContextConfiguration implements AsyncConfigurer, SchedulingConfigurer { private static final Logger log = LogManager.getLogger(); private static final Logger schedulingLogger = LogManager.getLogger(log.getName() + ".[scheduling]"); ... @Bean public ThreadPoolTaskScheduler taskScheduler() { log.info("Setting up thread pool task scheduler with 20 threads.");406 ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(20); scheduler.setThreadNamePrefix("task-"); scheduler.setAwaitTerminationSeconds(60); scheduler.setWaitForTasksToCompleteOnShutdown(true); scheduler.setErrorHandler(t -> schedulingLogger.error( "Unknown error occurred while executing task.", t )); scheduler.setRejectedExecutionHandler( (r, e) -> schedulingLogger.error( "Execution of task {} was rejected for unknown reasons.",r ) ); return scheduler; } @Override public Executor getAsyncExecutor() { Executor executor = this.taskScheduler(); log.info("Configuring asynchronous method executor {}.", executor); return executor; } @Override public void configureTasks(ScheduledTaskRegistrar registrar) { TaskScheduler scheduler = this.taskScheduler(); log.info("Configuring scheduled method executor {}.", scheduler); registrar.setTaskScheduler(scheduler); } }
Solo mostramos el nuevo codigo añadido, el atributo “proxyTargetClass” dentro de la anotacion@EnableAsync le indica a Spring que debe usar la libreria CGLIB para aplicar mediante ella proxy a las clases con metodos anotados como async o scheduled en lugar de tener que crear interfaces Java. Esto permite declarar metodos async en beans que incluso no implementan dicho interfaz. Si pusiesemos el atributo a “false”, restringiriamos el uso de metodos async y scheduled a aquellos que implementan la interfaz. El metodo de creacion de @Bean muestra scheduler como un bean mas que puede ser inyectado en cualquier otro. El metodo “getAsyncExecutor” (de la interfaz “AsyncConfigurer” ) le indica a Spring que use la misma planificacion para los metodos asincronos, y el metodo “configureTasks” (de “SchedulingConfigurer” ), le indica a Spring que use el mismo planificador para todos los metodos de ejecucion scheduled.
La manera como funciona esto es que “getAsyncExecutor” y “configureTasks” invocan a “taskScheduler” , no se tratra de dos TaskScheduler distintos? E incluso un tercero cuando Spring invoca el metodo anotado con @Bean? En realidad, solamente se invoca una instancia de “TaskScheduler”. Los proxies Spring invocan los metodos @Bean de manera que no se crea mas de una vez. Como resultado de la primera invocacion a @Bean, se cachea de manera que se usa en llamadas futuras. Esto permite que varios metodos de la configuracion invoquen metodos anotados con @Bean sin que esto implique la creacion del mismo. Asi que solo se instancia a un TaskScheduler en la configuracion, la que se indica en la declaracion del bean y se usara por los metodos “getAsyncExecutor” y “configureTasks”. Es posible verlo en los logs dentro de cada llamada.
El framework Spring soporta la ejecucion de metodos @Async “wrapeando” dichos bean en un proxy.
Cuando Spring inyecta un bean con metodos @Async dentro de otro que depende de este, lo que hace es inyectar el proxy, no el propio bean. Por lo que el bean, invoca la llamada del metodo del proxy. Cuando se trata de metodos “no asincronos”, lo que hace el proxy es unicamente delegar en el metodo del bean. Pero en metodos anotados @Async o @javax.ejb.Asynchronous , el proxy indica al “executor” que debe ejecutar dicho metodo y termina inmediatamente. Este funcionamiento tiene una consecuencia importante: Si un bean invoca a uno de sus propios metodos anotado con @Async, dicho metodo no se ejecutara de forma asincrona, ya que no existe un proxy de esta manera.
Asi que si, lo que queremos es que un metodo se ejecute de manera asincrona, debemos invocarlo desde otro bean.
Nota: En realidad esto no es una regla fija. Esto sucede cuando aplicamos proxies basandonos en interfaces Java, es entonces cuando no podemos ejecutar dicho metodo desde el mismo objeto. Sin embargo, con proxies que aporta CGLIB, podemos crear un proxy sobrecargando cada metodo de la clase original. Asi es como funciona por ejemplo, el cacheo del metodo anotado con @Bean – Spring aplica dicha tecnica de de proxy a las clases anotadas con @Configuration. Al estar habilitada la libreria CGLIB para proxy desde la configuracion si es posible invocar los metodos anotados con @Async dentro de la misma clase, y se ejecutaran de forma asincrona. Sin embargo, no es una buena practica, ya que un cambio en la configuracion puede desmontar todo el funcionamiento.
Para mostrar el funcionamiento, veremos como los metodos se marcan @Async tanto en la interfaz como en la implementacion y aunque esto no es estrictamente requerido, es una buena practica, cuando lo que se pretende es que el funcionamiento sea siempre asincrono de manera que se indica al resto de consumidores acerca de esto.
... public interface ClockAlarmService { @Async void sendClockAlarm(String subject, String message, Date date, Collectionrecipients); } @Service public class FakeClockAlarmService implements ClockAlarmService { private static final Logger log = LogManager.getLogger(); @Override @Async public void sendClockAlarm(String subject, String message, Date date, Collection recipients) { log.info("Envio de Alarma a los recipients {}.", recipients); try { Thread.sleep(5_000L); } catch (InterruptedException ignore) { } log.info("Fin envio alarma a recipients."); } }
El servicio DefaultReplyService recoge una instancia @Inject de ClockAlarmService y, en pocas lineas de codigo, llama al metodo asincrono saveReply si se trata de una nueva reply .
... Setrecipients = new HashSet<>(hilo.getSubscribedUsers()); recipients.remove(reply.getUser()); Date d = new Date(); this.clockAlarmService.sendClockAlarm("Reply posted", "Someone replied to \"" + hilo.getSubject()+ ".\"", d , recipients);
Crear metodos de tipo @Scheduled no cambia mucho respecto de metodos tipo @Async. Lo que necesitamos hacer es escribir el metodo y anotarlo de esta manera. La parte mas importante respecto de anotarlo @Scheduled es que dicho metodo no puede contener parametros en su signatura, (como puede determinar Spring que argumentos usar?), esto no implica que no podamos realizar una llamada a un metodo @Scheduled directa.
Es posible realizar dicha llamada directamente, aunque no deberiamos, incluso es posible anotar un metodo @Scheduled tambien con @Async de manera que se ejecute de manera asincrona si es llamado directamente.
La ejecucion planificada, del ejemplo, borra aquellas conversaciones almacenadas que no se han modificado en mas de un año. Para esto, necesitamos realizar algunos trucos respecto de los “repositorios”, de manera que permitan el borrado. Dentro de “ReplyRepostory” y su implementacion, añadimos el metodo “deleteHiloById”