CLOVER🍀

That was when it all began.

Spring BatchでItemProcessorを複数適用してみる

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

Spring Batchで、複数のItemProcessorを使うパターンをちょっと試しておこうかなと。

CompositeItemProcessor

結論を言うと、CompositeItemProcessorを使うことになります。

ドキュメントとしてはこちらに載っています。

Item processing / Chaining ItemProcessors

ItemProcessorではItemを変換したり検証したり、なにか処理をできるわけですが。複数のItemProcessorをつなげる場合に
CompositeItemProcessorを使います。

ドキュメントを見たらだいたい雰囲気はわかるのですが、実際に自分でも書いてみます。

お題

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

  • 書籍データをCSVStringとしてItemReaderが提供
  • ItemProcessorで…
    • split
    • 自分で用意したクラスに変換
  • ItemWriterでログ出力

環境

今回の環境は、こちら。

$ 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-113-generic", arch: "amd64", family: "unix"

Spring Batchが使用するデータベースには、MySQLを使用します。MySQLは、172.17.0.2で動作しているものとします。

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

プロジェクトを作成する

まずはSpring Bootプロジェクトを作成します。依存関係にはbatchmysqlを追加。

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

プロジェクト内に移動。

$ cd batch-chaining-itemprocessors

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

$ rm src/main/java/org/littlewings/spring/batch/BatchChainingItemprocessorsApplication.java src/test/java/org/littlewings/spring/batch/BatchChainingItemprocessorsApplicationTests.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>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>

最終的に変換先となるクラスは、こちらとします。

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

package org.littlewings.spring.batch;

public class Book {
    String isbn;
    String title;
    Integer price;

    public static Book create(String isbn, String title, Integer price) {
        Book book = new Book();
        book.setIsbn(isbn);
        book.setTitle(title);
        book.setPrice(price);

        return book;
    }

    // getter/setterは省略
}

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

ここから、ItemReaderItemProcessorItemWriterJobの定義の順に書いていきます。

ItemReader/ItemProcessor/ItemWriter/Jobを定義する

変換元のデータを提供するItemReaderはこちら。

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

package org.littlewings.spring.batch;

import java.util.Iterator;
import java.util.List;

import org.springframework.batch.item.NonTransientResourceException;
import org.springframework.batch.item.ParseException;
import org.springframework.batch.item.UnexpectedInputException;
import org.springframework.batch.item.support.AbstractItemStreamItemReader;

public class StringItemReader extends AbstractItemStreamItemReader<String> {
    Iterator<String> iterator;

    public StringItemReader() {
        List<String> bookAsStrings = List.of(
                "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-1484237236,The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud,7361",
                "978-4798161488,MySQL徹底入門 第4版 MySQL 8.0対応,4180",
                "978-4797393118,基礎からのMySQL 第3版 (基礎からシリーズ),6038",
                "978-4873116389,実践ハイパフォーマンスMySQL 第3版,5280",
                "978-4295000198,やさしく学べるMySQL運用・管理入門【5.7対応】,2860",
                "978-4798147406,詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド (NEXT ONE),3960",
                "978-4774170206,MariaDB&MySQL全機能バイブル,3860"
        );

        iterator = bookAsStrings.iterator();
    }

    @Override
    public String read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException {
        if (iterator.hasNext()) {
            return iterator.next();
        }

        return null;
    }
}

こんな感じですね。

"978-4798142470,Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発,4400",

こちらをsplitするItemProcessorStringからList<String>に変換します。

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

package org.littlewings.spring.batch;

import java.util.Arrays;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.item.ItemProcessor;

public class SplitItemProcessor implements ItemProcessor<String, List<String>> {
    Logger logger = LoggerFactory.getLogger(SplitItemProcessor.class);

    @Override
    public List<String> process(String item) throws Exception {
        logger.info("split: {}", item);
        return Arrays.asList(item.split(","));
    }
}

