Primi passi con Spring Batch

Realizzando applicazioni business-to-business non è raro incontrare la necessità di fare movimentazione massiva di dati in modo automatico, schedulato e fail-safe. Un bel batch quindi è quello che ci vuole! 🙂 Ovviamente si può fare in mille modi, usare mille framework differenti se necessario, schedularli tramite Cron per esempio o Quartz e così via.

Ultimamente mi è capitato di dover realizzare un batch di migrazione dati da una versione X di un applicativo ad una Y. Viste le combinazioni che si possono fare al varieare di X e Y, è necessario che questo batch sia configurabile per accoppiare il giusto “lettore” della versione X allo “scrittore” della versione Y. Mi è venuto così in mente di prendere in considerazione Spring Batch

Perché Spring Batch?

La risposta a questa domanda potrebbe essere fatta con un’altra: perché innanzitutto NON Spring? E’ così comodo lavorare in un contesto di iniezione delle dipendenze, perché farne a meno anche in un batch? Con gli archetipi Maven per Spring è così semplice mettere in piedi il progetto in pochi click. Spring mi offre tutta la flessibilità nella configurazione che mi è necessaria con un po’ di xml.

Questo post non vuole essere un tutorial per Spring Batch: quello ufficiale infatti è fatto molto bene. Volevo semplicemente fare una panoramica degli elementi fondamentali che mi sono serviti per cominciare a capire come funziona il framework. La documentazione infatti è molto prolissa e spiega molte cose nel dettaglio, il che non guasta ma si sa, troppi dettagli spesso creano confusione: è il collegamento tra i dettagli che invece crea informazione (questa non è mia, l’ho riciclata 😉 )

L’archetipo Maven per Spring Batch

Tra tutti i modi con cui può essere usato Spring Batch, al momento a me interessa solo l’esecuzione secca da linea di comando. Alternativamente infatti potrei creare un batch che si ripete periodicamente o che si riavvia da solo in caso di fallimento.

Cominciamo quindi con l’installare gli Spring Tools Suite (STS) in versione standalone o dal Marketplace di Eclipse.

Una volta installato, creiamo un nuovo progetto Spring:

spring_project

e scegliamo come templare un “Simple Spring Batch Project”

spring_batch_template

Come risultato, avremo un progetto Maven con la struttura già pronta necessaria a Spring Batch. Diamo un occhio alle cartelle src/main/resources e src/main/java:

spring_batch_project

La prima contiene i file di configurazione del progetto (che vedremo a breve), la seconda invece implementa tre classi di esempio necessarie per far funzionare il batch. I nomi delle classi ExampleItemReader e ExampleItemWriter sono piuttosto eloquenti, ma cosa sono gli ItemReader e gli ItemWriter?? Facciamo un breve salto alla teoria.

L’archiettura di un batch secondo Spring

L’architettura che dobbiamo avere in mente se vogliamo usare questo framework è quella degli ETL, ovvero Extract, Transform and Load. Uno dei primi schemi mostrati nella documentazione ufficiale infatti è il seguente:

spring-batch-reference-model

Ovvero che un batch è definito come un Job (gestito da un repository), composto da n Step, ognuno dei quali esegue un processo di Lettura (Elaboration => ItemReader), Processazione (Transformation => ItemProcess) e Scrittura (Load => ItemWriter).
Ovviamente questo è il caso più generico: infatti possiamo avere step senza processazione oppure degli step che sono solo processazione, per esempio, e non hanno fasi di lettura e scrittura.

Un occhio ai colori dello schema: tutti i componenti in celeste sono già disponibili nel framework, l’unica cosa che dobbiamo fare è configurarli. Le parti in giallo invece sono il vero codice che andremo ad implementare.
Il primo vantaggio che credo balzi all’occhio immediatamente è che chi sviluppa può subito concentrarsi sul core del batch, senza perdere tempo a creare tutti gli elementi “accessori” necessari a farlo girare.

La configurazione

