Lote de Primavera - Tasklets vs Chunks

Lote de Primavera - Tasklets vs Chunks

1. Introdução

Spring Batch provides two different ways for implementing a job: using tasklets and chunks.

Neste artigo, aprenderemos como configurar e implementar os dois métodos usando um exemplo simples da vida real.

2. Dependências

Vamos começar poradding the required dependencies:


    org.springframework.batch
    spring-batch-core
    4.0.0.RELEASE


    org.springframework.batch
    spring-batch-test
    4.0.0.RELEASE
    test

Para obter a versão mais recente despring-batch-coreespring-batch-test, consulte o Maven Central.

3. Nosso Caso de Uso

Vamos considerar um arquivo CSV com o seguinte conteúdo:

Mae Hodges,10/22/1972
Gary Potter,02/22/1953
Betty Wise,02/17/1968
Wayne Rose,04/06/1977
Adam Caldwell,09/27/1995
Lucille Phillips,05/14/1992

Ofirst position of each line represents a person’s name and the second position represents his/her date of birth.

Nosso caso de uso é paragenerate another CSV file that contains each person’s name and age:

Mae Hodges,45
Gary Potter,64
Betty Wise,49
Wayne Rose,40
Adam Caldwell,22
Lucille Phillips,25

Agora que nosso domínio está claro, vamos prosseguir e construir uma solução usando ambas as abordagens. Vamos começar com tasklets.

4. Abordagem de Tasklets

4.1. Introdução e Design

Os Tasklets destinam-se a executar uma única tarefa em uma etapa. Nosso trabalho consistirá em várias etapas que serão executadas uma após a outra. Each step should perform only one defined task.

Nosso trabalho consistirá em três etapas:

  1. Leia linhas do arquivo CSV de entrada.

  2. Calcule a idade de todas as pessoas no arquivo CSV de entrada.

  3. Escreva o nome e a idade de cada pessoa em um novo arquivo CSV de saída.

Agora que o quadro geral está pronto, vamos criar uma aula por etapa.

LinesReader será responsável pela leitura dos dados do arquivo de entrada:

public class LinesReader implements Tasklet {
    // ...
}

LinesProcessor irá calcular a idade para cada pessoa no arquivo:

public class LinesProcessor implements Tasklet {
    // ...
}

Finalmente,LinesWriter terá a responsabilidade de escrever nomes e idades em um arquivo de saída:

public class LinesWriter implements Tasklet {
    // ...
}

Neste ponto,all our steps implement Tasklet interface. Isso nos forçará a implementar seu métodoexecute:

@Override
public RepeatStatus execute(StepContribution stepContribution,
  ChunkContext chunkContext) throws Exception {
    // ...
}

Este método é onde adicionaremos a lógica para cada etapa. Antes de começar com esse código, vamos configurar nosso trabalho.

4.2. Configuração

Precisamosadd some configuration to Spring’s application context. Depois de adicionar a declaração de bean padrão para as classes criadas na seção anterior, estamos prontos para criar nossa definição de trabalho:

@Configuration
@EnableBatchProcessing
public class TaskletsConfig {

    @Autowired
    private JobBuilderFactory jobs;

    @Autowired
    private StepBuilderFactory steps;

    @Bean
    protected Step readLines() {
        return steps
          .get("readLines")
          .tasklet(linesReader())
          .build();
    }

    @Bean
    protected Step processLines() {
        return steps
          .get("processLines")
          .tasklet(linesProcessor())
          .build();
    }

    @Bean
    protected Step writeLines() {
        return steps
          .get("writeLines")
          .tasklet(linesWriter())
          .build();
    }

    @Bean
    public Job job() {
        return jobs
          .get("taskletsJob")
          .start(readLines())
          .next(processLines())
          .next(writeLines())
          .build();
    }

    // ...

}

Isso significa que nosso“taskletsJob” consistirá em três etapas. O primeiro (readLines) executará o tasklet definido no beanlinesReadere moverá para a próxima etapa:processLines. ProcessLines executará o tasklet definido no beanlinesProcessore para a etapa final:writeLines.