そして、List<String>からBookに変換するItemProcessor

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

package org.littlewings.spring.batch;

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.item.ItemProcessor;

public class ToBookItemProcessor implements ItemProcessor<List<String>, Book> {
    Logger logger = LoggerFactory.getLogger(ToBookItemProcessor.class);

    @Override
    public Book process(List<String> item) throws Exception {
        logger.info("to book: {}", String.join(", ", item));

        return Book.create(
                item.get(0),
                item.get(1),
                Integer.parseInt(item.get(2))
        );
    }
}

ログ出力を行うItemWriter。ここでは、ItemBookとして扱うように作成しています。

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

package org.littlewings.spring.batch;

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.item.ItemWriter;

public class LoggingItemWriter implements ItemWriter<Book> {
    Logger logger = LoggerFactory.getLogger(LoggingItemWriter.class);

    @Override
    public void write(List<? extends Book> books) throws Exception {
        books.forEach(book ->
                logger.info("write: isbn = {}, title = {}, price = {}", book.getIsbn(), book.getTitle(), book.getPrice())
        );
    }
}

ItemProcessorにもログ出力は含めているのですが。

そしてJob定義。

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

package org.littlewings.spring.batch;

import java.util.List;

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.item.ItemProcessor;
import org.springframework.batch.item.support.CompositeItemProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JobConfig {
    @Bean
    public Job job(JobBuilderFactory jobBuilderFactory) {
        return jobBuilderFactory
                .get("job")
                .incrementer(new RunIdIncrementer())
                .start(step(null))
                .build();
    }

    @Bean
    public Step step(StepBuilderFactory stepBuilderFactory) {
        return stepBuilderFactory
                .get("step")
                .<String, Book>chunk(3)
                .reader(itemReader())
                .processor(compositeItemProcessor())
                .writer(itemWriter())
                .build();
    }

    @Bean
    @StepScope
    public StringItemReader itemReader() {
        return new StringItemReader();
    }

    @Bean
    @StepScope
    public SplitItemProcessor splitItemProcessor() {
        return new SplitItemProcessor();
    }

    @Bean
    @StepScope
    public ToBookItemProcessor toBookItemProcessor() {
        return new ToBookItemProcessor();
    }

    @Bean
    @StepScope
    public CompositeItemProcessor<String, Book> compositeItemProcessor() {
        List<ItemProcessor<?, ?>> delegates = List.of(
                splitItemProcessor(),  // String -> List<String>
                toBookItemProcessor()  // List<String> -> Book
        );

        CompositeItemProcessor<String, Book> itemProcessor = new CompositeItemProcessor<>();
        itemProcessor.setDelegates(delegates);

        return itemProcessor;
    }

    @Bean
    @StepScope
    public LoggingItemWriter itemWriter() {
        return new LoggingItemWriter();
    }
}

ItemProcessorにフォーカスして取り上げます。

まずは、ここまで作成したItemProcessorをBean定義。

    @Bean
    @StepScope
    public SplitItemProcessor splitItemProcessor() {
        return new SplitItemProcessor();
    }

    @Bean
    @StepScope
    public ToBookItemProcessor toBookItemProcessor() {
        return new ToBookItemProcessor();
    }

そして、CompositeItemProcessor

    @Bean
    @StepScope
    public CompositeItemProcessor<String, Book> compositeItemProcessor() {
        List<ItemProcessor<?, ?>> delegates = List.of(
                splitItemProcessor(),  // String -> List<String>
                toBookItemProcessor()  // List<String> -> Book
        );

        CompositeItemProcessor<String, Book> itemProcessor = new CompositeItemProcessor<>();
        itemProcessor.setDelegates(delegates);

        return itemProcessor;
    }

使用するItemProcessorListにまとめ、CompositeItemProcessor#setDelegatesで設定します。
Listには、ItemProcessorを適用する順番で登録します。

これで、StringList<String>Bookという変換が行われることになります。

