Nel post Primi Passi con Spring Batch abbiamo descritto un semplice esempio di job realizzato con Spring Batch. Nell’introduzione abbiamo anche detto che per la schedulazione dei job è necessario integrare un tool che si occupa dell’avvio temporizzato dei batch.
Nel post vediamo due tool di schedulazione: TaskScheduler di Spring e il più noto Quartz. Per farlo ci riferiremo al progetto descritto nel post sopra indicato, in cui è configurato un in memory database come repository, ed un semplice job di lettura e stampa a video.
TaskScheduler
TaskExecutor
e TaskScheduler
sono interfacce introdotte in Spring 3.0 per la schedulazione di batch. Seguono il modello architetturale degli executor presenti già nel JDK e descritti nel post Programmazione concorrente in java (parte 2). Essendo parte del core non è necessario introdurre altre dipendenze nel progetto.
La configurazione, come al solito, può avvenire tramite annotation o per mezzo dell’XML di contesto. Per omogeneità con il progetto utilizzeremo la seconda modalità. Introduciamo quindi bell’XML context.xml
il namespace e lo schema location:
1 2 3 4 5 6 7 8 9 |
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:task="http://www.springframework.org/schema/task" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.0.xsd"> .... </beans> |
Quindi configuriamo lo scheduler nell’XML utilizzando un bean helloWorldLauncher che definiremo nel seguito e che farà quello che nel progetto originale faceva il main()
.
1 2 3 |
<task:scheduled-tasks> <task:scheduled ref="helloWorldLauncher" method="run" fixed-delay="5000"/> </task:scheduled-tasks> |
<task:scheduled>
possiamo configurare, oltre al bean ed al metodo metodo da eseguire, le proprietà:
fixed-delay
: indica l’intervallo (in millisecondi) tra una esecuzione e la successiva;initial-delay
: indica il ritardo (in millisecondi) per la prima esecuzione del task;fixed-rate
: indica un intervallo (in millisecondi) tra due esecuzioni successive;cron
: consente di indicare una espressione cron.
La classe HelloWorldLauncher
si occupa dell’avvio vero e proprio del batch. Per farlo riceve dall’IoC container il Job
e il JobLauncher
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class HelloWorldLauncher { private JobLauncher jobLauncher; public void setJobLauncher(JobLauncher jobLauncher) { this.jobLauncher = jobLauncher; } private Job job; public void setJob(Job job) { this.job = job; } public void run() { try { JobParameters jobParameters = new JobParameters() JobExecution execution = jobLauncher.run(job, jobParameters ); System.out.println("Exit Status : " + execution.getStatus()); } catch (Exception e) { e.printStackTrace(); } } } |
context.xml
:
1 2 3 4 |
<bean id="helloWorldLauncher" class="it.javaboss.batch.HelloWorldLauncher"> <property name="jobLauncher" ref="jobLauncher"/> <property name="job" ref="helloWorldJob"/> </bean> |
main()
si limiterà a creare il contesto Spring:
1 2 |
String[] springConfig = { "spring/context.xml", "spring/job-hello-world.xml"}; ApplicationContext context = new ClassPathXmlApplicationContext(springConfig); |
Se proviamo ad avviare l’applicazione così com’è, alla secondo avvio del job, e per tutte le esecuzioni successive, otterremo l’eccezione:
A job instance already exists and is complete for parameters={}. If you want to run this job again, change the parameters.
Quello che accade è che il JobLauncher
di Spring si accorge che nel repository esiste già un job eseguito e completato sui parametri indicati, che nel nostro caso non sono valorizzati. Per ovviare al problema, e in considerazione del fatto che il nostro batch non ha parametri di esecuzione, è sufficiente costruire un parametro a partire dall’ora attuale, nel modo seguente:
1 2 3 |
JobParameters jobParameters = new JobParametersBuilder() .addLong("time",System.currentTimeMillis()) .toJobParameters(); |
Quartz Scheduler
Il framework Quartz è una libreria open source le la schedulazione dei job, che può essere integrata con una qualsiasi applicazione java. Per integrarlo con Spring introduciomone la dipendenza nel pom.xm
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<!-- QuartzJobBean in spring-context-support --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>${spring.version}</version> </dependency> <!-- Quartz framework --> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>${quartz.version}</version> </dependency> |
Prima di procedere introduciamo il concetto di JobRegistry
. Si tratta di un componente di Spring che semplicemente tiene traccia di tutti i job definiti nel contesto. La sola implementazione presente nel framework realizza tale scopo attraverso una semplice mappa tra il nome del job e la sua istanza. Per poter registrare i job nel registro deve essere definito un post processor che li inserisce al momento della sua definizione nel contesto. Introduciamo quindi nel context.xml
il seguente codice XML:
1 2 3 4 5 6 7 |
<bean id="jobRegistry" class="org.springframework.batch.core.configuration.support.MapJobRegistry" /> <bean class="org.springframework.batch.core.configuration.support.JobRegistryBeanPostProcessor"> <property name="jobRegistry" ref="jobRegistry" /> </bean> |
Per l’integrazione di Quartz in Spring il framework mette a disposizione l’interfaccia QuartzJobBean. Implementiamo quindi tale interfaccia per generare il bean che si occuperà di lanciare il job. Come nel caso precedente abbiamo bisogno di iniettare il JobLauncher
mentre per il recupero del job utilizziamo il repository appena definito. La classe HelloWorldJobLauncherDetails
avrà il seguente aspetto:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
public class HelloWorldJobLauncherDetails extends QuartzJobBean { private JobLocator jobLocator; public void setJobLocator(JobLocator jobLocator) { this.jobLocator = jobLocator; } private JobLauncher jobLauncher; public void setJobLauncher(JobLauncher jobLauncher) { this.jobLauncher = jobLauncher; } @Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { // Recupero i pafametri in <property name="jobDataAsMap"> Map<String, Object> jobDataMap = context.getMergedJobDataMap(); // Recupero il nome del job String jobName = (String) jobDataMap.get( "jobName" ); JobParameters jobParameters = new JobParametersBuilder() .addLong("time",System.currentTimeMillis()).toJobParameters(); try { // Recupero l'istanza del job dal repository JobExecution execution = jobLauncher.run(jobLocator.getJob(jobName), jobParameters); System.out.println("HelloWorldJobLauncherDetails::Exit Status : " + execution.getStatus()); } catch (JobExecutionAlreadyRunningException | JobRestartException | JobInstanceAlreadyCompleteException | JobParametersInvalidException | NoSuchJobException e) { e.printStackTrace(); } } } |
context.xml
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <property name="triggers"> <bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean"> <property name="jobDetail" ref="jobDetail" /> <property name="cronExpression" value="*/5 * * * * ?" /> </bean> </property> </bean> <bean id="jobDetail" class="org.springframework.scheduling.quartz.JobDetailBean"> <property name="jobClass" value="it.javaboss.batch.HelloWorldJobLauncherDetails" /> <property name="group" value="quartz-batch" /> <property name="jobDataAsMap"> <map> <entry key="jobName" value="helloWorldJob" /> <entry key="jobLocator" value-ref="jobRegistry" /> <entry key="jobLauncher" value-ref="jobLauncher" /> </map> </property> </bean> |
Codice Sorgente
Il codice sorgente è scaricabile qui spring-batch.