Como crear una aplicación con Spring Batch

Para configurar un proyecto de Spring Batch partiremos con las dependencias que necesitamos utilizando Maven, luego definiremos la configuración básica y algunos aspectos simples para conectarnos a una base de datos embebida.

¿Qué es Spring Batch?

Se trata de un framework destinado al proceso de grandes lotes de información en “modo batch”. La ejecución de procesos batch está enfocada en resolver el procesamiento sin intervención del usuario y de forma periódica.
Pero para explicarlo mejor, diremos que con Spring Batch podemos generar trabajos (job) y dividirlos en pasos (steps), como por ejemplo, leer datos de una base de datos para procesarlos aplicando alguna regla de negocio, y luego escribirlos en un archivo o de nuevo en la base de datos.

spring-batch-domain-language

Vemos que un Job puede tener varios Step y cada Step un Reader, un Processor y un Writer.

¿Qué harás en este ejemplo y para entender cómo funciona Spring Batch?

  • Conectarnos a una Base de Datos
  • Leer todos los registros de una Tabla
  • Procesar cada registro y generar nueva información
  • Escribir el resultado del proceso en otra Tabla de la base de datos.

Veamos las dependencias que necesitas.

Dependencias necesarias para Spring Batch

Para tu proyecto en Spring Batch necesitas la dependencia “org.springframework.boot:spring-boot-starter-batch” . Además para este ejemplo en el que usaremos una base de datos en memoria necesitamos la dependencia de “com.h2database:h2” También necesitarás “org.springframework.boot:spring-boot-starter-data-jpa” para conectar Spring Batch con la base de datos.

Estas son todas las dependencias que usarás.

 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
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-batch</artifactId>
		</dependency>

		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>org.springframework.batch</groupId>
			<artifactId>spring-batch-test</artifactId>
			<scope>test</scope>
		</dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
            <version>RELEASE</version>
            <scope>compile</scope>
        </dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
			<version>RELEASE</version>
			<scope>compile</scope>
		</dependency>
	</dependencies>

Estructura general de tu proyecto Spring Batch

Tu proyecto Spring Batch lucirá de la siguiente manera para este ejemplo .

Spring Batch Example

¿Que harás con este tutorial para crear una aplicación Spring Batch?

Lo que haremos en este ejemplo de proyecto será crear un Job que lea de una tabla contenido de tarjetas de crédito, luego al procesarlas determine en base a una supuesta fecha de pago el riesgo de la tarjeta, y al finalizar guarde otra entidad con el resultado.

¿Que harás en este ejemplo? - Leer de la base de datos las tarjetas de crédito - Procesar las tarjetas y aplicar una regla de negocio para dichas tarjetas. - El proceso debe generar una nueva entidad con el resultado del riesgo - Al finalizar se guarda el resultado del proceso

Spring Batch Example

Las Entidades

Define dos entidades, una para la CreditCard y otra para CreditCardRisk

 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
package dev.experto.demo.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.util.Date;

@Entity
public class CreditCard {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Long cardNumber;
    private Date lastPay;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getCardNumber() {
        return cardNumber;
    }

    public void setCardNumber(Long cardNumber) {
        this.cardNumber = cardNumber;
    }

    public Date getLastPay() {
        return lastPay;
    }

    public void setLastPay(Date lastPay) {
        this.lastPay = lastPay;
    }


    @Override
    public String toString() {
        return "CreditCard{" +
                "id=" + id +
                ", cardNumber=" + cardNumber +
                ", lastPay=" + lastPay +
                '}';
    }
}
 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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package dev.experto.demo.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import java.util.Date;

@Entity
public class CreditCardRisk {

    public static final int HIGH = 3;
    public static final int LOW = 2;
    public static final int NORMAL = 1;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Date date;
    private int risk;

    @OneToOne(optional = false)
    private CreditCard creditCard;

    public CreditCardRisk() {
    }

    public CreditCardRisk(CreditCard creditCard, Date date, int risk) {
        this.creditCard = creditCard;
        this.date = date;
        this.risk = risk;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }

    public int getRisk() {
        return risk;
    }

    public void setRisk(int risk) {
        this.risk = risk;
    }

    public CreditCard getCreditCard() {
        return creditCard;
    }

    public void setCreditCard(CreditCard creditCard) {
        this.creditCard = creditCard;
    }

    @Override
    public String toString() {
        return "CreditCardRisk{" +
                "id=" + id +
                ", date=" + date +
                ", risk=" + risk +
                '}';
    }
}

Los Repository

Tendrás dos repository uno para cada entidad que usaremos luego dentro del ItemReader y del ItemWriter

1
2
3
4
5
6
7
8
9
package dev.experto.demo.repository;