あとは、CompositeItemProcessorItemProcessorとしてStepに登録すればOKです。

    @Bean
    public Step step(StepBuilderFactory stepBuilderFactory) {
        return stepBuilderFactory
                .get("step")
                .<String, Book>chunk(3)
                .reader(itemReader())
                .processor(compositeItemProcessor())
                .writer(itemWriter())
                .build();
    }

動作確認する

準備はできたので、パッケージングして

$ mvn package

実行。

$ java -jar target/batch-chaining-itemprocessors-0.0.1-SNAPSHOT.jar

ログ。

2022-05-29 00:02:09.061  INFO 114305 --- [           main] o.s.b.a.b.JobLauncherApplicationRunner   : Running default command line with: []
2022-05-29 00:02:09.225  INFO 114305 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=job]] launched with the following parameters: [{run.id=1}]
2022-05-29 00:02:09.335  INFO 114305 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [step]
2022-05-29 00:02:09.417  INFO 114305 --- [           main] o.l.spring.batch.SplitItemProcessor      : split: 978-4798142470,Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発,4400
2022-05-29 00:02:09.423  INFO 114305 --- [           main] o.l.spring.batch.ToBookItemProcessor     : to book: 978-4798142470, Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発, 4400
2022-05-29 00:02:09.424  INFO 114305 --- [           main] o.l.spring.batch.SplitItemProcessor      : split: 978-4774182179,[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ,4180
2022-05-29 00:02:09.424  INFO 114305 --- [           main] o.l.spring.batch.ToBookItemProcessor     : to book: 978-4774182179, [改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ, 4180
2022-05-29 00:02:09.424  INFO 114305 --- [           main] o.l.spring.batch.SplitItemProcessor      : split: 978-1492076988,Spring Boot: Up and Running: Building Cloud Native Java and Kotlin Applications,6265
2022-05-29 00:02:09.424  INFO 114305 --- [           main] o.l.spring.batch.ToBookItemProcessor     : to book: 978-1492076988, Spring Boot: Up and Running: Building Cloud Native Java and Kotlin Applications, 6265
2022-05-29 00:02:09.430  INFO 114305 --- [           main] o.l.spring.batch.LoggingItemWriter       : write: isbn = 978-4798142470, title = Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発, price = 4400
2022-05-29 00:02:09.430  INFO 114305 --- [           main] o.l.spring.batch.LoggingItemWriter       : write: isbn = 978-4774182179, title = [ 改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ, price = 4180
2022-05-29 00:02:09.431  INFO 114305 --- [           main] o.l.spring.batch.LoggingItemWriter       : write: isbn = 978-1492076988, title = Spring Boot: Up and Running: Building Cloud Native Java and Kotlin Applications, price = 6265
2022-05-29 00:02:09.465  INFO 114305 --- [           main] o.l.spring.batch.SplitItemProcessor      : split: 978-1484237236,The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud,7361
2022-05-29 00:02:09.465  INFO 114305 --- [           main] o.l.spring.batch.ToBookItemProcessor     : to book: 978-1484237236, The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud, 7361
2022-05-29 00:02:09.466  INFO 114305 --- [           main] o.l.spring.batch.SplitItemProcessor      : split: 978-4798161488,MySQL徹底入門 第4 版 MySQL 8.0対応,4180
2022-05-29 00:02:09.466  INFO 114305 --- [           main] o.l.spring.batch.ToBookItemProcessor     : to book: 978-4798161488, MySQL徹底入門  第4版 MySQL 8.0対応, 4180
2022-05-29 00:02:09.466  INFO 114305 --- [           main] o.l.spring.batch.SplitItemProcessor      : split: 978-4797393118,基礎からのMySQL 第3版 (基礎からシリーズ),6038
2022-05-29 00:02:09.467  INFO 114305 --- [           main] o.l.spring.batch.ToBookItemProcessor     : to book: 978-4797393118, 基礎からのMySQL 第3版 (基礎からシリーズ), 6038
2022-05-29 00:02:09.467  INFO 114305 --- [           main] o.l.spring.batch.LoggingItemWriter       : write: isbn = 978-1484237236, title = The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud, price = 7361
2022-05-29 00:02:09.467  INFO 114305 --- [           main] o.l.spring.batch.LoggingItemWriter       : write: isbn = 978-4798161488, title = MySQL徹底入門 第4版 MySQL 8.0対応, price = 4180
2022-05-29 00:02:09.467  INFO 114305 --- [           main] o.l.spring.batch.LoggingItemWriter       : write: isbn = 978-4797393118, title = 基礎からのMySQL 第3版 (基礎からシリーズ), price = 6038
2022-05-29 00:02:09.492  INFO 114305 --- [           main] o.l.spring.batch.SplitItemProcessor      : split: 978-4873116389,実践ハイパフォーマンスMySQL 第3版,5280
2022-05-29 00:02:09.493  INFO 114305 --- [           main] o.l.spring.batch.ToBookItemProcessor     : to book: 978-4873116389, 実践ハイパフォ ーマンスMySQL 第3版, 5280
2022-05-29 00:02:09.494  INFO 114305 --- [           main] o.l.spring.batch.SplitItemProcessor      : split: 978-4295000198,やさしく学べるMySQL運用・管理入門【5.7対応】,2860
2022-05-29 00:02:09.494  INFO 114305 --- [           main] o.l.spring.batch.ToBookItemProcessor     : to book: 978-4295000198, やさしく学べるMySQL運用・管理入門【5.7対応】, 2860
2022-05-29 00:02:09.495  INFO 114305 --- [           main] o.l.spring.batch.SplitItemProcessor      : split: 978-4798147406,詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド (NEXT ONE),3960
2022-05-29 00:02:09.495  INFO 114305 --- [           main] o.l.spring.batch.ToBookItemProcessor     : to book: 978-4798147406, 詳解MySQL 5.7  止まらぬ進化に乗り遅れないためのテクニカルガイド (NEXT ONE), 3960
2022-05-29 00:02:09.496  INFO 114305 --- [           main] o.l.spring.batch.LoggingItemWriter       : write: isbn = 978-4873116389, title = 実践ハイパフォーマンスMySQL 第3版, price = 5280
2022-05-29 00:02:09.496  INFO 114305 --- [           main] o.l.spring.batch.LoggingItemWriter       : write: isbn = 978-4295000198, title = やさしく学べるMySQL運用・管理入門【5.7対応】, price = 2860
2022-05-29 00:02:09.496  INFO 114305 --- [           main] o.l.spring.batch.LoggingItemWriter       : write: isbn = 978-4798147406, title = 詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド (NEXT ONE), price = 3960
2022-05-29 00:02:09.539  INFO 114305 --- [           main] o.l.spring.batch.SplitItemProcessor      : split: 978-4774170206,MariaDB&MySQL全機 能バイブル,3860
2022-05-29 00:02:09.539  INFO 114305 --- [           main] o.l.spring.batch.ToBookItemProcessor     : to book: 978-4774170206, MariaDB&MySQL全機能バイブル, 3860
2022-05-29 00:02:09.539  INFO 114305 --- [           main] o.l.spring.batch.LoggingItemWriter       : write: isbn = 978-4774170206, title = MariaDB&MySQL全機能バイブル, price = 3860
2022-05-29 00:02:09.568  INFO 114305 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [step] executed in 233ms
2022-05-29 00:02:09.624  INFO 114305 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=job]] completed with the following parameters: [{run.id=1}] and the following status: [COMPLETED] in 350ms

OKですね。

まとめ

CompositeItemProcessorを使って、ItemProcessorを複数適用してみました。

特にハマりどころもなくあっさりと動かせました。よく使いたくなるものだと思うので、1度確認しておきたかったんですよね。

覚えておきましょう。