Importare un file Excel usando Spring Batch

Spring Batch è un utilissimo framework per eseguire processi lunghi e massicci dividendoli in singoli chunk (pezzi), che vengono eseguiti uno alla volta.

Schema di funzionamento di Spring Batch

Ogni “processo” batch è un Job che a sua volta è composto da uno o più Step. Ogni Step è a sua volta diviso in tre parti: lettura, processo e scrittura. Queste tre parti non sono obbligatorie e sono le sole che dovremo scrivere. Il resto dell’architettura è fornito dal framework.

In questo esempio importeremo il file vino.xls che metteremo (a scopo dimostrativo) fra le resources del nostro progetto Spring. Il file contiene un insieme di nomi di vini sulla colonna A, con il relativo prezzo sulla colonna B. Non è necessario che il file risieda nelle resourcespoiché, come vedremo, verrà letto usando un normalissimo InputStreamReader di Java.

Iniziamo con l’importare le necessarie librerie:

<dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-batch</artifactId>
</dependency>
<dependency> 
	<groupId>org.apache.poi</groupId> 
	<artifactId>poi</artifactId> 
	<version>3.12</version> 
</dependency> 
<dependency> 
	<groupId>org.apache.poi</groupId> 
	<artifactId>poi-ooxml</artifactId> 
	<version>3.12</version> 
</dependency>

Proseguiamo quindi col creare la classe che rappresenta la nostra Entity. In questo caso: le bottiglie di vino!

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@NoArgsConstructor
@Getter
@Setter
@AllArgsConstructor
@Entity
public class Wine {
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private int id;
    
    public Wine(double price, String name) {
        this.price = price;
        this.name = name;
    }

    private double price;
    private String name;

    public String toString() {
        return name + ": " + price;
    }
}

Ora scriviamo il nostro ItemReader, “fantasiosamente” chiamato ExcelReader:


import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;


import org.apache.poi.hssf.usermodel.HSSFCell;
import org.apache.poi.hssf.usermodel.HSSFRow;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.NonTransientResourceException;
import org.springframework.batch.item.ParseException;
import org.springframework.batch.item.UnexpectedInputException;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;

public class ExcelReader implements ItemReader<Wine> {

    Iterator<Wine> results;
    
    @Override
    public Wine read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException {
        if (this.results == null) {
            Resource resource = new ClassPathResource("files/excel/vino.xls");
            List<Wine> list = readFromExcel(resource);
            this.results = list.iterator();
        }
        return this.results.hasNext() ? this.results.next() : null; 
    }

    /**
     * Java method to read dates from Excel file in Java.
     * This method read value from .XLS file, which is an OLE
     * format. 
     * 
     * @param file
     * @throws IOException 
     */
    public List<Wine> readFromExcel(Resource resource) throws IOException{
        
        List<Wine> result = new ArrayList<>();
        InputStream input = resource.getInputStream();
        int rowIndex = 2;
        
        HSSFWorkbook myExcelBook = new HSSFWorkbook(input);
        HSSFSheet myExcelSheet = myExcelBook.getSheet("vini");

        double price = 0.0;
        String name = null;
        do {
            HSSFRow row = myExcelSheet.getRow(rowIndex);
            if (row == null) break;
            if(row.getCell(0).getCellType() == HSSFCell.CELL_TYPE_NUMERIC){
                price = row.getCell(0).getNumericCellValue();
            }
            if(row.getCell(1).getCellType() == HSSFCell.CELL_TYPE_STRING){
                name = row.getCell(1).getStringCellValue().trim();
            }
            Wine p = new Wine(price, name);
            result.add(p);

            rowIndex++;
        } while(name != null && ! name.isEmpty());

        myExcelBook.close();
        return result;
        
    }

}

e passiamo all’ItemWriter:

import java.util.List;

import org.springframework.batch.item.ItemWriter;
import org.springframework.beans.factory.annotation.Autowired;

public class WineWriter implements ItemWriter<Wine> {

    @Autowired
    WineRepositoryInterface repository;

    @Override
    public void write(List<? extends Wine> items) throws Exception {
        for(Wine wine: items) {
            repository.save(wine);
        }   
    }

}

Ora mettiamo insieme i pezzi che abbiamo costruito. Il nostro progetto è semplice e non abbiamo bisogno di processare i dati prima di trasferirli sul database, quindi nello step non avremo un processor.

Aggiungiamo quindi un file di configurazione:

import javax.sql.DataSource;

import com.example.demo.batch.ExcelReader;
import com.example.demo.batch.PhaseWriter;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableBatchProcessing
public class BatchConfiguration {

    @Autowired
    public JobBuilderFactory jobBuilderFactory;

    @Autowired
    public StepBuilderFactory stepBuilderFactory;

    @Bean
    public ExcelReader reader() {
        return new ExcelReader();
    }

    @Bean
    public WineWriter writer(DataSource dataSource) {
        return new WineWriter();
    }

    @Bean
    public Job importUserJob(JobCompletionNotificationListener listener, Step step1) {
    return jobBuilderFactory.get("importWines")
        .incrementer(new RunIdIncrementer())
        .listener(listener)
        .flow(step1)
        .end()
        .build();
    }

    @Bean
    public Step step1(PhaseWriter writer) {
        return stepBuilderFactory.get("step1")
            .<Wine, Wine> chunk(10)
            .reader(reader())
            .writer(writer)
            .build();
    }
}

Nella configurazione qui sopra si fa riferimento ad un Listener per interagire con il ciclo di vita del Job. Eccolo:

package com.example.demo.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.listener.JobExecutionListenerSupport;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class JobCompletionNotificationListener extends JobExecutionListenerSupport {

  private static final Logger log = LoggerFactory.getLogger(JobCompletionNotificationListener.class);

  @Autowired
  private WineRepositoryInterface repository;

  @Override
  public void afterJob(JobExecution jobExecution) {
    if(jobExecution.getStatus() == BatchStatus.COMPLETED) {
      log.info("!!! JOB FINISHED! Time to verify the results");

      repository
        .findAll()
        .forEach(phase -> log.info("Found <" + wine + "> in the database."));
    }
  }
}

Ultima nota. Spring non crea automaticamente le tabelle di supporto al framework Batch, perché lo faccia occorre aggiungere un valore nell’application.properties.

spring.batch.initialize-schema=ALWAYS
spring.batch.job.enabled=false

Comments are closed.