Spring Batch è un framework, rilasciato da Spring, finalizzato alla implementazione di applicazioni batch per l’elaborazione di grandi volumi di dati, senza alcuna interazione con l’utente. La figura seguente mostra la struttura di una tipica applicazione batch.
Le principali caratteristiche offerte dal framework sono:
- Supporto a sorgenti di differente tipo: database, flat file (xml, csv, etc.), code JMS, JPA, MongoDB, Neo4J, etc;
- Gestione automatica dei retry e failure;
- Utilizzo di un linguaggio per la definizione dei job;
- Tracciamento della stato di esecuzione dei batch attivi e completati e generazione di statistiche;
- Supporto a differenti modalità di avvio dei job: script; http, message, etc;
- Supporto all’esecuzione di job concorrenti;
- Servizi di logging, gestione delle risorse, skip e riavvio delle esecuzioni;
- Scomposizione di grandi quantità di dati e processamento parallelo.
Si noti che Spring Batch non è un framework di schedulazione, ma lavorapuò essere integrato con tali framework, ad esempio con Quartz.
Componenti
La figura seguente mostra le componenti che caratterizzano Spring Batch e le relazioni tra le stesse. Nel seguito sono brevemente descritte.
Il JobLauncher
è la componente responsabile dello start dei job. Ad ogni richiesta di avvio verifica nel JobRepository
che il job non sia stato già eseguito e la validità dei parametri. Il JobRepository
è responsabile quindi della conservazione delle informazioni di esecuzione attraverso un repository che può essere in memory o su DB dedicato.
Job
è ovviamente la descrizione del processo che deve essere eseguito, mentre la JobInstance
è la sua istanziazione sui JobParameter
, ovvero sui parametri utilizzati dal job. La singola esecuzione del job è rappresentata da una JobExecution
che contiene informazioni sull’esecuzione quali: stato, data ed ora di avvio e conclusione, eventuale eccezione, etc. Poiché una JobInstance
può essere eseguita più volte ad essa potranno essere associate più JobExecution
.
Il Job
è suddiviso in una sequenza di Step
. Ogni step consiste di tre attività: data reading, processing e data writing. Spring Batch fornisce specifiche interfacce per l’implementazione di tali attività, rispettivamente ItemReader
, ItemProcessor
e ItemWriter
.
La singola esecuzione di uno step è rappresentata da un oggetto StepExecution
che contiene tutte le informazioni di esecuzione: stato, data ed ora di avvio e completamento, numero di letture, scritture e commit, etc.
Spring Batch utilizza un meccanismo di elaborazione a chunk (blocchi). Questo implica che i dati vengono letti e processati a blocchi utilizzando l’ItemReader e l’ItemProcessor, quindi vengono aggregati e solamente quando è raggiunto l’intervallo di commit richiesto, l’intero blocco è passato all’ItemWriter e la transazione è committata.
Il Progetto di Esempio
Per la realizzazione del nostro primo progetto utilizziamo Maven generando un progetto base senza selezionare alcun archetype. Quindi definiamo le proprietà del progetto come segue:
- groupId:
it.javaboss
- artifactId:
spring-batch
Apriamo quindi il pom.xml
ed inseriamo le dipendenze necessaria a spring bath. L’aspetto del ile sarà:
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>it.javaboss</groupId> <artifactId>spring-batch</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <jdk.version>1.8</jdk.version> <spring.version>3.2.2.RELEASE</spring.version> <spring.batch.version>2.2.0.RELEASE</spring.batch.version> </properties> <dependencies> <!-- Spring Core --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${spring.version}</version> </dependency> <!-- Spting EL --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-expression</artifactId> <version>${spring.version}</version> </dependency> <!-- Spting tx --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>${spring.version}</version> </dependency> <!-- Spring Batch dependencies --> <dependency> <groupId>org.springframework.batch</groupId> <artifactId>spring-batch-core</artifactId> <version>${spring.batch.version}</version> </dependency> <dependency> <groupId>org.springframework.batch</groupId> <artifactId>spring-batch-infrastructure</artifactId> <version>${spring.batch.version}</version> </dependency> </dependencies> <build> ... </build> </project> |
Come primo esempio consideriamo un job che si compone di un solo step. Come detto precedentemente lo step sarà caratterizzato dalle attività di read, process e write specificate attraverso tre bean. La definizione del batch è fatta mediante file XML. Nel nostro esempio il file job-hello-world.xml
sarà:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:batch="http://www.springframework.org/schema/batch" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch-2.2.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd"> <bean id="helloItemreader" class="it.javaboss.batch.HelloItemReader"/> <bean id="helloItemProcessor" class="it.javaboss.batch.HelloItemProcessor"/> <bean id="helloItemWroter" class="it.javaboss.batch.HelloItemWriter"/> <batch:job id="helloWorldJob"> <batch:step id="step1"> <batch:tasklet> <batch:chunk reader="helloItemreader" processor="helloItemProcessor" writer="helloItemWroter" commit-interval="3"/> </batch:tasklet> </batch:step> </batch:job> </beans> |
job
, lo step
ed il chunk
. Nel file sono inoltre definiti i bean associati chunk che riportiamo nel seguito.
HelloItemReader
restituisce nomi propri di persone leggendoli da un array di costanti:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class HelloItemReader implements ItemReader<String>{ private final String[] names = new String[]{"Massimo","Alberto","Paolo","Giovanni","Filippo"}; private int index = 0; public String read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException { System.out.println( "Reading next item." ); if ( index < names.length ) { return names[index++]; } else { return null; // Indica al motore che gli item sono terminati } } } |
HelloItemProcessor
aggiunge la stringa “Hello
” al nome passato come parametro.
1 2 3 4 5 6 7 8 |
public class HelloItemProcessor implements ItemProcessor<String,String> { public String process(String name) throws Exception { System.out.println( "Processing item." ); return "Hello " + name + "!"; } } |
HelloItemWriter
stampa sullo standard output la stringa creata da HelloItemProcessor.
1 2 3 4 5 6 7 8 9 10 11 |
public class HelloItemWriter implements ItemWriter<String> { @Override public void write(List<? extends String> items) throws Exception { System.out.println( "Writing " + items.size() + " items." ); for ( String item : items ) { System.out.println( item ); } } } |
Per completare il progetto abbiamo bisogno di un file XML di contesto in cui sono definiti i bean per JobRepository
e JobLauncher
. In particolare il repository è conservato in memoria quindi ad ogni riavvio le informazioni di esecuzione sono perse.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd"> <!-- stored job-meta in memory --> <bean id="jobRepository" class="org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean"> <property name="transactionManager" ref="transactionManager" /> </bean> <bean id="transactionManager" class="org.springframework.batch.support.transaction.ResourcelessTransactionManager" /> <bean id="jobLauncher" class="org.springframework.batch.core.launch.support.SimpleJobLauncher"> <property name="jobRepository" ref="jobRepository" /> </bean> </beans> |
main()
che avvia il job
utilizzando il bean jobLauncher
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class App { public static void main(String[] args) { String[] springConfig = { "spring/context.xml","spring/job-hello-world.xml"}; ApplicationContext context = new ClassPathXmlApplicationContext(springConfig); JobLauncher jobLauncher = (JobLauncher) context.getBean("jobLauncher"); Job job = (Job) context.getBean("helloWorldJob"); try { JobExecution execution = jobLauncher.run(job, new JobParameters()); System.out.println("Exit Status : " + execution.getStatus()); } catch (Exception e) { e.printStackTrace(); } System.out.println("Done"); } } |
Esecuzione del Progetto
Una volta avviato il main()
l’output generato da tutte le System.out
, ripulito di eventuali messaggi di Spring, è:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
Reading next item. Reading next item. Reading next item. Processing item. Processing item. Processing item. Writing 3 items. Hello Massimo! Hello Alberto! Hello Paolo! Reading next item. Reading next item. Reading next item. Processing item. Processing item. Writing 2 items. Hello Giovanni! Hello Filippo! |
Come ci aspettavamo avendo indicato il commit-interval a 3 ed dovendo elaborare 5 nomi (item), si ha:
- un primo chunk in cui vengono letti 3 nomi (dal reader) che sono poi passati in sequenza al processor. I tre nomi elaborati sono quindi inviati al writer che li stampa;
- un secondo chunk dove sono letti i restanti due nomi e, come si vede al terzo tentativo il reader restituisce NULL, di fatto bloccando il batch. I due nomi restanti sono processati e stampati come da specifica.
Codie Sorgente
Il codice sorgente del progetto è disponibile qui spring-batch.