Nosso fluxo de trabalho está definido e estamos prontos para adicionar alguma lógica!

4.3. Modelo e utilitários

Como manipularemos linhas em um arquivo CSV, vamos criar uma classeLine:

public class Line implements Serializable {

    private String name;
    private LocalDate dob;
    private Long age;

    // standard constructor, getters, setters and toString implementation

}

Observe queLine implementaSerializable. Isso ocorre porqueLine atuará como um DTO para transferir dados entre as etapas. De acordo com Spring Batch,objects that are transferred between steps must be serializable.

Por outro lado, podemos começar a pensar em ler e escrever linhas.

Para isso, faremos uso do OpenCSV:


    com.opencsv
    opencsv
    4.1

Procure a versãoOpenCSV mais recente no Maven Central.

Assim que o OpenCSV for incluído,we’re also going to create a FileUtils class. Ele fornecerá métodos para ler e escrever linhas CSV:

public class FileUtils {

    public Line readLine() throws Exception {
        if (CSVReader == null)
          initReader();
        String[] line = CSVReader.readNext();
        if (line == null)
          return null;
        return new Line(
          line[0],
          LocalDate.parse(
            line[1],
            DateTimeFormatter.ofPattern("MM/dd/yyyy")));
    }

    public void writeLine(Line line) throws Exception {
        if (CSVWriter == null)
          initWriter();
        String[] lineStr = new String[2];
        lineStr[0] = line.getName();
        lineStr[1] = line
          .getAge()
          .toString();
        CSVWriter.writeNext(lineStr);
    }

    // ...
}

Observe quereadLine atua como um wrapper sobre o métodoreadNext do OpenCSV e retorna um objetoLine.

Da mesma forma,writeLine envolvewriteNext do OpenCSV recebendo um objetoLine. A implementação completa dessa classe pode ser encontrada emthe GitHub Project.

Neste ponto, estamos prontos para começar a implementação de cada etapa.

4.4. LinesReader

Vamos prosseguir e concluir nossa aulaLinesReader:

public class LinesReader implements Tasklet, StepExecutionListener {

    private final Logger logger = LoggerFactory
      .getLogger(LinesReader.class);

    private List lines;
    private FileUtils fu;

    @Override
    public void beforeStep(StepExecution stepExecution) {
        lines = new ArrayList<>();
        fu = new FileUtils(
          "taskletsvschunks/input/tasklets-vs-chunks.csv");
        logger.debug("Lines Reader initialized.");
    }

    @Override
    public RepeatStatus execute(StepContribution stepContribution,
      ChunkContext chunkContext) throws Exception {
        Line line = fu.readLine();
        while (line != null) {
            lines.add(line);
            logger.debug("Read line: " + line.toString());
            line = fu.readLine();
        }
        return RepeatStatus.FINISHED;
    }

    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        fu.closeReader();
        stepExecution
          .getJobExecution()
          .getExecutionContext()
          .put("lines", this.lines);
        logger.debug("Lines Reader ended.");
        return ExitStatus.COMPLETED;
    }
}

O métodoLinesReader’s execute cria uma instânciaFileUtils sobre o caminho do arquivo de entrada. Então,adds lines to a list until there’re no more lines to read.

Nossa classealso implements StepExecutionListener que fornece dois métodos extras:beforeStepeafterStep. Usaremos esses métodos para inicializar e fechar coisas antes e depois das execuções deexecute.

Se dermos uma olhada no códigoafterStep, iremos notar a linha onde a lista de resultados (lines) é colocada no contexto do trabalho para torná-lo disponível para a próxima etapa:

stepExecution
  .getJobExecution()
  .getExecutionContext()
  .put("lines", this.lines);

Neste ponto, nosso primeiro passo já cumpriu sua responsabilidade: carregar linhas CSV em umList na memória. Vamos passar para a segunda etapa e processá-los.

4.5. LinesProcessor

LinesProcessor will also implement StepExecutionListener and of course, Tasklet. Isso significa que implementará os métodosbeforeStep,executeeafterStep também:

public class LinesProcessor implements Tasklet, StepExecutionListener {

    private Logger logger = LoggerFactory.getLogger(
      LinesProcessor.class);

    private List lines;

    @Override
    public void beforeStep(StepExecution stepExecution) {
        ExecutionContext executionContext = stepExecution
          .getJobExecution()
          .getExecutionContext();
        this.lines = (List) executionContext.get("lines");
        logger.debug("Lines Processor initialized.");
    }

    @Override
    public RepeatStatus execute(StepContribution stepContribution,
      ChunkContext chunkContext) throws Exception {
        for (Line line : lines) {
            long age = ChronoUnit.YEARS.between(
              line.getDob(),
              LocalDate.now());
            logger.debug("Calculated age " + age + " for line " + line.toString());
            line.setAge(age);
        }
        return RepeatStatus.FINISHED;
    }

    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        logger.debug("Lines Processor ended.");
        return ExitStatus.COMPLETED;
    }
}

É fácil entender queit loads lines list from the job’s context and calculates the age of each person.

Não há necessidade de colocar outra lista de resultados no contexto, pois as modificações acontecem no mesmo objeto que vem da etapa anterior.

E estamos prontos para nossa última etapa.

4.6. LinesWriter

LinesWriter‘s task is to go over lines list and write name and age to the output file:

public class LinesWriter implements Tasklet, StepExecutionListener {

    private final Logger logger = LoggerFactory
      .getLogger(LinesWriter.class);

    private List lines;
    private FileUtils fu;

    @Override
    public void beforeStep(StepExecution stepExecution) {
        ExecutionContext executionContext = stepExecution
          .getJobExecution()
          .getExecutionContext();
        this.lines = (List) executionContext.get("lines");
        fu = new FileUtils("output.csv");
        logger.debug("Lines Writer initialized.");
    }

    @Override
    public RepeatStatus execute(StepContribution stepContribution,
      ChunkContext chunkContext) throws Exception {
        for (Line line : lines) {
            fu.writeLine(line);
            logger.debug("Wrote line " + line.toString());
        }
        return RepeatStatus.FINISHED;
    }

    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        fu.closeWriter();
        logger.debug("Lines Writer ended.");
        return ExitStatus.COMPLETED;
    }
}

Concluímos a implementação do nosso trabalho! Vamos criar um teste para executá-lo e ver os resultados.

4.7. Executando o Trabalho

Para executar o trabalho, criaremos um teste:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = TaskletsConfig.class)
public class TaskletsTest {

    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;

    @Test
    public void givenTaskletsJob_whenJobEnds_thenStatusCompleted()
      throws Exception {

        JobExecution jobExecution = jobLauncherTestUtils.launchJob();
        assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus());
    }
}

A anotaçãoContextConfiguration está apontando para a classe de configuração de contexto Spring, que tem nossa definição de trabalho.

Precisaremos adicionar alguns beans extras antes de executar o teste:

@Bean
public JobLauncherTestUtils jobLauncherTestUtils() {
    return new JobLauncherTestUtils();
}

@Bean
public JobRepository jobRepository() throws Exception {
    MapJobRepositoryFactoryBean factory
      = new MapJobRepositoryFactoryBean();
    factory.setTransactionManager(transactionManager());
    return (JobRepository) factory.getObject();
}

@Bean
public PlatformTransactionManager transactionManager() {
    return new ResourcelessTransactionManager();
}

@Bean
public JobLauncher jobLauncher() throws Exception {
    SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
    jobLauncher.setJobRepository(jobRepository());
    return jobLauncher;
}

Tudo está pronto! Vá em frente e faça o teste!

Após a conclusão do trabalho,output.csv tem o conteúdo esperado e os logs mostram o fluxo de execução:

[main] DEBUG o.b.t.tasklets.LinesReader - Lines Reader initialized.
[main] DEBUG o.b.t.tasklets.LinesReader - Read line: [Mae Hodges,10/22/1972]
[main] DEBUG o.b.t.tasklets.LinesReader - Read line: [Gary Potter,02/22/1953]
[main] DEBUG o.b.t.tasklets.LinesReader - Read line: [Betty Wise,02/17/1968]
[main] DEBUG o.b.t.tasklets.LinesReader - Read line: [Wayne Rose,04/06/1977]
[main] DEBUG o.b.t.tasklets.LinesReader - Read line: [Adam Caldwell,09/27/1995]
[main] DEBUG o.b.t.tasklets.LinesReader - Read line: [Lucille Phillips,05/14/1992]
[main] DEBUG o.b.t.tasklets.LinesReader - Lines Reader ended.
[main] DEBUG o.b.t.tasklets.LinesProcessor - Lines Processor initialized.
[main] DEBUG o.b.t.tasklets.LinesProcessor - Calculated age 45 for line [Mae Hodges,10/22/1972]
[main] DEBUG o.b.t.tasklets.LinesProcessor - Calculated age 64 for line [Gary Potter,02/22/1953]
[main] DEBUG o.b.t.tasklets.LinesProcessor - Calculated age 49 for line [Betty Wise,02/17/1968]
[main] DEBUG o.b.t.tasklets.LinesProcessor - Calculated age 40 for line [Wayne Rose,04/06/1977]
[main] DEBUG o.b.t.tasklets.LinesProcessor - Calculated age 22 for line [Adam Caldwell,09/27/1995]
[main] DEBUG o.b.t.tasklets.LinesProcessor - Calculated age 25 for line [Lucille Phillips,05/14/1992]
[main] DEBUG o.b.t.tasklets.LinesProcessor - Lines Processor ended.
[main] DEBUG o.b.t.tasklets.LinesWriter - Lines Writer initialized.
[main] DEBUG o.b.t.tasklets.LinesWriter - Wrote line [Mae Hodges,10/22/1972,45]
[main] DEBUG o.b.t.tasklets.LinesWriter - Wrote line [Gary Potter,02/22/1953,64]
[main] DEBUG o.b.t.tasklets.LinesWriter - Wrote line [Betty Wise,02/17/1968,49]
[main] DEBUG o.b.t.tasklets.LinesWriter - Wrote line [Wayne Rose,04/06/1977,40]
[main] DEBUG o.b.t.tasklets.LinesWriter - Wrote line [Adam Caldwell,09/27/1995,22]
[main] DEBUG o.b.t.tasklets.LinesWriter - Wrote line [Lucille Phillips,05/14/1992,25]
[main] DEBUG o.b.t.tasklets.LinesWriter - Lines Writer ended.

É isso para Tasklets. Agora podemos seguir para a abordagem Chunks.

5. Abordagem de pedaços

5.1. Introdução e Design

Como o nome sugere, essa abordagemperforms actions over chunks of data. Ou seja, em vez de ler, processar e gravar todas as linhas de uma vez, ele lerá, processará e gravará uma quantidade fixa de registros (bloco) por vez.

Em seguida, ele repetirá o ciclo até que não haja mais dados no arquivo.

Como resultado, o fluxo será um pouco diferente:

  1. Enquanto houver linhas:

    • Faça pela quantidade X de linhas:

      • Leia uma linha

      • Processar uma linha

    • Escreva X quantidade de linhas.

Portanto, também precisamos criarthree beans for chunk oriented approach:

public class LineReader {
     // ...
}
public class LineProcessor {
    // ...
}
public class LinesWriter {
    // ...
}

Antes de passar para a implementação, vamos configurar nosso trabalho.

5.2. Configuração

A definição do trabalho também parecerá diferente:

@Configuration
@EnableBatchProcessing
public class ChunksConfig {

    @Autowired
    private JobBuilderFactory jobs;

    @Autowired
    private StepBuilderFactory steps;

    @Bean
    public ItemReader itemReader() {
        return new LineReader();
    }

    @Bean
    public ItemProcessor itemProcessor() {
        return new LineProcessor();
    }

    @Bean
    public ItemWriter itemWriter() {
        return new LinesWriter();
    }

