Java EE 7バッチ処理

Java EE 7バッチ処理

1. 前書き

給与明細の処理、利息の計算、請求書の作成などのタスクを手動で完了する必要があると想像してください。 それは非常に退屈で、エラーが発生しやすく、手動タスクの終わりのないリストになります!

このチュートリアルでは、Jakarta EEプラットフォームの一部であるJavaバッチ処理(JSR 352)と、このようなタスクを自動化するための優れた仕様について説明します。 It offers application developers a model for developing robust batch processing systems so that they can focus on the business logic.

2. Mavenの依存関係

JSR 352は単なる仕様であるため、jberetのようにits APIimplementationを含める必要があります。


    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

また、インメモリデータベースを追加して、より現実的なシナリオを検討できるようにします。

3. キーコンセプト

JSR 352にはいくつかの概念が導入されており、次のような概念を見ることができます。

image

まず、各部分を定義しましょう。

  • 左側から、JobOperatorがあります。 manages all aspects of job processing such as starting, stopping, and restarting

  • 次に、Jobがあります。 ジョブは、ステップの論理的な集合です。バッチ処理全体をカプセル化します

  • ジョブには1〜nStepsが含まれます。 各ステップは、独立した連続した作業単位です。 ステップは、reading input、その入力processing、およびwriting outputで構成されます

  • そして最後に、重要なことですが、ジョブの実行情報を格納するJobRepository があります。 ジョブ、ジョブの状態、および完了結果を追跡するのに役立ちます

手順はこれよりも少し詳細なので、次にそれを見てみましょう。 最初にChunkステップを確認し、次にBatchletsを確認します。

4. チャンクの作成

先に述べたように、チャンクは一種のステップ.です。チャンクを使用して、たとえば一連のアイテムに対して何度も実行される操作を表すことがよくあります。 これは、Javaストリームからの中間操作のようなものです。

チャンクを記述するときは、アイテムの取得元、アイテムの処理方法、アイテムの送信先を表現する必要があります。

4.1. 読書アイテム

アイテムを読むには、ItemReader.を実装する必要があります

この場合、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;
    }
}

ここでは、クラスの内部状態から読み取っています。 ただし、もちろん、ファイルシステムまたはその他の外部ソースからのreadItem could pull from a database

この内部状態の一部は、後で役立つJobContext#setTransientUserData()を使用して保存していることに注意してください。

Also, note the checkpoint parameter。 それもまた取り上げます。

4.2. 処理アイテム

もちろん、チャンクしている理由は、アイテムに対して何らかの操作を実行したいからです!

アイテムプロセッサからnullを返すときはいつでも、そのアイテムをバッチから削除します。

したがって、ここで、偶数のみを保持したいとします。 nullを返すことで、奇数を拒否するItemProcessorを使用できます。

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

processItemは、ItemReaderが発行するアイテムごとに1回呼び出されます。

4.3. アイテムを書く

最後に、ジョブはItemWriterを呼び出して、変換されたアイテムを記述できるようにします。

@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?すぐに、チャンクのサイズを定義します。これにより、writeItemsに送信されるリストのサイズが決まります。

4.4. ジョブでのチャンクの定義

JSLまたはジョブ仕様言語を使用して、これらすべてをXMLファイルにまとめます。 リーダー、プロセッサー、チャンカー、およびチャンクサイズもリストすることに注意してください。


    
        
            
            
            
        
    

完了を保証するために重要なThe chunk size is how often progress in the chunk is committed to the job repositoryは、システムの一部に障害が発生した場合に発生します。

このファイルは、。jar filesの場合はMETA-INF/batch-jobsに、.warファイルの場合はWEB-INF/classes/META-INF/batch-jobsに配置する必要があります.

ジョブにID“simpleChunk”, を指定したので、単体テストで試してみましょう。

Now, jobs are executed asynchronously, which makes them tricky to test.サンプルでは、​​ポーリングしてジョブが完了するまで待機するBatchTestHelper を確認してください。

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

それがチャンクです。 それでは、バッチレットを見てみましょう。

5. Batchletを作成する

すべてが反復モデ​​ルにうまく収まるわけではありません。 たとえば、単にinvoke once, run to completion, and return an exit status.する必要があるタスクがある場合があります。

バッチレットの契約は非常に簡単です。

@Named
public class SimpleBatchLet extends AbstractBatchlet {

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

JSLと同様に:


    
        
    

そして、以前と同じアプローチを使用してテストできます。

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

そのため、手順を実装するためのいくつかの異なる方法を検討しました。

それでは、marking and guaranteeing progress.のメカニズムを見てみましょう。

6. カスタムチェックポイント

失敗は、ジョブの途中で発生します。 Should we just start over the whole thing, or can we somehow start where we left off?

名前が示すように、checkpointsは、失敗した場合にブックマークを定期的に設定するのに役立ちます。

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

ただし、独自のCheckpointAlgorithmを使用してカスタマイズできます。

@Named
public class CustomCheckPoint extends AbstractCheckpointAlgorithm {

    @Inject
    JobContext jobContext;

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

以前に一時データに入れたカウントを覚えていますか? ここで、we can pull it out with JobContext#getTransientUserData は、処理された5番目の数値ごとにコミットすることを示しています。

これがない場合、コミットは各チャンクの最後、またはこの場合は3番目の番号ごとに発生します。

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


    
        
            
            
            
            
        
    

コードをテストしてみましょう。ここでも、定型的な手順の一部がBatchTestHelperに隠されていることに注意してください。

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

したがって、10個のアイテムがあり、コミットが5番目ごとのアイテムになるように構成されているため、コミットカウントは2になると予想されます。 ただし、すべてが処理されたことを確認するためのthe framework does one more final read commit at the endは、最大3になります。

次に、エラーの処理方法を見てみましょう。

7. 例外処理

デフォルトでは、the job operator will mark our job as FAILED in case of an exception.

アイテムリーダーを変更して、失敗することを確認しましょう。

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

そしてテストします:

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

ただし、このデフォルトの動作はいくつかの方法でオーバーライドできます。

  • [.s1]#skip-limit #は、このステップが失敗する前に無視する例外の数を指定します

  • retry-limit は、ジョブオペレータが失敗する前にステップを再試行する必要がある回数を指定します

  • [.s1]#skippable-exception-class #は、チャンク処理が無視する一連の例外を指定します

したがって、説明のために、RuntimeExceptionやその他のいくつかを無視するようにジョブを編集できます。


    
        
            
            
            
            
                
                
            
            
                
                
            
        
    

そして今、私たちのコードは通ります:

@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. 複数のステップの実行

ジョブには任意の数のステップを含めることができると前述したので、それを見てみましょう。

8.1. 次のステップの発射

デフォルトでは、each step is the last step in the jobです。

バッチジョブ内で次のステップを実行するには、ステップ定義内でnext属性を使用して明示的に指定する必要があります。


    
        
            
            
            
        
    
    
        
    

この属性を忘れると、次のステップは実行されません。

そして、これが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. 流れ

一連のステップをflowにカプセル化することもできます。 When the flow is finished, it is the entire flow that transitions to the execution element。 また、フロー内の要素をフロー外の要素に移行することはできません。

たとえば、フロー内で2つのステップを実行し、そのフローを分離されたステップに移行させることができます。


    
        
            
            
        
        
        
    
    
        
    
    
    
     
    

そして、各ステップの実行を個別に見ることができます:

@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. 決定事項

decisionsの形式でif / elseサポートもあります。 Decisions providea customized way of determining a sequence among steps, flows, and splits

ステップと同様に、ジョブの実行を指示または終了できるnextなどの遷移要素で機能します。

ジョブを構成する方法を見てみましょう。


     
     
     
     
        
        
     
     
    
     
     
    
     

decision要素は、Deciderを実装するクラスで構成する必要があります。 その仕事は、決定をStringとして返すことです。

decision内の各nextは、switch ステートメントのcase のようなものです。

8.4. 分割

Splitsは、フローを同時に実行できるので便利です。


   
      
      
              
           
      
      
          
              
      
      
   
   
      
   

もちろん、this means that the order isn’t guaranteed

それらがすべて実行されることを確認しましょう。 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. ジョブの分割

また、ジョブで定義されたJavaコード内のバッチプロパティを使用することもできます。

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

彼らがどのように消費したかの例をいくつか見てみましょう。

ジョブレベルでプロパティを使用する場合:

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

これは、ステップレベルでも使用できます。

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

バッチアーティファクトレベルでプロパティを使用する場合:

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

これはパーティションに便利です。

スプリットを使用すると、フローを同時に実行できることがわかります。 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.

各パーティションが行うべき作業セグメントを理解するために、プロパティをパーティションと組み合わせることができます。


    
        
    
    
        
            
        
    
        
        
        
    
    
        
        
            
        
        
            
        
        
    
    

10. 停止して再起動

これで、ジョブの定義は終わりです。 それでは、それらの管理について少し話しましょう。

ユニットテストでは、BatchRuntimeからJobOperator のインスタンスを取得できることをすでに確認しています。

JobOperator jobOperator = BatchRuntime.getJobOperator();

そして、ジョブを開始できます。

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

ただし、ジョブを停止することもできます。

jobOperator.stop(executionId);

最後に、ジョブを再開できます。

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

実行中のジョブを停止する方法を見てみましょう。

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

バッチがSTOPPEDの場合、再起動できます。

@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. 求人の取得

バッチジョブが送信されると、the batch runtime creates an instance of JobExecution to track itになります。

実行IDのJobExecutionを取得するには、JobOperator#getJobExecution(executionId)メソッドを使用できます。

そして、StepExecution provides helpful information for tracking a step’s execution

実行IDのStepExecutionを取得するには、JobOperator#getStepExecutions(executionId)メソッドを使用できます。

そして、そこから、StepExecution#getMetrics:を介してステップに関するseveral metricsを取得できます。

@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. デメリット

JSR 352は強力ですが、多くの領域に欠けています。

  • JSONなどの他の形式を処理できるリーダーとライターが不足しているようです

  • ジェネリックのサポートはありません

  • パーティショニングは単一ステップのみをサポートします

  • APIは、スケジューリングをサポートするものを何も提供していません(ただし、J2EEには個別のスケジューリングモジュールがあります)

  • 非同期の性質のため、テストは難しい場合があります

  • APIは非常に冗長です

13. 結論

この記事では、JSR 352を見て、チャンク、バッチレット、分割、フローなどについて学びました。 それでも、表面をほとんど傷つけていません。

いつものように、デモコードはover on GitHubで見つけることができます。