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:
e scegliamo come templare un “Simple Spring Batch Project”
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:
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:
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:
- 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.
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 read(); } public interface ItemWriter { 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 { 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:
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{ 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{ 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:
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 😉