kagamihogeの日記

kagamihogeの日記です。

Spring Batch 4.1.x - Reference Documentation - Unit Testingのテキトー翻訳

https://docs.spring.io/spring-batch/4.1.x/reference/html/testing.html#testing

https://qiita.com/kagamihoge/items/12fbbc2eac5b8a5ac1e0 俺の訳一覧リスト

1. Unit Testing

バッチ以外のアプリケーション同様に、バッチジョブの一部として書いたコードのユニットテストは極めて重要です。SpringのコアドキュメントにSpringでのユニットおよびインテグレーションテストの方法が良くまとめられているため、ここではそれについては触れません。ここでは、バッチジョブの'end to end'なテスト方法について解説します。spring-batch-testにはend-to-endのテスト手法を行うためのクラスがあります。

1.1. Creating a Unit Test Class

バッチジョブをユニットテストとして動作させるには、フレームワークでジョブのApplicationContextをロードする必要があります。これを行うための2つのアノテーションがあります。

v4.1から、@SpringBatchTestを使用するテストコンテキストでJobLauncherTestUtilsJobRepositoryTestUtilsなどSpring Batchのテストユーティリティをインジェクション可能になりました。

以下はアノテーションの使用例です。

Using Java Configuration

@SpringBatchTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes=SkipSampleConfiguration.class)
public class SkipSampleFunctionalTests { ... }

1.2. End-To-End Testing of Batch Jobs

'End To End'の定義はバッチジョブを最初から最後まで実行するテストです。テストコンディションのセットアップ・ジョブ実行・最終結果の検証をテストします。

以下はDBから読み込みフラットファイルに書き込むバッチジョブの例です。テストメソッドはまずテストデータをDBにセットアップします。CUSTOMERテーブルをクリアし、10レコードを新規追加します。それからテストはlaunchJob()Jobを起動します。launchJob()JobLauncherTestUtilsにあります。JobLauncherTestUtilsにはlaunchJob(JobParameters)もあり、テストにパラメータを指定できます。launchJob()JobExecutionを返し、Job実行に関する情報をassertするなどに使います。以下の例では、テストでJobが"COMPLETED"で終了することを検証しています。

Java Based Configuration

@SpringBatchTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes=SkipSampleConfiguration.class)
public class SkipSampleFunctionalTests {

    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;

    private SimpleJdbcTemplate simpleJdbcTemplate;

    @Autowired
    public void setDataSource(DataSource dataSource) {
        this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);
    }

    @Test
    public void testJob() throws Exception {
        simpleJdbcTemplate.update("delete from CUSTOMER");
        for (int i = 1; i <= 10; i++) {
            simpleJdbcTemplate.update("insert into CUSTOMER values (?, 0, ?, 100000)",
                                      i, "customer" + i);
        }

        JobExecution jobExecution = jobLauncherTestUtils.launchJob();


        Assert.assertEquals("COMPLETED", jobExecution.getExitStatus().getExitCode());
    }
}

1.3. Testing Individual Steps

複雑なバッチジョブでは、end-to-endのテストケースは扱い辛くなりがちです。この場合、stepごとにテストケースを作る方が良い場合があります。JobLauncherTestUtilsにはlaunchStepがあり、これはstep名でそのStepを実行します。これにより、特定step用のテストデータのセットアップをしてテスト実行をし、step実行結果を直接検証できます。以下はstep名でロードするのにlaunchStepを使用する例です。

JobExecution jobExecution = jobLauncherTestUtils.launchStep("loadFileStep");

1.4. Testing Step-Scoped Components

stepに設定するコンポーネントが実行時にstepスコープと遅延バインディングでstepやjobのコンテキストをインジェクトする場合があります。これをスタンドアローンコンポーネントとしてテストするのはトリッキーで、そのコンポーネントをstepに配置した状態を再現してコンテキストを設定する方法が必要です。このためにSpring BatchではStepScopeTestExecutionListenerStepScopeTestUtilsを用意しています。

このリスナーをクラスレベルに宣言すると、jobはstepコンテキストをテストメソッドごとに生成します。以下が例です。

@ContextConfiguration
@TestExecutionListeners( { DependencyInjectionTestExecutionListener.class,
    StepScopeTestExecutionListener.class })
@RunWith(SpringRunner.class)
public class StepScopeTestExecutionListenerIntegrationTests {

    // このコンポーネントはstepスコープで定義しており、stepがアクティブでないと
    // インジェクトできない。
    @Autowired
    private ItemReader<String> reader;

    public StepExecution getStepExecution() {
        StepExecution execution = MetaDataInstanceFactory.createStepExecution();
        execution.getExecutionContext().putString("input.data", "foo,bar,spam");
        return execution;
    }

    @Test
    public void testReader() {
        // readerを初期化して入力データをバインド
        assertNotNull(reader.read());
    }

}

ここには2つのTestExecutionListenersがあります。1つはSpring Testフレームワークのもので、設定したアプリケーションコンテキストからreaderのDIを処理するものです。もう1つはStepScopeTestExecutionListenerです。仕組みとしては、実行時にStepがアクティブかのように振る舞うように、StepExecutionのテストケースでファクトリメソッドを参照します。ファクトリメソッドはシグネチャStepExecutionを返す)で検出されます。ファクトリメソッドが無い場合、デフォルトのStepExecutionを生成します。

v4.1からは、@SpringBatchTestがある場合、StepScopeTestExecutionListenerJobScopeTestExecutionListenerをテストクラスにtest execution listenersとしてインポートします。上の例は以下のようにも書けます。

@SpringBatchTest
@RunWith(SpringRunner.class)
@ContextConfiguration
public class StepScopeTestExecutionListenerIntegrationTests {

    // このコンポーネントはstepスコープで定義しており、stepがアクティブでないと
    // インジェクトできない。
    @Autowired
    private ItemReader<String> reader;

    public StepExecution getStepExecution() {
        StepExecution execution = MetaDataInstanceFactory.createStepExecution();
        execution.getExecutionContext().putString("input.data", "foo,bar,spam");
        return execution;
    }

    @Test
    public void testReader() {
        // readerを初期化して入力データをバインド
        assertNotNull(reader.read());
    }

}

テストメソッド実行をstepスコープに入れたい場合にリスナーを使う方法は手軽です。柔軟ではあるものの美しくない方法に、StepScopeTestUtilsを使うものがあります。以下の例は前述の例のreaderでアイテム数をカウントしています。

int count = StepScopeTestUtils.doInStepScope(stepExecution,
    new Callable<Integer>() {
      public Integer call() throws Exception {

        int count = 0;

        while (reader.read() != null) {
           count++;
        }
        return count;
    }
});

1.5. Validating Output Files

バッチジョブがDB書き込みする場合、その出力が期待通りか検証するのはDBにクエリを投げるだけです。しかし、バッチジョブがファイルに書き込む場合も、同じく、出力の検証は重要です。Spring Batchには出力ファイルの検証を行うAssertFileがあります。assertFileEqualsは2つのFile(もしくは2つのResource)を取り、1行ずつ同一内容を持つファイルかをアサートします。これにより、期待される出力のファイルを作成し、実際の結果と比較できます。

private static final String EXPECTED_FILE = "src/main/resources/data/input.txt";
private static final String OUTPUT_FILE = "target/test-outputs/output.txt";

AssertFile.assertFileEquals(new FileSystemResource(EXPECTED_FILE),
                            new FileSystemResource(OUTPUT_FILE));

1.6. Mocking Domain Objects

Spring Batchのユニットおよびインテグレーションテストの作成でよくあるもう一つの課題はドメインオブジェクトのモックです。好例は、以下コードのような、StepExecutionListenerです。

public class NoWorkFoundStepExecutionListener extends StepExecutionListenerSupport {

    public ExitStatus afterStep(StepExecution stepExecution) {
        if (stepExecution.getReadCount() == 0) {
            return ExitStatus.FAILED;
        }
        return null;
    }
}

上のリスナーの例は、何も処理しなかったことを意味するreadカウントがゼロを、StepExecutionでチェックしています。このサンプルは極めてシンプルですが、Spring Batchのドメインオブジェクトを必要とするインタフェースを実装するクラスのユニットテストで問題が発生する可能性を示しています。上の例のリスナーのユニットテストを考えます。

private NoWorkFoundStepExecutionListener tested = new NoWorkFoundStepExecutionListener();

@Test
public void noWork() {
    StepExecution stepExecution = new StepExecution("NoProcessingStep",
                new JobExecution(new JobInstance(1L, new JobParameters(),
                                 "NoProcessingJob")));

    stepExecution.setExitStatus(ExitStatus.COMPLETED);
    stepExecution.setReadCount(0);

    ExitStatus exitStatus = tested.afterStep(stepExecution);
    assertEquals(ExitStatus.FAILED.getExitCode(), exitStatus.getExitCode());
}

Spring Batchのドメインモデルはオブジェクト指向に基づくため、StepExecutionJobExecutionを必要とし, JobExecutionは正しくStepExecutionを生成するためにJobParametersJobInstanceを必要とします。堅実なドメインモデルですが、ユニットテスト用のスタブオブジェクト生成は冗長です。このため、Spring Batchテストモジュールにはドメインオブジェクトを生成するファクトリMetaDataInstanceFactoryがあります。このファクトリにより、ユニットテストは以下のように簡潔にできます。

private NoWorkFoundStepExecutionListener tested = new NoWorkFoundStepExecutionListener();

@Test
public void testAfterStep() {
    StepExecution stepExecution = MetaDataInstanceFactory.createStepExecution();

    stepExecution.setExitStatus(ExitStatus.COMPLETED);
    stepExecution.setReadCount(0);

    ExitStatus exitStatus = tested.afterStep(stepExecution);
    assertEquals(ExitStatus.FAILED.getExitCode(), exitStatus.getExitCode());
}

上の例ではStepExecutionの生成をファクトリのメソッドの1つを使用して行っています。メソッドの全リストはJavadocにあります。

Spring Batch 4.1.x - Reference Documentation - Retryのテキトー翻訳

https://docs.spring.io/spring-batch/4.1.x/reference/html/retry.html#retry

https://qiita.com/kagamihoge/items/12fbbc2eac5b8a5ac1e0 俺の訳一覧リスト

1. Retry

処理をロバストかつ失敗の可能性を下げるには、次回以降には成功する確率が高い場合、その失敗した処理を自動リトライするのが有効な場合があります。断続的な障害に影響を受けやすいエラーは、実際のところ一時的な事が多いです。たとえば、ネットワークエラーやDB更新によるDeadlockLoserDataAccessExceptionが原因でwebサービスが失敗する、などです。

1.1. RetryTemplate

※ リトライ機能は2.2.0以降Spring Batchから分離しました。現在はSpring Retryの一部になっています。

自動リトライにはSpring BatchのRetryOperationsがあります。以下がRetryOperationsの定義です。

public interface RetryOperations {

    <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback) throws E;

    <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RecoveryCallback<T> recoveryCallback)
        throws E;

    <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RetryState retryState)
        throws E, ExhaustedRetryException;

    <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RecoveryCallback<T> recoveryCallback,
        RetryState retryState) throws E;

}

基本のコールバックはシンプルなインタフェースでここにリトライしたいビジネスロジックを書きます。

public interface RetryCallback<T, E extends Throwable> {

    T doWithRetry(RetryContext context) throws E;

}

コールバックを実行して、失敗(Exceptionスロー)する場合、成功するか実装でアボートするまでリトライします。RetryOperationsにはいくつかのexecuteオーバーロードメソッドがあります。リトライ全滅時のリカバリや、クライアントと実装でコールバック間の情報を受け渡すためのリトライ状態など、各種ユースケースを扱うためのメソッドです(詳細は本チャプター後半で扱います)。

RetryOperationsの一番シンプルで汎用の実装はRetryTemplateです。以下のように使います。

RetryTemplate template = new RetryTemplate();

TimeoutRetryPolicy policy = new TimeoutRetryPolicy();
policy.setTimeout(30000L);

template.setRetryPolicy(policy);

Foo result = template.execute(new RetryCallback<Foo>() {

    public Foo doWithRetry(RetryContext context) {
        // webサービスなど、失敗する可能性のある何らかの処理
        return result;
    }

});

上の例では、webサービスを呼んで結果を返しています。もしwebサービスが失敗する場合、タイムアウトに達するまでリトライします。

1.1.1. RetryContext

RetryCallbackのメソッド引数はRetryContextです。コールバックでこのコンテキストが必要になることはほとんどありませんが、イテレーション中のデータ格納場所として使えます。

同一スレッド実行中にネストしたリトライががあると、RetryContextは親コンテキストを持ちます。親コンテキストはexecute間でデータ共有したい場合に有用な場合があります。

1.1.2. RecoveryCallback

リトライが全滅すると、RetryOperationsRecoveryCallbackという別のコールバックに制御を渡します。このコールバックを使うには、以下のように、これらのコールバックを一緒に渡します。

Foo foo = template.execute(new RetryCallback<Foo>() {
    public Foo doWithRetry(RetryContext context) {
        // ビジネスロジックをここに書く。
    },
  new RecoveryCallback<Foo>() {
    Foo recover(RetryContext context) throws Exception {
          // リカバリのロジックをここに書く。
    }
});

テンプレートがアボートするより前の段階でビジネスロジックが失敗する場合、リカバリコールバックで何らかの代替処理ができます。

1.1.3. Stateless Retry

極めて単純化すれば、リトライとはwhileループです。RetryTemplateは成功するか失敗するまでトライし続けますRetryContextにはリトライかアボートするかを決定するための情報を持ちますが、この状態はスタックにありグローバルに持つ必要がないので、これをステートレスリトライと呼びます。ステートレスかステートフルかの区別はRetryPolicyの実装によります(RetryTemplateはどちらも可能)。ステートレスでは、失敗したリトライと同一スレッドでリトライコールバックを実行します。

1.1.4. Stateful Retry

ある失敗がトランザクショナルなリソースをinvalidにする場合、特別な考慮が必要です。シンプルなリモートコールはトランザクショナルなリソースが(基本的には)無いのでこれに当てはまりませんが、Hibernateなどを使用するデータベース更新は該当します。この場合、即時失敗するように例外を再スローするのが唯一正解で、これによりトランザクションロールバックして新しくvalidなトランザクションを開始します。

このようにトランザクションを含む場合、ステートレスリトライは不十分で、because the re-throw and roll back necessarily involve leaving the RetryOperations.execute() method and potentially losing the context that was on the stack. データロスを回避するには、スタックを何らかの保存機構に移してヒープに移動します。これのために、Spring BatchにはRetryContextCacheがあり、RetryTemplateに設定可能です。RetryContextCacheのデフォルトリスナはインメモリのシンプルなMapです。クラスタ環境のマルチプロセッサの高度な使用法はある種のクラスタキャッシュでRetryContextCacheを実装すると思われます(ただし、クラスタ環境であっても、やりすぎな感があります)。

RetryOperationsのその他の責務は失敗したオペレーションが新規実行で復帰(通常は新規トランザクションでラップ)した事の識別です。これにはSpring BatchのRetryStateがあります。RetryOperationsの特殊なexecuteメソッドと組み合わせて使います。

失敗したオペレーションの識別方法は、リトライ複数実行間で共有する状態により行います。状態の識別には、アイテム識別のユニークキーを返す責務のRetryStateを設定します。識別子はRetryContextCacheのキーとして使われます。

RetryStateが返すキーのObject.equals()Object.hashCode()の実装には十分注意してください。アイテム識別用のビジネスロジック上のキーを使用してください。JMSメッセージの場合、メッセージIDを使用可能です。

リトライ全滅時、RetryCallbackの代わりに、別の方法で失敗アイテムを処理するオプションがあります (which is now presumed to be likely to fail)。ステートレスの場合同様、このオプションはRecoveryCallbackで行い、RetryOperationsexecuteにこのコールバックを渡します。

リトライするかどうかの決定はRetryPolicyにデリゲートするので、リミットやタイムアウトなど一般的な設定事項はここで行います(詳細はこのチャプター後半)。

1.2. Retry Policies

RetryTemplateは内部的に、executeメソッドのリトライか失敗かの決定はRetryPolicyで行い、これはRetryContextのファクトリでもあります。RetryTemplateRetryContextの作成にその時点で設定されているポリシーを使用し、RetryCallbackの毎回の呼び出しに渡します。コールバックが失敗すると、RetryTemplateRetryPolicyに(RetryContextの)状態を更新するよう依頼し、次回のリトライをするかどうかをポリシーに問い合わせます。次回リトライしない場合(リミットやタイムアウトなど)、ポリシーはリトライ全滅状態をハンドリングする責務を持ちます。シンプルな実装はRetryExhaustedExceptionをスローし、エンクロージングのトランザクションロールバックします。より複雑な実装でなんらかのリカバリアクションを取る事が可能で、トランザクションを残したままに出来ます。

※ 失敗は本来的にリトライ可能かそうでないかのどちらかです。ビジネスロジックが同一の例外を常にスローする場合、リトライするのは良くありません。またすべての例外型をリトライは禁止です。そうではなく、リトライ可能と人が判断可能な例外のみを対象にしてください。通常、ビジネスロジックで何でもかんでもリトライするのは有害ではないですが、ただの浪費です。失敗が決定論的な場合、致命的と分かっているものをリトライするのは時間の無駄です。

Spring BatchはステートレスRetryPolicyのシンプルな汎用実装、SimpleRetryPolicyTimeoutRetryPolicy、を提供します(例は前述)。

SimpleRetryPolicyは例外リストに対して固定回数リトライします。また、リトライしたくない"fatal"例外も設定可能で、これはリトライ可能例外リストをオーバーライドします。これにより、リトライの振る舞いをより細かく制御します。以下はその例です。

SimpleRetryPolicy policy = new SimpleRetryPolicy();
// 最大リトライ回数
policy.setMaxAttempts(5);
// すべての例外に対しリトライ(デフォルト)
policy.setRetryableExceptions(new Class[] {Exception.class});
// ただし、IllegalStateExceptionはリトライしない
policy.setFatalExceptions(new Class[] {IllegalStateException.class});

// 上記ポリシーを使用
RetryTemplate template = new RetryTemplate();
template.setRetryPolicy(policy);
template.execute(new RetryCallback<Foo>() {
    public Foo doWithRetry(RetryContext context) {
        // ここにビジネスロジック
    }
});

より柔軟な実装ExceptionClassifierRetryPolicyもあり、ExceptionClassifierで例外セットに対して異なるリトライを設定できます。例外をRetryPolicyにデリゲートするclassifierを呼ぶという動作にポリシーがなります。たとえば、ある例外については何回かリトライするように異なるポリシーでマッピングする、が出来ます。

場合によって、更にカスタマイズした判断ロジックでリトライポリシーを実装したいケースがあります。たとえば、あるカスタムリトライポリシーで、well-knownでソリューション固有の例外をリトライ可能かそうでないか、というものを作れます。

1.3. Backoff Policies

一時的なエラー後にリトライする場合、これらの問題はwait以外に解決策が無い場合が多いため、再試行前にwaitするのが有用です。RetryCallback失敗時に、RetryTemplateBackoffPolicyに従った実行の一時停止が出来ます。

以下コードはBackOffPolicyのインタフェース定義です。

public interface BackoffPolicy {

    BackOffContext start(RetryContext context);

    void backOff(BackOffContext backOffContext)
        throws BackOffInterruptedException;

}

BackoffPolicyでバックオフをどう実装するかは自由です。Spring Batchが提供するポリシーはデフォルト設定ではすべてObject.wait()を使います。バックオフのよくある使い方はwait間隔を指数関数的に増加させるもので、これは2つのリトライがロックしたり両方とも失敗するのを避けるためです(イーサネットから得た教訓)。これをするには、Spring BatchのExponentialBackoffPolicyを使います。

1.4. Listeners

複数リトライの横断的関心事を扱うコールバックを追加すると便利な場合があります。これのために、Spring BatchはRetryListenerを提供しています。RetryTemplateにはRetryListenersを登録可能で、リスナーにはイテレーション中に発生するThrowableRetryContextを渡します。

RetryListenerのインタフェース定義は以下の通りです。

public interface RetryListener {

    <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback);

    <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable);

    <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable);
}

