Batch
Introdução
No desenvolvimento de sistemas corporativos, é muito comum lidar com processamentos em lote, como importação de arquivos, migração de dados, geração de relatórios e tarefas agendadas. Para esse tipo de demanda, o Spring Batch é uma das soluções mais robustas e amplamente utilizadas na plataforma Java.
Spring Batch é um framework da família Spring projetado especificamente para processar grandes volumes de dados de forma eficiente, confiável e escalável. Ele fornece suporte para:
- Leitura e escrita de dados (em bancos, arquivos, filas, etc.);
- Processamento em etapas (steps), com controle transacional;
- Tratamento de falhas e reinício automático;
- Monitoramento, logging e estatísticas de execução;
- Integração com Spring Boot, facilitando a configuração e execução.
A ideia central do Spring Batch é dividir um job em steps (etapas), que por sua vez seguem uma estrutura comum: leitura, processamento e escrita (Reader, Processor, Writer). Essa abordagem modular facilita o desenvolvimento e a manutenção dos processos.
Se você já tem familiaridade com o ecossistema Spring, vai perceber que o Spring Batch se encaixa bem nas arquiteturas modernas, permitindo agilidade e produtividade mesmo em cenários complexos de processamento de dados.
Contexto
Imagine que uma empresa desenvolveu um sistema de PDV (Ponto de Venda) para uma grande rede de supermercados. Por questões de infraestrutura e disponibilidade, cada terminal de atendimento (caixa) funciona com seu próprio banco de dados local. Isso garante que o sistema continue operando mesmo se houver falha na conexão com o servidor central.
No entanto, ao final de cada dia, a empresa precisa consolidar todas as vendas realizadas por cada terminal, em todas as lojas. Para isso, cada terminal gera automaticamente um arquivo .csv com os dados das vendas realizadas naquele dia.
Esses arquivos são então enviados para o servidor central, onde serão processados por um sistema de backoffice — e é exatamente aqui que entra o Spring Batch.
O objetivo do processamento é:
- Ler os arquivos gerados pelos terminais;
- Validar e transformar os dados conforme necessário;
- Gravar essas informações no banco de dados central da empresa.
Estrutura do Arquivo
Cada linha do arquivo representa uma venda realizada no terminal. O layout do arquivo contém os seguintes campos:
| Campo | Descrição | Exemplo |
|---|---|---|
id_terminal | Identificador único do terminal de PDV | TT01 |
data_movimento | Data da venda (formato dd/MM/yyyy) | 17/05/2025 |
valor_venda | Valor total da venda | 199.90 |
forma_pagamento | Tipo de pagamento (Dinheiro,Pix, Débito, Crédito) | PIX |
Exemplo de conteúdo do .csv:
T01;1;17/05/25;199.90;CCR
T01;2;17/05/25;45.50;DIN
T02;3;17/05/25;120.00;PIX
T02;4;17/05/25;120.00;DEB
T03;5;17/05/25;89.00;CCR
T03;6;17/05/25;10.00;DIN
T01;7;18/05/25;350.00;PIX
T02;8;18/05/25;50.00;DIN
T04;9;18/05/25;60.00;DEB
T05;10;18/05/25;99.90;CCR
Esse cenário é perfeito para aplicação de um job em Spring Batch, pois envolve:
- Processamento em lote (diário);
- Grandes volumes de dados (vendas de todos os terminais);
- Validação, transformação e persistência;
- Possibilidade de falhas (e necessidade de reprocessamento controlado).
Setup
Perfeito! Vamos montar esse projeto passo a passo, simulando tudo como se você estivesse no Spring Initializr, usando o H2 em modo arquivo e o Spring Batch para processar o .csv que definimos.
Spring Initializr
Acesse: https://start.spring.io
- Project: Maven
- Language: Java
- Spring Boot: escolha a versão estável mais recente (ex:
3.2.x) - Group:
com.super-mercado - Artifact:
spring-batch-pdv - Name:
spring-batch-pdv - Package Name:
com.super.mercado.springbatchpdv - Packaging: Jar
- Java: 17 ou 21 (dependendo da sua preferência/ambiente)
Dependências
- Spring Batch
- Spring Data JPA
- Postgre Database
- Lombok
- Spring Boot DevTools (opcional para facilitar o desenvolvimento)
Clique em Generate para baixar o .zip.
Estrutura do Projeto
Depois de descompactar e abrir no IDE (IntelliJ, Eclipse, VS Code), a estrutura básica será algo como:
spring-batch-pdv/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/seuprojeto/springbatchpdv/
│ │ │ ├── SpringBatchPdvApplication.java
│ │ │ └── config/
│ │ │ └── job/
│ │ │ └── model/
│ │ │ └── processor/
│ │ │ └── writer/
│ ├── main/resources/
│ │ ├── application.properties
Postgres
No arquivo application.properties, configure o banco H2 para salvar os dados em arquivo:
#Configuração do PostgreSQL
spring.datasource.url=jdbc:postgresql://localhost:5432/super-mercado-db
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.datasource.driver-class-name=org.postgresql.Driver
#JPA
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
#Ativar o console do Spring Batch
spring.batch.jdbc.initialize-schema=always
#Desativa a execução automática dos jobs (executa só no @Scheduled)
spring.batch.job.enabled=false
#Pasta de arquivos CSV
input.folder=/super-mercado/pdvs
Batch tables
O Spring Batch cria um conjunto de tabelas padrão para gerenciar e persistir o histórico dos jobs e steps executados. Essas tabelas são essenciais para garantir controle transacional, reatividade a falhas, e a possibilidade de recomeçar jobs do ponto em que pararam.
| Tabela | Descrição rápida |
|---|---|
BATCH_JOB_INSTANCE | Registro único de cada instância lógica de job |
BATCH_JOB_EXECUTION | Execuções concretas de uma instância de job |
BATCH_JOB_EXECUTION_PARAMS | Parâmetros usados na execução do job |
BATCH_STEP_EXECUTION | Detalhes da execução de cada step dentro do job |
BATCH_STEP_EXECUTION_CONTEXT | Contexto serializado dos steps |
BATCH_JOB_EXECUTION_CONTEXT | Contexto serializado do job |
BATCH_JOB_INSTANCE | Link entre nome do job e os parâmetros que o definem |
BATCH_JOB_EXECUTION_SEQ | Gerador de ID para JOB_EXECUTION (PostgreSQL etc.) |
BATCH_STEP_EXECUTION_SEQ | Gerador de ID para STEP_EXECUTION |
BATCH_JOB_SEQ | Gerador de ID para JOB_INSTANCE |
Model
Vamos definir as classes de modelo da aplicação como Venda e FormaPagamento.
public enum FormaPagamento {
DINHEIRO("Dinheiro", "DIN"),
PIX("Pix", "PIX"),
DEBITO("Débito", "DEB"),
CREDITO("Crédito", "CCR");
private final String nome;
private final String sigla;
// Construtor
private FormaPagamento(String nome, String sigla) {
this.nome = nome;
this.sigla = sigla;
}
// Getters
public String getNome() {
return nome;
}
public String getSigla() {
return sigla;
}
// Método para buscar pelo sigla
public static FormaPagamento fromSigla(String sigla) {
for (FormaPagamento formaPagamento : FormaPagamento.values()) {
if (formaPagamento.getSigla().equalsIgnoreCase(sigla)) {
return formaPagamento;
}
}
throw new IllegalArgumentException("Sigla de pagamento inválida: " + sigla);
}
}
Config
Vamos definir as classes responsáveis para realizar a execução do processo
import com.example.demo.config.listener.ArquivoProcessadoListener;
import com.example.demo.model.FormaPagamento;
import com.example.demo.model.Venda;
import jakarta.persistence.EntityManagerFactory;
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.database.JpaItemWriter;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.FileSystemResource;
import org.springframework.transaction.PlatformTransactionManager;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
@Configuration
public class BatchConfig {
@Value("${input.folder}")
private String inputFolder;
private final JobRepository jobRepository;
private final PlatformTransactionManager transactionManager;
private final EntityManagerFactory entityManagerFactory;
public BatchConfig(JobRepository jobRepository,
PlatformTransactionManager transactionManager,
EntityManagerFactory entityManagerFactory) {
this.jobRepository = jobRepository;
this.transactionManager = transactionManager;
this.entityManagerFactory = entityManagerFactory;
}
@Bean
public FlatFileItemReader<Venda> leitorVenda() {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yy");
return new FlatFileItemReaderBuilder<Venda>()
.name("leitorVenda")
.resource(new FileSystemResource(inputFolder + "/vendas.csv"))
.delimited()
.delimiter(";")
.names("idTerminal","numeroTransacao", "dataMovimento", "valorVenda", "formaPagamento")
.fieldSetMapper(fieldSet -> {
Venda venda = new Venda();
venda.setNumeroTransacao(fieldSet.readInt("numeroTransacao"));
venda.setIdTerminal(fieldSet.readString("idTerminal"));
venda.setDataMovimento(LocalDate.parse(fieldSet.readString("dataMovimento"), formatter));
venda.setValorVenda(fieldSet.readDouble("valorVenda"));
venda.setFormaPagamento(FormaPagamento.fromSigla(fieldSet.readString("formaPagamento")));
return venda;
})
.build();
}
@Bean
public JpaItemWriter<Venda> writerVenda() {
JpaItemWriter<Venda> writer = new JpaItemWriter<>();
writer.setEntityManagerFactory(entityManagerFactory);
return writer;
}
@Bean
public Step step1() {
return new StepBuilder("step1", jobRepository)
.<Venda, Venda>chunk(10, transactionManager)
.reader(leitorVenda())
.writer(writerVenda())
.faultTolerant()
.skip(Exception.class)
.skipLimit(1)
.listener(new ArquivoProcessadoListener(inputFolder, inputFolder + "/processados"))
.build();
}
@Bean
public Job importVendasJob(Step step1) {
return new JobBuilder("importVendasJob", jobRepository)
.start(step1)
.build();
}
}
Troubleshooting
Troubleshooting é o processo de identificar, diagnosticar e resolver problemas em um sistema, aplicação, infraestrutura ou qualquer ambiente técnico.
Em português, pode ser traduzido como “diagnóstico de problemas” ou “resolução de falhas”.
Vamos imaginar que é recorrente a recepção de arquivos contendo uma definição equivocada das formas de pagamentos disponíveis onde impossibilita o processamento dos arquivos recebidos.
public enum FormaPagamento {
DIN("DIN"), PIX("PIX"), DEB("DEB"), CCR("CCR");
private final String sigla;
FormaPagamento(String sigla) {
this.sigla = sigla;
}
public static FormaPagamento fromSigla(String sigla) {
return Arrays.stream(FormaPagamento.values())
.filter(fp -> fp.sigla.equalsIgnoreCase(sigla))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Forma de pagamento inválida: " + sigla));
}
}