Desenvolvimento com Spring Batch — Jobs

Giuliana Bezerra
Nov 5 · 5 min read

Existem inúmeras configurações que podem ser realizadas nos jobs do Spring Batch visando aproveitar ao máximo o uso desse framework. Para abordar o assunto de maneira prática e resumida, elaborei esse post que apresenta trechos de código com a implementação dessas configurações em diferentes cenários.

Se você ainda não conhece o Spring Batch, sugiro a leitura desse post antes de continuar.

Esse post é baseado principalmente neste livro, que contém as orientações mais atuais em 2019 sobre o uso do Spring Batch.

Implementação e execução do Job

A implementação de jobs é bem simples. Uma vez criado o projeto, o job pode ser declarado na classe da Application que foi gerada:

@Bean
public Job job() {
return this.jobBuilderFactory
.get("nomeDoJob")
.start(step())
.build();
}

Vários steps podem ser encadeados para criar uma sequência:

@Bean
public Job job() {
return this.jobBuilderFactory
.get("nomeDoJob")
.start(step1())
.next(step2())

.build();
}

O seguinte comando pode ser utilizado para executar um job, se mantivermos o lançador padrão (JobLauncherCommandLineRunner):

$ java -jar demo.jar

Com parâmetros:

$ java -jar demo.jar param=valor

É possível informar o tipo dos parâmetros:

$ java -jar demo.jar param(date)=01/11/2019

Quando executamos um job, é criado um objeto JobInstance, que representa uma execução completa do job. Essa instância é por padrão identificada pelos parâmetros fornecidos. Se você quiser informar que um parâmetro específico não deve compor a identidade dessa instância, é possível fazer isso adicionando um “-” na frente do parâmetro (isso é bem útil quando queremos reiniciar um job mudando algum parâmetro que passamos com valor errado, por exemplo):

$ java -jar demo.jar param1(date)=01/11/2019 -param2="Não é identidade"

Para acessar os parâmetros informados dentro do job:

@StepScope
@Bean
public Tasklet helloWorldTasklet(@Value("#{jobParameters['name']}") String name) {
return (contribution, chunkContext) ->{
System.out.println(String.format("Hello, %s!", name));
return RepeatStatus.FINISHED;
};
}

Observe que para ter acesso ao jobParameters é necessário que o escopo do bean seja de Step ou Job. Isso porque o objeto só estará disponível no escopo de execução do job e step.

E se você precisar rodar o job diariamente com os mesmos parâmetros? Por padrão, o Spring Batch não vai permitir a reexecução, a não ser que você modifique os parâmetros. Um workaround é utilizar o JobParametersIncrementer:

@Beanpublic Job job() {
return this.jobBuilderFactory
.get("nomeDoJob")
.start(step())
.incrementer(new RunIdIncrementer())
.build();
}

O incrementador vai gerar um novo parâmetro de execução chamado run.id, que é o que vai permitir múltiplas execuções do job.

Validação de parâmetros

A sintaxe de construção dos jobs permite adicionar um mecanismo de validação:

public Job job() {
return this.jobBuilderFactory
.get("nomeDoJob")
.start(step())
.incrementer(new DailyJobTimestamper())
.validator(validator())
.build();
}

O validador pode verificar se os parâmetros opcionais e obrigatórios foram informados:

public DefaultJobParametersValidator defaultJobParametersValidator()
{
DefaultJobParametersValidator validator =
new DefaultJobParametersValidator();
validator.setRequiredKeys(new String[] { "fileName" });
validator.setOptionalKeys(new String[] { "name", "run.id", "currentDate" });
return validator;
}

Também é possível adicionar mensagens customizadas de validação dos parâmetros:

class ParameterValidator implements JobParametersValidator {
@Override
public void validate(JobParameters parameters) throws JobParametersInvalidException {
String fileName = parameters.getString("fileName");
if (!StringUtils.hasText(fileName)) {
throw new JobParametersInvalidException("fileName parameter is missing");
} else if (!StringUtils.endsWithIgnoreCase(fileName, "csv")) {
throw new JobParametersInvalidException("fileName parameter does " + "not use the csv file extension");
}
}
}

O problema é que agora existem 2 validadores. Para utilizarmos múltiplos validadores, o Spring Batch provê um componente chamado CompositeJobParametersValidator:

public CompositeJobParametersValidator validator() {         
CompositeJobParametersValidator validator =
new CompositeJobParametersValidator();
DefaultJobParametersValidator defaultJobParametersValidator =
defaultJobParametersValidator();
defaultJobParametersValidator.afterPropertiesSet();
validator.setValidators(Arrays.asList(
new ParameterValidator(),
defaultJobParametersValidator));
return validator;
}

