kagamihogeの日記

kagamihogeの日記です。

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

https://docs.spring.io/spring-batch/4.1.x/reference/html/common-patterns.html#commonPatterns

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

1. Common Batch Patterns

ある種のジョブはSpring Batch標準コンポーネントのみの組み合わせで構築できます。ItemReaderItemWriterの実装は様々なケースに適用可能です。しかし、基本的には、カスタムコードの実装が必要です。アプリケーション開発者に対するSpring BatchのエントリーポイントはTasklet, ItemReader, ItemWriterと各種リスナーです。シンプルなバッチジョブではSpring BatchのItemReaderを使用出来ますが、ItemWriterItemProcessorにカスタムの処理な書き込みが必要となる場合があります。

このチャプターでは、カスタムビジネスロジックにおける共通パターンの例について解説します。これらは主としてリスナーを活用します。なお、ItemReaderItemWriter実装はリスナーも実装可能です。

1.1. Logging Item Processing and Failures

1アイテムごとにstepでエラーハンドリングを行い、特別なチャネルにロギングしたりDBにレコードを追加したり、をするための共通パターンです。chunk指向Step(stepファクトリビーンで生成)では単純に、readエラーはItemReadListenerwriteエラーはItemWriteListenerの実装により実現します。以下コードはreadとwriteエラーログ出力するリスナの例です。

public class ItemFailureLoggerListener extends ItemListenerSupport {

    private static Log logger = LogFactory.getLog("item.error");

    public void onReadError(Exception ex) {
        logger.error("Encountered error on read", e);
    }

    public void onWriteError(Exception ex, List<? extends Object> items) {
        logger.error("Encountered error on write", ex);
    }
}

リスナ実装したら以下例のようにstepに登録します。

Java Configuration

@Bean
public Step simpleStep() {
        return this.stepBuilderFactory.get("simpleStep")
                                ...
                                .listener(new ItemFailureLoggerListener())
                                .build();
}

※ リスナーのonError()での処理は、後にロールバックされるトランザクション内での処理になります。DBなどトランザクショナルなリソースをonError()で使う場合、リスナーメソッドに宣言的トランザクション(詳細はSpring Core Reference Guide参照)を追加し、propagation attributeをREQUIRES_NEWにしてください。

1.2. Stopping a Job Manually for Business Reasons

Spring BatchにはJobLauncherstop()がありますが、これはアプリケーションプログラマというよりオペレータが使うものです。ただ、ビジネスロジック内でのジョブ実行停止したい場合がありえます。

一番シンプルな方法はRuntimeExceptionのスローです(無期限リトライやスキップしない例外)。例えば以下ではカスタム例外を使用しています。

public class PoisonPillItemProcessor<T> implements ItemProcessor<T, T> {

    @Override
    public T process(T item) throws Exception {
        if (isPoisonPill(item)) {
            throw new PoisonPillException("Poison pill detected: " + item);
        }
        return item;
    }
}

別の方法としてstepを止めるには、以下例のように、ItemReadernullを返します。

public class EarlyCompletionItemReader implements ItemReader<T> {

    private ItemReader<T> delegate;

    public void setDelegate(ItemReader<T> delegate) { ... }

    public T read() throws Exception {
        T item = delegate.read();
        if (isEndItem(item)) {
            return null; // ここでstep終了
        }
        return item;
    }

}

上の例はCompletionPolicyのデフォルト実装のnullがバッチ完了を意味する挙動を利用しています。より複雑な完了ポリシーを実装してStepに設定するにはSimpleStepFactoryBeanを使用します。

Java Configuration

@Bean
public Step simpleStep() {
        return this.stepBuilderFactory.get("simpleStep")
                                .<String, String>chunk(new SpecialCompletionPolicy())
                                .reader(reader())
                                .writer(writer())
                                .build();
}

また別の方にStepExecutionにフラグを設定し、アイテム処理中にフレームワーク内でStep実装がこれをチェックします。これを実装するには、StepExecutionにアクセスするためにStepListenerを実装してStepに登録します。以下はフラグを設定するリスナーの例です。

public class CustomItemWriter extends ItemListenerSupport implements StepListener {

    private StepExecution stepExecution;

    public void beforeStep(StepExecution stepExecution) {
        this.stepExecution = stepExecution;
    }

    public void afterRead(Object item) {
        if (isPoisonPill(item)) {
            stepExecution.setTerminateOnly(true);
       }
    }

}

フラグをセットする場合、デフォルトの振る舞いはstepでJobInterruptedExceptionをスローします。この振る舞いはStepInterruptionPolicyで制御できます。なお、例外スローかしないかの選択肢しか無いので、ジョブは常にabnormal endingになります。

1.3. Adding a Footer Record

フラットファイルに書き込む場合、すべての処理完了後にファイル末尾にフッター行を追加したい場合があります。これはSpring BatchのFlatFileFooterCallbackにより実装できます。FlatFileFooterCallback(と対になるFlatFileHeaderCallback)はFlatFileItemWriterのオプションプロパティで以下のようにwriterに追加します。

Java Configuration

@Bean
public FlatFileItemWriter<String> itemWriter(Resource outputResource) {
        return new FlatFileItemWriterBuilder<String>()
                        .name("itemWriter")
                        .resource(outputResource)
                        .lineAggregator(lineAggregator())
                        .headerCallback(headerCallback())
                        .footerCallback(footerCallback())
                        .build();
}

フッターのコールバックには1つだけメソッドがあり、フッター書き込み時に呼び出されます。

public interface FlatFileFooterCallback {

    void writeFooter(Writer writer) throws IOException;

}

1.3.1. Writing a Summary Footer

フッター行に対するよくある要望に、出力処理中の情報を集約してファイル末尾にそれを追加する、があります。このフッターはファイルの要約やチェックサムに使われます。

例えば、バッチジョブがフラットファイルにTrade行を書き込むとして、全Tradesの総合計をフッターに配置したい場合、ItemWriter実装を以下のようにします。

public class TradeItemWriter implements ItemWriter<Trade>,
                                        FlatFileFooterCallback {

    private ItemWriter<Trade> delegate;

    private BigDecimal totalAmount = BigDecimal.ZERO;

    public void write(List<? extends Trade> items) throws Exception {
        BigDecimal chunkTotal = BigDecimal.ZERO;
        for (Trade trade : items) {
            chunkTotal = chunkTotal.add(trade.getAmount());
        }

        delegate.write(items);

        // アイテム正常書き込み後に総合計を加算
        totalAmount = totalAmount.add(chunkTotal);
    }

    public void writeFooter(Writer writer) throws IOException {
        writer.write("Total Amount Processed: " + totalAmount);
    }

    public void setDelegate(ItemWriter delegate) {...}
}

TradeItemWritertotalAmountTradeアイテム書き込み後に加算します。すべてのTrade処理後、フレームワークwriteFooterを呼び、ファイルにtotalAmountを挿入します。なお、writeには一時変数chunkTotalがあり、これにchunkのTradeの合計を格納します。これは、writeでskipが発生した場合はtotalAmountを変更しないためです。writeメソッドを最後まで実行し、例外がスローされなかったことを確認できたら、totalAmountを更新します。

writeFooterの使用には、TradeItemWriterFlatFileFooterCallbackの実装クラス)をfooterCallbackとしてFlatFileItemWriterにワイヤリングします。以下はその方法の例です。

Java Configuration

@Bean
public TradeItemWriter tradeItemWriter() {
        TradeItemWriter itemWriter = new TradeItemWriter();

        itemWriter.setDelegate(flatFileItemWriter(null));

        return itemWriter;
}

@Bean
public FlatFileItemWriter<String> flatFileItemWriter(Resource outputResource) {
        return new FlatFileItemWriterBuilder<String>()
                        .name("itemWriter")
                        .resource(outputResource)
                        .lineAggregator(lineAggregator())
                        .footerCallback(tradeItemWriter())
                        .build();
}

このTradeItemWriterような作り方はStepが非リスタート可能の場合のみ正しく動作します。これは、このクラスはstateful(totalAmountがある)だが、そのtotalAmountをDBに永続化していないためです。よって、リスタート時にtotalAmountを取得できません。このクラスをリスタート可能にするには、ItemStreamは以下例のようにopenupdateを実装します。

public void open(ExecutionContext executionContext) {
    if (executionContext.containsKey("total.amount") {
        totalAmount = (BigDecimal) executionContext.get("total.amount");
    }
}

public void update(ExecutionContext executionContext) {
    executionContext.put("total.amount", totalAmount);
}

updateメソッドはExecutionContexttotalAmountの現在値を保存します。openメソッドはExecutionContextからtotalAmountを取得し処理開始時の値として使用し、Stepの前回終了時点からリスタートします。

1.4. Driving Query Based ItemReaders

chapter on readers and writersで、ページングを使用するDB入力について解説しました。DB2など、たいていのDBで、他のオンラインアプリケーションなどで必要なテーブルを読み込む場合、過度の悲観的ロックは問題を起こす場合があります。加えて、過度に大きなデータセットに対するカーソルオープンが特定DBで問題を起こす場合があります。よって、データ読み取りに'Driving Query'を取るプロジェクトがあります。この方針は、以下図のように、返す必要のあるオブジェクト全体ではなく、キーをイテレートする動作をします。

Figure 1. Driving Query Job

上記例は、カーソルベースのサンプルで使用したのと同じ'FOO'テーブルです。ただし、行全体を選択するのではなく、ここのSQLステートメントではIDのみ選択しています。よって、readFOOオブジェクトではなく、Integerを返します。このIDの数値を使用し、後でFooオブジェクトという"詳細"を取得します。

Figure 2. Driving Query Example

ItemProcessorはdriving queryから得たキーを基に'Foo'オブジェクトへと変換します。DAOを使用してキーを基にオブジェクト全体をクエリします。

1.5. Multi-Line Records

基本的にはフラットファイルは各レコードを1行になりますが、レコードを複数のフォーマットで複数行に展開することもあります。以下はそうした展開例の抜粋です。

HEA;0013100345;2007-02-15
NCU;Smith;Peter;;T;20014539;F
BAD;;Oak Street 31/A;;Small Town;00235;IL;US
FOT;2;2;267.34

1行の開始は'HEA'で始まり、1行の最終行は'FOT'で始まります。これを正しく扱うには以下を考慮する必要があります。

  • 1度に1レコード読み込むのでなく、ItemReaderは複数行レコードのグループとして読み込み、そのグループをItemWriterに渡します。
  • 各行の種類ごとにトークン処理を行う。

単一レコードを複数行に展開し、かつ、全体で何行来るかは事前に不明なため、ItemReaderはレコード全体を注意深く読みこむ必要があります。これを行うには、FlatFileItemReaderのラッパーとしてItemReaderを実装します。

Java Configuration

@Bean
public MultiLineTradeItemReader itemReader() {
        MultiLineTradeItemReader itemReader = new MultiLineTradeItemReader();

        itemReader.setDelegate(flatFileItemReader());

        return itemReader;
}

@Bean
public FlatFileItemReader flatFileItemReader() {
        FlatFileItemReader<Trade> reader = new FlatFileItemReaderBuilder<Trade>()
                        .name("flatFileItemReader")
                        .resource(new ClassPathResource("data/iosample/input/multiLine.txt"))
                        .lineTokenizer(orderFileTokenizer())
                        .fieldSetMapper(orderFieldSetMapper())
                        .build();
        return reader;
}

各行に応じた適切なトークン分割をするため、特に重要なのは固定幅入力なので、デリゲート先のFlatFileItemReaderに対してPatternMatchingCompositeLineTokenizerを使います。FlatFileItemReader in the Readers and Writers chapterに詳細があります。それから、デリゲート先のreaderは各行をラップ元のItemReaderFieldSetで返すためにPassThroughFieldSetMapperを使います。

Java Content

@Bean
public PatternMatchingCompositeLineTokenizer orderFileTokenizer() {
        PatternMatchingCompositeLineTokenizer tokenizer =
                        new PatternMatchingCompositeLineTokenizer();

        Map<String, LineTokenizer> tokenizers = new HashMap<>(4);

        tokenizers.put("HEA*", headerRecordTokenizer());
        tokenizers.put("FOT*", footerRecordTokenizer());
        tokenizers.put("NCU*", customerLineTokenizer());
        tokenizers.put("BAD*", billingAddressLineTokenizer());

        tokenizer.setTokenizers(tokenizers);

        return tokenizer;
}

ラッパーはレコード終端を解釈する必要があります。よって、レコード終端に達するまで、デリゲート先のreadを繰り返し呼びます。各行を読み込むためには、ラッパーで返すアイテムを組み立てる必要があります。フッターに達すると、アイテムはItemProcessorItemWriterに渡せるようになります。

private FlatFileItemReader<FieldSet> delegate;

public Trade read() throws Exception {
    Trade t = null;

    for (FieldSet line = null; (line = this.delegate.read()) != null;) {
        String prefix = line.readString(0);
        if (prefix.equals("HEA")) {
            t = new Trade(); // Record must start with header
        }
        else if (prefix.equals("NCU")) {
            Assert.notNull(t, "No header was found.");
            t.setLast(line.readString(1));
            t.setFirst(line.readString(2));
            ...
        }
        else if (prefix.equals("BAD")) {
            Assert.notNull(t, "No header was found.");
            t.setCity(line.readString(4));
            t.setState(line.readString(6));
          ...
        }
        else if (prefix.equals("FOT")) {
            return t; // Record must end with footer
        }
    }
    Assert.isNull(t, "No 'END' was found.");
    return null;
}

1.6. Executing System Commands

バッチジョブ内から外部のコマンドを呼ぶ必要があるケースは多いです。そうした処理はスケジューラで別途に開始出来ますが、実行時のメタデータの利点が失われます。また、マルチステップジョブを複数ジョブに分割する必要も発生します。

よくあるケースなので、Spring Batchは以下のようにシステムコマンドを呼ぶTaskletを用意しています。

Java Configuration

@Bean
public SystemCommandTasklet tasklet() {
        SystemCommandTasklet tasklet = new SystemCommandTasklet();

        tasklet.setCommand("echo hello");
        tasklet.setTimeout(5000);

        return tasklet;
}

1.7. Handling Step Completion When No Input is Found

DBからの取得結果やファイルが0件なケースは良くあります。Stepでは単純に何も処理が無いので0アイテムを読み込んで完了します。Spring Batch標準クラスのItemReaderはすべてデフォルトではこの方針になっています。入力があるのに何も書き込まないのは、混乱を招く場合があります(ファイル名がおかしくなったり、他同様な問題の発生)。For this reason, the metadata itself should be inspected to determine how much work the framework found to be processed. しかし、入力0件が例外的の場合にはどうすれば良いでしょうか。この場合、0件処理のメタデータをプログラム的にチェックして失敗とするのがベストです。これは一般的なユースケースなため、Spring Batchはその機能をリスナーNoWorkFoundStepExecutionListenerで提供しています。定義は以下の通りです。

public class NoWorkFoundStepExecutionListener extends StepExecutionListenerSupport {

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

}

上のStepExecutionListenerは'afterStep'でStepExecutionreadCountプロパティで読み込み0件かどうかをチェックします。0件の場合、FAILEDのexit codeでリターンし、Stepは失敗になります。そうでない場合、nullが返され、Stepのステータスは何も変更しません。

1.8. Passing Data to Future Steps

stepから別のstepにデータを渡したい場合が良くあります。これにはExecutionContextを使います。2つのExecutionContextsStepJob、はそれぞれ一長一短です。StepExecutionContextはstep中のみ、JobExecutionContextJob全体です。また、StepExecutionContextStepのchunkがコミットする際に更新し、JobExecutionContextStep終了時にだけ更新します。

このためStep実行中はStepExecutionContextに全データを保存してください。これによりStep実行中にデータが適切に保存されます。もしデータをJobExecutionContextに置く場合、Stepの実行中には永続化しません。Stepが失敗するとそのデータはロストします。

public class SavingItemWriter implements ItemWriter<Object> {
    private StepExecution stepExecution;

    public void write(List<? extends Object> items) throws Exception {
        // ...

        ExecutionContext stepContext = this.stepExecution.getExecutionContext();
        stepContext.put("someKey", someObject);
    }

    @BeforeStep
    public void saveStepExecution(StepExecution stepExecution) {
        this.stepExecution = stepExecution;
    }
}

次以降のStepsでデータを利用可能にするには、step終了後にJobExecutionContextへ"昇格"の必要があります。Spring BatchではExecutionContextPromotionListenerを使います。このリスナーにはExecutionContextで昇格させたいキーを設定します。また、オプションで、昇格するexit codeのリストパターンも設定出来ます(COMPLETEDがデフォルト)。他リスナー同様、以下サンプルのようにStepに登録します。

Java Configuration

@Bean
public Job job1() {
        return this.jobBuilderFactory.get("job1")
                                .start(step1())
                                .next(step1())
                                .build();
}

@Bean
public Step step1() {
        return this.stepBuilderFactory.get("step1")
                                .<String, String>chunk(10)
                                .reader(reader())
                                .writer(savingWriter())
                                .listener(promotionListener())
                                .build();
}

@Bean
public ExecutionContextPromotionListener promotionListener() {
        ExecutionContextPromotionListener listener = new ExecutionContextPromotionListener();

        listener.setKeys(new String[] {"someKey" });

        return listener;
}

以下サンプルのように、JobExecutionContextから保存した値を取得するには以下のようにします。

public class RetrievingItemWriter implements ItemWriter<Object> {
    private Object someObject;

    public void write(List<? extends Object> items) throws Exception {
        // ...
    }

    @BeforeStep
    public void retrieveInterstepData(StepExecution stepExecution) {
        JobExecution jobExecution = stepExecution.getJobExecution();
        ExecutionContext jobContext = jobExecution.getExecutionContext();
        this.someObject = jobContext.get("someKey");
    }
}

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を設定してください。