Desenvolvimento com Spring Batch — Jobs

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.jarCom parâmetros:
$ java -jar demo.jar param=valorÉ possível informar o tipo dos parâmetros:
$ java -jar demo.jar param(date)=01/11/2019Quando 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:
- 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.
