Spring Boot e Spring Cloud

Fundamentos do Spring Batch 5: jobs, steps, chunks, restart e escalabilidade

Intermediário10 min de leituraAtualizado

Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês

Você vai saber projetar e executar um job do Spring Batch 5 no Spring Boot 3: modelar o trabalho como jobs e steps, montar steps orientados a chunks com readers, processors e writers, passar parâmetros com segurança usando step scope, tornar falhas ignoráveis ou passíveis de nova tentativa, reiniciar um job com falha de onde ele parou e escolher a estratégia de escalabilidade certa para grandes volumes.

O que você vai aprender

  • Explicar o modelo de domínio do Spring Batch: Job, JobInstance, JobExecution, Step, StepExecution e o JobRepository
  • Montar um step orientado a chunks com as APIs JobBuilder e StepBuilder do Spring Batch 5
  • Ler arquivos texto e bancos de dados e gravar em lote com os item readers e writers que já vêm no framework
  • Configurar skip, retry e restart e entender o que as tabelas do job repository registram
  • Passar parâmetros do job para beans com step scope e controlar o fluxo com transições condicionais
  • Escolher entre steps multithread, fluxos paralelos, particionamento e remote chunking

Há trabalhos que não se encaixam no modelo requisição-resposta: importar toda noite um arquivo com dois milhões de linhas, recalcular o saldo de todos os clientes, arquivar os eventos do trimestre passado, gerar os extratos mensais. Jobs assim precisam processar grandes volumes com memória limitada, gravar o progresso de forma incremental, sobreviver a um registro ruim sem descartar os bons e retomar depois de uma queda em vez de começar do zero. O Spring Batch é o framework que incorpora esses requisitos, para você não precisar reinventá-los com um laço for e uma entrada no cron. Esta página trata do Spring Batch 5 como autoconfigurado pelo Spring Boot 3, usando a API de builders em Java que substituiu as antigas factories e o XML.

O modelo de domínio

O vocabulário do Spring Batch corresponde diretamente aos metadados que ele armazena:

ConceitoSignificado
JobUm processo batch completo, formado por steps em ordem; identificado pelo nome
JobInstanceUma execução lógica de um job para um conjunto de parâmetros identificadores, por exemplo “import-orders de 2026-09-01”
JobExecutionUma tentativa física de executar uma JobInstance; uma instância com falha pode ter várias execuções
JobParametersPares chave-valor tipados informados no disparo; fazem parte da identidade da instância, a menos que sejam marcados como não identificadores
StepUma fase independente do job, com seu próprio controle transacional e seu próprio status
StepExecutionUma tentativa de execução de um step, com contadores: lidos, gravados, filtrados, ignorados, commits, rollbacks
ExecutionContextUm mapa persistente associado à execução de um job ou step, usado para registrar o progresso com vistas ao restart
JobRepositoryO armazenamento de tudo isso: um conjunto de tabelas BATCH_* em um banco de dados relacional
JobLauncher / JobOperatorOs pontos de entrada para iniciar, parar e reiniciar jobs

É o repositório que diferencia o Spring Batch de um agendador com um laço. Cada execução de step registra até onde chegou, então o restart de uma execução com falha começa no step que falhou, e um reader reiniciável continua a partir do último item com commit.

Em bancos embarcados, o Boot cria automaticamente as tabelas BATCH_JOB_INSTANCE, BATCH_JOB_EXECUTION, BATCH_JOB_EXECUTION_PARAMS, BATCH_STEP_EXECUTION e as duas *_EXECUTION_CONTEXT; em um banco de verdade, defina spring.batch.jdbc.initialize-schema=always uma vez ou, melhor ainda, copie o script de schema do seu banco de dados de dentro do jar spring-batch-core para uma migration do Flyway, para que as tabelas sejam versionadas junto com todo o resto.

Steps tasklet e steps orientados a chunks

Um step é ou uma tasklet (uma unidade de trabalho executada uma vez) ou orientado a chunks (lê os itens um a um, opcionalmente processa cada um e grava em grupos). Tasklets servem para tarefas de manutenção: descompactar um arquivo, chamar uma stored procedure, apagar um diretório. Chunks servem para tudo que processa um fluxo de registros.

@Configuration
class ImportOrdersJobConfig {

    @Bean
    Job importOrdersJob(JobRepository jobRepository, Step unpackStep, Step importStep) {
        return new JobBuilder("importOrders", jobRepository)
                .incrementer(new RunIdIncrementer())   // adds run.id: a new instance per launch, unless the last one failed
                .start(unpackStep)
                .next(importStep)
                .build();
    }

