Hello Spring Batch

Leonardo Ferreira
Dev Cave
Published in
5 min readJun 13, 2017

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! :D

--

--