Ejemplo de Spring Batch

Bienvenido al Ejemplo de Spring Batch. Spring Batch es un módulo del framework Spring para la ejecución de trabajos por lotes. Podemos usar Spring Batch para procesar una serie de trabajos.

Ejemplo de Spring Batch

Antes de ver el programa de ejemplo de Spring Batch, vamos a tener una idea sobre los términos de Spring Batch.

  • A job can consist of ‘n’ number of steps. Each step contains Read-Process-Write task or it can have single operation, which is called tasklet.
  • Leer-Procesar-Escribir básicamente implica leer de una fuente como una base de datos, CSV, etc., luego procesar los datos y escribirlos en una fuente como una base de datos, CSV, XML, etc.
  • Tasklet significa realizar una sola tarea u operación, como limpiar las conexiones, liberar recursos después de que se haya completado el procesamiento.
  • La lectura-procesamiento-escritura y los tasklets se pueden encadenar para ejecutar un trabajo.

Ejemplo de Spring Batch

Ahora consideremos un ejemplo práctico para la implementación de Spring Batch. Consideraremos el siguiente escenario para la implementación. Se necesita convertir un archivo CSV que contiene datos a XML, junto con los datos y las etiquetas que se nombrarán según el nombre de la columna. A continuación, se mencionan las herramientas y bibliotecas importantes utilizadas para el ejemplo de Spring Batch.

  1. Apache Maven 3.5.0: para la construcción del proyecto y la gestión de dependencias.
  2. Eclipse Oxygen Release 4.7.0: IDE para crear aplicaciones de spring batch con maven.
  3. Java 1.8
  4. Spring Core 4.3.12.RELEASE
  5. Spring OXM 4.3.12.RELEASE
  6. Spring JDBC 4.3.12.RELEASE
  7. Spring Batch 3.0.8.RELEASE
  8. MySQL Java Driver 5.1.25: usar según la instalación de MySQL. Esto es necesario para las tablas de metadatos de Spring Batch.

Estructura de directorios de ejemplo de Spring Batch

La imagen siguiente ilustra todos los componentes en nuestro proyecto de ejemplo de Spring Batch.

Dependencias de Maven de Spring Batch

A continuación se muestra el contenido del archivo pom.xml con todas las dependencias requeridas para nuestro proyecto de ejemplo de spring batch.

