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:
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
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 n 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.