    @Bean
    Step unpackStep(JobRepository jobRepository, PlatformTransactionManager tx) {
        return new StepBuilder("unpack", jobRepository)
                .tasklet((contribution, chunkContext) -> {
                    Files.createDirectories(Path.of("work"));
                    return RepeatStatus.FINISHED;
                }, tx)
                .build();
    }

    @Bean
    Step importStep(JobRepository jobRepository, PlatformTransactionManager tx,
                    FlatFileItemReader<OrderCsvRow> reader,
                    ItemProcessor<OrderCsvRow, Order> processor,
                    JdbcBatchItemWriter<Order> writer) {
        return new StepBuilder("import", jobRepository)
                .<OrderCsvRow, Order>chunk(500, tx)     // 500 items per transaction
                .reader(reader)
                .processor(processor)
                .writer(writer)
                .build();
    }
}

JobBuilder e StepBuilder recebem o JobRepository diretamente; JobBuilderFactory e StepBuilderFactory, do Batch 4, estão depreciadas desde a 5.0 e não devem aparecer em código novo. O transaction manager agora é um argumento explícito de chunk() e tasklet(), o que deixa óbvio qual deles um step usa quando a aplicação tem vários data sources.

O laço de chunk funciona assim: o reader é chamado até que 500 itens tenham sido lidos (ou até ele retornar null, que indica o fim da entrada), cada item passa pelo processor (que pode retornar null para filtrá-lo), o writer recebe o Chunk resultante em uma única chamada e a transação recebe o commit. O Batch 5 mudou ItemWriter.write para aceitar um Chunk<? extends T> em vez de uma List; de resto, as interfaces continuam como sempre foram:

public interface ItemReader<T>    { T read() throws Exception; }                  // null = end of data
public interface ItemProcessor<I, O> { O process(I item) throws Exception; }      // null = filter out
public interface ItemWriter<T>    { void write(Chunk<? extends T> chunk) throws Exception; }

O tamanho do chunk é o principal parâmetro de ajuste. Chunks maiores significam menos commits e mais throughput, mas também mais trabalho perdido em um rollback e mais memória ocupada. De centenas a alguns milhares é o típico para gravações em banco; meça em vez de chutar.

Readers e writers

O Spring Batch traz readers e writers para as fontes mais comuns, todos construídos com builders fluentes no Batch 5:

Origem ou destinoReader / writer
Arquivos delimitados ou de largura fixaFlatFileItemReader, FlatFileItemWriter
JDBCJdbcCursorItemReader (um cursor aberto, em streaming), JdbcPagingItemReader (consultas por página, reiniciável e thread-safe), JdbcBatchItemWriter
JPAJpaPagingItemReader, JpaCursorItemReader, JpaItemWriter
Spring DataRepositoryItemReader, RepositoryItemWriter
XMLStaxEventItemReader, StaxEventItemWriter
JSONJsonItemReader, JsonFileItemWriter
MensageriaJmsItemReader/Writer, KafkaItemReader/Writer, AmqpItemReader/Writer
ComposiçãoCompositeItemProcessor, CompositeItemWriter, ClassifierCompositeItemWriter

Lendo um CSV para um record e gravando com batching JDBC:

@Bean
@StepScope   // created per step execution so the file name can come from job parameters
FlatFileItemReader<OrderCsvRow> orderReader(@Value("#{jobParameters['inputFile']}") Resource inputFile) {
    return new FlatFileItemReaderBuilder<OrderCsvRow>()
            .name("orderReader")                  // required: key for restart state in the execution context
            .resource(inputFile)
            .linesToSkip(1)                       // header
            .delimited().delimiter(';')
            .names("orderNumber", "customerId", "sku", "quantity", "unitPrice")
            .targetType(OrderCsvRow.class)        // maps columns onto a record's components by name
            .build();
}

@Bean
JdbcBatchItemWriter<Order> orderWriter(DataSource dataSource) {
    return new JdbcBatchItemWriterBuilder<Order>()
            .dataSource(dataSource)
            .sql("""
                 insert into orders (order_number, customer_id, total_cents, created_at)
                 values (:orderNumber, :customerId, :totalCents, :createdAt)
                 """)
            .beanMapped()                         // :name placeholders resolved from the item's JavaBean getters
            .build();
}

O JdbcBatchItemWriter envia o chunk inteiro como um único batch JDBC, e é daí que vem a maior parte do throughput de um job batch. O beanMapped() depende de propriedades no estilo getter; para records ou colunas calculadas, forneça, em vez disso, um ItemSqlParameterSourceProvider que monte um MapSqlParameterSource a partir do item. Ao gravar via JPA, o JpaItemWriter faz flush por chunk; ative o batching JDBC do Hibernate (hibernate.jdbc.batch_size) e use identificadores SEQUENCE, como explicado no guia de Spring Data JPA, senão o “batch” continuará sendo um comando por linha.

Escolha o JdbcCursorItemReader para uma leitura simples, em uma única thread, de uma tabela grande: uma consulta, um result set aberto, memória constante. Escolha o JdbcPagingItemReader quando o step for multithread ou particionado, porque um cursor não pode ser compartilhado e porque a posição de um reader paginado é uma chave de ordenação que pode ser persistida para o restart. A paginação exige uma chave de ordenação determinística (uma coluna única), senão as páginas se sobrepõem.

Parâmetros do job e step scope

Normalmente os beans são criados quando o contexto sobe, muito antes de alguém saber qual arquivo a execução desta noite deve importar. @StepScope (e @JobScope) adia a criação para o momento em que o step começa, que é quando as expressões #{jobParameters['...']}, #{jobExecutionContext['...']} e #{stepExecutionContext['...']} podem ser resolvidas. Todo bean que lê um parâmetro do job precisa ter step scope; um erro comum é esquecer a anotação e receber null.

Os parâmetros são tipados e, desde o Batch 5, podem ser de qualquer tipo que tenha um conversor registrado; na linha de comando, a forma textual padrão é name=value,type,identifying, por exemplo inputFile=orders-2026-09-01.csv,java.lang.String,true. O Boot repassa os argumentos da linha de comando para o job quando spring.batch.job.enabled é true (o padrão): java -jar import.jar inputFile=file:/data/orders.csv. Com vários jobs na mesma aplicação, spring.batch.job.name escolhe qual roda na inicialização, e spring.batch.job.enabled=false permite disparar os jobs pelo JobLauncher a partir de um agendador ou de um endpoint HTTP.

Use parâmetros identificadores para o que define a execução (data de negócio, nome do arquivo) e não identificadores para ajustes técnicos (tamanho de chunk alternativo, flag de dry-run), para que uma nova tentativa com outro ajuste continue sendo a mesma instância e possa ser reiniciada.

Falhas: skip, retry e restart

Um batch que para na primeira linha malformada não serve para muita coisa. A tolerância a falhas é configurada no step:

@Bean
Step importStep(/* ... */) {
    return new StepBuilder("import", jobRepository)
            .<OrderCsvRow, Order>chunk(500, tx)
            .reader(reader).processor(processor).writer(writer)
            .faultTolerant()
            .skip(FlatFileParseException.class)          // bad line: drop it and continue
            .skip(UnknownCustomerException.class)
            .skipLimit(100)                              // more than 100 skips fails the step
            .listener(rejectedLinesListener)             // SkipListener: write rejects to a file
            .retry(DeadlockLoserDataAccessException.class)
            .retryLimit(3)                               // transient database error: try again
            .noRollback(ValidationWarning.class)         // exception that should not roll back the chunk
            .build();
}

Ignorar itens na fase de gravação sai mais caro do que parece: como o writer recebeu o chunk inteiro, o Spring Batch não consegue saber qual item causou a falha, então faz rollback e reprocessa o chunk item a item, em transações de um item só, para isolar o culpado. Mantenha limites de skip modestos e faça a validação no processor, onde um skip não custa nada.

O restart é o que você ganha com o repositório. Se uma execução falha, disparar o job de novo com os mesmos parâmetros identificadores cria uma nova JobExecution para a mesma JobInstance, pula os steps que já terminaram e reinicia o step com falha. Readers que implementam ItemStream (todos os de arquivo e os paginados implementam, desde que recebam um name) guardam a sua posição no execution context do step a cada commit e retomam a partir dela. Duas opções mudam isso: .allowStartIfComplete(true) em um step o executa de novo mesmo depois de um sucesso, e .preventRestart() no job proíbe restarts em jobs que não são idempotentes. O startLimit de um step limita quantas vezes ele pode ser tentado.

Controlando o fluxo

Por padrão, os steps rodam em sequência. Transições condicionais desviam o fluxo com base no ExitStatus de um step:

return new JobBuilder("importOrders", jobRepository)
        .start(importStep)
            .on("FAILED").to(notifyOpsStep)          // exit code pattern matching, "*" is a wildcard
            .from(importStep).on("*").to(reconcileStep)
        .from(reconcileStep)
            .on("NOTHING_TO_DO").end()                // custom exit status set by a StepExecutionListener
            .from(reconcileStep).on("*").to(reportStep)
        .end()
        .build();

ExitStatus é diferente de BatchStatus: o batch status (COMPLETED, FAILED, STOPPED…) é o que o repositório registra, enquanto o exit status é uma string que um listener ou um JobExecutionDecider pode definir livremente para conduzir o roteamento. Um decider é um bean que implementa decide(JobExecution, StepExecution) e retorna um FlowExecutionStatus; é a ferramenta certa quando a decisão depende dos dados e não do resultado de um step. Os listeners (JobExecutionListener, StepExecutionListener, ChunkListener, ItemReadListener, ItemProcessListener, ItemWriteListener, SkipListener) se conectam a cada nível do ciclo de vida e são o lugar certo para logging, auditoria e arquivos de rejeitados.

Escalabilidade

Quando um step de chunks em uma única thread não é rápido o bastante, o Spring Batch oferece quatro opções, mais ou menos em ordem de complexidade:

  1. Step multithread. Dê ao step um TaskExecutor; cada thread processa os seus próprios chunks. Readers e writers precisam ser thread-safe, o que descarta cursores e o FlatFileItemReader (envolva este último em um SynchronizedItemStreamReader), e o restart deixa de ser confiável, porque a ordem dos itens não é definida.
  2. Steps paralelos. Steps independentes rodam ao mesmo tempo em um split com um TaskExecutor. Não há preocupação com thread-safety dentro de cada step; o paralelismo é entre os steps.
  3. Particionamento. Um Partitioner divide a entrada em faixas (faixas de ids, um arquivo por partição, uma região por partição), e um PartitionHandler executa uma cópia do step worker por partição, com a sua própria StepExecution, localmente em threads ou remotamente em outras JVMs. Cada partição pode ser reiniciada de forma independente. É a opção de referência para tabelas grandes de banco de dados.
  4. Remote chunking. O manager lê os itens e os envia por mensageria; os workers processam e gravam. Útil quando o gargalo é o processamento, e não a leitura, e a entrada não pode ser particionada.

Um step particionado local fica assim:

@Bean
Step partitionedImport(JobRepository jobRepository, Step importWorkerStep, TaskExecutor executor) {
    return new StepBuilder("partitionedImport", jobRepository)
            .partitioner("importWorker", new IdRangePartitioner(dataSource))   // yields minId/maxId per partition
            .step(importWorkerStep)
            .gridSize(8)
            .taskExecutor(executor)
            .build();
}

O reader do worker tem step scope e lê #{stepExecutionContext['minId']} e ['maxId'] para montar a sua consulta. Antes de qualquer coisa, confirme que o gargalo é o banco de dados, e não a JVM: um writer sem batching JDBC, ou um processor que faz uma consulta por item, costuma ser mais lento do que qualquer falta de threads.

Execução e testes

O spring-batch-test fornece @SpringBatchTest, que registra um JobLauncherTestUtils e um JobRepositoryTestUtils em um contexto @SpringBootTest. Dispare o job inteiro ou um único step com parâmetros e faça as asserções sobre a JobExecution resultante:

@SpringBootTest
@SpringBatchTest
class ImportOrdersJobTest {

    @Autowired JobLauncherTestUtils jobLauncherTestUtils;

    @Test
    void importsValidRowsAndSkipsBadOnes() throws Exception {
        JobParameters params = new JobParametersBuilder()
                .addString("inputFile", "classpath:orders-sample.csv")
                .addLong("run.id", System.currentTimeMillis())
                .toJobParameters();

        JobExecution execution = jobLauncherTestUtils.launchJob(params);

        assertThat(execution.getStatus()).isEqualTo(BatchStatus.COMPLETED);
        StepExecution step = execution.getStepExecutions().stream()
                .filter(s -> s.getStepName().equals("import")).findFirst().orElseThrow();
        assertThat(step.getWriteCount()).isEqualTo(998);
        assertThat(step.getSkipCount()).isEqualTo(2);
    }
}

Defina spring.batch.job.enabled=false nas propriedades de teste para que o Boot não dispare também o job quando o contexto subir. Na operação, o Spring Batch não tem console web embutido desde que o Spring Batch Admin foi aposentado; Spring Cloud Data Flow, um pequeno endpoint do Actuator sobre o JobExplorer ou SQL puro nas tabelas BATCH_* são as formas usuais de ver o que rodou.