<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.journaldev.spring</groupId>
	<artifactId>SpringBatchExample</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>SpringBatchDemo</name>
	<url>https://maven.apache.org</url>

	<properties>
		<jdk.version>1.8</jdk.version>
		<spring.version>4.3.12.RELEASE</spring.version>
		<spring.batch.version>3.0.8.RELEASE</spring.batch.version>
		<mysql.driver.version>5.1.25</mysql.driver.version>
		<junit.version>4.11</junit.version>
	</properties>

	<dependencies>

		<!-- Spring Core -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-core</artifactId>
			<version>${spring.version}</version>
		</dependency>

		<!-- Spring jdbc, for database -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-jdbc</artifactId>
			<version>${spring.version}</version>
		</dependency>

		<!-- Spring XML to/back object -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-oxm</artifactId>
			<version>${spring.version}</version>
		</dependency>

		<!-- MySQL database driver -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>${mysql.driver.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>

		<!-- Spring Batch unit test -->
		<dependency>
			<groupId>org.springframework.batch</groupId>
			<artifactId>spring-batch-test</artifactId>
			<version>${spring.batch.version}</version>
		</dependency>

		<!-- Junit -->
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>${junit.version}</version>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>com.thoughtworks.xstream</groupId>
			<artifactId>xstream</artifactId>
			<version>1.4.10</version>
		</dependency>

	</dependencies>
	<build>
		<finalName>spring-batch</finalName>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-eclipse-plugin</artifactId>
				<version>2.9</version>
				<configuration>
					<downloadSources>true</downloadSources>
					<downloadJavadocs>false</downloadJavadocs>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>2.3.2</version>
				<configuration>
					<source>${jdk.version}</source>
					<target>${jdk.version}</target>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

Procesamiento de lotes de primavera de archivos CSV

Aquí está el contenido de nuestro archivo CSV de muestra para el procesamiento de lotes de primavera.

1001,Tom,Moody, 29/7/2013
1002,John,Parker, 30/7/2013
1003,Henry,Williams, 31/7/2013

Configuración de Trabajo de Lote de Primavera

Debemos definir un bean de primavera y un trabajo de lote de primavera en un archivo de configuración. A continuación se muestra el contenido del archivo job-batch-demo.xml, es la parte más importante del proyecto de lotes de primavera.

<beans xmlns="https://www.springframework.org/schema/beans"
	xmlns:batch="https://www.springframework.org/schema/batch" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="https://www.springframework.org/schema/batch
		https://www.springframework.org/schema/batch/spring-batch-3.0.xsd
		https://www.springframework.org/schema/beans 
		https://www.springframework.org/schema/beans/spring-beans-4.3.xsd
	">

	<import resource="../config/context.xml" />
	<import resource="../config/database.xml" />

	<bean id="report" class="com.journaldev.spring.model.Report"
		scope="prototype" />
	<bean id="itemProcessor" class="com.journaldev.spring.CustomItemProcessor" />

	<batch:job id="DemoJobXMLWriter">
		<batch:step id="step1">
			<batch:tasklet>
				<batch:chunk reader="csvFileItemReader" writer="xmlItemWriter"
					processor="itemProcessor" commit-interval="10">
				</batch:chunk>
			</batch:tasklet>
		</batch:step>
	</batch:job>

	<bean id="csvFileItemReader" class="org.springframework.batch.item.file.FlatFileItemReader">

		<property name="resource" value="classpath:csv/input/report.csv" />

		<property name="lineMapper">
			<bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
				<property name="lineTokenizer">
					<bean
						class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer">
						<property name="names" value="id,firstname,lastname,dob" />
					</bean>
				</property>
				<property name="fieldSetMapper">
					<bean class="com.journaldev.spring.ReportFieldSetMapper" />

					<!-- if no data type conversion, use BeanWrapperFieldSetMapper to map 
						by name <bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"> 
						<property name="prototypeBeanName" value="report" /> </bean> -->
				</property>
			</bean>
		</property>

	</bean>

	<bean id="xmlItemWriter" class="org.springframework.batch.item.xml.StaxEventItemWriter">
		<property name="resource" value="file:xml/outputs/report.xml" />
		<property name="marshaller" ref="reportMarshaller" />
		<property name="rootTagName" value="report" />
	</bean>

	<bean id="reportMarshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
		<property name="classesToBeBound">
			<list>
				<value>com.journaldev.spring.model.Report</value>
			</list>
		</property>
	</bean>

</beans>
  1. Estamos usando FlatFileItemReader para leer el archivo CSV, CustomItemProcessor para procesar los datos y escribir en un archivo XML usando StaxEventItemWriter.
  2. batch:job – Esta etiqueta define el trabajo que queremos crear. La propiedad Id especifica el ID del trabajo. Podemos definir varios trabajos en un solo archivo XML.
  3. batch:step – Esta etiqueta se utiliza para definir diferentes pasos de un trabajo de lote de primavera.
  4. Dos tipos diferentes de estilos de procesamiento son ofrecidos por el Marco de Trabajo Spring Batch, los cuales son “Orientado a TaskletStep” y “Orientado a Chunk”. El estilo Orientado a Chunk se utiliza en este ejemplo para leer los datos uno por uno y crear ‘chunks’ que serán escritos, dentro de un límite de transacción.
  5. lector: bean de Spring usado para leer los datos. Hemos utilizado el bean csvFileItemReader en este ejemplo, que es una instancia de FlatFileItemReader.
  6. procesador: esta es la clase que se utiliza para procesar los datos. Hemos utilizado CustomItemProcessor en este ejemplo.
  7. escritor: bean usado para escribir datos en un archivo XML.
  8. intervalo-de-confirmación: Esta propiedad define el tamaño del chunk que se confirmará una vez que el procesamiento esté hecho. Básicamente significa que ItemReader leerá los datos uno por uno y ItemProcessor los procesará de la misma manera, pero ItemWriter escribirá los datos solo cuando sea igual al tamaño del intervalo de confirmación.
  9. Tres interfaces importantes que se utilizan como parte de este proyecto son ItemReader, ItemProcessor y ItemWriter del paquete org.springframework.batch.item.

Clase de Modelo Spring Batch

En primer lugar, estamos leyendo el archivo CSV en un objeto de Java y luego usando JAXB para escribirlo en un archivo XML. A continuación se muestra nuestra clase de modelo con las anotaciones JAXB requeridas.

package com.journaldev.spring.model;

import java.util.Date;

import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "record")
public class Report {

