これは、なにをしたくて書いたもの?
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
<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)
ここで、以下のように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)
評価順は、先に登録したSkipPolicy
から順に行われるようなので、AlwaysSkipItemSkipPolicy
を使用するとスキップ対象の例外を
どのように指定してもスキップ数をどのように指定しても、常にスキップ可能と判定されます。
ちなみに、他の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()) ); } }