Torniamo quindi sulla terra e guardiamo com’è strutturata la configurazione del batch. Per chi conosce Spring, sa che la configurazione dei suoi bean può essere fatta tramite annotations che file xml, e in questo caso li troviamo entrambi. Partiamo dal file launch-context.xml. Contiene poche righe xml piuttosto chiare:

<context:property-placeholder location="classpath:batch.properties" />
<context:component-scan base-package="it.cosenonjaviste.batch" />
<jdbc:initialize-database data-source="dataSource">
   <jdbc:script location="${batch.schema.script}" />
</jdbc:initialize-database>
<batch:job-repository id="jobRepository" />
<import resource="classpath:/META-INF/spring/module-context.xml" />

  • viene caricato il file delle properties.
  • si attiva la scansione delle annotation per il package indicato.
  • si inizializza un database locale. Spring Batch infatti tiene traccia su un db di tutte le volte che i batch vengono lanciati, degli esiti e così via in modo da poterne fare delle statistiche. Di default, il database è di tipo “in memory”, quindi distrutto ogni volta che si riavvia un batch. E’ possibile cambiarne la tipologia dal file batch.properties. Il data source di nome dataSource, non compare nell’xml, ma è creato dal bean di configurazione ExampleConfiguration.
  • si definisce il repository che gestirà tutti i job configurati nel file module-context.xml, incluso alla fine del file.

Questo file probabilmente non cambierà mai: quello invece a cui metteremo mani è module-context.xml perché definisce la struttura del nostro Job.

<batch:job id="job1">
   <batch:step id="step1">
      <batch:tasklet transaction-manager="transactionManager" start-limit="100" >
         <batch:chunk reader="reader" writer="writer" commit-interval="1" />
      </batch:tasklet>
   </batch:step>
</batch:job> 

identificato da un id e composto da un singolo step che consiste in una lettura (reader) e una scrittura (writer) di un item alla volta perché il commit-interval è impostato ad 1. Questo parametro praticamente definisce il buffer degli item letti e processati che poi vengono passati tutti insieme al writer. Tutto ciò che avviene tra un commit interval e un altro è transazionale. Per capire meglio, diamo un’occhiata a come sono fatte le interfacce ItemReader e ItemWriter:

public interface ItemReader<T> {
   T read();
}

public interface ItemWriter<T> {
   void write(List<? extends T> items);
}

Il framwork chiama tante volte read quante volte specificato dal commit interval: ogni chiamata deve restituire l’elemento di tipo T successivo al corrente. Una volta “bufferizzati” gli item, vengono passati alla lista del writer. A questo punto viene invocato nuovamente il metodo read e il ciclo ricomincia. Se il metodo rende null, stiamo dicendo al framework che non ci sono più elementi da leggere: gli items letti vengono passati writer e il batch termina.

Tra il reader e il witer possiamo aver bisogno di un processor per elaborare e adattare gli elementi letti al formato della destinazione. L’interfaccia del processor è la seguente:

public interface ItemProcessor<I, O> {
   O process(I item);
}

L’item di tipo I proviene dal reader mentre l’output di tipo O è quello che verrà passato al writer. Nulla ovviamente vieta che i tipi rimangano gli stessi. Se il process rende null, l’item verrà scartato e non verrà passato al writer.

Meno chiacchiere, più codice

Prima di modificare l’archetipo, vediamo se tutto funziona correttamente. Come si lancia da riga di comando? Spring Batch offre la classe CommandLineJobRunner con un metodo main che possiamo sfruttare per lanciare i batch senza preoccuparci di altro: basta passare come argomenti:

  • il nome del file di configurazione principale
  • il nome del job da eseguire

Lanciamo quindi la classe come Java Application direttamente da Eclipse:

CommandLineJobRunner main

CommandLineJobRunner arguments

Tra le mille cose che vengono scritte in console, se troviamo “Hello world” significa che il batch ha girato correttamente.

