Un interessante utilizzo del framework BeanIO, che abbiamo introdotto nella serie di articoli ad esso dedicati (Primi Passi con BeanIO parte 1, parte 2 e parte 3), è la sua integrazione con Spring Batch. Come descritto in Primi Passi con Spring Batch una tipica applicazione Spring Batch si compone di un job che combina step eseguiti sequenzialmente. Ogni step consiste di tre attività: data reading, processing e data writing. tali attività sono codificate attraverso specifiche interfacce, rispettivamente denominate ItemReader
, ItemProcessor
e ItemWriter
.
Interfacce BeanIO
BeanIO implementa le interfacce ItemReader
e ItemWriter
, rispettivamente per scrivere e leggere flat file codificati secondo quanto specificato nel file di mapping, attraverso le classi BeanIOFlatFileItemReader<T>
e BeanIOFlatFileItemWriter<T>
. Tali classi sono utilizzabili nella definizione dei job di Spring Batch semplicemente definendo e configurando opportunamente il bean relativo. In particolare le proprietà minime necessarie a configurare le due tipologie di classi sono:
- il file di mapping utilizzato per configurare SpringIO e che definisce il modo in cui i bean sono scritti o letti su file (proprietà
streamMapping
); - il nome dello stream definito nel file di mapping da utilizzare per la serializzazione o deserializzazione (proprietà
streamName
); - il nome (e path) del file di input da deserializzare o di output in cui serializzare gli oggetti (proprietà
resource
).
Il Progetto di Esempio
Per comprendere il funzionamento di tale integrazione consideriamo un semplice esempio in cui un job è utilizzato per leggere i dati, su clienti ed ordini, da un file di input in formato CSV e di produrre un report come output su un secondo file CSV.
Dependency
Iniziamo, quindi, creando un progetto maven ed inserendo le dipendenze necessarie nel file pom.xml
:
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 53 54 55 56 57 58 |
<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-boot-bean-io</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.1.0.RELEASE</spring.batch.version> <beanio.version>2.1.0.M1</beanio.version> </properties> <dependencies> <!-- BeanIO dependency --> <dependency> <groupId>org.beanio</groupId> <artifactId>beanio</artifactId> <version>${beanio.version}</version> </dependency> <!-- 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> </project> |
Si noti che è importante utilizzare la versione 2.1.x di Spring Batch affiché l’integrazione possa funzionare correttamente con l’ultima versione di BeanIO, ovvero la 2.1.0.M1.
File di Input
Come detto il file di input customer-orders.csv
contiene gli ordini dei clienti e si presenta nel seguente modo:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Customer,Jennifer,Jones Order,1,2019-02-07,Apple products,10,true Order,2,2019-03-08,Microsoft products,20,false Order,3,2019-04-12,Apple products,15,true Order,4,2019-06-21,Microsoft products,45,false Customer,Paul,Adams Order,3,2019-02-11,Monitors,100,false Order,4,2019-02-18,Mobile phones,50,false Customer,Eric,Smith Order,3,2019-12-10,Switch,15,true Order,4,2019-11-01,Routers,50,true Order,4,2019-11-01,Monitors,73,true eof |
Ogni linea inizia con un identificatore del record (Customer
, Order
, eof
) ed i diversi record si presentano secondo un ordine prestabilito in cui ogni cliente è seguito dagli ordini da lui effettuati. Si noti che per mantenere l’esempio semplice si è stabilito di utilizzare un record di fine file denominato eof
(End Of File).
Utilizziamo parte del codice già visto negli articoli precedenti dedicati al framework e definiamo le classi Customer
e Order
di cui abbiamo bisogno per leggere il file:
|
|
Per la configurazione di BenaIO definiamo il file di mapping order-mapping.xml
in cui è definito lo stream denominato customerOrderFile
, ed in cui la corretta sequenzializzazione dei record è garantita utilizzando un tag <group>
. Il file si presenta quindi nel seguente modo:
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 |
<beanio xmlns="http://www.beanio.org/2012/03" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.beanio.org/2012/03 http://www.beanio.org/2012/03/mapping.xsd"> <stream name="customerOrdersFile" format="csv"> <group name="customerOrders" order="1" minOccurs="0" maxOccurs="unbounded"> <record name="customer" class="it.javaboss.bean.Customer" order="1" minOccurs="1" maxOccurs="1"> <field name="recordType" rid="true" literal="Customer" ignore="true"/> <field name="firstName" /> <field name="lastName" /> </record> <record name="order" class="it.javaboss.bean.Order" order="2" minOccurs="1" maxOccurs="unbounded"> <field name="recordType" rid="true" literal="Order" ignore="true" /> <field name="id" /> <field name="date" format="yyyy-MM-dd" /> <field name="description" /> <field name="amount" /> <field name="completed"/> </record> </group> <record name="eof" class="it.javaboss.bean.EndOfFile" order="2"> <field name="recordType" rid="true" literal="eof" ignore="true"/> </record> </stream> </beanio> |
Si noti l’uso di un bean EndOfFile
senza proprietà, ma col solo recordType
utile al suo riconoscimento nel file.
Unendo tutte le definizioni fatte fino ad ora possiamo infine configurare il bean che implementa la classe BeanIOFlatFileItemReader
, e che sarà utilizzato nel job di Spring Batch:
1 2 3 4 5 |
<bean id="beanioReader" class="org.beanio.spring.BeanIOFlatFileItemReader"> <property name="streamMapping" value="classpath:/order-mapping.xml" /> <property name="streamName" value="customerOrdersFile" /> <property name="resource" value="file:src/main/resources/customer-orders.csv" /> </bean> |
Processamento
Il report che vogliamo produrre nell’esempio è un semplice riepilogo del numero di ordini effettuati da ciascun cliente e del valore monetario complessivo di tali ordini. A tale scopo definiamo la classe Report
così definita:
1 2 3 4 5 6 7 |
public class Report { private String customerName; private Integer orders = 0; private BigDecimal total = BigDecimal.ZERO; /* GETTER AND SETTER */ } |
Per il processamento dei record è necessario implementare l’interfaccia ItemProcessor
di Spring Batch. Realizziamo quindi la classe OrderProcessor
codificata nel seguente modo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class OrderProcessor implements ItemProcessor { private Report report = null; private Report prev = null; public Object process(Object record) throws Exception { if ( record instanceof Customer ) { prev = report; Customer customer = (Customer) record; report = new Report(); report.setCustomerName( customer.getLastName() ); return prev; } else if ( record instanceof Order) { Order order = (Order) record; report.setOrders( report.getOrders() + 1); report.setTotal( report.getTotal().add( order.getAmount() ) ); return null; } else { return report; } } } |
Ogni record letto da Spring è processato dal metodo process il cui comportamento è differente a seconda del tipo di record. Innanzitutto si noti che il bean è un singleton e si utilizza tale caratteristica per mantenere lo stato dell’ultimo report nella variabile report
. Soluzione non elegantissima ma funzionale allo scopo della demo.
A seguito della lettura di un record di tipo customer viene creato un nuovo oggetto Report
ed il precedente oggetto, se presente, è restituito in output. Si noti che restituire un oggetto NULL
non ha alcun effetto sulla scrittura del file di output. Se il record è di tipo order viene semplicemente aggiornato il report corrente incrementando il numero di ordini ed aggiornando il totale. Infine se il record è di tipo eof
(caso else
senza condizioni) si restituisce l’ultimo report in quanto il file è terminato.
File di Output
Per la scrittura del file di output utilizziamo la classe Report
ed il file report-mapping.xml
per configurare il mapping su BeanIO, in cui definiamo lo stream reportFile
:
1 2 3 4 5 6 7 8 9 10 11 12 |
<beanio xmlns="http://www.beanio.org/2012/03" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.beanio.org/2012/03 http://www.beanio.org/2012/03/mapping.xsd"> <stream name="reportFile" format="csv"> <record name="report" class="it.javaboss.bean.Report" minOccurs="0" maxOccurs="unbounded"> <field name="customerName" /> <field name="orders" /> <field name="total" /> </record> </stream> </beanio> |
Configuriamo infine il bean che implementa la classe BeanIOFlatFileItemWriter
, e che sarà utilizzato nel job di Spring Batch, nel seguente modo:
1 2 3 4 5 |
<bean id="beaoioWriter" class="org.beanio.spring.BeanIOFlatFileItemWriter"> <property name="streamMapping" value="classpath:/report-mapping.xml" /> <property name="streamName" value="reportFile" /> <property name="resource" value="file:src/main/resources/report.csv" /> </bean> |
report.csv
prodotto sarà quindi del tipo:
1 2 3 |
Jones,4,90 Adams,2,150 Smith,3,138 |
Spring Batch
Per configurare Spring Batch definiamo due xml, uno di contesto (context.xml
) ed uno per il job (beanio-job.xml
). Per un ulteriore dettaglio sul primo si rimanda all’articolo Primi Passi con Spring Batch. Il secondo, invece, oltre ad ospitare i bean beanioReader
, beanioWriter
e processor
definiti nei paragrafi precedenti, definisce anche il job di Spring:
1 2 3 4 5 6 7 8 9 10 |
<batch:job id="beanioJob"> <batch:step id="step1"> <batch:tasklet> <batch:chunk reader="beanioReader" processor="processor" writer="beaoioWriter" commit-interval="1" /> </batch:tasklet> </batch:step> </batch:job> |
Main
di avvio del batch:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class Main { public static void main(String[] args) { String[] springConfig = { "spring/context.xml","spring/beanio-job.xml"}; ApplicationContext context = new ClassPathXmlApplicationContext(springConfig); JobLauncher jobLauncher = (JobLauncher) context.getBean("jobLauncher"); Job job = (Job) context.getBean("beanioJob"); try { JobExecution execution = jobLauncher.run(job, new JobParameters()); System.out.println("Exit Status : " + execution.getExitStatus().getExitCode()); } catch (Exception e) { e.printStackTrace(); } System.out.println("Report done"); } } |
Codie Sorgente
Il codice sorgente del progetto è disponibile qui spring-batch-bean-io.