opencloseは最もシンプルなケースではリトライ全体の前後に呼び出され、onErrorRetryCallbackのたびに呼ばれます。closeにはThrowableも渡されます。エラーがあった場合、RetryCallbackが最後にスローしたものが渡されます。

注意点として、複数リスナーがある場合、これはリストになり順序を持ちます。この場合、openはリスト順になりますがonErrorcloseは逆順になります。

1.5. Declarative Retry

ビジネスロジック実行をリトライしたい場合があります。これの古典的なサンプルはリモートサービス呼び出しです。Spring BatchにはRetryOperationsでメソッド呼び出しをラップするAOPインターセプターの仕組みがあります。RetryOperationsInterceptorインターセプトメソッドを実行し、RetryTemplateに指定した``RetryPolicy```に従って失敗時にリトライします。

以下は宣言的リトライのサンプルで、remoteCallというサービスメソッドのリトライをjava configurationでしています(詳細はSpring User GuideのAOPインターセプター設定を参照してください)。

@Bean
public MyService myService() {
        ProxyFactory factory = new ProxyFactory(RepeatOperations.class.getClassLoader());
        factory.setInterfaces(MyService.class);
        factory.setTarget(new MyService());

        MyService service = (MyService) factory.getProxy();
        JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
        pointcut.setPatterns(".*remoteCall.*");

        RetryOperationsInterceptor interceptor = new RetryOperationsInterceptor();

        ((Advised) service).addAdvisor(new DefaultPointcutAdvisor(pointcut, interceptor));

        return service;
}

上のサンプルはインターセプターにデフォルトのRetryTemplateを使用します。ポリシーやリスナーの変更には、RetryTemplateを設定してください。

Spring Batch 4.1.x - Reference Documentation - Repeatのテキトー翻訳

https://docs.spring.io/spring-batch/4.1.x/reference/html/repeat.html#repeat

https://qiita.com/kagamihoge/items/12fbbc2eac5b8a5ac1e0 俺の訳一覧リスト

1. Repeat

1.1. RepeatTemplate

バッチ処理とは繰り返し処理であり、simple optimizationとなるかjobの一部となるか、のどちらかです。strategyパターン化・繰り返しの汎用化・iterator相当の機能、を提供するために、Spring BatchにはRepeatOperationsがあります。RepeatOperationsは以下の定義となります。

public interface RepeatOperations {

    RepeatStatus iterate(RepeatCallback callback) throws RepeatException;

}

引数のcallbackはインタフェースで以下の定義となっており、繰り返し処理のビジネスロジックを記述します。

public interface RepeatCallback {

    RepeatStatus doInIteration(RepeatContext context) throws Exception;

}

コールバックがイテレーションを終了するまで、コールバックを繰り返し実行します。上記インタフェースの戻り値はenumRepeatStatus.CONTINUABLERepeatStatus.FINISHEDのどちらかになります。RepeatStatus enumは呼び出し元に繰り返し処理を続行するかどうかを示します。基本的には、RepeatOperationsの実装はRepeatStatusをチェックしてイテレーションの終了判断の一つに使用します。コールバックでこれ以上の処理が無い事を呼び出し元に知らせるにはRepeatStatus.FINISHEDを返します。

RepeatOperationsの最もシンプルで汎用な実装はRepeatTemplateで、以下のように使います。

RepeatTemplate template = new RepeatTemplate();

template.setCompletionPolicy(new SimpleCompletionPolicy(2));

template.iterate(new RepeatCallback() {

    public RepeatStatus doInIteration(RepeatContext context) {
        // Do stuff in batch...
        return RepeatStatus.CONTINUABLE;
    }

});

上の例では、RepeatStatus.CONTINUABLEで、処理続行を示しています。また、コールバックは処理終了を呼び出し元に伝えるためにRepeatStatus.FINISHEDを返すことも出来ます。コールバック処理に内在する考慮事項によって、強制終了するイテレーションがありえます。上のsetCompletionPolicyのように終了条件を外部にデリゲートするコールバック以外は無限ループです。

1.1.1. RepeatContext

RepeatCallbackの引数はRepeatContextです。基本的にはコールバックで使うことは稀です。ただし、必要に応じて、イテレーション中の一時データを格納する場所として使えます。iterateが返ると、コンテキストは無くなります。

イテレーションがネストする場合、RepeatContextは親コンテキストを持ちます。親コンテキストは複数のiterate間でデータ共有したいときに便利です。たとえば、複数のサブシーケンス呼び出し後もイテレーションでのイベント発生数を保持し続けたい場合、などです。

1.1.2. RepeatStatus

Spring Batchで処理を終了するかどうかを指示するのにはRepeatStatusを使います。以下表のように、RepeatStatusには2種類の値があります。

Table 1. RepeatStatus Properties

Value Description
CONTINUABLE 未処理アイテムが存在
FINISHED ループ処理終了

また、RepeatStatusand()``でANDを取れます。これはcontinuableフラグとのANDです。つまり、両方ともCONTINUABLEでなければ、FINISHED```になります。

