Java EE 7-Stapelverarbeitung

Java EE 7-Stapelverarbeitung

1. Einführung

Stellen Sie sich vor, wir müssten Aufgaben wie das Verarbeiten von Gehaltsabrechnungen, das Berechnen von Zinsen und das Erstellen von Rechnungen manuell erledigen. Es würde ziemlich langweilig, fehleranfällig und eine endlose Liste manueller Aufgaben werden!

In diesem Tutorial werfen wir einen Blick auf die Java-Stapelverarbeitung (JSR 352), einen Teil der Jakarta EE-Plattform, und eine hervorragende Spezifikation für die Automatisierung solcher Aufgaben. It offers application developers a model for developing robust batch processing systems so that they can focus on the business logic.

2. Maven-Abhängigkeiten

Da JSR 352 nur eine Spezifikation ist, müssen wirits API undimplementation wiejberet einschließen:


    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

Wir werden auch eine In-Memory-Datenbank hinzufügen, damit wir einige realistischere Szenarien betrachten können.

3. Schlüssel Konzepte

JSR 352 stellt einige Konzepte vor, die wir folgendermaßen betrachten können:

image

Definieren wir zunächst jedes Stück:

  • Links beginnend haben wir dieJobOperator. Esmanages all aspects of job processing such as starting, stopping, and restarting

  • Als nächstes haben wir dieJob. Ein Job ist eine logische Ansammlung von Schritten. Es kapselt einen gesamten Batch-Prozess

  • Ein Job enthält zwischen 1 und nSteps. Jeder Schritt ist eine eigenständige, sequentielle Arbeitseinheit. Ein Schritt besteht ausreading input,processing dieser Eingabe undwriting output

  • Und zu guter Letzt haben wir denJobRepository -Schalter, der die laufenden Informationen der Jobs speichert. Es hilft, den Überblick über Jobs, deren Status und die Ergebnisse ihrer Fertigstellung zu behalten

Die Schritte sind etwas detaillierter. Schauen wir uns das als nächstes an. Zuerst betrachten wir die Schritte vonChunkund dannBatchlets.

4. Einen Chunk erstellen

Wie bereits erwähnt, ist ein Block eine Art Schritt.. Wir verwenden häufig einen Block, um eine Operation auszudrücken, die immer wieder ausgeführt wird, beispielsweise über eine Reihe von Elementen. Es ist wie eine Zwischenoperation von Java Streams.

Bei der Beschreibung eines Blocks müssen wir angeben, woher die Elemente genommen werden sollen, wie sie verarbeitet werden sollen und wohin sie anschließend gesendet werden sollen.

4.1. Artikel lesen

Um Elemente zu lesen, müssen wirItemReader. implementieren

In diesem Fall erstellen wir einen Leser, der einfach die Zahlen 1 bis 10 ausgibt:

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

Jetzt lesen wir hier nur aus dem internen Zustand der Klasse. Aber natürlichreadItem could pull from a database aus dem Dateisystem oder einer anderen externen Quelle.

Beachten Sie, dass wir einen Teil dieses internen Status mitJobContext#setTransientUserData() speichern, was später nützlich sein wird.

Also, note the checkpoint parameter. Wir werden das auch wieder aufgreifen.

4.2. Artikel bearbeiten

Der Grund dafür ist natürlich, dass wir eine Operation an unseren Gegenständen durchführen wollen!

Jedes Mal, wenn wirnull von einem Artikelprozessor zurückgeben, wird dieser Artikel aus dem Stapel entfernt.

Nehmen wir hier an, wir möchten nur die geraden Zahlen behalten. Wir können einItemProcessor verwenden, das die ungeraden zurückweist, indem wirnull zurückgeben:

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

processItem wird für jeden Gegenstand, den unserItemReader ausgibt, einmal aufgerufen.

4.3. Artikel schreiben

Schließlich ruft der Job dieItemWriter auf, damit wir unsere transformierten Elemente schreiben können:

@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? In einem Moment definieren wir die Größe eines Chunks, die die Größe der Liste bestimmt, die anwriteItems gesendet wird.

4.4. Einen Block in einem Job definieren

Jetzt fügen wir all dies mithilfe von JSL oder Job Specification Language in einer XML-Datei zusammen. Beachten Sie, dass wir unseren Reader, Prozessor, Chunker und auch eine Chunk-Größe auflisten:


    
        
            
            
            
        
    

The chunk size is how often progress in the chunk is committed to the job repository, was wichtig ist, um die Fertigstellung zu gewährleisten, sollte ein Teil des Systems ausfallen.