	private int id;
	private String firstName;
	private String lastName;
	private Date dob;

	@XmlAttribute(name = "id")
	public int getId() {
		return id;
	}

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

	@XmlElement(name = "firstname")
	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	@XmlElement(name = "lastname")
	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	@XmlElement(name = "dob")
	public Date getDob() {
		return dob;
	}

	public void setDob(Date dob) {
		this.dob = dob;
	}

	@Override
	public String toString() {
		return "Report [id=" + id + ", firstname=" + firstName + ", lastName=" + lastName + ", DateOfBirth=" + dob
				+ "]";
	}

}

Nótese que los campos de la clase de modelo deben ser los mismos que los definidos en la configuración del mapeador de lotes de primavera, es decir, property name="names" value="id,firstname,lastname,dob" en nuestro caso.

Mapeador de conjuntos de campos de lotes de primavera

A custom FieldSetMapper is needed to convert a Date. If no data type conversion is required, then only BeanWrapperFieldSetMapper should be used to map the values by name automatically. The java class which extends FieldSetMapper is ReportFieldSetMapper.

package com.journaldev.spring;

import java.text.ParseException;
import java.text.SimpleDateFormat;

import org.springframework.batch.item.file.mapping.FieldSetMapper;
import org.springframework.batch.item.file.transform.FieldSet;
import org.springframework.validation.BindException;

import com.journaldev.spring.model.Report;

public class ReportFieldSetMapper implements FieldSetMapper {

	private SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy");

	public Report mapFieldSet(FieldSet fieldSet) throws BindException {

		Report report = new Report();
		report.setId(fieldSet.readInt(0));
		report.setFirstName(fieldSet.readString(1));
		report.setLastName(fieldSet.readString(2));

		// Formato predeterminado yyyy-MM-dd
		// fieldSet.readDate(4);
		String date = fieldSet.readString(3);
		try {
			report.setDob(dateFormat.parse(date));
		} catch (ParseException e) {
			e.printStackTrace();
		}

		return report;

	}

}

Procesador de lotes de primavera

Ahora, como se define en la configuración de trabajo, un itemProcessor se ejecutará antes de itemWriter. Hemos creado una clase CustomItemProcessor.java para lo mismo.

package com.journaldev.spring;

import org.springframework.batch.item.ItemProcessor;

import com.journaldev.spring.model.Report;

public class CustomItemProcessor implements ItemProcessor<Report, Report> {

	public Report process(Report item) throws Exception {
		
		System.out.println("Processing..." + item);
		String fname = item.getFirstName();
		String lname = item.getLastName();
		
		item.setFirstName(fname.toUpperCase());
		item.setLastName(lname.toUpperCase());
		return item;
	}

}

Podemos manipular datos en la implementación de ItemProcessor, como pueden ver, estoy convirtiendo los valores de nombre y apellido a mayúsculas.

