Schedulare dei timer con Spring @Scheduled e Quartz

Vediamo come realizzare dei timer nella nostra applicazione sfruttando due diverse modalità richiamabili dalla nostra applicazione Spring.

@Scheduled

Utilizzando questa annotation possiamo accedere direttamente alle API messe a disposizione da Spring senza dover ricorrere a librerie di terze parti. L’annotation deve essere semplicemente messa sul metodo di un componente Spring che desideriamo richiamare in maniera schedulata e dobbiamo poi configurarla attraverso i diversi parametri che vengono messi a disposizione. Se vogliamo utilizzare una classica espressione crontab dobbiamo specificare il parametro cron. Ad esempio per richiamare un metodo ogni volta che cambia l’ora possiamo utilizzare il seguente codice

@Scheduled(cron = "0 0 * * * *")
public void segnaleOrario() {
    int ora=Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
    LOG.info("Sono le ore {}",ora);
}

Per avviare invece un metodo ogni intervallo di tempo fisso dobbiamo utilizzare il parametro fixedRate

@Scheduled(fixedRate = 15000)
public void avvioConIntervalloFisso() {
    LOG.info("Io vengo avviato ogni 15000 millisecondi");
}

Con il precedente esempio potrebbe succedere di avere dei metodi che vengono eseguiti in maniera concorrente, visto che l’esecuzione potrebbe durare più a lungo rispetto all’intervallo selezionato. Se questa cosa può crearci dei problemi possiamo optare per la configurazione dove invece definiamo l’intervallo di tempo che intercorre tra due diverse esecuzioni attraverso l’intervallo fixedDelay

@Scheduled(fixedDelay = 20000)
public void avvioConIntervalloFissoTraEsecuzioni() {
    LOG.info("Io vengo avviato con un intervallo di 20000 millisecondi rispetto alla precedente esecuzione");
}

Oltre a fixedDelay è possibile anche definire initialDelay che è l’intervallo di tempo che si attende inizialmente quando il bean viene creato

@Scheduled(fixedDelay = 30000, initialDelay = 1000)
public void avvioConIntervalloFissoERitardoIniziale() {
    LOG.info("Io vengo avviato con un intervallo di 30000 millisecondi rispetto alla precedente esecuzione ma con un ritardo iniziale di 1000 millisecondi");
}

L’annotation @Scheduled sotto in realtà maschera l’implementazione di Spring con TaskExecutor e TaskScheduler, che può essere utilizzata nel caso si volesse creare qualcosa di più raffinato e specifico. Per maggiori informazioni potete vedere la documentazione ufficiale su questa tematica

Spring Boot e Quartz

Quartz è una libreria opensource che permette di definire e gestire la schedulazione di job. In questo esempio vedremo come integrarlo con Spring eseguendo un semplice job, ma ovviamente Quartz rispetto al semplice meccanismo dell’annotation Scheduled permette svariate opzioni che possono essere utili nella definizione dei timer

1) Persistenza dei job
2) Transazioni
3) Clustering
4) Trigger e listener configurabili

Passiamo al nostro semplice esempio dove definiamo una classe MioJob che implementa l’interfaccia Job di Quartz

package com.javastaff.spring.scheduler;

import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.quartz.JobDetailFactoryBean;
import org.springframework.scheduling.quartz.SimpleTriggerFactoryBean;
import org.springframework.stereotype.Component;

@Component
@DisallowConcurrentExecution
public class MioJob implements Job {

    private static final Logger LOG = LoggerFactory.getLogger(MioJob.class);
    
    @Value("${quartz.miojob.intervallo}")
    private long intervallo;

    @Override
    public void execute(JobExecutionContext jobExecutionContext) {
        LOG.info("Sono un job lanciato con quartz ogni {} millisecondi"
                 , intervallo);
    }
    
    @Bean(name = "jobDetail")
    public JobDetailFactoryBean creaMioJobDetail() {
        return QuartzConfiguration.createJobDetail(this.getClass());
    }

    @Bean(name = "jobTrigger")
    public SimpleTriggerFactoryBean creaMioJobTrigger(
        @Qualifier("jobDetail") JobDetail jobDetail) {
        return QuartzConfiguration.createTrigger(jobDetail,intervallo);
    }
}