    @Bean
    protected Step processLines(ItemReader reader,
      ItemProcessor processor, ItemWriter writer) {
        return steps.get("processLines"). chunk(2)
          .reader(reader)
          .processor(processor)
          .writer(writer)
          .build();
    }

    @Bean
    public Job job() {
        return jobs
          .get("chunksJob")
          .start(processLines(itemReader(), itemProcessor(), itemWriter()))
          .build();
    }

}

Nesse caso, há apenas uma etapa realizando apenas uma tarefa.

No entanto, esse taskletdefines a reader, a writer and a processor that will act over chunks of data.

Observe quecommit interval indicates the amount of data to be processed in one chunk. Nosso trabalho irá ler, processar e escrever duas linhas por vez.

Agora estamos prontos para adicionar nossa lógica de bloco!

5.3. LineReader

LineReader será responsável por ler um registro e retornar uma instânciaLine com seu conteúdo.

Para se tornar um leitor,our class has to implement ItemReader interface:

public class LineReader implements ItemReader {
     @Override
     public Line read() throws Exception {
         Line line = fu.readLine();
         if (line != null)
           logger.debug("Read line: " + line.toString());
         return line;
     }
}

O código é simples, apenas lê uma linha e a retorna. Também implementaremosStepExecutionListener para a versão final desta classe:

public class LineReader implements
  ItemReader, StepExecutionListener {

    private final Logger logger = LoggerFactory
      .getLogger(LineReader.class);

    private FileUtils fu;

    @Override
    public void beforeStep(StepExecution stepExecution) {
        fu = new FileUtils("taskletsvschunks/input/tasklets-vs-chunks.csv");
        logger.debug("Line Reader initialized.");
    }

    @Override
    public Line read() throws Exception {
        Line line = fu.readLine();
        if (line != null) logger.debug("Read line: " + line.toString());
        return line;
    }

    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        fu.closeReader();
        logger.debug("Line Reader ended.");
        return ExitStatus.COMPLETED;
    }
}

Deve-se notar quebeforeStepeafterStep são executados antes e depois de toda a etapa, respectivamente.

5.4. LineProcessor

LineProcessor segue praticamente a mesma lógica queLineReader.

No entanto, neste caso,we’ll implement ItemProcessor and its method process():

public class LineProcessor implements ItemProcessor {

    private Logger logger = LoggerFactory.getLogger(LineProcessor.class);

    @Override
    public Line process(Line line) throws Exception {
        long age = ChronoUnit.YEARS
          .between(line.getDob(), LocalDate.now());
        logger.debug("Calculated age " + age + " for line " + line.toString());
        line.setAge(age);
        return line;
    }

}

The process() method takes an input line, processes it and returns an output line. Novamente, também implementaremosStepExecutionListener:

public class LineProcessor implements
  ItemProcessor, StepExecutionListener {

    private Logger logger = LoggerFactory.getLogger(LineProcessor.class);

    @Override
    public void beforeStep(StepExecution stepExecution) {
        logger.debug("Line Processor initialized.");
    }

    @Override
    public Line process(Line line) throws Exception {
        long age = ChronoUnit.YEARS
          .between(line.getDob(), LocalDate.now());
        logger.debug(
          "Calculated age " + age + " for line " + line.toString());
        line.setAge(age);
        return line;
    }

    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        logger.debug("Line Processor ended.");
        return ExitStatus.COMPLETED;
    }
}

5.5. LinesWriter

Ao contrário do leitor e processador,LinesWriter will write an entire chunk of lines para que receba umList deLines:

public class LinesWriter implements
  ItemWriter, StepExecutionListener {

    private final Logger logger = LoggerFactory
      .getLogger(LinesWriter.class);

    private FileUtils fu;

    @Override
    public void beforeStep(StepExecution stepExecution) {
        fu = new FileUtils("output.csv");
        logger.debug("Line Writer initialized.");
    }

    @Override
    public void write(List lines) throws Exception {
        for (Line line : lines) {
            fu.writeLine(line);
            logger.debug("Wrote line " + line.toString());
        }
    }

    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        fu.closeWriter();
        logger.debug("Line Writer ended.");
        return ExitStatus.COMPLETED;
    }
}