Archivos de configuración de primavera

En nuestro archivo de configuración de lote de primavera, hemos importado dos archivos de configuración adicionales: context.xml y database.xml.

<beans xmlns="https://www.springframework.org/schema/beans"
	xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="
		https://www.springframework.org/schema/beans 
		https://www.springframework.org/schema/beans/spring-beans-4.3.xsd">

	<!-- stored job-meta in memory -->
	<!--  
	<bean id="jobRepository"
		class="org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean">
		<property name="transactionManager" ref="transactionManager" />
	</bean>
 	 -->
 	 
 	 <!-- stored job-meta in database -->
	<bean id="jobRepository"
		class="org.springframework.batch.core.repository.support.JobRepositoryFactoryBean">
		<property name="dataSource" ref="dataSource" />
		<property name="transactionManager" ref="transactionManager" />
		<property name="databaseType" value="mysql" />
	</bean>
	
	<bean id="transactionManager"
		class="org.springframework.batch.support.transaction.ResourcelessTransactionManager" />
	 
	<bean id="jobLauncher"
		class="org.springframework.batch.core.launch.support.SimpleJobLauncher">
		<property name="jobRepository" ref="jobRepository" />
	</bean>

</beans>
  • jobRepository – El JobRepository es responsable de almacenar cada objeto Java en su tabla de metadatos correcta para el lote de primavera.
  • transactionManager – este es responsable de confirmar la transacción una vez que el tamaño del intervalo de confirmación y los datos procesados son iguales.
  • jobLauncher – Este es el corazón del lote de primavera. Esta interfaz contiene el método run que se utiliza para activar el trabajo.
<beans xmlns="https://www.springframework.org/schema/beans"
	xmlns:jdbc="https://www.springframework.org/schema/jdbc" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="https://www.springframework.org/schema/beans 
		https://www.springframework.org/schema/beans/spring-beans-4.3.xsd
		https://www.springframework.org/schema/jdbc 
		https://www.springframework.org/schema/jdbc/spring-jdbc-4.3.xsd">

	<!-- connect to database -->
	<bean id="dataSource"
		class="org.springframework.jdbc.datasource.DriverManagerDataSource">
		<property name="driverClassName" value="com.mysql.jdbc.Driver" />
		<property name="url" value="jdbc:mysql://localhost:3306/Test" />
		<property name="username" value="test" />
		<property name="password" value="test123" />
	</bean>

	<bean id="transactionManager"
		class="org.springframework.batch.support.transaction.ResourcelessTransactionManager" />

	<!-- create job-meta tables automatically -->
	<!-- <jdbc:initialize-database data-source="dataSource"> <jdbc:script location="org/springframework/batch/core/schema-drop-mysql.sql" 
		/> <jdbc:script location="org/springframework/batch/core/schema-mysql.sql" 
		/> </jdbc:initialize-database> -->
</beans>

Spring Batch utiliza algunas tablas de metadatos para almacenar información de trabajos por lotes. Podemos hacer que se creen a partir de configuraciones de lote de primavera, pero es recomendable hacerlo manualmente ejecutando los archivos SQL, como se puede ver en el código comentado arriba. Desde el punto de vista de la seguridad, es mejor no dar acceso de ejecución de DDL al usuario de la base de datos de lote de primavera.

Tablas de Spring Batch