Il metodo che verrà schedulato periodicamente è execute e in base alla configurazione che abbiamo definito verrà eseguito ogni X secondi dove X è il valore della proprietà quartz.miojob.intervallo. Questa classe è il Job che verrà eseguito, però per agganciare questa classe all’infrastruttura di Quartz dobbiamo prima di tutto definire alcune informazioni essenziali che possiamo riportare nel file quartz.properties

org.quartz.scheduler.instanceName=spring-boot-quartz-javastaff
org.quartz.scheduler.instanceId=AUTO
org.quartz.threadPool.threadCount=5
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
org.quartz.jobStore.misfireThreshold=60000

In questo modo abbiamo definito che ci sarà un pool di 5 Thread (anche se nello specifico non ci serviranno) e il repository dei job sarà il RAMJobStore, ovvero che tutte le informazioni dei job saranno gestite in memoria. Quest’ultima configurazione è molto importante in quanto possiamo decidere di utilizzare un repository dei job che vada a memorizzare le informazioni relative all’esecuzione sul database. Utilizzando quindi un jobStore come JDBCJobStore avremo sempre memorizzato sul db quello che sta succedendo durante l’esecuzione dei nostri job, anche se l’applicazione viene riavviata.

Tornando al nostro esempio, visto che stiamo utilizzando Spring Boot, definiremo una classe di tipo @Configuration che ci servirà per creare diverse factory necessarie per inserire il nostro Job nel ciclo di vista di Quartz

package com.javastaff.spring.scheduler;

import java.io.IOException;
import java.util.List;
import java.util.Properties;
 
import org.quartz.JobDetail;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
import org.quartz.spi.JobFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.quartz.JobDetailFactoryBean;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.scheduling.quartz.SimpleTriggerFactoryBean;
 
@Configuration
public class QuartzConfiguration {
 
    @Autowired
    List<Trigger> listaTrigger;
    
    @Bean
    public JobFactory jobFactory(ApplicationContext applicationContext) {
        AutowiringSpringBeanJobFactory jobFactory = 
            new AutowiringSpringBeanJobFactory();
        jobFactory.setApplicationContext(applicationContext);
        return jobFactory;
    }
 
    @Bean
    public SchedulerFactoryBean schedulerFactoryBean(JobFactory jobFactory) throws IOException {
        SchedulerFactoryBean factory = new SchedulerFactoryBean();
        factory.setOverwriteExistingJobs(true);
        factory.setAutoStartup(true);
        factory.setJobFactory(jobFactory);
        factory.setQuartzProperties(quartzProperties());
        if (listaTrigger!=null && !listaTrigger.isEmpty()) {
            factory.setTriggers(listaTrigger.toArray(
               new Trigger[listaTrigger.size()]));
        }
        return factory;
    }
 
    @Bean
    public Properties quartzProperties() throws IOException {
        PropertiesFactoryBean propertiesFactoryBean = 
           new PropertiesFactoryBean();
        propertiesFactoryBean.setLocation(
           new ClassPathResource("/quartz.properties"));
        propertiesFactoryBean.afterPropertiesSet();
        return propertiesFactoryBean.getObject();
    }
 
    public static SimpleTriggerFactoryBean createTrigger(
          JobDetail jobDetail, long intervallo) {
        SimpleTriggerFactoryBean factoryBean = 
           new SimpleTriggerFactoryBean();
        factoryBean.setJobDetail(jobDetail);
        factoryBean.setStartDelay(0L);
        factoryBean.setRepeatInterval(intervallo);
        factoryBean.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY);
        return factoryBean;
    }
 
    public static JobDetailFactoryBean createJobDetail(Class jobClass) {
        JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();
        factoryBean.setJobClass(jobClass);
        return factoryBean;
    }
 
}

Nel progetto Github che segue potete trovare entrambi gli esempi utilizzati, lanciarli tramite la classe ApplicationStarter che mette in moto il contesto con Spring Boot e ci permette di verificare i due esempi

 

Federico Paparoni

Looking for a right "about me"...

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.