Per capire però meglio quello che accade, modifichiamo il codice generato dall’archetipo come segue, partendo dal reader

@Component("reader")
public class ExampleItemReader implements ItemReader<String> {
   
   private static final Log log = LogFactory.getLog(ExampleItemReader.class);
   
   private String[] input = {"Hello world!", "Ciao mondo!", "Hola mundo!"};
   
   private int index = 0;
   
   /**
    * Reads next record from input
    */
   public String read() throws Exception {
      if (index < input.length) {
         log.info("Leggo l'item " + input[index]);
         return input[index++];
      }
      else {
         log.info("Non ci sono più elementi da leggere");
         return null;
      }
   }
}

Lasciamo il writer com’è e creiamo un processor che trasforma il tipo da stringa a oggetto ed esclude l’elemento di lingua italiana:
@Component("processor")
public class ExampleItemProcessor implements ItemProcessor<String, Object> {

   private static final Log log = LogFactory.getLog(ExampleItemProcessor.class);
   
   @Override
   public Object process(String item) throws Exception {
      log.info("Processo l'item " + item);
      if (!item.equals("Ciao mondo!")) {
         return item;
      } else {
         return null;
      }
   }
}

ricordiamoci di aggiungere il processor al nostro job e lasciamo il commit interval a 1 al momento:

<batch:chunk reader="reader" processor="processor" writer="writer" commit-interval="1" />

Lanciando il batch, l’output che si ottiene (tra tutto quello che scrive Spring Batch) è:

ExampleItemReader - Leggo l'item Hello world!
ExampleItemProcessor - Processo l'item Hello world!
ExampleItemWriter - [Hello world!]

ExampleItemReader - Leggo l'item Ciao mondo!
ExampleItemProcessor - Processo l'item Ciao mondo!
ExampleItemWriter - []

ExampleItemReader - Leggo l'item Hola mundo!
ExampleItemProcessor - Processo l'item Hola mundo!
ExampleItemWriter - [Hola mundo!]

ExampleItemReader - Non ci sono più elementi da leggere

Con il commit interval impostato ad 1, il processo è sempre reader => processor => writer. Nel secondo caso, la lista arrivata al writer è vuota perché il processor l’ha filtrata rendendo null. Alla fine, il reader rende null e la catena di chiamate si interrompe.

Cambiando il commit-interval a 2, otteniamo quanto segue:

ExampleItemReader - Leggo l'item Hello world!
ExampleItemReader - Leggo l'item Ciao mondo!
ExampleItemProcessor - Processo l'item Hello world!
ExampleItemProcessor - Processo l'item Ciao mondo!
ExampleItemWriter - [Hello world!]

ExampleItemReader - Leggo l'item Hola mundo!
ExampleItemReader - Non ci sono più elementi da leggere
ExampleItemProcessor - Processo l'item Hola mundo!
ExampleItemWriter - [Hola mundo!]

Viene quindi chiamato il reader 2 volte, poi il processor sempre 2 volte e infine tutti gli item (non filtrati dal processor come “Ciamo mondo!”) vengono passati al writer.
Quindi il commit-interval, nonostante sembri un innocuo parametro, in realtà regola tutto il ciclo di lettura/scrittura del batch dividendo gli item in “chunk”, ovvero in blocchi.

Batch standalone

Lanciando il batch fuori da Eclipse ho avuto però una spiacevole sorpresa: praticamente i namespace dei file xml di Spring come context, batch, tx… non vengono risolti e l’avvio schianta. Per aggirare il problema, ho dovuto riportare nel progetto le dichiarazioni degli handler di quelli che mi servivano. Come? Aprendo un jar di Spring possiamo trovare 2 file nella cartella META-INF:

  • spring.handlers
  • spring.schemas

E’ necessario quindi ricreare questi file nella cartella META-INF del nostro progetto, riportando tutte le dichiarazioni presenti nei jar dei namespace di nostro interesse! Come soluzione è poco ortodossa, ma non ho trovato di meglio! Se qualcuno ha una soluzione migliore mi faccia sapere 😉

