Spring Batch - Tasklets vs Chunks

Lot de printemps - Tasklets vs morceaux

1. introduction

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

Dans cet article, nous allons apprendre à configurer et à mettre en œuvre les deux méthodes à l'aide d'un exemple simple et réel.

2. Les dépendances

Commençons paradding the required dependencies:


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


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

Pour obtenir la dernière version despring-batch-core etspring-batch-test, veuillez vous référer à Maven Central.

3. Notre cas d'utilisation

Prenons un fichier CSV avec le contenu suivant:

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

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

Notre cas d'utilisation est degenerate 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

Maintenant que notre domaine est clair, allons-y et créons une solution en utilisant les deux approches. Nous allons commencer par les tasklets.

4. Approche des Tasklets

4.1. Introduction et conception

Les tasklets sont conçus pour effectuer une seule tâche dans une étape. Notre travail consistera en plusieurs étapes qui s'exécutent les unes après les autres. Each step should perform only one defined task.

Notre travail comportera trois étapes:

  1. Lire les lignes du fichier CSV d'entrée.

  2. Calculez l'âge pour chaque personne dans le fichier CSV en entrée.

  3. Ecrivez le nom et l'âge de chaque personne dans un nouveau fichier CSV de sortie.

Maintenant que la vue d’ensemble est prête, créons un cours par étape.

LinesReader sera en charge de la lecture des données du fichier d'entrée:

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

LinesProcessor calculera l'âge de chaque personne du fichier:

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

Enfin,LinesWriter aura la responsabilité d'écrire les noms et les âges dans un fichier de sortie:

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

À ce stade,all our steps implement Tasklet interface. Cela nous obligera à implémenter sa méthodeexecute:

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

C'est dans cette méthode que nous ajouterons la logique de chaque étape. Avant de commencer avec ce code, configurons notre travail.

4.2. Configuration

Nous avons besoin deadd some configuration to Spring’s application context. Après avoir ajouté une déclaration de bean standard pour les classes créées dans la section précédente, nous sommes prêts à créer notre définition de travail:

@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();
    }

    // ...

}

Cela signifie que nos“taskletsJob” se composeront de trois étapes. Le premier (readLines) exécutera le tasklet défini dans le beanlinesReader et passera à l'étape suivante:processLines. ProcessLines exécutera le tasklet défini dans le beanlinesProcessor et ira à la dernière étape:writeLines.

Notre flux de travail est défini et nous sommes prêts à ajouter un peu de logique!

4.3. Modèle et utils

Comme nous manipulerons des lignes dans un fichier CSV, nous allons créer une classeLine:

public class Line implements Serializable {

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

    // standard constructor, getters, setters and toString implementation

}

Veuillez noter queLine implémenteSerializable. C'est parce queLine agira comme un DTO pour transférer des données entre les étapes. Selon Spring Batch,objects that are transferred between steps must be serializable.

D'autre part, nous pouvons commencer à penser à lire et à écrire des lignes.

Pour cela, nous utiliserons OpenCSV:


    com.opencsv
    opencsv
    4.1

Recherchez la dernière version deOpenCSV dans Maven Central.

Une fois OpenCSV inclus,we’re also going to create a FileUtils class. Il fournira des méthodes pour lire et écrire des lignes 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);
    }

    // ...
}

Notez quereadLine agit comme un wrapper sur la méthodereadNext d'OpenCSV et renvoie un objetLine.

De la même manière,writeLine encapsule leswriteNext d'OpenCSV recevant un objetLine. L'implémentation complète de cette classe peut être trouvée dansthe GitHub Project.

À ce stade, nous sommes tous prêts à commencer par chaque étape de mise en œuvre.

4.4. LinesReader

Allons-y et terminons notre coursLinesReader:

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;
    }
}

La méthodeLinesReader’s execute crée une instanceFileUtils sur le chemin du fichier d'entrée. Ensuite,adds lines to a list until there’re no more lines to read.

Notre classealso implements StepExecutionListener qui fournit deux méthodes supplémentaires:beforeStep etafterStep. Nous utiliserons ces méthodes pour initialiser et fermer des éléments avant et après l'exécution deexecute.