Erros comuns

Adicionar @EnableBatchProcessing por causa de um tutorial antigo. No Boot 3, isso desliga a autoconfiguração; os sintomas são beans JobRepository ausentes ou jobs que nunca disparam.

Esquecer @StepScope em beans que usam parâmetros do job. A expressão é resolvida como null na subida do contexto, e a falha aparece longe da causa.

Fazer commit por item. Um tamanho de chunk 1, ou um writer que abre a própria transação para cada item, joga fora o motivo de usar um framework batch.

Ler com JPA sem nunca limpar o contexto de persistência. JpaPagingItemReader e JpaItemWriter cuidam disso a cada chunk, mas um reader escrito à mão que carrega entidades em um laço faz o cache de primeiro nível crescer até a memória acabar.

Paginação não determinística. Um reader paginado ordenado por uma coluna não única pula ou duplica linhas entre as páginas. Ordene pela chave primária.

Tornar tudo ignorável. skip(Exception.class) com um limite alto transforma corrupção de dados em um aviso que ninguém lê. Ignore apenas exceções específicas e compreendidas, e mande os rejeitados para algum lugar visível.

Disparar a mesma instância duas vezes. Sem um incrementer ou um parâmetro que a diferencie, o segundo disparo de um job concluído falha; com run id em tudo, reiniciar uma instância com falha fica impossível, porque todo disparo é novo. Use parâmetros de negócio para a identidade e reserve o run.id para jobs que realmente não têm chave natural.

Principais pontos

  • O Spring Batch modela o trabalho como jobs compostos de steps e registra cada execução em um JobRepository apoiado em banco de dados, que é o que torna possíveis o restart, a auditoria e a contagem de skips.
  • No Spring Boot 3, use JobBuilder e StepBuilder com um JobRepository e um transaction manager explícitos; deixe @EnableBatchProcessing de fora.
  • Steps orientados a chunks leem os itens um a um, processam e gravam em chunks transacionais; o tamanho do chunk é o principal parâmetro de ajuste, e o batching JDBC, a principal fonte de velocidade.
  • Beans com step scope recebem os parâmetros do job e os valores do execution context tarde o bastante para que eles já existam; os parâmetros identificadores definem a JobInstance.
  • Skip e retry tornam os steps tolerantes a registros ruins e erros transitórios; o restart retoma uma execução com falha a partir do último commit.
  • Escale com particionamento para tabelas grandes, steps multithread para readers thread-safe, fluxos paralelos para steps independentes e remote chunking quando o processamento domina.

Fontes e leituras complementares

Perguntas frequentes

Ainda preciso de @EnableBatchProcessing no Spring Boot 3?
Não, e adicioná-la muda o comportamento. O Spring Boot 3 autoconfigura o JobRepository, o JobLauncher e os beans relacionados a partir do seu DataSource; @EnableBatchProcessing diz ao Boot para sair totalmente de cena, e você passa a cuidar da infraestrutura por conta própria. Não use, a menos que queira deliberadamente configurar o Spring Batch à mão, por exemplo para usar um DataSource dedicado às tabelas de metadados.
Por que meu job diz “A job instance already exists and is complete”?
Uma JobInstance é identificada pelo nome do job mais os seus parâmetros identificadores. Depois que uma instância termina com sucesso, o Spring Batch se recusa a executá-la de novo com os mesmos parâmetros, para proteger você de reprocessar dados. Passe um parâmetro que mude a cada execução (um run id do RunIdIncrementer, o nome do arquivo de entrada, uma data de negócio) ou marque um parâmetro como não identificador se ele não deve contar.
Dá para rodar o Spring Batch sem banco de dados?
O Spring Batch 5 removeu o antigo job repository em memória baseado em maps, então o JobRepository padrão precisa de um banco relacional para as tabelas de metadados, que é também o que faz o restart funcionar. Para desenvolvimento, um H2 embarcado basta. Versões 5.x recentes adicionam um repositório sem recursos para os casos em que o restart não é necessário; leia as release notes da versão que você usa antes de depender dele.
Qual a diferença entre commit interval e page size?
O commit interval (o tamanho do chunk) é quantos itens são processados antes de o writer ser chamado e a transação receber o commit. O page size é dos readers paginados, como o JdbcPagingItemReader, e controla quantas linhas cada consulta ao banco busca. São independentes; um erro comum é configurá-los com valores diferentes e depois estranhar que as consultas não se alinhem com os commits. Deixá-los iguais é um padrão sensato.