import dev.experto.demo.domain.CreditCard;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface CreditCardRepository extends JpaRepository<CreditCard, Long> {
}
1
2
3
4
5
6
7
8
9
package dev.experto.demo.repository;

import dev.experto.demo.domain.CreditCardRisk;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface CreditCardRiskRespository extends JpaRepository<CreditCardRisk, Long> {
}

El Reader

Existen muchas implementaciones para los Reader en Spring Batch disponibles para leer distintas fuentes de datos (ej. archivos, base de datos).

Spring Batch Reader

Aquí crearás una propia, a partir de la interfaz principal ItemReader .
Definimos el reader que implementará ItemReader y que usará un repositorio de Spring para obtener todas las CreditCard
Con la anotación BeforeStep realizamos una operación de lectura sobre la base de datos previo a iniciar el Reader.
El método read() irá entregando cada ítem de la lista al Processor .

 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
import dev.experto.demo.domain.CreditCard;
import dev.experto.demo.repository.CreditCardRepository;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.annotation.BeforeStep;
import org.springframework.batch.item.ItemReader;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Iterator;

public class CreditCardItemReader implements ItemReader<CreditCard> {

    @Autowired
    private CreditCardRepository respository;

    private Iterator<CreditCard> usersIterator;

    @BeforeStep
    public void before(StepExecution stepExecution) {
        usersIterator = respository.findAll().iterator();
    }

    @Override
    public CreditCard read() {
        if (usersIterator != null && usersIterator.hasNext()) {
            return usersIterator.next();
        } else {
            return null;
        }
    }
}

El Processor

El Processor se encargará de recibir una CreditCard y entregar una CreditCardRisk

 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
import dev.experto.demo.domain.CreditCard;
import dev.experto.demo.domain.CreditCardRisk;
import org.springframework.batch.item.ItemProcessor;

import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;

import static java.time.temporal.ChronoUnit.DAYS;

public class CreditCardItemProcessor implements ItemProcessor<CreditCard, CreditCardRisk> {

    @Override
    public CreditCardRisk process(CreditCard item) {

        LocalDate today = LocalDate.now();
        LocalDate lastDate = item.getLastPay().toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
        long daysBetween = DAYS.between(today, lastDate);

        int risk;
        if (daysBetween >= 20) {
            risk = CreditCardRisk.HIGH;
        } else if (daysBetween > 10) {
            risk = CreditCardRisk.LOW;;
        }else {
            risk = CreditCardRisk.NORMAL;;
        }

        CreditCardRisk creditCardRisk = new CreditCardRisk(item, new Date(), risk);
        return creditCardRisk;
    }
}

El Writer

El Writer también tiene muchas implementaciones en Spring Batch que puedes utilizar según sea qué o dónde estés escribiendo.

Spring Batch Writer

Al igual que hicimos para el Reader, crearemos nuestro propio Writer a partir de la interfaz. El Writer recibirá la lista de CreditCardRisk que el Processor ha procesado para guardar el resultado en la base de datos.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import dev.experto.demo.domain.CreditCardRisk;
import dev.experto.demo.repository.CreditCardRiskRespository;
import org.springframework.batch.item.ItemWriter;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;

public class CreditCardItemWriter implements ItemWriter<CreditCardRisk> {

    @Autowired
    private CreditCardRiskRespository respository;

    @Override
    public void write(List<? extends CreditCardRisk> list) throws Exception {
        respository.saveAll(list);
    }
}

Los Listeners

Los Listeners interceptan partes del proceso del Job y reciben o escuchan lo que está pasando durante la ejecución. Spring Batch dispone de numerosos listeners para cada parte del paso del job y puedes recurrir a ellos para conocer qué sucede en cada etapa.

Por ejemplo, la siguiente interfaz JobExecutionListener escucha la ejecución previamente a empezar el job y luego de haber terminado el Job.

1
2
3
4
5
public interface JobExecutionListener {
    void beforeJob(JobExecution var1);

    void afterJob(JobExecution var1);
}

Implementa a continuación los listeners más comunes para entenderlos.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package dev.experto.demo.listener;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobExecutionListener;
import org.springframework.stereotype.Component;

@Component
public class CreditCardJobExecutionListener implements JobExecutionListener {

    private static final Logger LOGGER = LoggerFactory.getLogger(CreditCardJobExecutionListener.class);

    @Override
    public void beforeJob(JobExecution jobExecution) {
        LOGGER.info("beforeJob");
    }

    @Override
    public void afterJob(JobExecution jobExecution) {
        LOGGER.info("afterJob: " + jobExecution.getStatus());
    }
}
 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
package dev.experto.demo.listener;