Si nous examinons le code deafterStep, nous remarquerons la ligne où la liste de résultats (lines) est placée dans le contexte de la tâche pour la rendre disponible pour l'étape suivante:

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

À ce stade, notre première étape a déjà rempli sa responsabilité: charger les lignes CSV dans unList en mémoire. Passons à la deuxième étape et traitons-les.

4.5. LinesProcessor

LinesProcessor will also implement StepExecutionListener and of course, Tasklet. Cela signifie qu'il implémentera également les méthodesbeforeStep,execute etafterStep:

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;
    }
}

Il est facile de comprendre queit loads lines list from the job’s context and calculates the age of each person.

Il n'est pas nécessaire de placer une autre liste de résultats dans le contexte car des modifications se produisent sur le même objet que celui issu de l'étape précédente.

Et nous sommes prêts pour notre dernière étape.

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;
    }
}

Nous avons terminé la mise en œuvre de notre travail! Créons un test pour l'exécuter et voir les résultats.

4.7. Exécution du travail

Pour exécuter la tâche, nous allons créer un test:

@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());
    }
}

L'annotationContextConfiguration pointe vers la classe de configuration de contexte Spring, qui a notre définition de travail.

Nous devrons ajouter quelques beans supplémentaires avant d'exécuter le test:

@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;
}

Tout est prêt! Allez-y et lancez le test!

Une fois le travail terminé,output.csv a le contenu attendu et les journaux affichent le flux d'exécution:

[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.

C’est tout pour les Tasklets. Nous pouvons maintenant passer à l’approche des morceaux.

5. Approche des morceaux

5.1. Introduction et conception

Comme son nom l'indique, cette approcheperforms actions over chunks of data. Autrement dit, au lieu de lire, traiter et écrire toutes les lignes à la fois, il lira, traitera et écrira une quantité fixe d’enregistrements (bloc) à la fois.

Ensuite, il répète le cycle jusqu'à ce qu'il n'y ait plus de données dans le fichier.

En conséquence, le flux sera légèrement différent:

  1. Tant qu'il y a des lignes:

    • Faire pour X nombre de lignes:

      • Lire une ligne

      • Traiter une ligne

    • Écrivez X nombre de lignes.

Donc, nous devons également créerthree beans for chunk oriented approach:

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

Avant de passer à la mise en œuvre, configurons notre travail.

5.2. Configuration

La définition du travail sera également différente:

@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();
    }

}

Dans ce cas, une seule étape exécute une seule tâche.

Cependant, cette taskletdefines a reader, a writer and a processor that will act over chunks of data.

Notez que lescommit interval indicates the amount of data to be processed in one chunk. Notre travail va lire, traiter et écrire deux lignes à la fois.

Nous sommes maintenant prêts à ajouter notre logique de bloc!

5.3. LineReader

LineReader sera chargé de lire un enregistrement et de renvoyer une instance deLine avec son contenu.

Pour devenir lecteur,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;
     }
}

Le code est simple, il ne lit qu'une ligne et la renvoie. Nous allons également implémenterStepExecutionListener pour la version finale de cette 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;
    }
}

Il faut remarquer quebeforeStep etafterStep s'exécutent respectivement avant et après toute l'étape.

5.4. LineProcessor

LineProcessor suit à peu près la même logique queLineReader.

Cependant, dans ce cas,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. Encore une fois, nous allons également implémenterStepExecutionListener:

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

Contrairement au lecteur et au processeur,LinesWriter will write an entire chunk of lines pour qu'il reçoive unList 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;
    }
}

Le code deLinesWriter parle de lui-même. Et encore une fois, nous sommes prêts à tester notre travail.

5.6. Exécution du travail

Nous allons créer un nouveau test, identique à celui que nous avons créé pour l'approche des 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());
    }
}

Après avoir configuréChunksConfig comme expliqué ci-dessus pourTaskletsConfig, nous sommes tous prêts à exécuter le test!

Une fois le travail terminé, nous pouvons voir queoutput.csv contient à nouveau le résultat attendu, et les journaux décrivent le flux:

[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. Les journaux indiquent comment le travail s'exécute en suivant cette approche.

6. Conclusion

Différents contextes montreront la nécessité d'une approche ou d'une autre. 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.

L'implémentation complète de cet exemple peut être trouvée dansthe GitHub project.