Traitement par lots Java EE 7

Traitement par lots Java EE 7

1. introduction

Imaginons que nous devions effectuer manuellement des tâches telles que le traitement des bulletins de paie, le calcul des intérêts et la génération des factures. Cela deviendrait assez ennuyeux, sujet aux erreurs et à une liste interminable de tâches manuelles!

Dans ce didacticiel, nous allons examiner le traitement par lots Java (JSR 352), une partie de la plate-forme Jakarta EE, et une excellente spécification pour automatiser des tâches comme celles-ci. It offers application developers a model for developing robust batch processing systems so that they can focus on the business logic.

2. Dépendances Maven

Puisque JSR 352 n'est qu'une spécification, nous devrons inclureits API etimplementation, commejberet:


    javax.batch
    javax.batch-api
    1.0.1


    org.jberet
    jberet-core
    1.0.2.Final


    org.jberet
    jberet-support
    1.0.2.Final


    org.jberet
    jberet-se
    1.0.2.Final

Nous allons également ajouter une base de données en mémoire afin que nous puissions examiner des scénarios plus réalistes.

3. Concepts clés

La JSR 352 introduit quelques concepts que nous pouvons examiner de la manière suivante:

image

Définissons d'abord chaque pièce:

  • En commençant à gauche, nous avons lesJobOperator. Ilmanages all aspects of job processing such as starting, stopping, and restarting

  • Ensuite, nous avons lesJob. Un travail est un ensemble logique d'étapes. il encapsule un processus de traitement par lots entier

  • Un travail contiendra entre 1 et nSteps. Chaque étape est une unité de travail séquentielle indépendante. Une étape est composée dereading input,processing that input, etwriting output

  • Enfin, nous avons le commutateurJobRepository qui stocke les informations en cours d'exécution des jobs. Il est utile de garder une trace des travaux, de leur état et de leurs résultats.

Les étapes sont un peu plus détaillées que cela, alors jetons un coup d'œil à cela ensuite. Tout d'abord, nous allons regarder les étapes deChunk, puis deBatchlets.

4. Créer un morceau

Comme indiqué précédemment, un bloc est une sorte d’étape. Nous utiliserons souvent un bloc pour exprimer une opération répétée, par exemple sur un ensemble d’éléments. C'est un peu comme des opérations intermédiaires à partir de Java Streams.

Lorsque vous décrivez un morceau, nous devons indiquer où prendre les articles, comment les traiter et où les envoyer ensuite.

4.1. Lire des articles

Pour lire les éléments, nous devons implémenterItemReader.

Dans ce cas, nous allons créer un lecteur qui émettra simplement les nombres 1 à 10:

@Named
public class SimpleChunkItemReader extends AbstractItemReader {
    private Integer[] tokens;
    private Integer count;

    @Inject
    JobContext jobContext;

    @Override
    public Integer readItem() throws Exception {
        if (count >= tokens.length) {
            return null;
        }

        jobContext.setTransientUserData(count);
        return tokens[count++];
    }

    @Override
    public void open(Serializable checkpoint) throws Exception {
        tokens = new Integer[] { 1,2,3,4,5,6,7,8,9,10 };
        count = 0;
    }
}

Maintenant, nous lisons simplement l'état interne de la classe ici. Mais, bien sûr,readItem could pull from a database, du système de fichiers, ou d'une autre source externe.

Notez que nous sauvegardons une partie de cet état interne en utilisantJobContext#setTransientUserData(), ce qui sera utile plus tard.

Also, note the checkpoint parameter. Nous reprendrons cela aussi.

4.2. Traitement des articles

Bien sûr, la raison pour laquelle nous coupons est que nous voulons effectuer une sorte d'opération sur nos articles!

Chaque fois que nous renvoyonsnull d'un processeur d'article, nous supprimons cet article du lot.

Alors, disons ici que nous souhaitons ne conserver que les nombres pairs. On peut utiliser unItemProcessor qui rejette les impairs en renvoyantnull:

@Named
public class SimpleChunkItemProcessor implements ItemProcessor {
    @Override
    public Integer processItem(Object t) {
        Integer item = (Integer) t;
        return item % 2 == 0 ? item : null;
    }
}

processItem sera appelé une fois pour chaque élément émis par nosItemReader.

4.3. Articles d'écriture

Enfin, le travail invoquera lesItemWriter afin que nous puissions écrire nos éléments transformés:

@Named
public class SimpleChunkWriter extends AbstractItemWriter {
    List processed = new ArrayList<>();
    @Override
    public void writeItems(List items) throws Exception {
        items.stream().map(Integer.class::cast).forEach(processed::add);
    }
}


How long is items? Dans un instant, nous définirons la taille d'un bloc, qui déterminera la taille de la liste envoyée àwriteItems.

4.4. Définir un morceau dans un travail

Maintenant, nous rassemblons tout cela dans un fichier XML en utilisant JSL ou Job Specification Language. Notez que nous allons lister notre lecteur, processeur, bloc et également une taille de bloc:


    
        
            
            
            
        
    

The chunk size is how often progress in the chunk is committed to the job repository, qui est important pour garantir l'achèvement, en cas de défaillance d'une partie du système.

Nous devrons placer ce fichier dansMETA-INF/batch-jobs pour les fichiers.jar  et dansWEB-INF/classes/META-INF/batch-jobs pour les fichiers.war.

Nous avons donné à notre travail l'ID“simpleChunk”, , alors essayons cela dans un test unitaire.

Now, jobs are executed asynchronously, which makes them tricky to test. Dans l'exemple, assurez-vous de vérifier notreBatchTestHelper which interroge et attend que le travail soit terminé:

@Test
public void givenChunk_thenBatch_completesWithSuccess() throws Exception {
    JobOperator jobOperator = BatchRuntime.getJobOperator();
    Long executionId = jobOperator.start("simpleChunk", new Properties());
    JobExecution jobExecution = jobOperator.getJobExecution(executionId);
    jobExecution = BatchTestHelper.keepTestAlive(jobExecution);
    assertEquals(jobExecution.getBatchStatus(), BatchStatus.COMPLETED);
}

Voilà donc ce que sont les morceaux. Voyons maintenant les batchlets.

5. Créer un batchlet

Tout ne rentre pas parfaitement dans un modèle itératif. Par exemple, nous pouvons avoir une tâche dont nous avons simplement besoin pourinvoke once, run to completion, and return an exit status.

Le contrat pour un lot est assez simple:

@Named
public class SimpleBatchLet extends AbstractBatchlet {

    @Override
    public String process() throws Exception {
        return BatchStatus.COMPLETED.toString();
    }
}

Comme c'est le JSL:


    
        
    

Et nous pouvons le tester en utilisant la même approche qu'auparavant:

@Test
public void givenBatchlet_thenBatch_completeWithSuccess() throws Exception {
    JobOperator jobOperator = BatchRuntime.getJobOperator();
    Long executionId = jobOperator.start("simpleBatchLet", new Properties());
    JobExecution jobExecution = jobOperator.getJobExecution(executionId);
    jobExecution = BatchTestHelper.keepTestAlive(jobExecution);
    assertEquals(jobExecution.getBatchStatus(), BatchStatus.COMPLETED);
}

Nous avons donc examiné différentes manières de mettre en œuvre les étapes.

Voyons maintenant les mécanismes pourmarking and guaranteeing progress.

6. Point de contrôle personnalisé

Les échecs sont inévitables au milieu d'un travail. Should we just start over the whole thing, or can we somehow start where we left off?

Comme son nom l'indique,checkpoints nous aide à définir périodiquement un signet en cas d'échec.

By default, the end of chunk processing is a natural checkpoint.

Cependant, nous pouvons le personnaliser avec nos propresCheckpointAlgorithm:

@Named
public class CustomCheckPoint extends AbstractCheckpointAlgorithm {

    @Inject
    JobContext jobContext;

    @Override
    public boolean isReadyToCheckpoint() throws Exception {
        int counterRead = (Integer) jobContext.getTransientUserData();
        return counterRead % 5 == 0;
    }
}

Rappelez-vous le nombre que nous avons placé dans les données transitoires plus tôt? Ici,we can pull it out with JobContext#getTransientUserData to indique que nous voulons valider tous les 5 nombres traités.

Sans cela, un commit se produirait à la fin de chaque morceau, ou dans notre cas, tous les 3 chiffres.

And then, we match that up with the checkout-algorithm directive in our XML underneath our chunk:


    
        
            
            
            
            
        
    

Testons le code, en notant à nouveau que certaines des étapes standard sont cachées dansBatchTestHelper:

@Test
public void givenChunk_whenCustomCheckPoint_thenCommitCountIsThree() throws Exception {
    // ... start job and wait for completion

    jobOperator.getStepExecutions(executionId)
      .stream()
      .map(BatchTestHelper::getCommitCount)
      .forEach(count -> assertEquals(3L, count.longValue()));
    assertEquals(jobExecution.getBatchStatus(), BatchStatus.COMPLETED);
}

Nous pouvons donc nous attendre à un nombre de validations de 2 puisque nous avons dix éléments et que nous configurons les commits de manière à ce qu'ils soient tous les cinq éléments. Mais,the framework does one more final read commit at the end pour s'assurer que tout a été traité, ce qui nous amène à 3.

Voyons ensuite comment gérer les erreurs.

7. Gestion des exceptions

Par défaut,the job operator will mark our job as FAILED in case of an exception.

Modifions notre lecteur d'articles pour nous assurer qu'il échoue:

@Override
public Integer readItem() throws Exception {
    if (tokens.hasMoreTokens()) {
        String tempTokenize = tokens.nextToken();
        throw new RuntimeException();
    }
    return null;
}

Et puis testez:

@Test
public void whenChunkError_thenBatch_CompletesWithFailed() throws Exception {
    // ... start job and wait for completion
    assertEquals(jobExecution.getBatchStatus(), BatchStatus.FAILED);
}

Mais, nous pouvons remplacer ce comportement par défaut de plusieurs manières:

  • [.s1]#skip-limit # spécifie le nombre d'exceptions que cette étape ignorera avant d'échouer

  • retry-limit  spécifie le nombre de fois que l'opérateur de travail doit réessayer l'étape avant d'échouer

  • [.s1]#skippable-exception-class #spécifie un ensemble d'exceptions que le traitement des blocs ignorera

Ainsi, nous pouvons éditer notre travail pour qu'il ignoreRuntimeException, ainsi que quelques autres, juste à titre d'illustration:


    
        
            
            
            
            
                
                
            
            
                
                
            
        
    

Et maintenant notre code va passer:

@Test
public void givenChunkError_thenErrorSkipped_CompletesWithSuccess() throws Exception {
   // ... start job and wait for completion
   jobOperator.getStepExecutions(executionId).stream()
     .map(BatchTestHelper::getProcessSkipCount)
     .forEach(skipCount -> assertEquals(1L, skipCount.longValue()));
   assertEquals(jobExecution.getBatchStatus(), BatchStatus.COMPLETED);
}

8. Exécution de plusieurs étapes

Nous avons mentionné précédemment qu’un travail peut comporter un certain nombre d’étapes, alors voyons cela maintenant.

8.1. Lancer la prochaine étape

Par défaut,each step is the last step in the job.

Afin d'exécuter l'étape suivante dans une tâche par lots, nous devrons spécifier explicitement en utilisant l'attributnext dans la définition de l'étape:


    
        
            
            
            
        
    
    
        
    

Si nous oublions cet attribut, la prochaine étape de la séquence ne sera pas exécutée.

Et nous pouvons voir à quoi cela ressemble dans l'API:

@Test
public void givenTwoSteps_thenBatch_CompleteWithSuccess() throws Exception {
    // ... start job and wait for completion
    assertEquals(2 , jobOperator.getStepExecutions(executionId).size());
    assertEquals(jobExecution.getBatchStatus(), BatchStatus.COMPLETED);
}

8.2. Les flux

Une séquence d'étapes peut également être encapsulée dans unflow. When the flow is finished, it is the entire flow that transitions to the execution element. De plus, les éléments à l'intérieur du flux ne peuvent pas effectuer de transition vers des éléments extérieurs au flux.

Nous pouvons, par exemple, exécuter deux étapes dans un flux, puis faire passer ce flux à une étape isolée:


    
        
            
            
        
        
        
    
    
        
    
    
    
     
    

Et nous pouvons toujours voir chaque étape de manière indépendante:

@Test
public void givenFlow_thenBatch_CompleteWithSuccess() throws Exception {
    // ... start job and wait for completion

    assertEquals(3, jobOperator.getStepExecutions(executionId).size());
    assertEquals(jobExecution.getBatchStatus(), BatchStatus.COMPLETED);
}

8.3. Les décisions

Nous avons également le support if / else sous la forme dedecisions. Decisions providea customized way of determining a sequence among steps, flows, and splits.

Comme les étapes, il fonctionne sur des éléments de transition tels quenext qui peuvent diriger ou terminer l'exécution du travail.

Voyons comment la tâche peut être configurée:


     
     
     
     
        
        
     
     
    
     
     
    
     

Tout élémentdecision doit être configuré avec une classe qui implémenteDecider. Son travail est de renvoyer une décision sous forme deString.

Chaquenext à l'intérieur dedecision est comme une déclarationcase in aswitch .

8.4. Splits

Splits sont pratiques car ils nous permettent d'exécuter des flux simultanément:


   
      
      
              
           
      
      
          
              
      
      
   
   
      
   

Bien sûr,this means that the order isn’t guaranteed.

Confirmons qu'ils sont toujours exécutés. The flow steps will be performed in an arbitrary order, but the isolated step will always be last:

@Test
public void givenSplit_thenBatch_CompletesWithSuccess() throws Exception {
    // ... start job and wait for completion
    List stepExecutions = jobOperator.getStepExecutions(executionId);

    assertEquals(3, stepExecutions.size());
    assertEquals("splitJobSequenceStep3", stepExecutions.get(2).getStepName());
    assertEquals(jobExecution.getBatchStatus(), BatchStatus.COMPLETED);
}

9. Partitionner un travail

Nous pouvons également utiliser les propriétés de lot dans notre code Java qui ont été définies dans notre travail.

They can be scoped at three levels – the job, the step, and the batch-artifact.

Voyons quelques exemples de leur consommation.

Lorsque nous voulons consommer les propriétés au niveau du travail:

@Inject
JobContext jobContext;
...
jobProperties = jobContext.getProperties();
...

Cela peut également être consommé à un niveau:

@Inject
StepContext stepContext;
...
stepProperties = stepContext.getProperties();
...

Lorsque nous voulons consommer les propriétés au niveau des artefacts par lots:

@Inject
@BatchProperty(name = "name")
private String nameString;

Ceci est pratique avec les partitions.

Vous voyez, avec les scissions, nous pouvons exécuter des flux simultanément. But we can also partition a step into sets of items or set separate inputs, allowing us another way to split up the work across multiple threads.

Pour comprendre le segment de travail que chaque partition doit effectuer, nous pouvons combiner des propriétés avec des partitions:


    
        
    
    
        
            
        
    
        
        
        
    
    
        
        
            
        
        
            
        
        
    
    

10. Arrêter et redémarrer

Maintenant, c’est pour définir les tâches. Parlons maintenant une minute de leur gestion.

Nous avons déjà vu dans nos tests unitaires que nous pouvons obtenir une instance deJobOperator  à partir deBatchRuntime:

JobOperator jobOperator = BatchRuntime.getJobOperator();

Et ensuite, nous pouvons commencer le travail:

Long executionId = jobOperator.start("simpleBatchlet", new Properties());

Cependant, nous pouvons également arrêter le travail:

jobOperator.stop(executionId);

Et enfin, nous pouvons relancer le travail:

executionId = jobOperator.restart(executionId, new Properties());

Voyons comment nous pouvons arrêter une tâche en cours d'exécution:

@Test
public void givenBatchLetStarted_whenStopped_thenBatchStopped() throws Exception {
    JobOperator jobOperator = BatchRuntime.getJobOperator();
    Long executionId = jobOperator.start("simpleBatchLet", new Properties());
    JobExecution jobExecution = jobOperator.getJobExecution(executionId);
    jobOperator.stop(executionId);
    jobExecution = BatchTestHelper.keepTestStopped(jobExecution);
    assertEquals(jobExecution.getBatchStatus(), BatchStatus.STOPPED);
}

Et si un lot estSTOPPED, alors nous pouvons le redémarrer:

@Test
public void givenBatchLetStopped_whenRestarted_thenBatchCompletesSuccess() {
    // ... start and stop the job

    assertEquals(jobExecution.getBatchStatus(), BatchStatus.STOPPED);
    executionId = jobOperator.restart(jobExecution.getExecutionId(), new Properties());
    jobExecution = BatchTestHelper.keepTestAlive(jobOperator.getJobExecution(executionId));

    assertEquals(jobExecution.getBatchStatus(), BatchStatus.COMPLETED);
}

11. Récupération des travaux

Lorsqu'un travail par lots est soumis, alorsthe batch runtime creates an instance of JobExecution to track it.

Pour obtenir lesJobExecution pour un identifiant d'exécution, nous pouvons utiliser la méthodeJobOperator#getJobExecution(executionId).

Et,StepExecution provides helpful information for tracking a step’s execution.

Pour obtenir lesStepExecution pour un identifiant d'exécution, nous pouvons utiliser la méthodeJobOperator#getStepExecutions(executionId).

Et à partir de là, nous pouvons obtenirseveral metrics sur l'étape viaStepExecution#getMetrics:

@Test
public void givenChunk_whenJobStarts_thenStepsHaveMetrics() throws Exception {
    // ... start job and wait for completion
    assertTrue(jobOperator.getJobNames().contains("simpleChunk"));
    assertTrue(jobOperator.getParameters(executionId).isEmpty());
    StepExecution stepExecution = jobOperator.getStepExecutions(executionId).get(0);
    Map metricTest = BatchTestHelper.getMetricsMap(stepExecution.getMetrics());
    assertEquals(10L, metricTest.get(Metric.MetricType.READ_COUNT).longValue());
    assertEquals(5L, metricTest.get(Metric.MetricType.FILTER_COUNT).longValue());
    assertEquals(4L, metricTest.get(Metric.MetricType.COMMIT_COUNT).longValue());
    assertEquals(5L, metricTest.get(Metric.MetricType.WRITE_COUNT).longValue());
    // ... and many more!
}

12. Désavantages

La JSR 352 est puissante, même si elle manque dans plusieurs domaines:

  • Il semble y avoir un manque de lecteurs et d'écrivains capables de traiter d'autres formats tels que JSON.

  • Il n'y a pas de soutien des génériques

  • Le partitionnement ne prend en charge qu'une seule étape

  • L'API n'offre rien pour prendre en charge la planification (bien que J2EE ait un module de planification séparé)

  • En raison de sa nature asynchrone, les tests peuvent être un défi

  • L'API est assez prolixe

13. Conclusion

Dans cet article, nous avons examiné la JSR 352 et avons découvert des fragments, des lots, des scissions, des flux et bien plus encore. Pourtant, nous avons à peine effleuré la surface.

Comme toujours, le code de démonstration peut être trouvéover on GitHub.