Las tablas de Spring Batch se ajustan muy de cerca a los objetos de dominio que los representan en Java. Por ejemplo, JobInstance, JobExecution, JobParameters y StepExecution se asignan a BATCH_JOB_INSTANCE, BATCH_JOB_EXECUTION, BATCH_JOB_EXECUTION_PARAMS y BATCH_STEP_EXECUTION respectivamente. ExecutionContext se asigna tanto a BATCH_JOB_EXECUTION_CONTEXT como a BATCH_STEP_EXECUTION_CONTEXT. El JobRepository es responsable de guardar y almacenar cada objeto Java en su tabla correcta. A continuación se detallan los datos de cada tabla de metadatos.

  1. Batch_job_instance: La tabla BATCH_JOB_INSTANCE contiene toda la información relevante para una JobInstance.
  2. Batch_job_execution_params: La tabla BATCH_JOB_EXECUTION_PARAMS contiene toda la información relevante para el objeto JobParameters.
  3. Batch_job_execution: La tabla BATCH_JOB_EXECUTION contiene datos relevantes para el objeto JobExecution. Se agrega una nueva fila cada vez que se ejecuta un trabajo.
  4. Batch_step_execution: La tabla BATCH_STEP_EXECUTION contiene toda la información relevante para el objeto StepExecution.
  5. Batch_job_execution_context: La tabla BATCH_JOB_EXECUTION_CONTEXT contiene datos relevantes para el ExecutionContext de un trabajo. Existe exactamente un ExecutionContext de trabajo para cada JobExecution, y contiene todos los datos a nivel de trabajo que son necesarios para esa ejecución de trabajo en particular. Estos datos representan típicamente el estado que debe recuperarse después de un fallo para que una JobInstance pueda reiniciarse desde donde falló.
  6. Contexto_de_ejecución_por_lotes: La tabla BATCH_STEP_EXECUTION_CONTEXT contiene datos relevantes para el ExecutionContext de un Paso. Hay exactamente un ExecutionContext para cada StepExecution, y contiene todos los datos que necesitan ser persistidos para una ejecución de paso particular. Estos datos representan típicamente el estado que debe ser recuperado después de un fallo para que una JobInstance pueda reiniciarse desde donde falló.
  7. Secuencia_de_ejecución_de_trabajo_por_lotes: Esta tabla contiene la secuencia de ejecución de datos del trabajo.
  8. Secuencia_de_ejecución_de_paso_por_lotes: Esta tabla contiene los datos para la secuencia de ejecución de paso.
  9. Secuencia_de_trabajo_por_lotes: Esta tabla contiene los datos para la secuencia de trabajos en caso de que tengamos múltiples trabajos, obtendremos múltiples filas.

Programa de Prueba de Spring Batch

Nuestro proyecto de ejemplo de Spring Batch está listo, el paso final es escribir una clase de prueba para ejecutarlo como un programa java.

package com.journaldev.spring;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class App {
	public static void main(String[] args) {

		String[] springConfig = { "spring/batch/jobs/job-batch-demo.xml" };

		ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(springConfig);

		JobLauncher jobLauncher = (JobLauncher) context.getBean("jobLauncher");
		Job job = (Job) context.getBean("DemoJobXMLWriter");

		JobParameters jobParameters = new JobParametersBuilder().addLong("time", System.currentTimeMillis())
				.toJobParameters();

		try {

			JobExecution execution = jobLauncher.run(job, jobParameters);
			System.out.println("Exit Status : " + execution.getStatus());

		} catch (Exception e) {
			e.printStackTrace();
		}

		System.out.println("Done");
		context.close();
	}
}

Solo ejecuta el programa anterior y obtendrás un XML de salida como el siguiente.

<?xml version="1.0" encoding="UTF-8"?><report><record id="1001"><dob>2013-07-29T00:00:00+05:30</dob><firstname>TOM</firstname><lastname>MOODY</lastname></record><record id="1002"><dob>2013-07-30T00:00:00+05:30</dob><firstname>JOHN</firstname><lastname>PARKER</lastname></record><record id="1003"><dob>2013-07-31T00:00:00+05:30</dob><firstname>HENRY</firstname><lastname>WILLIAMS</lastname></record></report>

Eso es todo para el ejemplo de Spring Batch, puedes descargar el proyecto final desde el siguiente enlace.

Descargar Proyecto de Ejemplo de Spring Batch

Referencia: Guía Oficial

Source:
https://www.digitalocean.com/community/tutorials/spring-batch-example