Andrea Como

Sono un software engineer focalizzato nella progettazione e sviluppo di applicazioni web in Java. Presso OmniaGroup ricopro il ruolo di Tech Leader sulle tecnologie legate alla piattaforma Java EE 5 (come WebSphere 7.0, EJB3, JPA 1 (EclipseLink), JSF 1.2 (Mojarra) e RichFaces 3) e Java EE 6 con JBoss AS 7, in particolare di CDI, JAX-RS, nonché di EJB 3.1, JPA2, JSF2 e RichFaces 4. Al momento mi occupo di ECM, in particolar modo sulla customizzazione di Alfresco 4 e sulla sua installazione con tecnologie da devops come Vagrant e Chef. In passato ho lavorato con la piattaforma alternativa alla enterprise per lo sviluppo web: Java SE 6, Tomcat 6, Hibernate 3 e Spring 2.5. Nei ritagli di tempo sviluppo siti web in PHP e ASP. Per maggiori informazioni consulta il mio curriculum pubblico. Follow me on Twitter - LinkedIn profile - Google+

  • Antonio Musarra

    Ciao Andrea.
    Non è male Spring Batch ma in ottica ETL io preferisco di gran lunga Kettle+Spoon e non scrivo praticamente codice.

    Bye,
    Antonio.

    • Nell’ottica di realizzare un “vero” ETL sono d’accordo con te. Nel mio caso il post nasce dall’esigenza di migrare documenti (versionati), utenti, gruppi e così via da Alfresco 3 ad Alfresco 4. Ho trovato interessante l’approccio ETL applicato ad un batch. Inoltre spring batch dà una serie di classi già fatte (solo da configurare) per leggere e/o scrivere su file o su db per esempio.

      Alla prossima e grazie del suggerimento!

  • Marco

    Di Batch ne ho scritti veramente tanti a mano… la prossima volta almeno Spring Batch è assicurato.

  • darkcg

    La dependency injection ormai è standard in Java EE da un pezzo. Java EE 7 ha introdotto una batch API che è standard e disponibile su tutte le implementazioni, anche sugli application server non Java EE 7 compliant, visto che la reference implementation è scaricabile a parte. Quindi “perchè non spring?”
    Perchè è abusato come al solito e duplica tonnellate di roba già presente nella piattaforma standard. Inoltre, molti ignorano le API Java EE e credono che per determinate cose ci voglia sempre e solo Spring.
    In realtà nel 2014 Spring è molto meno essenziale di un tempo e sarebbe auspicabile che gli sviluppatori Java tornassero a scrivere codice Java delegando determinati compiti a framework e librerie esterne solo quando necessario (belli i tempi quando lo slogan “100% pure Java” era un vanto). Perdonate lo sfogo ma ho le scatole piene di applicazioni che includono sofisticati framework di logging salvo poi trovarsi a usare sempre e solo log.error();
    Siamo nel 2014, per fare una dispatch di una richiesta non ci serve piu’ Spring.

    • Grazie delle sfogo! :p E’ da anni che lavoro con Java EE e la versione 6 con CDI ha notevolmente semplificato la vita. C’è da dire però che gran parte di quello che diventa specifica viene proprio dal mondo parallelo di Spring (e Seam)… disponibile quindi anni prima. La nuova API batch di Java EE 7 non è da meno perché ha “standardizzato” i concetti di reader-processor-writer. Mi hai dato lo spunto per un post di confronto!! 😉

  • alessandro

    Esiste il plugin FatJar per Eclipse che permette di esportare il jar con tutti i riferimenti basta indicargli la classe main. Però non l’ho mai provato con un progetto spring-batch.
    Appena provo ti faccio sapere.

    • Giampaolo Trapasso

      Ciao Alessandro, facci sapere come va con FatJar, e se ti va raccontalo a tutti qui su CNJ! 🙂