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.
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.
<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 .
¿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
Las Entidades
Define dos entidades, una para la CreditCard y otra para CreditCardRisk
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 +
'}';
}
}
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
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> {
}
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).
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 .
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
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.
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.
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.
public interface JobExecutionListener {
void beforeJob(JobExecution var1);
void afterJob(JobExecution var1);
}
Implementa a continuación los listeners más comunes para entenderlos.
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());
}
}
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");
}
}
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");
}
}
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.
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.
-- 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 .
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.
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.
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.
$ ./mvnw spring-boot:run
Desde una consola windows dentro de la carpeta del proyecto.
> mvnw spring-boot:run
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.
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