Spring Batch utilisant le partitionneur

Lot de printemps utilisant le partitionneur

1. Vue d'ensemble

Dans nosintroduction to Spring Batch précédents, nous avons introduit le framework comme un outil de traitement par lots. Nous avons également exploré les détails de la configuration et la mise en œuvre pour l'exécution d'un travail à un seul processus et à un seul thread.

Pour mettre en œuvre une tâche avec un traitement parallèle, une gamme d’options est fournie. A un niveau supérieur, il existe deux modes de traitement parallèle:

  1. Processus unique, multithread

  2. Multi-processus

Dans cet article rapide, nous aborderons le partitionnement deStep, qui peut être implémenté pour les tâches à processus unique et multi-processus.

2. Partitionner une étape

Spring Batch avec partitionnement nous offre la possibilité de diviser l'exécution d'unStep:

image

L'image ci-dessus montre une implémentation d'unJob avec unStep partitionné.

Il y a unStep appelé "Master", dont l'exécution est divisée en quelques étapes "Slave". Ces esclaves peuvent prendre la place d'un maître et le résultat reste inchangé. Le maître et l'esclave sont des instances deStep. Les esclaves peuvent être des services distants ou simplement des threads exécutant localement.

Si nécessaire, nous pouvons transmettre des données du maître à l'esclave. Les métadonnées (c'est-à-dire leJobRepository), s'assure que chaque esclave n'est exécuté qu'une seule fois en une seule exécution desJob.

Voici le diagramme de séquence montrant comment cela fonctionne:

image

Comme indiqué, lePartitionStep pilote l'exécution. LePartitionHandler est responsable de la division du travail du «maître» en «esclaves». LeStep le plus à droite est l'esclave.

3. Le Maven POM

Les dépendances Maven sont les mêmes que celles mentionnées dans nos précédentsarticle. Autrement dit, Spring Core, Spring Batch et la dépendance de la base de données (dans notre cas,SQLite).

4. Configuration

Dans notre introductionarticle, nous avons vu un exemple de conversion de certaines données financières de CSV en fichier XML. Prenons le même exemple.

Ici, nous convertirons les informations financières de 5 fichiers CSV en fichiers XML correspondants, en utilisant une implémentation multithread.

Nous pouvons y parvenir en utilisant un seul partitionnementJob etStep. Nous aurons cinq fils, un pour chacun des fichiers CSV.

Tout d’abord, créons un travail:

@Bean(name = "partitionerJob")
public Job partitionerJob()
  throws UnexpectedInputException, MalformedURLException, ParseException {
    return jobs.get("partitioningJob")
      .start(partitionStep())
      .build();
}

Comme nous pouvons le voir, ceJob commence par lesPartitioningStep. C'est notre étape principale qui sera divisée en différentes étapes esclaves:

@Bean
public Step partitionStep()
  throws UnexpectedInputException, MalformedURLException, ParseException {
    return steps.get("partitionStep")
      .partitioner("slaveStep", partitioner())
      .step(slaveStep())
      .taskExecutor(taskExecutor())
      .build();
}

Ici, nous allons créer lesPartitioningStep using the StepBuilderFactory. Pour cela, nous devons donner les informations sur lesSlaveSteps et lesPartitioner.

LePartitioner est une interface qui permet de définir un ensemble de valeurs d'entrée pour chacun des esclaves. En d'autres termes, la logique pour diviser les tâches en threads respectifs va ici.

Créons-en une implémentation, appeléeCustomMultiResourcePartitioner, où nous placerons les noms de fichiers d'entrée et de sortie dans lesExecutionContext pour les transmettre à chaque étape esclave:

public class CustomMultiResourcePartitioner implements Partitioner {

    @Override
    public Map partition(int gridSize) {
        Map map = new HashMap<>(gridSize);
        int i = 0, k = 1;
        for (Resource resource : resources) {
            ExecutionContext context = new ExecutionContext();
            Assert.state(resource.exists(), "Resource does not exist: "
              + resource);
            context.putString(keyName, resource.getFilename());
            context.putString("opFileName", "output"+k+++".xml");
            map.put(PARTITION_KEY + i, context);
            i++;
        }
        return map;
    }
}

Nous allons également créer le bean pour cette classe, où nous donnerons le répertoire source pour les fichiers d’entrée:

@Bean
public CustomMultiResourcePartitioner partitioner() {
    CustomMultiResourcePartitioner partitioner
      = new CustomMultiResourcePartitioner();
    Resource[] resources;
    try {
        resources = resoursePatternResolver
          .getResources("file:src/main/resources/input/*.csv");
    } catch (IOException e) {
        throw new RuntimeException("I/O problems when resolving"
          + " the input file pattern.", e);
    }
    partitioner.setResources(resources);
    return partitioner;
}

Nous définirons l’étape esclave, comme toute étape avec le lecteur et l’écrivain. Le lecteur et l'écrivain seront les mêmes que ceux que nous avons vus dans notre exemple d'introduction, sauf qu'ils recevront le paramètre de nom de fichier desStepExecutionContext.

Notez que ces beans doivent être étendus pour qu'ils puissent recevoir les paramètresstepExecutionContext, à chaque étape. S'ils ne sont pas étendus, leurs beans seront créés initialement et n'accepteront pas les noms de fichiers au niveau de l'étape:

@StepScope
@Bean
public FlatFileItemReader itemReader(
  @Value("#{stepExecutionContext[fileName]}") String filename)
  throws UnexpectedInputException, ParseException {

    FlatFileItemReader reader
      = new FlatFileItemReader<>();
    DelimitedLineTokenizer tokenizer = new DelimitedLineTokenizer();
    String[] tokens
      = {"username", "userid", "transactiondate", "amount"};
    tokenizer.setNames(tokens);
    reader.setResource(new ClassPathResource("input/" + filename));
    DefaultLineMapper lineMapper
      = new DefaultLineMapper<>();
    lineMapper.setLineTokenizer(tokenizer);
    lineMapper.setFieldSetMapper(new RecordFieldSetMapper());
    reader.setLinesToSkip(1);
    reader.setLineMapper(lineMapper);
    return reader;
}
@Bean
@StepScope
public ItemWriter itemWriter(Marshaller marshaller,
  @Value("#{stepExecutionContext[opFileName]}") String filename)
  throws MalformedURLException {
    StaxEventItemWriter itemWriter
      = new StaxEventItemWriter();
    itemWriter.setMarshaller(marshaller);
    itemWriter.setRootTagName("transactionRecord");
    itemWriter.setResource(new ClassPathResource("xml/" + filename));
    return itemWriter;
}

Tout en mentionnant le lecteur et l'écrivain dans l'étape esclave, nous pouvons passer les arguments comme null, car ces noms de fichiers ne seront pas utilisés, car ils recevront les noms de fichiers destepExecutionContext:

@Bean
public Step slaveStep()
  throws UnexpectedInputException, MalformedURLException, ParseException {
    return steps.get("slaveStep").chunk(1)
      .reader(itemReader(null))
      .writer(itemWriter(marshaller(), null))
      .build();
}

5. Conclusion

Dans ce didacticiel, nous avons expliqué comment implémenter une tâche avec un traitement parallèle à l'aide de Spring Batch.

Comme toujours, l'implémentation complète de cet exemple est disponibleover on GitHub.