import dev.experto.demo.domain.CreditCard;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.ItemReadListener;

public class CreditCardIItemReaderListener implements ItemReadListener<CreditCard> {

    private static final Logger LOGGER = LoggerFactory.getLogger(CreditCardIItemReaderListener.class);

    @Override
    public void beforeRead() {
        LOGGER.info("beforeRead");
    }

    @Override
    public void afterRead(CreditCard creditCard) {
        LOGGER.info("afterRead: " + creditCard.toString());
    }

    @Override
    public void onReadError(Exception e) {
        LOGGER.info("onReadError");
    }
}
 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
package dev.experto.demo.listener;

import dev.experto.demo.domain.CreditCard;
import dev.experto.demo.domain.CreditCardRisk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.ItemProcessListener;

public class CreditCardItemProcessListener implements ItemProcessListener<CreditCard, CreditCardRisk> {

    private static final Logger LOGGER = LoggerFactory.getLogger(CreditCardItemProcessListener.class);

    @Override
    public void beforeProcess(CreditCard creditCard) {
        LOGGER.info("beforeProcess");
    }

    @Override
    public void afterProcess(CreditCard creditCard, CreditCardRisk creditCardRisk) {
        LOGGER.info("afterProcess: " + creditCard + " ---> " + creditCardRisk);
    }

    @Override
    public void onProcessError(CreditCard creditCard, Exception e) {
        LOGGER.info("onProcessError");
    }
}
 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
package dev.experto.demo.listener;

import dev.experto.demo.domain.CreditCardRisk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.ItemWriteListener;

import java.util.List;

public class CreditCardIItemWriterListener implements ItemWriteListener<CreditCardRisk> {

    private static final Logger LOGGER = LoggerFactory.getLogger(CreditCardIItemWriterListener.class);

    @Override
    public void beforeWrite(List<? extends CreditCardRisk> list) {
        LOGGER.info("beforeWrite");
    }


    @Override
    public void afterWrite(List<? extends CreditCardRisk> list) {
        for (CreditCardRisk creditCardRisk : list) {
            LOGGER.info("afterWrite :" + creditCardRisk.toString());
        }
    }

    @Override
    public void onWriteError(Exception e, List<? extends CreditCardRisk> list) {
        LOGGER.info("onWriteError");
    }
}

El archivo application.properties

Vas a configurar aquí la conexión a la base de datos. Este proyecto usa una base de datos H2 en memoria para simplificar la ejecución de este ejemplo.

1
2
3
4
5
6
7
8
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

# create, create-drop, validate, update
spring.jpa.hibernate.ddl-auto = update

El archivo data.sql

Spring busca por defecto los archivos con extensión sql dentro de la carpeta resources y los ejecuta. Este archivo a continuación completará la base de datos H2 en memoria con información previa al momento de iniciar la aplicación.
De este modo tenemos así datos para procesar en memoria previo al iniciar el Job.

1
2
3
4
5
6
7
8
9
-- risk HIGH
INSERT INTO credit_card (card_number,last_pay) VALUES (9991,CURRENT_DATE()-30);
INSERT INTO credit_card (card_number,last_pay) VALUES (9992,CURRENT_DATE()-21);
-- risk LOW
INSERT INTO credit_card (card_number,last_pay) VALUES (9993,CURRENT_DATE()-20);
INSERT INTO credit_card (card_number,last_pay) VALUES (9994,CURRENT_DATE()-11);
-- risk NORMAL
INSERT INTO credit_card (card_number,last_pay) VALUES (9995,CURRENT_DATE()-10);
INSERT INTO credit_card (card_number,last_pay) VALUES (9996,CURRENT_DATE()-5);

Los chunk en Spring Batch?

Este es un concepto que usarás al configurar los Job. Un chunk es una unidad de procesamiento. Con este valor le dices a Spring Batch que procese una determinada cantidad de registros y al completar la cantidad envíe todo al reader para hacer el commit. Nota que la clase la Interfaz Item reader tiene el metodo read que recibe una lista. Esta lista es la cantidad de chunk que fueron procesados previamente y cuya cantidad estableciste en la config del chunk .

Spring Batch Chunk

Cómo configurar el Job en Spring Batch

Dijimos antes que para Spring Batch un Job contiene Steps y cada Step requiere generalmente (no siempre) un Reader, un Processor, y un Writer. Opcionalmente puedes agregar Listener para escuchar y saber que esta pasando en cada parte del proceso del batch.

  • El Reader lee datos.
  • El Procesor recibe los datos del Reader y los procesa para luego entregarlos al Writer.
  • El Writer recepta los datos que fueron procesados y se encarga de guardarlos.
  • Los Listener que “escuchan” lo que sucede durante el proceso. Para este ejemplo usaremos algunos listener, los más comunes, que dijimos previamente.