Wir müssen diese Datei inMETA-INF/batch-jobs für.jar -Dateien und inWEB-INF/classes/META-INF/batch-jobs für.war Dateien. platzieren

Wir haben unserem Job die ID“simpleChunk”, gegeben. Versuchen wir das in einem Unit-Test.

Now, jobs are executed asynchronously, which makes them tricky to test. Überprüfen Sie in der Stichprobe unbedingt unsereBatchTestHelper -Schläge, die abfragen und warten, bis der Auftrag abgeschlossen ist:

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

Das sind also Brocken. Schauen wir uns nun die Batchlets an.

5. Ein Batchlet erstellen

Nicht alles passt gut in ein iteratives Modell. Zum Beispiel haben wir möglicherweise eine Aufgabe, die wir einfach zuinvoke once, run to completion, and return an exit status. benötigen

Der Vertrag für ein Batchlet ist ganz einfach:

@Named
public class SimpleBatchLet extends AbstractBatchlet {

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

Wie ist die JSL:


    
        
    

Und wir können es mit dem gleichen Ansatz wie zuvor testen:

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

Daher haben wir uns verschiedene Möglichkeiten angesehen, um Schritte zu implementieren.

Betrachten wir nun die Mechanismen fürmarking and guaranteeing progress.

6. Benutzerdefinierter Prüfpunkt

Misserfolge sind mitten in einem Job zu erwarten. Should we just start over the whole thing, or can we somehow start where we left off?

Wie der Name schon sagt, helfen unscheckpoints, im Fehlerfall regelmäßig ein Lesezeichen zu setzen.

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

Wir können es jedoch mit unseren eigenenCheckpointAlgorithm anpassen:

@Named
public class CustomCheckPoint extends AbstractCheckpointAlgorithm {

    @Inject
    JobContext jobContext;

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

Erinnern Sie sich an die Zählung, die wir früher in vorübergehende Daten gestellt haben? Hier gebenwe can pull it out with JobContext#getTransientUserData to an, dass wir uns auf jede fünfte verarbeitete Zahl festlegen möchten.

Ohne dies würde ein Commit am Ende jedes Chunks oder in unserem Fall jeder dritten Zahl erfolgen.

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


    
        
            
            
            
            
        
    

Testen wir den Code und stellen erneut fest, dass einige der Boilerplate-Schritte inBatchTestHelper versteckt sind:

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

Daher erwarten wir möglicherweise eine Anzahl von Commits von 2, da wir zehn Elemente haben und die Commits so konfiguriert haben, dass sie für jedes fünfte Element gelten. Aberthe framework does one more final read commit at the end, um sicherzustellen, dass alles verarbeitet wurde, was uns auf 3 bringt.

Schauen wir uns als nächstes an, wie mit Fehlern umgegangen wird.

7. Ausnahmebehandlung

Standardmäßig istthe job operator will mark our job as FAILED in case of an exception.

Ändern wir unseren Artikelleser, um sicherzustellen, dass er fehlschlägt:

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

Und dann testen:

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

Wir können dieses Standardverhalten jedoch auf verschiedene Arten überschreiben:

  • [.s1]#skip-limit #gibt die Anzahl der Ausnahmen an, die dieser Schritt ignoriert, bevor er fehlschlägt

  • retry-limit gibt an, wie oft der Joboperator den Schritt wiederholen soll, bevor er fehlschlägt

  • [.s1]#skippable-exception-class #gibt eine Reihe von Ausnahmen an, die von der Blockverarbeitung ignoriert werden

Wir können unseren Job also so bearbeiten, dass erRuntimeException sowie einige andere ignoriert, nur zur Veranschaulichung:


    
        
            
            
            
            
                
                
            
            
                
                
            
        
    

Und jetzt wird unser Code übergeben:

@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. Mehrere Schritte ausführen

Wir haben bereits erwähnt, dass ein Job eine beliebige Anzahl von Schritten haben kann. Lassen Sie uns das jetzt sehen.

8.1. Den nächsten Schritt starten

Standardmäßig isteach step is the last step in the job.

Um den nächsten Schritt innerhalb eines Stapeljobs auszuführen, müssen Sie explizit angeben, indem Sie das Attributnext in der Schrittdefinition verwenden:


    
        
            
            
            
        
    
    
        
    

Wenn wir dieses Attribut vergessen, wird der nächste Schritt in Folge nicht ausgeführt.

Und wir können sehen, wie das in der API aussieht:

@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. Fließt

Eine Folge von Schritten kann auch inflow eingekapselt werden. When the flow is finished, it is the entire flow that transitions to the execution element. Außerdem können Elemente innerhalb des Flusses nicht zu Elementen außerhalb des Flusses übergehen.

Wir können beispielsweise zwei Schritte innerhalb eines Ablaufs ausführen und diesen Ablauf dann in einen isolierten Schritt überführen:


    
        
            
            
        
        
        
    
    
        
    
    
    
     
    

Und wir können trotzdem jede Schrittausführung unabhängig sehen:

@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. Entscheidungen

Wir haben auch if / else-Unterstützung in Form vondecisions. Decisions providea customized way of determining a sequence among steps, flows, and splits.

Wie Schritte funktioniert es mit Übergangselementen wienext, die die Jobausführung steuern oder beenden können.

Mal sehen, wie der Job konfiguriert werden kann:


     
     
     
     
        
        
     
     
    
     
     
    
     

Jedesdecision-Element muss mit einer Klasse konfiguriert werden, dieDecider implementiert. Seine Aufgabe ist es, eine Entscheidung alsString zurückzugeben.

Jedesnext innerhalb vondecision ist wie einecase in aswitch -Statement.

8.4. Spaltungen

Splits sind praktisch, da sie es uns ermöglichen, Flows gleichzeitig auszuführen:


   
      
      
              
           
      
      
          
              
      
      
   
   
      
   

Natürlichthis means that the order isn’t guaranteed.

Lassen Sie uns bestätigen, dass alle noch ausgeführt werden. 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. Einen Job partitionieren

Wir können auch die Batch-Eigenschaften in unserem Java-Code verwenden, die in unserem Job definiert wurden.

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

Sehen wir uns einige Beispiele an, wie sie konsumiert wurden.

Wenn wir die Eigenschaften auf Jobebene konsumieren möchten:

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

Dies kann auch schrittweise konsumiert werden:

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

Wenn wir die Eigenschaften auf Batch-Artefakt-Ebene verwenden möchten:

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

Dies ist praktisch bei Partitionen.

Sehen Sie, mit Splits können wir Flows gleichzeitig ausführen. 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.

Um den Arbeitsabschnitt zu verstehen, den jede Partition ausführen soll, können wir Eigenschaften mit Partitionen kombinieren:


    
        
    
    
        
            
        
    
        
        
        
    
    
        
        
            
        
        
            
        
        
    
    

10. Anhalten und neu starten

Nun, das ist es, um Jobs zu definieren. Lassen Sie uns nun eine Minute über deren Verwaltung sprechen.

Wir haben bereits in unseren Komponententests gesehen, dass wir eine Instanz vonJobOperator vonBatchRuntime erhalten können:

JobOperator jobOperator = BatchRuntime.getJobOperator();

Und dann können wir anfangen:

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

Wir können den Job aber auch beenden:

jobOperator.stop(executionId);

Und zuletzt können wir den Job neu starten:

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

Mal sehen, wie wir einen laufenden Job stoppen können:

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

Und wenn ein StapelSTOPPED ist, können wir ihn neu starten:

@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. Jobs abrufen

Wenn ein Stapeljob übergeben wird, dannthe batch runtime creates an instance of JobExecution to track it.

Um dieJobExecution für eine Ausführungs-ID zu erhalten, können wir die MethodeJobOperator#getJobExecution(executionId) verwenden.

UndStepExecution provides helpful information for tracking a step’s execution.

Um dieStepExecution für eine Ausführungs-ID zu erhalten, können wir die MethodeJobOperator#getStepExecutions(executionId) verwenden.

Und daraus können wirseveral metrics über den Schritt überStepExecution#getMetrics: erhalten

@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. Nachteile

JSR 352 ist mächtig, obwohl es in einer Reihe von Bereichen fehlt:

  • Es scheint an Lesern und Schreibern zu mangeln, die andere Formate wie JSON verarbeiten können

  • Generika werden nicht unterstützt

  • Die Partitionierung unterstützt nur einen einzigen Schritt

  • Die API bietet keine Unterstützung für die Zeitplanung (obwohl J2EE über ein separates Zeitplanungsmodul verfügt).

  • Aufgrund seiner asynchronen Natur kann das Testen eine Herausforderung sein

  • Die API ist ziemlich ausführlich

13. Fazit

In diesem Artikel haben wir uns mit JSR 352 befasst und Informationen zu Stücken, Batchlets, Splits, Flows und vielem mehr erhalten. Wir haben die Oberfläche jedoch kaum zerkratzt.

Wie immer kann der Demo-Codeover on GitHub gefunden werden.