Spring Batch

Spring Batch provides reusable functions that are essential in processing large volumes of records, including logging/tracing, transaction management, job processing statistics, job restart, skip, and resource management. It also provides more advanced technical services and features that will enable extremely high-volume and high performance batch jobs through optimization and partitioning techniques. Simple as well as complex, high-volume batch jobs can leverage the framework in a highly scalable manner to process significant volumes of information.

Ok, isso é a definição da documentação oficial, mas e ai como eu uso esse negócio?

Considerando uma aplicação com Spring Boot, o primeiro passo é a dependência e a anotação de configuração:

compile("org.springframework.boot:spring-boot-starter-batch")
@SpringBootApplication
@EnableBatchProcessing
public class Application {
  public static void main (String[] args) {
    SpringApplication.run(Application.class, args);
  }
}

A partir desse ponto nossa aplicação já faz uso do Spring Batch e podemos começar a escrever código \o/mas por onde devemos começar? O Spring Batch possui o seguinte fluxo:

Reader > Processor > Writer

Os nomes são bem explicativos mas de maneira geral o Reader é o componente que irá ler os dados e disponibilizar para o próximo passo, o Processor é o passo seguinte no qual escrevemos alguma mudança nos nossos objetos e/ou filtramos qual queremos, e por fim o Writer é quem vai escrever nossos dados em algum lugar.