A continuación vas a crear una clase de configuración para definir todas las partes del Job.

Spring Batch Example

Esta clase la anotas con @Configuration y @EnableBatchProcessing

Dentro de la clase que llamarás JobBatchConfiguration debes definir: - Un bean de Spring para el Reader CreditCardItemReader - Un bean de Spring para el Processor CreditCardItemProcessor - Un bean de Spring para el Writer CreditCardItemWriter - Los bean para los Listeners CreditCardJobExecutionListener CreditCardItemReaderListener CreditCardItemProcessListener CreditCardItemWriterListener

Presta atención a cómo defines el Job y el Step que se ejecutará dentro del Job. Al Job le indicamos el listener que escuchara y le establecemos el / los steps. En la definición del Step también le indicamos los listeners y cada uno de los beans que mencionamos para el reader, procesor y writer. El “chunk” lo establecimos arbitrariamente en 100. Este valor lo debes pensar acorde a la necesidad de tu proyecto.

 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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
package dev.experto.demo.config;

import dev.experto.demo.domain.CreditCard;
import dev.experto.demo.domain.CreditCardRisk;
import dev.experto.demo.job.CreditCardItemProcessor;
import dev.experto.demo.job.CreditCardItemReader;
import dev.experto.demo.job.CreditCardItemWriter;
import dev.experto.demo.listener.CreditCardItemProcessListener;
import dev.experto.demo.listener.CreditCardIItemReaderListener;
import dev.experto.demo.listener.CreditCardIItemWriterListener;
import dev.experto.demo.listener.CreditCardJobExecutionListener;
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.step.tasklet.TaskletStep;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableBatchProcessing
public class JobBatchConfiguration {

    @Autowired
    public JobBuilderFactory jobBuilderFactory;

    @Autowired
    public StepBuilderFactory stepBuilderFactory;

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

    @Bean
    public CreditCardItemProcessor processor() {
        return new CreditCardItemProcessor();
    }

    @Bean
    public CreditCardItemWriter writer() {
        return new CreditCardItemWriter();
    }

    @Bean
    public CreditCardJobExecutionListener jobExecutionListener() {
        return new CreditCardJobExecutionListener();
    }

    @Bean
    public CreditCardIItemReaderListener readerListener() {
        return new CreditCardIItemReaderListener();
    }

    @Bean
    public CreditCardItemProcessListener creditCardItemProcessListener() {
        return new CreditCardItemProcessListener();
    }

    @Bean
    public CreditCardIItemWriterListener writerListener() {
        return new CreditCardIItemWriterListener();
    }

    @Bean
    public Job job(Step step, CreditCardJobExecutionListener jobExecutionListener) {
        Job job = jobBuilderFactory.get("job1")
                .listener(jobExecutionListener)
                .flow(step)
                .end()
                .build();
        return job;
    }

    @Bean
    public Step step(CreditCardItemReader reader,
                     CreditCardItemWriter writer,
                     CreditCardItemProcessor processor,
                     CreditCardIItemReaderListener readerListener,
                     CreditCardItemProcessListener creditCardItemProcessListener,
                     CreditCardIItemWriterListener writerListener) {

        TaskletStep step = stepBuilderFactory.get("step1")
                .<CreditCard, CreditCardRisk>chunk(100)
                .reader(reader)
                .processor(processor)
                .writer(writer)
                .listener(readerListener)
                .listener(creditCardItemProcessListener)
                .listener(writerListener)
                .build();
        return step;
    }

}

Cómo ejecutar Spring Batch

Este proyecto está creado con maven .

Desde una consola de linux dentro de la carpeta del proyecto.

1
$ ./mvnw spring-boot:run

Desde una consola windows dentro de la carpeta del proyecto.

1
> mvnw spring-boot:run

Spring Batch Run Console

Observa con más detalle la salida del log. - Se inicia el Job con el Step asociado y el listener.
- Se ejecuta el reader y el listener del reader.
- Se ejecuta el procesador y el listener del procesador.
- Se ejecuta el writer y el listener del writer.

Spring Batch Example Run Console

Conclusión:

Con la ayuda de este ejemplo has creado una aplicación Spring Batch haciendo uso de algunas de sus funcionalidades más importantes.
Si necesitas crear procesos batch que ejecuta acciones desatendidas y de gran carga este es un excelente framework para cubrir este objetivo.

Código fuente de este ejemplo

Como siempre te dejo el código fuente de este ejemplo para que lo tengas a mano.

https://github.com/gustavopeiretti/springbatch-example
https://gitlab.com/gustavopeiretti/springbatch-example