Procesos batch en Java: guía breve
Un proceso batch procesa un conjunto de registros automáticamente, sin que un usuario tenga que enviar una petición por cada registro. Normalmente lee datos, los valida o transforma y guarda un resultado.
Flujo mental: origen, leer, validar o transformar, guardar, resumir errores.
Ejemplos típicos: calcular nóminas, importar un CSV, actualizar millones de registros o generar un reporte nocturno.
Batch no significa necesariamente "en paralelo" ni "todo a la vez": puede procesar registros secuencialmente y por grupos. La ventaja es controlar el procesamiento de muchos datos, los errores y las escrituras.
Problema 1: leer ventas de un CSV y calcular el total por sucursal
El problema
Hay un archivo ventas.csv con muchas filas. Cada fila contiene una sucursal y un importe. Hay que sumar las ventas por sucursal e ignorar las filas inválidas sin detener todo el proceso.
Ejemplo de archivo:
Sucursal,Importe
Norte,120.50
Sur,80.00
Norte,30.00
Sur,ERROR
Código
import java.io.BufferedReader;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
public class ReporteVentasBatch {
public static void main(String[] args) throws IOException {
Map<String, BigDecimal> totales = new HashMap<>();
int procesadas = 0;
int rechazadas = 0;
try (BufferedReader lector = Files.newBufferedReader(Path.of("ventas.csv"))) {
lector.readLine(); // Saltar la cabecera del CSV
String linea;
while ((linea = lector.readLine()) != null) {
String[] campos = linea.split(",");
if (campos.length != 2) {
rechazadas++;
continue;
}
String sucursal = campos[0].trim();
try {
BigDecimal importe = new BigDecimal(campos[1].trim());
if (sucursal.isBlank() || importe.signum() < 0) {
rechazadas++;
continue;
}
totales.merge(sucursal, importe, BigDecimal::add);
procesadas++;
} catch (NumberFormatException e) {
rechazadas++; // El importe no es un número válido
}
}
}
System.out.println("Totales: " + totales);
System.out.println("Procesadas: " + procesadas);
System.out.println("Rechazadas: " + rechazadas);
}
}
Cómo funciona
BufferedReaderlee una línea a la vez: no necesita cargar todo el archivo en memoria.- El
whilecontinúa hasta quereadLine()devuelvenull, que significa fin de archivo. - Se valida que la fila tenga dos campos y que el importe sea válido y no negativo.
Mapguarda el total de cada sucursal.mergesuma el importe al total existente o lo crea si es la primera venta de esa sucursal.- Una fila inválida aumenta
rechazadasy elcontinuepasa a la siguiente fila; el proceso no se detiene por ese error. try-with-resourcescierra el archivo automáticamente.
Idea clave
Este es un batch básico hecho con Java estándar: lee muchos registros, aplica reglas, acumula resultados e informa cuántos se procesaron y rechazaron. Para CSV reales con comas dentro de campos entrecomillados, se debe usar una librería CSV, no split(",").
Problema 2: calcular bonos de empleados con Spring Batch
El problema
Una empresa necesita leer empleados desde una base de datos, calcular un bono para cada empleado activo y guardar los resultados. Puede haber decenas de miles de empleados; interesa evitar procesarlos todos en una sola gran escritura.
Spring Batch organiza normalmente este trabajo en un Job y uno o más Steps. En un Step basado en chunks se leen elementos, se procesan y se escriben por grupos. El tamaño del chunk marca cuántos elementos se acumulan antes de escribir y confirmar la transacción.
Procesador
import java.math.BigDecimal;
import org.springframework.batch.item.ItemProcessor;
public class EmpleadoProcessor implements ItemProcessor<Empleado, Empleado> {
@Override
public Empleado process(Empleado empleado) {
if (!empleado.isActivo()) {
return null; // Filtra: este empleado no se envía al writer
}
BigDecimal bono = empleado.getSalario()
.multiply(new BigDecimal("0.10"));
empleado.setBono(bono);
return empleado;
}
}
Configuración del Step y Job (Spring Batch 5.x)
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
@Configuration
@EnableBatchProcessing
public class NominaBatchConfig {
@Bean
public Step procesarNomina(
JobRepository jobRepository,
PlatformTransactionManager transactionManager,
ItemReader<Empleado> lectorEmpleados,
EmpleadoProcessor empleadoProcessor,
ItemWriter<Empleado> escritorEmpleados) {
return new StepBuilder("procesarNomina", jobRepository)
.<Empleado, Empleado>chunk(100, transactionManager)
.reader(lectorEmpleados)
.processor(empleadoProcessor)
.writer(escritorEmpleados)
.build();
}
@Bean
public EmpleadoProcessor empleadoProcessor() {
return new EmpleadoProcessor();
}
@Bean
public Job nominaJob(JobRepository jobRepository, Step procesarNomina) {
return new JobBuilder("nominaJob", jobRepository)
.start(procesarNomina)
.build();
}
}
Nota: el ejemplo se centra en el flujo batch. Para ejecutar la aplicación completa también hay que configurar los beans lectorEmpleados y escritorEmpleados, conectados a la fuente y al destino (por ejemplo, mediante lectores y escritores JDBC), además de las dependencias y la base de datos de Spring Batch.
Cómo funciona
- Job: representa el trabajo completo, aquí el proceso de nómina.
- Step: una fase del Job; aquí procesa los empleados.
- ItemReader: obtiene empleados, normalmente desde una base de datos o archivo.
- ItemProcessor: aplica la regla de negocio; calcula el bono y filtra empleados inactivos.
- ItemWriter: guarda el resultado, por ejemplo actualizando registros en la base de datos.
chunk(100, transactionManager): procesa los elementos en grupos de hasta 100. Al completar el grupo, el writer recibe los resultados y se confirma la transacción de ese chunk. El último grupo puede tener menos de 100.- Si
processdevuelvenull, Spring Batch omite ese elemento en la escritura normal del chunk.
¿Por qué usar chunks?
No hace falta acumular todos los empleados en memoria ni escribir cada registro en una transacción individual. Los grupos permiten equilibrar memoria, rendimiento y límites transaccionales. Si falla una transacción, se puede revertir el chunk actual; eso no significa que todo el Job sea una única transacción.
Conceptos clave de Spring Batch
- Job: el proceso completo.
- Step: una etapa del Job.
- ItemReader: lee los datos de origen.
- ItemProcessor: valida, filtra o transforma cada registro; es opcional si no hay lógica intermedia.
- ItemWriter: escribe los resultados por grupo.
- Chunk: grupo de elementos que se procesa y escribe dentro de un límite transaccional.
- Tasklet: alternativa para una tarea concreta que no encaja en leer-procesar-escribir registros, como limpiar archivos o ejecutar una operación puntual.
Errores: separar registros inválidos, registrar el motivo y decidir si se omiten (skip) o si se reintenta una operación temporal (retry). No se debe ignorar cualquier excepción sin criterio.
Reinicio: Spring Batch puede guardar metadatos del Job y Step y ayudar a reanudar ciertos procesos, pero debe configurarse y diseñarse correctamente; no cualquier código se reinicia mágicamente desde el punto exacto.
Idempotencia: ejecutar el mismo batch dos veces no debería crear pagos o movimientos duplicados. Es especialmente importante al reintentar o volver a ejecutar un proceso.
Batch vs. API: una API suele responder a una petición puntual; un batch procesa un conjunto de registros como un trabajo controlado, a menudo programado o ejecutado fuera del flujo interactivo.
Escalabilidad y errores
Procesar un millón de registros
Usa lectura por lotes o streaming para no cargar todo en memoria; divide el trabajo en chunks, valida y transforma cada registro, escribe los resultados dentro de transacciones, lleva métricas y logs, define una política de errores y diseña el proceso para poder reintentarlo sin duplicar efectos.
Cuando un registro es inválido
Depende de la regla de negocio: se valida antes de escribirlo, se registra el error y se decide si se rechaza, se omite o se detiene el Job. No conviene que una fila mala aborte miles de registros sin necesidad.
Regla para memorizar
Reader = leer. Processor = decidir y transformar. Writer = guardar. Chunk = tamaño del grupo. Job = trabajo completo. Step = etapa.
Documentación oficial
- Spring Batch, procesamiento por chunks: https://docs.spring.io/spring-batch/reference/5.2/step/chunk-oriented-processing.html
- Spring Batch, procesamiento de elementos: https://docs.spring.io/spring-batch/reference/5.2/processor.html
© Arturo Juárez Monroy