※以下コードは筆者補足

    System.out.println(RepeatStatus.CONTINUABLE.and(true));  //CONTINUABLE
    System.out.println(RepeatStatus.CONTINUABLE.and(false)); //FINISHED
    System.out.println(RepeatStatus.FINISHED.and(true));     //FINISHED
    System.out.println(RepeatStatus.FINISHED.and(false));    //FINISHED

1.2. Completion Policies

RepeatTemplateでは、iterateループの終了はCompletionPolicyが決定します。また、RepeatContextのファクトリも行います。RepeatTemplateRepeatContextの生成に設定されているポリシーを使用し、それをイテレーションRepeatCallbackに毎回渡します。コールバックがdoInIterationを完了すると、RepeatTemplateは状態(RepeatContextに保存)を更新するようCompletionPolicyを呼び出します。それから、イテレーションを完了するかどうかをポリシーで決定します。

Spring BatchはCompletionPolicyはシンプルで汎用の実装を用意しています。SimpleCompletionPolicyは固定回数まで実行します(RepeatStatus.FINISHEDで任意のタイミングで強制終了も可能)。

より複雑な終了判定を行う自前のポリシーを実装することも可能です。たとえば、オンラインシステムで使用中であればバッチジョブを起動しないようにするバッチ処理ウィンドウに対しては、カスタムのポリシーを作成します。

1.3. Exception Handling

RepeatCallback内で例外スローする場合、RepeatTemplateは例外再スローかどうかを判断するExceptionHandlerを参照します。

以下はExceptionHandlerの定義です。

public interface ExceptionHandler {

    void handleException(RepeatContext context, Throwable throwable)
        throws Throwable;

}

良くある使い方としては指定する例外スローの回数をカウントしてリミットに達したら失敗させます。Spring BatchはSimpleLimitExceptionHandlerと、これをもう少し柔軟にしたRethrowOnThresholdExceptionHandlerを用意しています。SimpleLimitExceptionHandlerはリミットのプロパティとチェック対象の例外の型を指定します。対象の例外の型のサブクラスはすべてカウント対象です。リミットに達するまでそれらの例外は無視し、達すると再スローします。それ以外の例外は常に再スローします。

SimpleLimitExceptionHandlerのオプションのプロパティuseParentは重要です。デフォルトではfalseで、リミットは現在のRepeatContextに対してのみ適用します。trueにすると、ネストしたイテレーションの兄弟コンテキスト間でリミットは保持されます。

1.4. Listeners

異なるイテレーションの横断的関心事を扱うコールバックがあると便利な場合があります。Spring BatchにはRepeatListenerがあります。RepeatTemplateRepeatListenerの実装を登録すると、イテレーション中にRepeatContextRepeatStatusのしかるべきポイントでコールバックが呼ばれます。