Com esse componente será possível combinar diversos validadores para o job.

Listeners

Existem momentos no ciclo de vida do job em que pode ser interessante realizar algum procedimento. Pensando nisso, o Spring Batch permite a criação de um JobListener:

class JobLoggerListener {
private static String START_MESSAGE = "%s is beginning execution";
private static String END_MESSAGE = "%s has completed with the status %s";
@BeforeJob
public void beforeJob(JobExecution jobExecution) {
System.out.println(String.format(START_MESSAGE,
jobExecution.getJobInstance().getJobName()));
}
@AfterJob
public void afterJob(JobExecution jobExecution) {
System.out.println(
String.format(END_MESSAGE,
jobExecution.getJobInstance().getJobName(),
jobExecution.getStatus()));
}
}

As anotações @BeforeJob e @AfterJob informam o momento no qual o método será executado. Finalmente, para utilizar o listener no job:

@Bean
public Job job() {
return this.jobBuilderFactory
.get("nome")
.start(step())
.incrementer(new DailyJobTimestamper())
.validator(validator())
.listener(
JobListenerFactoryBean.getListener(new JobLoggerListener())
)

.build();
}

Contexto de Execução

É possível ter o controle do fluxo de execução do job através do seu ExecutionContext. É ele que permite que um job possa reiniciar de onde parou na última execução, além de ser usado para passar informações durante a execução para outros componentes do job.

Por exemplo, o código abaixo recupera um parâmetro de execução do job e adiciona-o no ExecutionContext dentro de uma Tasklet:

public RepeatStatus execute(StepContribution step, ChunkContext context) throws Exception {
String name = (String) context.getStepContext()
.getJobParameters()
.get("name");
ExecutionContext stepContext = context.getStepContext()
.getStepExecution()
.getExecutionContext()
;
stepContext.put("user.name", name);
}

Uma outra necessidade é passar informações de um step para outro, e isso pode ser feito utilizando o ExecutionContextPromotionListener. O que ele faz é simplesmente pegar dados do contexto de execução do step e promovê-los para o contexto de execução do job. Dessa forma, o dado é acessível para outros steps do job. Para isso, temos que:

  1. Adicionar um dado no contexto de execução do step:
@StepScope
@Bean
public Tasklet helloWorldTasklet(
@Value("#{jobParameters['name']}") String name,
@Value("#{jobParameters['fileName']}") String fileName) {
return (contribution, chunkContext) -> {
ExecutionContext stepContext = chunkContext
.getStepContext()
.getStepExecution()
.getExecutionContext();
stepContext.put("user.name", name);
System.out.println(String.format("Hello, %s!", name));
System.out.println(String.format("fileName = %s", fileName));
return RepeatStatus.FINISHED;
};
}

2. Adicionar um listener no step que irá passar os dados:

@Bean
public Step step1() {
return stepBuilderFactory
.get("step1")
.tasklet(helloWorldTasklet(null,null))
.listener(promotionListener())
.build();
}

3. Criar o ExecutionContextPromotionListener:

@Bean
public StepExecutionListener promotionListener() {
ExecutionContextPromotionListener listener =
new ExecutionContextPromotionListener();
listener.setKeys(new String[] { "user.name" });
return listener;
}

4. Acessar o dado no próximo step:

@Bean
public Tasklet dummyTasklet() {
return (contribution, chunkContext) -> {
System.out.println(chunkContext
.getStepContext()
.getStepExecution()
.getJobExecution()
.getExecutionContext()
.get("user.name"));
return RepeatStatus.FINISHED;
};
}

O exemplo utilizou Tasklets, mas a mesma passagem de dados poderia ser feita entre elementos de um chunk (e.g., ItemWriter para ItemReader), seria necessário apenas obter o StepExecution da seguinte forma:

public class SavingItemWriter implements ItemWriter<Object> {       
private StepExecution stepExecution;
public void write(List<? extends Object> items) throws Exception {
ExecutionContext stepContext =
this.stepExecution.getExecutionContext();
stepContext.put("someKey", someObject);
}

@BeforeStep
public void saveStepExecution(StepExecution stepExecution) {
this.stepExecution = stepExecution;
}
}

Conclusão

Esse post explorou as diversas funcionalidades existentes no Spring Batch para a configuração de um job. Como o foco foi o job em si, os steps foram simples e não foram descritos em detalhes. Posts futuros abordarão esse tópico.

Referências

  • Michael T. Minella. 2019. The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud (2nd ed.). Apress, Berkely, CA, USA.

Giuliana Bezerra

Written by

Software Architect at Dataprev

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade