CLOVER🍀

That was when it all began.

Spring Batchで、Bean ValidationとItemのスキップの動作を確認する

これは、なにをしたくて書いたもの?

Spring BatchとBean Validationを組み合わせて、Itemのバリデーションをしたり、NGになったItemをスキップできたりするというので
ちょっと試してみようかなと。

Spring BatchとBean Validation

Spring BatchでBean Validationが使えることはドキュメントに書かれているわけですが、ItemProcessorの用途のひとつとして登場します。

Item processing / Validating Input

BeanValidatingItemProcessorというItemProcessorが提供されているので、こちらを使って実現するようですね。

BeanValidatingItemProcessor (Spring Batch 4.3.5 API)

Springのバリデーションを使う場合は、こちらのようです。

ValidatingItemProcessor (Spring Batch 4.3.5 API)

バリデーションでNGになると例外がスローされるので、そのままだとアプリケーションが停止するようです。

原因となったItemをスキップするには、Stepで設定するようです。

Configuring a Step / Chunk-oriented Processing / Configuring Skip Logic

今回は、このあたりを試してみます。

お題

今回のお題は、以下とします。

  • 書籍データのCSVを読み込み、読み込んだデータを標準出力に書き出すChunk形式のJobおよびStepを作成する
  • Bean Validationを行うitemProcessorを追加し、エラーになるItemを含んだCSVを入力する
  • まずはバリデーションNGになって停止するところを確認し、次にエラーになったItemをスキップする設定を行う

こんな感じでやっていきます。

環境

今回の環境は、こちら。

$ java --version
openjdk 17.0.3 2022-04-19
OpenJDK Runtime Environment (build 17.0.3+7-Ubuntu-0ubuntu0.20.04.1)
OpenJDK 64-Bit Server VM (build 17.0.3+7-Ubuntu-0ubuntu0.20.04.1, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.5 (3599d3414f046de2324203b78ddcf9b5e4388aa0)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.3, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-109-generic", arch: "amd64", family: "unix"

JobRepositoryのデータは、MySQLに格納することにします。バージョンは以下で、172.17.0.2で動作しているものとします。

$ mysql --version
mysql  Ver 8.0.28 for Linux on x86_64 (MySQL Community Server - GPL)

準備

では、Spring Bootプロジェクトを作成していきます。

依存関係にbatch、validation、mysqlを指定して、作成。

$ curl -s https://start.spring.io/starter.tgz \
  -d bootVersion=2.6.7 \
  -d javaVersion=17 \
  -d name=batch-beanvalidation \
  -d groupId=org.littlewings \
  -d artifactId=batch-beanvalidation \
  -d version=0.0.1-SNAPSHOT \
  -d packageName=org.littlewings.spring.batch \
  -d dependencies=batch,validation,mysql \
  -d baseDir=batch-beanvalidation | tar zxvf -

プロジェクト内に移動。

$ cd batch-beanvalidation

自動生成されたソースコードは、削除しておきます。

$ rm src/main/java/org/littlewings/spring/batch/BatchBeanvalidationApplication.java src/test/java/org/littlewings/spring/batch/BatchBeanvalidationApplicationTests.java

Mavenの依存関係およびプラグインの設定は、こちら。

 <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-batch</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.batch</groupId>
            <artifactId>spring-batch-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

続いて、ソースコードを作成していきます。

Item相当のクラス。

src/main/java/org/littlewings/spring/batch/Book.java

package org.littlewings.spring.batch;

import javax.validation.constraints.Min;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

public class Book {
    @Size(min = 14, max = 14)
    @Pattern(regexp = "\\d{3}-\\d{10}")
    String isbn;

    @Size(max = 200)
    String title;

    @Min(1000)
    Integer price;

    // getter/setterは省略
}

謎に、1,000円以上の本しか受け付けないようになっています。

Jobの定義。

src/main/java/org/littlewings/spring/batch/BeanValidationJobConfig.java

package org.littlewings.spring.batch;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.step.skip.AlwaysSkipItemSkipPolicy;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;
import org.springframework.batch.item.validator.BeanValidatingItemProcessor;
import org.springframework.batch.item.validator.ValidationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

@Configuration
public class BeanValidationJobConfig {
    @Autowired
    JobBuilderFactory jobBuilderFactory;

    @Autowired
    StepBuilderFactory stepBuilderFactory;

    @Autowired
    LocalValidatorFactoryBean localValidatorFactoryBean;

    @Bean
    public Job beanValidationJob() {
        return jobBuilderFactory
                .get("beanValidationJob")
                .incrementer(new RunIdIncrementer())
                // 後で
                .build();
    }

    // 後で

    @StepScope
    @Bean
    public FlatFileItemReader<Book> flatFileItemReader(@Value("#{jobParameters['filePath']}") String filePath) {
        Resource fileResource = new FileSystemResource(filePath);

        return new FlatFileItemReaderBuilder<Book>()
                .name("flatFileItemReader")
                .resource(fileResource)
                .encoding("UTF-8")
                .delimited()
                .names(new String[]{"isbn", "title", "price"})
                .linesToSkip(1)
                .targetType(Book.class)
                .saveState(false)
                .build();
    }

    // 後で

    @StepScope
    @Bean
    public ItemWriter<Book> consoleItemWriter() {
        Logger logger = LoggerFactory.getLogger("consoleItemWriter");

        return books -> books.forEach(book ->
                logger.info("[writer] isbn = {}, title = {}, price = {}", book.getIsbn(), book.getTitle(), book.getPrice())
        );
    }
}

Jobに含まれるStepの構成や

    @Bean
    public Job beanValidationJob() {
        return jobBuilderFactory
                .get("beanValidationJob")
                .incrementer(new RunIdIncrementer())
                // 後で
                .build();
    }

    // 後で

Bean Validationを使うItemProcessorの記述は後回しにしています。

    // 後で

なお、ファイルの読み込みはFlatFileItemReaderBuilderで行い、

    @StepScope
    @Bean
    public FlatFileItemReader<Book> flatFileItemReader(@Value("#{jobParameters['filePath']}") String filePath) {
        Resource fileResource = new FileSystemResource(filePath);

        return new FlatFileItemReaderBuilder<Book>()
                .name("flatFileItemReader")
                .resource(fileResource)
                .encoding("UTF-8")
                .delimited()
                .names(new String[]{"isbn", "title", "price"})
                .linesToSkip(1)
                .targetType(Book.class)
                .saveState(false)
                .build();
    }

ItemWriterは渡ってきたチャンクを標準出力に書き出すように作成。

    @StepScope
    @Bean
    public ItemWriter<Book> consoleItemWriter() {
        Logger logger = LoggerFactory.getLogger("consoleItemWriter");

        return books -> books.forEach(book ->
                logger.info("[writer] isbn = {}, title = {}, price = {}", book.getIsbn(), book.getTitle(), book.getPrice())
        );
    }

mainクラス。

src/main/java/org/littlewings/spring/batch/App.java

package org.littlewings.spring.batch;

import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@EnableBatchProcessing
public class App {
    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }
}

設定は、こんな感じにしておきます。

src/main/resources/application.properties

spring.datasource.url=jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=UTF-8
spring.datasource.username=kazuhira
spring.datasource.password=password

spring.batch.jdbc.initialize-schema=always

CSVも作成しましょう。

まずは、全Itemが問題ないデータのCSV。

src/main/resources/book.csv

isbn,title,price
978-4798142470,Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発,4400
978-4774182179,[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ,4180
978-1492076988,Spring Boot: Up and Running: Building Cloud Native Java and Kotlin Applications,6265
978-4295000198,やさしく学べるMySQL運用・管理入門【5.7対応】,2860
978-1484237236,The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud,7361
978-4798147406,詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド (NEXT ONE),3960
978-4798161488,MySQL徹底入門 第4版 MySQL 8.0対応,4180
978-4797393118,基礎からのMySQL 第3版 (基礎からシリーズ),6038
978-4873116389,実践ハイパフォーマンスMySQL 第3版,5280
978-4774170206,MariaDB&MySQL全機能バイブル,3850

全10件あり、ヘッダー入りです。

次に、ところどころおかしなデータが入ったCSV。

src/main/resources/book_invalid.csv

isbn,title,price
978-4798142470,Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発,4400
xx-xxxxxxx,[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ,4180
978-1492076988,Spring Boot: Up and Running: Building Cloud Native Java and Kotlin Applications,6265
978-4295000198,やさしく学べるMySQL運用・管理入門【5.7対応】,860
978-1484237236,The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud,7361
978-4798147406,詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド (NEXT ONE),960
978-4798161488,MySQL徹底入門 第4版 MySQL 8.0対応,4180
978-4797393118,基礎からのMySQL 第3版 (基礎からシリーズ),6038
978-4873116389,実践ハイパフォーマンスMySQL 第3版,5280
978-4774170206,MariaDB&MySQL全機能バイブル,850

4件、エラーになるデータ(ISBNの形式誤り、価格が1,000円未満)が入っています。

ここまでで、準備は完了です。

BeanValidatingItemProcessorを使う

では、作成したJobにBean Validationを行うようにStepを構成していきましょう。

ここからは、先ほど「// 後で」とコメントを書いていた部分を埋めたり、変更していったりします。

最初は、バリデーションなしで

まずは、Bean Validationを行わないようにStepを構成します。

Stepの定義。

    @Bean
    public Step noBeanValidationStep() {
        return stepBuilderFactory
                .get("withBeanValidationStep")
                .<Book, Book>chunk(3)
                .reader(flatFileItemReader(null))
                .writer(consoleItemWriter())
                .build();
    }

Jobの定義。

    @Bean
    public Job beanValidationJob() {
        return jobBuilderFactory
                .get("beanValidationJob")
                .incrementer(new RunIdIncrementer())
                .start(noBeanValidationStep())
                .build();
    }

これでパッケージングして

$ mvn package

実行。まずは、正しいデータが入ったCSVを読み込せてみます。

$ java -Dspring.batch.job.names=beanValidationJob -jar target/batch-beanvalidation-0.0.1-SNAPSHOT.jar filePath=src/main/resources/book.csv

結果。

2022-04-28 01:37:52.114  INFO 17646 --- [           main] o.s.b.a.b.JobLauncherApplicationRunner   : Running default command line with: [filePath=src/main/resources/book.csv]
2022-04-28 01:37:52.291  INFO 17646 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=beanValidationJob]] launched with the following parameters: [{run.id=5, filePath=src/main/resources/book.csv}]
2022-04-28 01:37:52.394  INFO 17646 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [withBeanValidationStep]
2022-04-28 01:37:52.510  INFO 17646 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4798142470, title = Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発, price = 4400
2022-04-28 01:37:52.511  INFO 17646 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4774182179, title = [改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ, price = 4180
2022-04-28 01:37:52.511  INFO 17646 --- [           main] consoleItemWriter                        : [writer] isbn = 978-1492076988, title = Spring Boot: Up and Running: Building Cloud Native Java and Kotlin Applications, price = 6265
2022-04-28 01:37:52.540  INFO 17646 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4295000198, title = やさしく学べるMySQL運用・管理入門【5.7対応】, price = 2860
2022-04-28 01:37:52.540  INFO 17646 --- [           main] consoleItemWriter                        : [writer] isbn = 978-1484237236, title = The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud, price = 7361
2022-04-28 01:37:52.540  INFO 17646 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4798147406, title = 詳解MySQL 5.7 止まらぬ進化に乗り遅 れないためのテクニカルガイド (NEXT ONE), price = 3960
2022-04-28 01:37:52.566  INFO 17646 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4798161488, title = MySQL徹底入門 第4版 MySQL 8.0対応, price = 4180
2022-04-28 01:37:52.566  INFO 17646 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4797393118, title = 基礎からのMySQL 第3版 (基礎からシリーズ), price = 6038
2022-04-28 01:37:52.567  INFO 17646 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4873116389, title = 実践ハイパフォーマンスMySQL 第3版, price = 5280
2022-04-28 01:37:52.594  INFO 17646 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4774170206, title = MariaDB&MySQL全機能バイブル, price = 3850
2022-04-28 01:37:52.621  INFO 17646 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [withBeanValidationStep] executed in 226ms
2022-04-28 01:37:52.675  INFO 17646 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=beanValidationJob]] completed with the following parameters: [{run.id=5, filePath=src/main/resources/book.csv}] and the following status: [COMPLETED] in 339ms

次に、エラーになるCSVを読み込ませてみます。

$ java -Dspring.batch.job.names=beanValidationJob -jar target/batch-beanvalidation-0.0.1-SNAPSHOT.jar filePath=src/main/resources/book_invalid.csv

ですが、そもそもBean Validationをまだ入れていないので全件通過します。

2022-04-28 01:38:43.458  INFO 17727 --- [           main] o.s.b.a.b.JobLauncherApplicationRunner   : Running default command line with: [filePath=src/main/resources/book_invali
d.csv]
2022-04-28 01:38:43.638  INFO 17727 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=beanValidationJob]] launched with the following parameters: [{run.id=6, filePath=src/main/resources/book_invalid.csv}]
2022-04-28 01:38:43.768  INFO 17727 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [withBeanValidationStep]
2022-04-28 01:38:43.890  INFO 17727 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4798142470, title = Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発, price = 4400
2022-04-28 01:38:43.890  INFO 17727 --- [           main] consoleItemWriter                        : [writer] isbn = xx-xxxxxxx, title = [改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ, price = 4180
2022-04-28 01:38:43.890  INFO 17727 --- [           main] consoleItemWriter                        : [writer] isbn = 978-1492076988, title = Spring Boot: Up and Running: Building Cloud Native Java and Kotlin Applications, price = 6265
2022-04-28 01:38:43.920  INFO 17727 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4295000198, title = やさしく学べるMySQL運用・管理入門【5.7対応】, price = 860
2022-04-28 01:38:43.921  INFO 17727 --- [           main] consoleItemWriter                        : [writer] isbn = 978-1484237236, title = The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud, price = 7361
2022-04-28 01:38:43.921  INFO 17727 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4798147406, title = 詳解MySQL 5.7 止まらぬ進化に乗り遅 れないためのテクニカルガイド (NEXT ONE), price = 960
2022-04-28 01:38:43.948  INFO 17727 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4798161488, title = MySQL徹底入門 第4版 MySQL 8.0対応, price = 4180
2022-04-28 01:38:43.948  INFO 17727 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4797393118, title = 基礎からのMySQL 第3版 (基礎からシリーズ), price = 6038
2022-04-28 01:38:43.948  INFO 17727 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4873116389, title = 実践ハイパフォーマンスMySQL 第3版, price = 5280
2022-04-28 01:38:43.975  INFO 17727 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4774170206, title = MariaDB&MySQL全機能バイブル, price = 850
2022-04-28 01:38:44.002  INFO 17727 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [withBeanValidationStep] executed in 234ms
2022-04-28 01:38:44.061  INFO 17727 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=beanValidationJob]] completed with the following parameters: [{run.id=6, filePath=src/main/resources/book_invalid.csv}] and the following status: [COMPLETED] in 373ms
BeanValidatingItemProcessorを追加する

では、BeanValidatingItemProcessorを追加してみましょう。

Item processing / Validating Input

以下の定義を追加します。

    @StepScope
    @Bean
    public BeanValidatingItemProcessor<Book> beanValidatingItemProcessor() {
        return new BeanValidatingItemProcessor<>(localValidatorFactoryBean);
    }

コンストラクタで指定しているLocalValidatorFactoryBeanは、Spring BootのAuto Configurationでセットアップされたものです。
指定しなくても内部的にLocalValidatorFactoryBeanを生成するのですが、せっかくならSpring Bootでセットアップされたものを
使った方がよいかなと思います。

Step定義は以下のようにItemProcessorを追加して

    @Bean
    public Step withBeanValidationStep() {
        return stepBuilderFactory
                .get("withBeanValidationStep")
                .<Book, Book>chunk(3)
                .reader(flatFileItemReader(null))
                .processor(beanValidatingItemProcessor())
                .writer(consoleItemWriter())
                .build();
    }

Jobを変更。

    @Bean
    public Job beanValidationJob() {
        return jobBuilderFactory
                .get("beanValidationJob")
                .incrementer(new RunIdIncrementer())
                .start(withBeanValidationStep())
                .build();
    }

パッケージングして実行。

$ mvn package
$ java -Dspring.batch.job.names=beanValidationJob -jar target/batch-beanvalidation-0.0.1-SNAPSHOT.jar filePath=src/main/resources/book.csv

エラーにならない方の結果は、省略します。

続いて、エラーになる方のCSVを読み込ませます。

$ java -Dspring.batch.job.names=beanValidationJob -jar target/batch-beanvalidation-0.0.1-SNAPSHOT.jar filePath=src/main/resources/book_invalid.csv

すると、エラーになるItemに遭遇した時点で例外をスローしてアプリケーションが終了します。

2022-04-28 01:42:46.217  INFO 17951 --- [           main] o.s.b.a.b.JobLauncherApplicationRunner   : Running default command line with: [filePath=src/main/resources/book_invalid.csv]
2022-04-28 01:42:46.432  INFO 17951 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=beanValidationJob]] launched with the following parameters: [{run.id=8, filePath=src/main/resources/book_invalid.csv}]
2022-04-28 01:42:46.530  INFO 17951 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [withBeanValidationStep]
2022-04-28 01:42:46.718 ERROR 17951 --- [           main] o.s.batch.core.step.AbstractStep         : Encountered an error executing step withBeanValidationStep in job beanValidationJob

org.springframework.batch.item.validator.ValidationException: Validation failed for org.littlewings.spring.batch.Book@11dee337:
Field error in object 'item' on field 'isbn': rejected value [xx-xxxxxxx]; codes [Pattern.item.isbn,Pattern.isbn,Pattern.java.lang.String,Pattern]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.isbn,isbn]; arguments []; default message [isbn],[Ljavax.validation.constraints.Pattern$Flag;@532a02d9,\d{3}-\d{10}]; default message [正規表現 "\d{3}-\d{10}" にマッチさせてください]
Field error in object 'item' on field 'isbn': rejected value [xx-xxxxxxx]; codes [Size.item.isbn,Size.isbn,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.isbn,isbn]; arguments []; default message [isbn],14,14]; default message [14 から 14 の間のサイズにしてください]
        at org.springframework.batch.item.validator.SpringValidator.validate(SpringValidator.java:54) ~[spring-batch-infrastructure-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.item.validator.ValidatingItemProcessor.process(ValidatingItemProcessor.java:84) ~[spring-batch-infrastructure-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.item.validator.ValidatingItemProcessor$$FastClassBySpringCGLIB$$39980290.invoke(<generated>) ~[spring-batch-infrastructure-4.3.5.jar!/:4.3.5]
        at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:793) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.support.DelegatingIntroductionInterceptor.doProceed(DelegatingIntroductionInterceptor.java:137) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.support.DelegatingIntroductionInterceptor.invoke(DelegatingIntroductionInterceptor.java:124) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:708) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.batch.item.validator.BeanValidatingItemProcessor$$EnhancerBySpringCGLIB$$b5c9b6d8.process(<generated>) ~[spring-batch-infrastructure-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.item.SimpleChunkProcessor.doProcess(SimpleChunkProcessor.java:134) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.item.SimpleChunkProcessor.transform(SimpleChunkProcessor.java:319) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.item.SimpleChunkProcessor.process(SimpleChunkProcessor.java:210) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.item.ChunkOrientedTasklet.execute(ChunkOrientedTasklet.java:77) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.tasklet.TaskletStep$ChunkTransactionCallback.doInTransaction(TaskletStep.java:407) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.tasklet.TaskletStep$ChunkTransactionCallback.doInTransaction(TaskletStep.java:331) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:140) ~[spring-tx-5.3.19.jar!/:5.3.19]
        at org.springframework.batch.core.step.tasklet.TaskletStep$2.doInChunkContext(TaskletStep.java:273) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.scope.context.StepContextRepeatCallback.doInIteration(StepContextRepeatCallback.java:82) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.repeat.support.RepeatTemplate.getNextResult(RepeatTemplate.java:375) ~[spring-batch-infrastructure-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.repeat.support.RepeatTemplate.executeInternal(RepeatTemplate.java:215) ~[spring-batch-infrastructure-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.repeat.support.RepeatTemplate.iterate(RepeatTemplate.java:145) ~[spring-batch-infrastructure-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.tasklet.TaskletStep.doExecute(TaskletStep.java:258) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.AbstractStep.execute(AbstractStep.java:208) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.job.SimpleStepHandler.handleStep(SimpleStepHandler.java:152) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.job.AbstractJob.handleStep(AbstractJob.java:413) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.job.SimpleJob.doExecute(SimpleJob.java:136) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.job.AbstractJob.execute(AbstractJob.java:320) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.launch.support.SimpleJobLauncher$1.run(SimpleJobLauncher.java:149) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.core.task.SyncTaskExecutor.execute(SyncTaskExecutor.java:50) ~[spring-core-5.3.19.jar!/:5.3.19]
        at org.springframework.batch.core.launch.support.SimpleJobLauncher.run(SimpleJobLauncher.java:140) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
        at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:198) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.batch.core.configuration.annotation.SimpleBatchConfiguration$PassthruAdvice.invoke(SimpleBatchConfiguration.java:128) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at jdk.proxy2/jdk.proxy2.$Proxy58.run(Unknown Source) ~[na:na]
        at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.execute(JobLauncherApplicationRunner.java:199) ~[spring-boot-autoconfigure-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.executeLocalJobs(JobLauncherApplicationRunner.java:173) ~[spring-boot-autoconfigure-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.launchJobFromProperties(JobLauncherApplicationRunner.java:160) ~[spring-boot-autoconfigure-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.run(JobLauncherApplicationRunner.java:155) ~[spring-boot-autoconfigure-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.run(JobLauncherApplicationRunner.java:150) ~[spring-boot-autoconfigure-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:768) ~[spring-boot-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:758) ~[spring-boot-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:310) ~[spring-boot-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1312) ~[spring-boot-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1301) ~[spring-boot-2.6.7.jar!/:2.6.7]
        at org.littlewings.spring.batch.App.main(App.java:11) ~[classes!/:0.0.1-SNAPSHOT]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
        at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:49) ~[batch-beanvalidation-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
        at org.springframework.boot.loader.Launcher.launch(Launcher.java:108) ~[batch-beanvalidation-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
        at org.springframework.boot.loader.Launcher.launch(Launcher.java:58) ~[batch-beanvalidation-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
        at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:88) ~[batch-beanvalidation-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
Caused by: org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 2 errors
Field error in object 'item' on field 'isbn': rejected value [xx-xxxxxxx]; codes [Pattern.item.isbn,Pattern.isbn,Pattern.java.lang.String,Pattern]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.isbn,isbn]; arguments []; default message [isbn],[Ljavax.validation.constraints.Pattern$Flag;@532a02d9,\d{3}-\d{10}]; default message [正規表現 "\d{3}-\d{10}" にマッチさせてください]
Field error in object 'item' on field 'isbn': rejected value [xx-xxxxxxx]; codes [Size.item.isbn,Size.isbn,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.isbn,isbn]; arguments []; default message [isbn],14,14]; default message [14 から 14 の間のサイズにしてください]
        ... 64 common frames omitted

2022-04-28 01:42:46.722  INFO 17951 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [withBeanValidationStep] executed in 192ms
2022-04-28 01:42:46.778  INFO 17951 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=beanValidationJob]] completed with the following parameters: [{run.id=8, filePath=src/main/resources/book_invalid.csv}] and the following status: [FAILED] in 303ms

FAILEDになってしまいました。

エラーになったItemを読み飛ばす

次に、エラーになったItemをスキップするように設定してみましょう。

Configuring a Step / Chunk-oriented Processing / Configuring Skip Logic

faultTolerantを使い、次にスキップする例外(skip)とスキップ可能な数(skipLimit)を指定します。

    @Bean
    public Step faultTolerantBeanValidationStep() {
        return stepBuilderFactory
                .get("withBeanValidationStep")
                .<Book, Book>chunk(3)
                .reader(flatFileItemReader(null))
                .processor(beanValidatingItemProcessor())
                .writer(consoleItemWriter())
                .faultTolerant()
                .skip(ValidationException.class)
                .skipLimit(5)
                .build();
    }

skipLimitは、0より大きな値を指定する必要があります。無制限、みたいなことはできなさそうです。

Job定義も変更。

    @Bean
    public Job beanValidationJob() {
        return jobBuilderFactory
                .get("beanValidationJob")
                .incrementer(new RunIdIncrementer())
                .start(faultTolerantBeanValidationStep())
                .build();
    }

パッケージングして、実行。ここから先は、エラーになるファイルのみ読み込ませます。

$ mvn package
$ java -Dspring.batch.job.names=beanValidationJob -jar target/batch-beanvalidation-0.0.1-SNAPSHOT.jar filePath=src/main/resources/book_invalid.csv

ログ。

2022-04-28 01:48:14.347  INFO 18194 --- [           main] org.littlewings.spring.batch.App         : Started App in 1.941 seconds (JVM running for 2.392)
2022-04-28 01:48:14.348  INFO 18194 --- [           main] o.s.b.a.b.JobLauncherApplicationRunner   : Running default command line with: [filePath=src/main/resources/book_invalid.csv]
2022-04-28 01:48:14.522  INFO 18194 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=beanValidationJob]] launched with the following parameters: [{run.id=9, filePath=src/main/resources/book_invalid.csv}]
2022-04-28 01:48:14.649  INFO 18194 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [withBeanValidationStep]
2022-04-28 01:48:14.849  INFO 18194 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4798142470, title = Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発, price = 4400
2022-04-28 01:48:14.849  INFO 18194 --- [           main] consoleItemWriter                        : [writer] isbn = 978-1492076988, title = Spring Boot: Up and Running: Building Cloud Native Java and Kotlin Applications, price = 6265
2022-04-28 01:48:14.883  INFO 18194 --- [           main] consoleItemWriter                        : [writer] isbn = 978-1484237236, title = The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud, price = 7361
2022-04-28 01:48:14.911  INFO 18194 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4798161488, title = MySQL徹底入門 第4版 MySQL 8.0対応, price = 4180
2022-04-28 01:48:14.911  INFO 18194 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4797393118, title = 基礎からのMySQL 第3版 (基礎からシリーズ), price = 6038
2022-04-28 01:48:14.911  INFO 18194 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4873116389, title = 実践ハイパフォーマンスMySQL 第3版, price = 5280
2022-04-28 01:48:14.965  INFO 18194 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [withBeanValidationStep] executed in 316ms
2022-04-28 01:48:15.019  INFO 18194 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=beanValidationJob]] completed with the following parameters: [{run.id=9, filePath=src/main/resources/book_invalid.csv}] and the following status: [COMPLETED] in 448ms

今度は、正常に終了するようになりました。

ItemWriterはそのまま処理されたItemのみが出力され、こちらは想定通りですが、どのようなItemがエラーになったかはわかりません。

スキップしたItemをログ出力する

スキップしたItemをログ出力するようにしてみましょう。

Common Batch Patterns / Logging Item Processing and Failures

SkipListenerインターフェースを実装するか、@OnSkipInProcessアノテーションを付与したメソッドを持つクラスを作成する必要があります。

SkipListener (Spring Batch 4.3.5 API)

OnSkipInProcess (Spring Batch 4.3.5 API)

このようなListenerを作成。

src/main/java/org/littlewings/spring/batch/RejectItemLoggingListener.java

package org.littlewings.spring.batch;

import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.annotation.OnSkipInProcess;
import org.springframework.batch.item.validator.ValidationException;
import org.springframework.validation.BindException;

public class RejectItemLoggingListener {
    Logger logger = LoggerFactory.getLogger(RejectItemLoggingListener.class);

    @OnSkipInProcess
    public void OnSkipInProcess(Book book, Throwable throwable) {
        if (throwable instanceof ValidationException) {
            ((BindException) throwable.getCause()).getMessage();
            logger.info(
                    "validation error, isbn = {}, title = {}, reject reason = {}",
                    book.getIsbn(),
                    book.getTitle(),
                    ((BindException) throwable.getCause())
                            .getBindingResult()
                            .getFieldErrors()
                            .stream().map(e -> e.getObjectName() + "#" + e.getField() + ":" + e.getDefaultMessage()).collect(Collectors.joining(", "))
            );
        } else {
            logger.error("error", throwable);
        }
    }
}

こちらをBean定義して

    @StepScope
    @Bean
    public RejectItemLoggingListener rejectItemLoggingListener() {
        return new RejectItemLoggingListener();
    }

faultTolerantの後にListenerとして追加します。

    @Bean
    public Step loggingInvalidItemBeanValidationStep() {
        return stepBuilderFactory
                .get("loggingInvalidItemBeanValidationStep")
                .<Book, Book>chunk(3)
                .reader(flatFileItemReader(null))
                .processor(beanValidatingItemProcessor())
                .writer(consoleItemWriter())
                .faultTolerant()
                .skip(ValidationException.class)
                .skipLimit(5)
                .listener(rejectItemLoggingListener())
                .build();
    }

Jobを構成。

    @Bean
    public Job beanValidationJob() {
        return jobBuilderFactory
                .get("beanValidationJob")
                .incrementer(new RunIdIncrementer())
                .start(loggingInvalidItemBeanValidationStep())
                .build();
    }

実行。

$ mvn package
$ java -Dspring.batch.job.names=beanValidationJob -jar target/batch-beanvalidation-0.0.1-SNAPSHOT.jar filePath=src/main/resources/book_invalid.csv

ログ。

2022-04-28 01:53:07.154  INFO 18429 --- [           main] o.s.b.a.b.JobLauncherApplicationRunner   : Running default command line with: [filePath=src/main/resources/book_invalid.csv]
2022-04-28 01:53:07.330  INFO 18429 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=beanValidationJob]] launched with the following parameters: [{run.id=10, filePath=src/main/resources/book_invalid.csv}]
2022-04-28 01:53:07.460  INFO 18429 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [loggingInvalidItemBeanValidationStep]
2022-04-28 01:53:07.693  INFO 18429 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4798142470, title = Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発, price = 4400
2022-04-28 01:53:07.693  INFO 18429 --- [           main] consoleItemWriter                        : [writer] isbn = 978-1492076988, title = Spring Boot: Up and Running: Building Cloud Native Java and Kotlin Applications, price = 6265
2022-04-28 01:53:07.697  INFO 18429 --- [           main] o.l.s.batch.RejectItemLoggingListener    : validation error, isbn = xx-xxxxxxx, title = [改訂新版]Spring入門 ――Javaフ レームワーク・より良い設計とアーキテクチャ, reject reason = item#isbn:正規表現 "\d{3}-\d{10}" にマッチさせてください, item#isbn:14 から 14 の間のサイズにしてください
2022-04-28 01:53:07.748  INFO 18429 --- [           main] consoleItemWriter                        : [writer] isbn = 978-1484237236, title = The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud, price = 7361
2022-04-28 01:53:07.748  INFO 18429 --- [           main] o.l.s.batch.RejectItemLoggingListener    : validation error, isbn = 978-4295000198, title = やさしく学べるMySQL運用・ 管理入門【5.7対応】, reject reason = item#price:1000 以上の値にしてください
2022-04-28 01:53:07.748  INFO 18429 --- [           main] o.l.s.batch.RejectItemLoggingListener    : validation error, isbn = 978-4798147406, title = 詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド (NEXT ONE), reject reason = item#price:1000 以上の値にしてください
2022-04-28 01:53:07.776  INFO 18429 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4798161488, title = MySQL徹底入門 第4版 MySQL 8.0対応, price = 4180
2022-04-28 01:53:07.776  INFO 18429 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4797393118, title = 基礎からのMySQL 第3版 (基礎からシリーズ), price = 6038
2022-04-28 01:53:07.776  INFO 18429 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4873116389, title = 実践ハイパフォーマンスMySQL 第3版, price = 5280
2022-04-28 01:53:07.805  INFO 18429 --- [           main] o.l.s.batch.RejectItemLoggingListener    : validation error, isbn = 978-4774170206, title = MariaDB&MySQL全機能バイブ ル, reject reason = item#price:1000 以上の値にしてください
2022-04-28 01:53:07.833  INFO 18429 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [loggingInvalidItemBeanValidationStep] executed in 372ms
2022-04-28 01:53:07.883  INFO 18429 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=beanValidationJob]] completed with the following parameters: [{run.id=10, filePath=src/main/resources/book_invalid.csv}] and the following status: [COMPLETED] in 514ms

エラーになったItemが、ログに出力されるようになりました。

ちなみに、以下のようにfaultTolerantの前にListenerを追加しても機能しません。

    @Bean
    public Step invalidSkipItemListenerBeanValidationStep() {
        return stepBuilderFactory
                .get("invalidSkipItemListenerBeanValidationStep")
                .<Book, Book>chunk(3)
                .reader(flatFileItemReader(null))
                .processor(beanValidatingItemProcessor())
                .writer(consoleItemWriter())
                .listener(rejectItemLoggingListener())
                .faultTolerant()
                .skip(ValidationException.class)
                .skipLimit(5)
                .build();
    }

listenerメソッドの引数の型がObjectのものがあるので渡せてしまうのですが、SkipListenerインターフェースを実装している場合は、
型が合わずにコンパイルが通らなくなるのでこのようなミスは発生しません…。

skipで指定した値を上回った場合

skipで指定した数を超えてItemをスキップした場合に、どうなるかを確認してみましょう。

バリデーションがNGになるItemの数は4なので、skipに3を指定してみます。

    @Bean
    public Step loggingInvalidItemBeanValidationStopStep() {
        return stepBuilderFactory
                .get("loggingInvalidItemBeanValidationStep")
                .<Book, Book>chunk(3)
                .reader(flatFileItemReader(null))
                .processor(beanValidatingItemProcessor())
                .writer(consoleItemWriter())
                .faultTolerant()
                .skip(ValidationException.class)
                .skipLimit(3)
                .listener(rejectItemLoggingListener())
                .build();
    }

Jobの構成。

    @Bean
    public Job beanValidationJob() {
        return jobBuilderFactory
                .get("beanValidationJob")
                .incrementer(new RunIdIncrementer())
                .start(loggingInvalidItemBeanValidationStopStep())
                .build();
    }

実行。

$ mvn package
$ java -Dspring.batch.job.names=beanValidationJob -jar target/batch-beanvalidation-0.0.1-SNAPSHOT.jar filePath=src/main/resources/book_invalid.csv

結果。

2022-04-28 01:57:38.529  INFO 18665 --- [           main] o.s.b.a.b.JobLauncherApplicationRunner   : Running default command line with: [filePath=src/main/resources/book_invalid.csv]
2022-04-28 01:57:38.734  INFO 18665 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=beanValidationJob]] launched with the following parameters: [{run.id=11, filePath=src/main/resources/book_invalid.csv}]
2022-04-28 01:57:38.827  INFO 18665 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [loggingInvalidItemBeanValidationStep]
2022-04-28 01:57:39.020  INFO 18665 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4798142470, title = Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発, price = 4400
2022-04-28 01:57:39.020  INFO 18665 --- [           main] consoleItemWriter                        : [writer] isbn = 978-1492076988, title = Spring Boot: Up and Running: Building Cloud Native Java and Kotlin Applications, price = 6265
2022-04-28 01:57:39.025  INFO 18665 --- [           main] o.l.s.batch.RejectItemLoggingListener    : validation error, isbn = xx-xxxxxxx, title = [改訂新版]Spring入門 ――Javaフ レームワーク・より良い設計とアーキテクチャ, reject reason = item#isbn:14 から 14 の間のサイズにしてください, item#isbn:正規表現 "\d{3}-\d{10}" にマッチさせてください
2022-04-28 01:57:39.055  INFO 18665 --- [           main] consoleItemWriter                        : [writer] isbn = 978-1484237236, title = The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud, price = 7361
2022-04-28 01:57:39.056  INFO 18665 --- [           main] o.l.s.batch.RejectItemLoggingListener    : validation error, isbn = 978-4295000198, title = やさしく学べるMySQL運用・ 管理入門【5.7対応】, reject reason = item#price:1000 以上の値にしてください
2022-04-28 01:57:39.057  INFO 18665 --- [           main] o.l.s.batch.RejectItemLoggingListener    : validation error, isbn = 978-4798147406, title = 詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド (NEXT ONE), reject reason = item#price:1000 以上の値にしてください
2022-04-28 01:57:39.082  INFO 18665 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4798161488, title = MySQL徹底入門 第4版 MySQL 8.0対応, price = 4180
2022-04-28 01:57:39.082  INFO 18665 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4797393118, title = 基礎からのMySQL 第3版 (基礎からシリーズ), price = 6038
2022-04-28 01:57:39.082  INFO 18665 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4873116389, title = 実践ハイパフォーマンスMySQL 第3版, price = 5280
2022-04-28 01:57:39.110 ERROR 18665 --- [           main] o.s.batch.core.step.AbstractStep         : Encountered an error executing step loggingInvalidItemBeanValidationStep in job beanValidationJob

org.springframework.batch.core.step.skip.SkipLimitExceededException: Skip limit of '3' exceeded
        at org.springframework.batch.core.step.skip.LimitCheckingItemSkipPolicy.shouldSkip(LimitCheckingItemSkipPolicy.java:133) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.skip.ExceptionClassifierSkipPolicy.shouldSkip(ExceptionClassifierSkipPolicy.java:70) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.item.FaultTolerantChunkProcessor.shouldSkip(FaultTolerantChunkProcessor.java:519) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.item.FaultTolerantChunkProcessor.access$500(FaultTolerantChunkProcessor.java:56) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.item.FaultTolerantChunkProcessor$2.recover(FaultTolerantChunkProcessor.java:289) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.retry.support.RetryTemplate.handleRetryExhausted(RetryTemplate.java:539) ~[spring-retry-1.3.3.jar!/:na]
        at org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:387) ~[spring-retry-1.3.3.jar!/:na]
        at org.springframework.retry.support.RetryTemplate.execute(RetryTemplate.java:255) ~[spring-retry-1.3.3.jar!/:na]
        at org.springframework.batch.core.step.item.BatchRetryTemplate.execute(BatchRetryTemplate.java:217) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.item.FaultTolerantChunkProcessor.transform(FaultTolerantChunkProcessor.java:308) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.item.SimpleChunkProcessor.process(SimpleChunkProcessor.java:210) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.item.ChunkOrientedTasklet.execute(ChunkOrientedTasklet.java:77) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.tasklet.TaskletStep$ChunkTransactionCallback.doInTransaction(TaskletStep.java:407) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.tasklet.TaskletStep$ChunkTransactionCallback.doInTransaction(TaskletStep.java:331) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:140) ~[spring-tx-5.3.19.jar!/:5.3.19]
        at org.springframework.batch.core.step.tasklet.TaskletStep$2.doInChunkContext(TaskletStep.java:273) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.scope.context.StepContextRepeatCallback.doInIteration(StepContextRepeatCallback.java:82) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.repeat.support.RepeatTemplate.getNextResult(RepeatTemplate.java:375) ~[spring-batch-infrastructure-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.repeat.support.RepeatTemplate.executeInternal(RepeatTemplate.java:215) ~[spring-batch-infrastructure-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.repeat.support.RepeatTemplate.iterate(RepeatTemplate.java:145) ~[spring-batch-infrastructure-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.tasklet.TaskletStep.doExecute(TaskletStep.java:258) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.AbstractStep.execute(AbstractStep.java:208) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.job.SimpleStepHandler.handleStep(SimpleStepHandler.java:152) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.job.AbstractJob.handleStep(AbstractJob.java:413) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.job.SimpleJob.doExecute(SimpleJob.java:136) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.job.AbstractJob.execute(AbstractJob.java:320) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.launch.support.SimpleJobLauncher$1.run(SimpleJobLauncher.java:149) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.core.task.SyncTaskExecutor.execute(SyncTaskExecutor.java:50) ~[spring-core-5.3.19.jar!/:5.3.19]
        at org.springframework.batch.core.launch.support.SimpleJobLauncher.run(SimpleJobLauncher.java:140) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
        at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:198) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.batch.core.configuration.annotation.SimpleBatchConfiguration$PassthruAdvice.invoke(SimpleBatchConfiguration.java:128) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at jdk.proxy2/jdk.proxy2.$Proxy58.run(Unknown Source) ~[na:na]
        at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.execute(JobLauncherApplicationRunner.java:199) ~[spring-boot-autoconfigure-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.executeLocalJobs(JobLauncherApplicationRunner.java:173) ~[spring-boot-autoconfigure-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.launchJobFromProperties(JobLauncherApplicationRunner.java:160) ~[spring-boot-autoconfigure-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.run(JobLauncherApplicationRunner.java:155) ~[spring-boot-autoconfigure-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.run(JobLauncherApplicationRunner.java:150) ~[spring-boot-autoconfigure-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:768) ~[spring-boot-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:758) ~[spring-boot-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:310) ~[spring-boot-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1312) ~[spring-boot-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1301) ~[spring-boot-2.6.7.jar!/:2.6.7]
        at org.littlewings.spring.batch.App.main(App.java:11) ~[classes!/:0.0.1-SNAPSHOT]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
        at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:49) ~[batch-beanvalidation-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
        at org.springframework.boot.loader.Launcher.launch(Launcher.java:108) ~[batch-beanvalidation-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
        at org.springframework.boot.loader.Launcher.launch(Launcher.java:58) ~[batch-beanvalidation-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
        at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:88) ~[batch-beanvalidation-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
Caused by: org.springframework.batch.item.validator.ValidationException: Validation failed for org.littlewings.spring.batch.Book@47428937:
Field error in object 'item' on field 'price': rejected value [850]; codes [Min.item.price,Min.price,Min.java.lang.Integer,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.price,price]; arguments []; default message [price],1000]; default message [1000 以上の値にしてください]
        at org.springframework.batch.item.validator.SpringValidator.validate(SpringValidator.java:54) ~[spring-batch-infrastructure-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.item.validator.ValidatingItemProcessor.process(ValidatingItemProcessor.java:84) ~[spring-batch-infrastructure-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.item.validator.ValidatingItemProcessor$$FastClassBySpringCGLIB$$39980290.invoke(<generated>) ~[spring-batch-infrastructure-4.3.5.jar!/:4.3.5]
        at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:793) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.support.DelegatingIntroductionInterceptor.doProceed(DelegatingIntroductionInterceptor.java:137) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.support.DelegatingIntroductionInterceptor.invoke(DelegatingIntroductionInterceptor.java:124) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:708) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.batch.item.validator.BeanValidatingItemProcessor$$EnhancerBySpringCGLIB$$b5c9b6d8.process(<generated>) ~[spring-batch-infrastructure-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.item.SimpleChunkProcessor.doProcess(SimpleChunkProcessor.java:134) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.item.FaultTolerantChunkProcessor$1.doWithRetry(FaultTolerantChunkProcessor.java:239) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:329) ~[spring-retry-1.3.3.jar!/:na]
        ... 52 common frames omitted
Caused by: org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'item' on field 'price': rejected value [850]; codes [Min.item.price,Min.price,Min.java.lang.Integer,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.price,price]; arguments []; default message [price],1000]; default message [1000 以上の値にしてください]
        ... 68 common frames omitted

2022-04-28 01:57:39.115  INFO 18665 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [loggingInvalidItemBeanValidationStep] executed in 288ms
2022-04-28 01:57:39.158  INFO 18665 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=beanValidationJob]] completed with the following parameters: [{run.id=11, filePath=src/main/resources/book_invalid.csv}] and the following status: [FAILED] in 376ms

スキップ可能なItemの数を超えたということで、例外がスローされアプリケーションが停止します。

org.springframework.batch.core.step.skip.SkipLimitExceededException: Skip limit of '3' exceeded
SkipPolicyを設定する

最後に、SkipPolicyを設定してみます。

SkipPolicy (Spring Batch 4.3.5 API)

SkipPolicyはスキップ可能な条件を判定するためのインターフェースで、SkipPolicy#shouldSkip(java.lang.Throwable t, int skipCount)メソッドで
判定を行います。

今まで設定していたスキップと判定する例外やスキップ可能な数は、SkipPolicyの実装であるExceptionClassifierSkipPolicyと
LimitCheckingItemSkipPolicyの組み合わせで機能していました。

ExceptionClassifierSkipPolicy (Spring Batch 4.3.5 API)

LimitCheckingItemSkipPolicy (Spring Batch 4.3.5 API)

https://github.com/spring-projects/spring-batch/blob/4.3.5/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilder.java#L575-L579

ここで、以下のようにskipPolicyメソッドで明示的にSkipPolicyのインスタンスを指定することで、スキップの条件をカスタマイズ
することができます。

    @Bean
    public Step loggingInvalidItemAlwaysSkipBeanValidationStep() {
        return stepBuilderFactory
                .get("loggingInvalidItemBeanValidationStep")
                .<Book, Book>chunk(3)
                .reader(flatFileItemReader(null))
                .processor(beanValidatingItemProcessor())
                .writer(consoleItemWriter())
                .faultTolerant()
                //.skip(ValidationException.class)
                .skipPolicy(new AlwaysSkipItemSkipPolicy())
                .listener(rejectItemLoggingListener())
                .build();
    }

ここで指定しているAlwaysSkipItemSkipPolicyは、例外やスキップしたItemの数に関わらず常にスキップを許可するクラスです。

AlwaysSkipItemSkipPolicy (Spring Batch 4.3.5 API)

skipPolicyメソッドを使ってSkipPolicyを指定すると、CompositeSkipPolicyというSkipPolicyを合成するクラスが使われ、
もともと利用されるExceptionClassifierSkipPolicyとLimitCheckingItemSkipPolicyの組み合わせと合成されます。

CompositeSkipPolicy (Spring Batch 4.3.5 API)

https://github.com/spring-projects/spring-batch/blob/4.3.5/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilder.java#L586

評価順は、先に登録したSkipPolicyから順に行われるようなので、AlwaysSkipItemSkipPolicyを使用するとスキップ対象の例外を
どのように指定してもスキップ数をどのように指定しても、常にスキップ可能と判定されます。

https://github.com/spring-projects/spring-batch/blob/4.3.5/spring-batch-core/src/main/java/org/springframework/batch/core/step/skip/CompositeSkipPolicy.java#L40-L44

ちなみに、他のSkipPolicyの実装はスキップしないNeverSkipItemSkipPolicyがあるようです。

NeverSkipItemSkipPolicy (Spring Batch 4.3.5 API)

Job定義を変更して

    @Bean
    public Job beanValidationJob() {
        return jobBuilderFactory
                .get("beanValidationJob")
                .incrementer(new RunIdIncrementer())
                .start(loggingInvalidItemAlwaysSkipBeanValidationStep())
                .build();
    }

パッケージングして実行。

$ mvn package
$ java -Dspring.batch.job.names=beanValidationJob -jar target/batch-beanvalidation-0.0.1-SNAPSHOT.jar filePath=src/main/resources/book_invalid.csv

ログ。

2022-05-22 00:44:05.818  INFO 66977 --- [           main] org.littlewings.spring.batch.App         : Started App in 2.269 seconds (JVM running for 2.677)
2022-05-22 00:44:05.819  INFO 66977 --- [           main] o.s.b.a.b.JobLauncherApplicationRunner   : Running default command line with: [filePath=src/main/resources/book_invalid.csv]
2022-05-22 00:44:06.012  INFO 66977 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=beanValidationJob]] launched with the following parameters: [{run.id=4, filePath=src/main/resources/book_invalid.csv}]
2022-05-22 00:44:06.141  INFO 66977 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [loggingInvalidItemBeanValidationStep]
2022-05-22 00:44:06.379  INFO 66977 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4798142470, title = Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発, price = 4400
2022-05-22 00:44:06.379  INFO 66977 --- [           main] consoleItemWriter                        : [writer] isbn = 978-1492076988, title = Spring Boot: Up and Running: Building Cloud Native Java and Kotlin Applications, price = 6265
2022-05-22 00:44:06.387  INFO 66977 --- [           main] o.l.s.batch.RejectItemLoggingListener    : validation error, isbn = xx-xxxxxxx, title = [改訂新版]Spring入門 ――Java フレームワーク・より良い設計とアーキテクチャ, reject reason = item#isbn:正規表現 "\d{3}-\d{10}" にマッチさせてください, item#isbn:14 から 14 の間のサイズにしてください
2022-05-22 00:44:06.510  INFO 66977 --- [           main] consoleItemWriter                        : [writer] isbn = 978-1484237236, title = The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud, price = 7361
2022-05-22 00:44:06.510  INFO 66977 --- [           main] o.l.s.batch.RejectItemLoggingListener    : validation error, isbn = 978-4295000198, title = やさしく学べるMySQL運用 ・管理入門【5.7対応】, reject reason = item#price:1000 以上の値にしてください
2022-05-22 00:44:06.511  INFO 66977 --- [           main] o.l.s.batch.RejectItemLoggingListener    : validation error, isbn = 978-4798147406, title = 詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド (NEXT ONE), reject reason = item#price:1000 以上の値にしてください
2022-05-22 00:44:06.543  INFO 66977 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4798161488, title = MySQL徹底入門 第4版 MySQL 8.0対応, price = 4180
2022-05-22 00:44:06.543  INFO 66977 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4797393118, title = 基礎からのMySQL 第3版 (基礎からシリーズ), price = 6038
2022-05-22 00:44:06.544  INFO 66977 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4873116389, title = 実践ハイパフォーマンスMySQL 第3版, price = 5280
2022-05-22 00:44:06.576  INFO 66977 --- [           main] o.l.s.batch.RejectItemLoggingListener    : validation error, isbn = 978-4774170206, title = MariaDB&MySQL全機能バイ ブル, reject reason = item#price:1000 以上の値にしてください
2022-05-22 00:44:06.607  INFO 66977 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [loggingInvalidItemBeanValidationStep] executed in 465ms
2022-05-22 00:44:06.665  INFO 66977 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=beanValidationJob]] completed with the following parameters: [{run.id=4, filePath=src/main/resources/book_invalid.csv}] and the following status: [COMPLETED] in 605ms

スキップ可能な例外もスキップ可能な回数もしていませんが、Itemの処理中に例外がスローされてもスキップとして判定するようになりました。

AlwaysSkipItemSkipPolicyを使用すると、一見スキップ回数の制限を撤廃できるようにも見えますが、例外もすべて無視するので
ちょっと微妙ですね。

もう少し凝ったスキップ条件にしたい場合は、自分でSkipPolicyを実装するのかもしれません。

まとめ

Spring BatchにBean Validationを加えるとともに、Itemをスキップした場合とその制御について確認してみました。

1度Spring Batchをしっかり見ておくと、このあたりは割とすんなりと入れましたね。

最後に、JobやStepを定義していたクラスのソースコード全体を載せておきます。

src/main/java/org/littlewings/spring/batch/BeanValidationJobConfig.java

package org.littlewings.spring.batch;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.step.skip.AlwaysSkipItemSkipPolicy;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;
import org.springframework.batch.item.validator.BeanValidatingItemProcessor;
import org.springframework.batch.item.validator.ValidationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

@Configuration
public class BeanValidationJobConfig {
    @Autowired
    JobBuilderFactory jobBuilderFactory;

    @Autowired
    StepBuilderFactory stepBuilderFactory;

    @Autowired
    LocalValidatorFactoryBean localValidatorFactoryBean;

    @Bean
    public Job beanValidationJob() {
        return jobBuilderFactory
                .get("beanValidationJob")
                .incrementer(new RunIdIncrementer())
                //.start(noBeanValidationStep())
                //.start(withBeanValidationStep())
                //.start(faultTolerantBeanValidationStep())
                //.start(invalidSkipItemListenerBeanValidationStep())
                .start(loggingInvalidItemBeanValidationStep())
                //.start(loggingInvalidItemBeanValidationStopStep())
                //.start(loggingInvalidItemAlwaysSkipBeanValidationStep())
                .build();
    }

    @Bean
    public Step noBeanValidationStep() {
        return stepBuilderFactory
                .get("withBeanValidationStep")
                .<Book, Book>chunk(3)
                .reader(flatFileItemReader(null))
                .writer(consoleItemWriter())
                .build();
    }

    @Bean
    public Step withBeanValidationStep() {
        return stepBuilderFactory
                .get("withBeanValidationStep")
                .<Book, Book>chunk(3)
                .reader(flatFileItemReader(null))
                .processor(beanValidatingItemProcessor())
                .writer(consoleItemWriter())
                .build();
    }

    @Bean
    public Step faultTolerantBeanValidationStep() {
        return stepBuilderFactory
                .get("withBeanValidationStep")
                .<Book, Book>chunk(3)
                .reader(flatFileItemReader(null))
                .processor(beanValidatingItemProcessor())
                .writer(consoleItemWriter())
                .faultTolerant()
                .skip(ValidationException.class)
                .skipLimit(5)
                .build();
    }

    @Bean
    public Step invalidSkipItemListenerBeanValidationStep() {
        return stepBuilderFactory
                .get("invalidSkipItemListenerBeanValidationStep")
                .<Book, Book>chunk(3)
                .reader(flatFileItemReader(null))
                .processor(beanValidatingItemProcessor())
                .writer(consoleItemWriter())
                .listener(rejectItemLoggingListener())
                .faultTolerant()
                .skip(ValidationException.class)
                .skipLimit(5)
                .build();
    }

    @Bean
    public Step loggingInvalidItemBeanValidationStep() {
        return stepBuilderFactory
                .get("loggingInvalidItemBeanValidationStep")
                .<Book, Book>chunk(3)
                .reader(flatFileItemReader(null))
                .processor(beanValidatingItemProcessor())
                .writer(consoleItemWriter())
                .faultTolerant()
                .skip(ValidationException.class)
                .skipLimit(5)
                .listener(rejectItemLoggingListener())
                .build();
    }

    @Bean
    public Step loggingInvalidItemBeanValidationStopStep() {
        return stepBuilderFactory
                .get("loggingInvalidItemBeanValidationStep")
                .<Book, Book>chunk(3)
                .reader(flatFileItemReader(null))
                .processor(beanValidatingItemProcessor())
                .writer(consoleItemWriter())
                .faultTolerant()
                .skip(ValidationException.class)
                .skipLimit(3)
                .listener(rejectItemLoggingListener())
                .build();
    }

    @Bean
    public Step loggingInvalidItemAlwaysSkipBeanValidationStep() {
        return stepBuilderFactory
                .get("loggingInvalidItemBeanValidationStep")
                .<Book, Book>chunk(3)
                .reader(flatFileItemReader(null))
                .processor(beanValidatingItemProcessor())
                .writer(consoleItemWriter())
                .faultTolerant()
                //.skip(ValidationException.class)
                .skipPolicy(new AlwaysSkipItemSkipPolicy())
                .listener(rejectItemLoggingListener())
                .build();
    }

    @StepScope
    @Bean
    public FlatFileItemReader<Book> flatFileItemReader(@Value("#{jobParameters['filePath']}") String filePath) {
        Resource fileResource = new FileSystemResource(filePath);

        return new FlatFileItemReaderBuilder<Book>()
                .name("flatFileItemReader")
                .resource(fileResource)
                .encoding("UTF-8")
                .delimited()
                .names(new String[]{"isbn", "title", "price"})
                .linesToSkip(1)
                .targetType(Book.class)
                .saveState(false)
                .build();
    }

    @StepScope
    @Bean
    public BeanValidatingItemProcessor<Book> beanValidatingItemProcessor() {
        return new BeanValidatingItemProcessor<>(localValidatorFactoryBean);
    }

    @StepScope
    @Bean
    public RejectItemLoggingListener rejectItemLoggingListener() {
        return new RejectItemLoggingListener();
    }

    @StepScope
    @Bean
    public ItemWriter<Book> consoleItemWriter() {
        Logger logger = LoggerFactory.getLogger("consoleItemWriter");

        return books -> books.forEach(book ->
                logger.info("[writer] isbn = {}, title = {}, price = {}", book.getIsbn(), book.getTitle(), book.getPrice())
        );
    }
}