RepeatListenerは以下のような定義です。

public interface RepeatListener {
    void before(RepeatContext context);
    void after(RepeatContext context, RepeatStatus result);
    void open(RepeatContext context);
    void onError(RepeatContext context, Throwable e);
    void close(RepeatContext context);
}

opencloseイテレーション全体の開始前後のコールバックです。before, after, onErrorRepeatCallbackの呼び出しに適用されます。

なお、1つ以上のリスナーがある場合、そのリストで順序が発生します。この場合、openbeforeは同一順序で呼ばれ、after, onError, closeは逆順で呼ばれます。

1.5. Parallel Processing

RepeatOperationsの実装でコールバックをシーケンシャルに実行するという制限はありません。コールバックをパラレル実行する実装が可能な点は重要です。Spring BatchにはTaskExecutorRepeatTemplateがあり、RepeatCallbackの実行にSpringのTaskExecutorを使います。デフォルトではSynchronousTaskExecutorを使用し、これは同一スレッドでイテレーション全体を実行します(つまりRepeatTemplateと基本的に同じ)。

1.6. Declarative Iteration

ビジネスロジック実行をリピートしたい場合があります。これの古典的なサンプルはメッセージパイプラインの最適化です。もし頻繁にメッセージが到着する場合、メッセージごとに別々のトランザクションを張るよりも、メッセージをバッチ処理するのが効果的です。Spring BatchはRepeatOperationsにメソッド呼び出しをラップするAOPインターセプターを用意しています。RepeatOperationsInterceptorインターセプト対象メソッドを実行するとCompletionPolicyに従いRepeatTemplateで繰り返し実行します。

以下の例はjava configurationでprocessMessageのサービスメソッド呼び出しを繰り返し実行する設定です(AOPインターセプターの詳細な設定例はSpring User Guideを参照してください)。

@Bean
public MyService myService() {
        ProxyFactory factory = new ProxyFactory(RepeatOperations.class.getClassLoader());
        factory.setInterfaces(MyService.class);
        factory.setTarget(new MyService());

        MyService service = (MyService) factory.getProxy();
        JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
        pointcut.setPatterns(".*processMessage.*");

        RepeatOperationsInterceptor interceptor = new RepeatOperationsInterceptor();

        ((Advised) service).addAdvisor(new DefaultPointcutAdvisor(pointcut, interceptor));

        return service;
}

上のサンプルはインターセプターにデフォルトのRepeatTemplateを使用します。ポリシー・リスナー・などを変更するには、インターセプターに```RepeatTemplate````を設定してください。

インターセプトメソッドがvoidを返す場合、インターセプターは常にRepeatStatus.CONTINUABLEを返します(CompletionPolicyで何らかの終了条件を指定しない場合無限ループの危険性がある)。もしくは、インターセプトメソッドの戻り値がnullになるまでRepeatStatus.CONTINUABLEを返し、nullの時点でRepeatStatus.FINISHEDを返します。よって、ターゲットメソッドのビジネスロジックは、nullを返すかRepeatTemplateExceptionHandlerが再スローする例外のスローにより、処理終了を伝えることが出来ます。