O códigoLinesWriter fala por si. E, novamente, estamos prontos para testar nosso trabalho.

5.6. Executando o Trabalho

Vamos criar um novo teste, igual ao que criamos para a abordagem de tasklets:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ChunksConfig.class)
public class ChunksTest {

    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;

    @Test
    public void givenChunksJob_whenJobEnds_thenStatusCompleted()
      throws Exception {

        JobExecution jobExecution = jobLauncherTestUtils.launchJob();

        assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus());
    }
}

Depois de configurarChunksConfig conforme explicado acima paraTaskletsConfig, estamos prontos para executar o teste!

Assim que o trabalho estiver concluído, podemos ver queoutput.csv contém o resultado esperado novamente e os logs descrevem o fluxo:

[main] DEBUG o.b.t.chunks.LineReader - Line Reader initialized.
[main] DEBUG o.b.t.chunks.LinesWriter - Line Writer initialized.
[main] DEBUG o.b.t.chunks.LineProcessor - Line Processor initialized.
[main] DEBUG o.b.t.chunks.LineReader - Read line: [Mae Hodges,10/22/1972]
[main] DEBUG o.b.t.chunks.LineReader - Read line: [Gary Potter,02/22/1953]
[main] DEBUG o.b.t.chunks.LineProcessor - Calculated age 45 for line [Mae Hodges,10/22/1972]
[main] DEBUG o.b.t.chunks.LineProcessor - Calculated age 64 for line [Gary Potter,02/22/1953]
[main] DEBUG o.b.t.chunks.LinesWriter - Wrote line [Mae Hodges,10/22/1972,45]
[main] DEBUG o.b.t.chunks.LinesWriter - Wrote line [Gary Potter,02/22/1953,64]
[main] DEBUG o.b.t.chunks.LineReader - Read line: [Betty Wise,02/17/1968]
[main] DEBUG o.b.t.chunks.LineReader - Read line: [Wayne Rose,04/06/1977]
[main] DEBUG o.b.t.chunks.LineProcessor - Calculated age 49 for line [Betty Wise,02/17/1968]
[main] DEBUG o.b.t.chunks.LineProcessor - Calculated age 40 for line [Wayne Rose,04/06/1977]
[main] DEBUG o.b.t.chunks.LinesWriter - Wrote line [Betty Wise,02/17/1968,49]
[main] DEBUG o.b.t.chunks.LinesWriter - Wrote line [Wayne Rose,04/06/1977,40]
[main] DEBUG o.b.t.chunks.LineReader - Read line: [Adam Caldwell,09/27/1995]
[main] DEBUG o.b.t.chunks.LineReader - Read line: [Lucille Phillips,05/14/1992]
[main] DEBUG o.b.t.chunks.LineProcessor - Calculated age 22 for line [Adam Caldwell,09/27/1995]
[main] DEBUG o.b.t.chunks.LineProcessor - Calculated age 25 for line [Lucille Phillips,05/14/1992]
[main] DEBUG o.b.t.chunks.LinesWriter - Wrote line [Adam Caldwell,09/27/1995,22]
[main] DEBUG o.b.t.chunks.LinesWriter - Wrote line [Lucille Phillips,05/14/1992,25]
[main] DEBUG o.b.t.chunks.LineProcessor - Line Processor ended.
[main] DEBUG o.b.t.chunks.LinesWriter - Line Writer ended.
[main] DEBUG o.b.t.chunks.LineReader - Line Reader ended.

We have the same result and a different flow. Os logs tornam evidente como o trabalho é executado seguindo essa abordagem.

6. Conclusão

Diferentes contextos mostrarão a necessidade de uma abordagem ou de outra. While Tasklets feel more natural for ‘one task after the other' scenarios, chunks provide a simple solution to deal with paginated reads or situations where we don’t want to keep a significant amount of data in memory.

A implementação completa deste exemplo pode ser encontrada emthe GitHub project.