O Spring Batch já possui alguns Readers, Processors e Writers padrões. Um dos mais conhecidos é o reader para csv FlatFileItemReader o guia oficial do spring fala do mesmo (https://spring.io/guides/gs/batch-processing/)

Beleza! Agora que temos tudo isso em mente, vamos criar o nosso job seguindo um exemplo um pouco menos comum, como a leitura de um arquivo .txt o que já é um pouco mais difícil de se achar nos tutoriais por ai.

Abaixo vamos simular um arquivo gerado por um leitor biométrico:

0000000001010120170900112344567890
0000000002010120171200112344567890
0000000003010120171300112344567890
0000000004010120171800112344567890
0000000005020120170800112344567890
0000000006020120171200112344567890
0000000007020120171400112344567890
0000000008020120171800112344567890
0000000009030120170800112344567890
0000000010030120171200112344567890
0000000011030120171300112344567890
0000000012030120171700112344567890

Nesse arquivo os dados estão divididos da seguinte forma:

0000000001|010120170900|112344567890

Onde a primeira parte é um número sequencial gerado pelo próprio leitor de ponto, a segunda é o dia e hora do registro e por último é o PIS do funcionário.

Agora precisamos da nossa entidade:

@Data
@Entity
public class Register {
  
  private static final SimpleDateFormat DEFAULT_FORMATTER = new SimpleDateFormat("ddMMyyyyHHmm");
  
  @Id
  public Long id;
  
  public Date registerDate;
  
  public String pis;
  
  @SneakyThrows
  public Register (final String line) {
    this.id = Long.parseLong(line.substring(0, 10));
    this.registerDate = Register.DEFAULT_FORMATTER.parse(line.substring(10, 22));
    this.pis = line.substring(22);
  }
}

Seguindo então o padrão do Spring Batch vamos primeiro escrever o nosso Reader:

@Slf4j
@StepScope
@Component
public class RegisterReaderBatch implements ItemReader<Register>, InitializingBean {
  
  private BufferedReader br;
  
  private File file;
  
  @Value("#{jobParameters[fileName]}")
  private String fileName;
  
  @Override
  public Register read () throws Exception {
    try {
      final String line;
      if ((line = br.readLine()) != null) {
        return new Register(line);
      }
    } catch (IOException e) {
      log.error("expected exception: {}", e.getMessage());
    }
  
    br.close();
    file.delete();
    return null;
  }
  
  @Override
  public void afterPropertiesSet () throws Exception {
    file = new File(fileName);
    br = new BufferedReader(new FileReader(file));
  }
}

Os pontos importantes desse arquivo são:

  • afterPropertiesSet esse método será chamado depois que as propriedades forem injetadas pelo spring e ele é responsável por abrir o BufferedReader para podermos ler o .txt.
  • @Value("#{jobParameters[fileName]}") esse anotação mostra ao spring que o valor a ser injetado desse atributo deve ser o passado como parametro na chamada do processo batch.
  • catch (IOException e) essa exception é esperada. Quando um processo identificar o fim do arquivo ele irá fechar o BufferedReader e remover o arquivo, a outra thread do processo irá tentar ainda utilizar esse BufferedReader isso gera uma IOException, mas no nosso caso apenas o ignoramos.

O próximo passo é o Processor. Um ponto importante a ser dito é que esse é um passo opcional, e se esse método retornar null o item não passará para o próximo passo, então, o que vamos utilizar de lógica é que se o id for igual a null não deixamos ele passar.

@Slf4j
@StepScope
@Component
public class RegisterProcessorBatch implements ItemProcessor<Register, Register> {
  
  @Override
  public Register process (final Register item) throws Exception {
    log.info("processor: {}", item);
    if (item.getId() == null){
      return null;
    }
          
    return item;
  }
}

Para salvar os dados no banco utilizaremos o repository do Spring Data:

@Repository
public interface RegisterRepository extends CrudRepository<Register, String> {
}

E o Writer apenas pegará essa lista de registro e irá salvar no banco através do RegisterRepository

@Slf4j
@StepScope
@Component
public class RegisterWriterBatch implements ItemWriter<Register> {
  
  @Autowired
  private RegisterRepository registerRepository;
  
  @Override
  public void write (final List<? extends Register> items) throws Exception {
    log.info("writer: {}", items);
    registerRepository.save(items);
  }
}

Nesse ponto temos todos os passos, portanto podemos escrever o nosso componente que iniciará o job.

@Slf4j
@Component
public class RegisterBatchJob {
  
  @Autowired
  private JobBuilderFactory jobBuilderFactory;
  
  @Autowired
  private StepBuilderFactory stepBuilderFactory;
}

Nesse classe vamos escrever o Bean que dará início a tudo isso:

@Bean
public Job importRegisterJob (final Step myStep) {
  return jobBuilderFactory.get("importRegisterJob")
    .incrementer(new RunIdIncrementer())
    .flow(myStep)
    .end()
    .build();
}

Esse Bean recebe como parâmetro um step, um job no Spring Batch pode ter vários steps e cada um dele terá os passos de Reader, Processor e Writer. Nesse passo vamos fazer uso dos componentes que criamos:

@Bean
public Step myStep (final ItemReader<Register> registerReaderBatch,
                    final ItemWriter<Register> registerWriterBatch,
                    final ItemProcessor<Register, Register> registerProcessorBatch,
                    final TaskExecutor myExecutor) {
  return stepBuilderFactory.get("importBillingLineJob_step1")
    .<Register, Register>chunk(200)
    .reader(registerReaderBatch)
    .processor(registerProcessorBatch)
    .writer(registerWriterBatch)
    .taskExecutor(myExecutor)
    .build();
}

Nesse método definimos também o chunk. Essa propriedade define a quantidade de registro que estará dentro de uma transação. Também temos como dependência o executor, ele define como o job será executado e o mesmo é definido da seguinte forma:

@Bean
public TaskExecutor myExecutor () {
  final ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
  taskExecutor.setCorePoolSize(20);
  return taskExecutor;
}

Esse Executor faz com que sejam criadas 20 threads e os registros sejam divididos entre elas.

Certo, muito bonito… mas e aí, como rodamos todo esse monte de código???

No componente que vamos utilizar para chamar o job precisamos injetar os seguintes atributos:

@Autowired
private JobLauncher jobLauncher;
  
@Autowired
private Job importRegisterJob;

E por último teremos finalmente o método que chama tudo isso:

@SneakyThrows
public void myMethod () {
  final JobParameters jobParameter = new JobParametersBuilder()
    .addLong("time", System.currentTimeMillis())
    .addString("fileName", "/home/s2it_leferreira/meu_arquivo.txt")
    .toJobParameters();
  
  jobLauncher.run(job, jobParameter);
}

E com isso nós temos nosso job no spring batch devidamente configurado e pronto para ser usado.

Os pontos de atenção para a passagem de parâmetros é que só podemos passar tipos “básicos” (Long, String, Date e Double) isso nos força a apenas passar como parâmetro a referência do que queremos e não o objeto, por exemplo não poderíamos passar o File como jobParameter para fazer isso passamos o caminho do arquivo e o nosso processo que abre o arquivo.

Isso é tudo, pessoal! 😀

Comentários