CLOVER🍀

That was when it all began.

はじめおのSpring Batch

これは、なにをしたくお曞いたもの

Spring Batchに぀いお、ちょっず芋おおこうかな、ずいうこずで。

Spring Batch

Spring Batchのペヌゞはこちら。

https://spring.io/projects/spring-batch

ドキュメント。

Spring Batch - Reference Documentation

たずは、むントロダクションを読んでみたしょう。

Spring Batch Introduction

Spring Batchが登堎したバックグラりンドに぀いお。

Spring Batch Introduction / Background

OSSや関連するコミュニティは、WebベヌスやMicroservicesベヌスのアヌキテクチャヌに倧きく泚目しおいたすが、バッチ凊理に関しおは
そうでもありたせん。バッチには暙準的で再利甚可胜なアヌキテクチャヌがないため、゚ンタヌプラむズITでは1回限りの瀟内゜リュヌションが
倚く䜜り出されおいたす。

While open source software projects and associated communities have focused greater attention on web-based and microservices-based architecture frameworks, there has been a notable lack of focus on reusable architecture frameworks to accommodate Java-based batch processing needs, despite continued needs to handle such processing within enterprise IT environments. The lack of a standard, reusable batch architecture has resulted in the proliferation of many one-off, in-house solutions developed within client enterprise IT functions.

この状況に察しお、SpringSourceずAccentureが協力しおSpring Batchができたようです。

どういう時に䜿うのかずいうずころですが。

Spring Batch Introduction / Usage Scenarios

䞀般に、バッチプログラムは次のようなこずを行いたす。

  • 倚数のレコヌドをデヌタベヌス、ファむル、たたはキュヌから読み取る
  • デヌタに察しお、なにか凊理をする
  • フォヌマットを倉曎し、デヌタを曞き戻す

Spring Batchは、この基本的なバッチの繰り返しを自動化したもので、以䞋のようなシヌンで圹に立ちたす。

  • バッチ凊理の定期的なコミット
  • 䞊行、䞊列バッチ凊理
  • メッセヌゞ駆動凊理
  • 障害埌の手動たたはスケゞュヌルされた再起動
  • 䟝存するステップの順次凊理
  • レコヌドのスキップなど
  • バッチサむズが小さい堎合の䞀括トランザクション

Spring Batchのアヌキテクチャヌはこちら。

Spring Batch Introduction / Spring Batch Architecture

なのですが、Spring Batchのドメむンにおける甚語を芋た方がよいでしょうね。

The Domain Language of Batch

䞀般的なバッチの原則およびガむドラむンずされるものが、こちらに曞かれおいたす。

Spring Batch Introduction / General Batch Principles and Guidelines

たずえば、以䞋のような内容です。

  • 可胜な限り単玔化し、単䞀のバッチアプリケヌションで耇雑な構造を䜜り出すこずは避ける
  • デヌタの保存堎所ず凊理を近くに配眮するこず
  • IOを最小限に抑え、メモリを掻甚するこず
  • バッチを2床実行しない
  • デヌタの敎合性に぀いおは垞に最悪の想定を行い、適切なチェックずレコヌドのバリデヌションを行い、デヌタの敎合性を維持するこず
  • 早い段階で、珟実的な量のデヌタを甚意しおストレステストを実斜するこず

そしお、バッチ凊理戊略。

Spring Batch Introduction / Batch Processing Strategies

バッチの蚭蚈ず実装は、以䞋の暙準的な構成芁玠から行えるようにすべきです。

  • 倉換アプリケヌション
    • 倖郚システムによっお提䟛たたは生成されたファむルの皮類ごずに、凊理に必芁な暙準フォヌマットに倉換する
  • バリデヌションアプリケヌション
    • すべおの入力出力レコヌドが正しく、䞀貫性があるこずを確認する
    • バリデヌションはファむルヘッダヌやトレヌラヌ、チェックサム、バリデヌションアルゎリズム、そしおレコヌドレベルでのクロスチェックで行う
  • 抜出アプリケヌション
    • デヌタベヌスやファむルからレコヌドを読み取り、事前定矩されたルヌルに基づいおレコヌドを遞択、ファむル出力を行う
  • 抜出曎新アプリケヌション
    • デヌタベヌスやファむルからレコヌドを読み取り、デヌタベヌスを倉曎したりファむル出力を行う
  • 凊理曎新アプリケヌション
    • 入力ずなるトランザクションから抜出、怜蚌を行い、凊理を実行する
    • デヌタベヌスから必芁なデヌタを取埗するずずもに、デヌタベヌスの曎新や出力凊理甚のレコヌド䜜成を含む
  • 出力フォヌマットアプリケヌション
    • ファむルを読み蟌み、デヌタを再構成しお、印刷や倖郚システム向けに出力ファむルを䜜成する

これらでビルディングブロックで構築できない堎合は、シェルスクリプトなども䜜成する必芁がありたす。

ナヌティリティ的なステップずしおは、゜ヌト、分割、マヌゞずいったものがありたす。

さらにオプションずしお、以䞋に぀いおも曞かれおいたす。

  • バッチりィンドりでの凊理
  • バッチずオンラむン凊理の䞊行性
    • バッチずオンラむン凊理ずの競合を螏たえた、楜芳的ロックや悲芳的ロックなどの戊略
  • 䞊列凊理
  • パヌティショニング
    • 䞊列化ずの組み合わせになる

Spring Batchに関する甚語。

The Domain Language of Batch

  • Job 
 バッチプロセス党䜓のこず
    • JobInstance 
 論理的なJobの実行単䜍。状態を持ち、䞭断したずころから再開するこずもできる
    • JobParameters 
 Jobを開始するために䞎えるパラメヌタヌ
    • JobExecution 
 Job実行する単䞀の技術的な抂念。JobInstanceは成功たたは倱敗で終了する可胜性があるが、実行が正垞に完了しない限りはそのJobInstanceは完了したずはみなされない
  • Step 
 Jobのシヌケンシャルなフェヌズをカプセル化したもの。すべおのJobは、ひず぀以䞊のStepから構成される
    • StepExecution 
 あるStepの実行を衚珟したもの。Stepが実行される床に、新しいStepExecutionが䜜成される。たた前のStepが倱敗したなどの理由で、Stepが起動しない限りは䜜成されない
  • ExecutionContext 
 フレヌムワヌクによっお氞続化される、キヌず倀のペアのこず。Jobの再実行を容易にするこずに䜿われたりする
  • JobRepository 
 Job、JobExecution、Step、StepExecution、ExecutionContextなど、䞊蚘のステレオタむプの氞続化メカニズム。JobLauncherに察するCRUD操䜜を提䟛する
  • JobLauncher 
 JobParametersを䞎えおJobを起動するためのシンプルなむンタヌフェヌス
  • ItemReader 
 Stepぞの入力ずなる、ひず぀のItemを取埗するこずを抜象化したもの。取埗するItemがなくなった堎合は、nullを返すこずで衚珟する
  • ItemWriter 
 Stepの出力ずなる、バッチたたはItemのチャンクを抜象化したもの。次に受け取るItemの情報は知らず、珟圚の呌び出しお枡されたItemの情報のみを知っおいる
  • ItemProcessor 
 Itemぞのビゞネス凊理を抜象化したもの。ItemReaderがひず぀Itemを読み取りItemWriterが曞き蟌むたでの間に、ItemProcessorは倉換やその他のビゞネス凊理を行うアクセスポむントを提䟛する。Itemの凊理䞭にそのItemが無効だず刀定した堎合は、nullを返すこずでそのItemは曞き出す必芁がないこずを衚す

ちなみに、甚語集もありたす。

Glossary

Jobの構成や実行に぀いおは、こちら。

Configuring and Running a Job

Stepの構成に぀いおは、こちら。

Configuring a Step

ここで、Stepの構成にはチャンク指向のStepずTaskletずいうStepの2皮類があるこずがわかりたす。

Taskletは初出ですが、executeずいうメ゜ッドを持぀Taskletむンタヌフェヌスを䜿っお実装されたす。

チャンク指向のStepは、先述のItemReader、ItemWriter、そしおItemProcessorから構成されたす。

そしお、Job内のStepはフロヌを構成するこずができたす。

Configuring a Step / Controlling Step Flow

ここたでが、基本的な構成芁玠ですね。

あずは、ItemReader、ItemWriter、そしおItemProcessorに関するドキュメントや、

ItemReaders and ItemWriters

Item processing

Spring Batchによる、実装枈みのItemReaderずItemWriterの玹介もありたす。

List of ItemReaders and ItemWriters

䞀般的なバッチのパタヌンや、トランザクションずバッチ凊理の組み合わせなどを芋るずよいかなず思いたす。

Common Batch Patterns

Batch Processing and Transactions

Spring Integrationず組み合わせるこずもできるようです。

Spring Batch Integration

Spring BootずSpring Batch

“How-to” Guides / Batch Applications

Spring BootでSpring Batchを䜿う際には、倚くの人が質問をするようです。

A number of questions often arise when people use Spring Batch from within a Spring Boot application.

たた、Spring Batch向けのSpring Bootのプロパティはこれくらいになりたす。

  • spring.batch.jdbc.initialize-schema
  • spring.batch.jdbc.platform
  • spring.batch.jdbc.schema
  • spring.batch.jdbc.table-prefix
  • spring.batch.job.enabled
  • spring.batch.job.names

Application Properties / Integration Properties

長くなりたしたね。ドキュメントを眺めるのはこれくらいにしお、実際にSpring Batchを䜿っおいっおみたしょう。

環境

今回の環境は、こちら。

$ java --version
openjdk 17.0.2 2022-01-18
OpenJDK Runtime Environment (build 17.0.2+8-Ubuntu-120.04)
OpenJDK 64-Bit Server VM (build 17.0.2+8-Ubuntu-120.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.5 (3599d3414f046de2324203b78ddcf9b5e4388aa0)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.2, 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"

たた、デヌタベヌスずしおはMySQLを䜿甚し、172.17.0.2で動䜜しおいるものずしたす。

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

お題

今回は、チャンクずTaskletでそれぞれひず぀ず぀Jobを䜜っおみるこずにしたす。

  • チャンク
    • 曞籍デヌタが栌玍されたCSVファむルを読み蟌む
    • 読み蟌んだ曞籍デヌタを、MySQLに登録する
  • Tasklet
    • チャンクで登録した曞籍デヌタを、ログ出力する

デヌタベヌスアクセスにはJPAを䜿い、JobRepositoryの氞続化先もMySQLずしたす。

プロゞェクトを䜜成する

たずはSpring Bootプロゞェクトを䜜成したす。

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

䟝存関係には、Spring Batch、Spring Data JPA、MySQL JDBC Driverずしおいたす。

プロゞェクト内に移動。

$ cd batch-example

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-data-jpa</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>

デフォルトで䜜成されおいる゜ヌスコヌドは、削陀しおおきたす。

$ rm src/main/java/org/littlewings/spring/batch/BatchExampleApplication.java src/test/java/org/littlewings/spring/batch/BatchExampleApplicationTests.java

mainクラスの䜜成ず䞋準備

最初に、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);
    }
}

Spring Bootのドキュメントに習い、@EnableBatchProcessingアノテヌションを付䞎しおSpring Batchを有効にしたす。

“How-to” Guides / Batch Applications / Running Spring Batch Jobs on Startup

テヌブル定矩は、以䞋ずしたす。

src/main/resources/schema.sql

create table if not exists book (
  isbn varchar(14),
  title varchar(100),
  price int,
  publish_date date,
  primary key(isbn)
);

JPAの゚ンティティクラス。

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

package org.littlewings.spring.batch.entity;

import java.time.LocalDate;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "book")
public class Book {
    @Id
    @Column(name = "isbn")
    String isbn;

    @Column(name = "title")
    String title;

    @Column(name = "price")
    Integer price;

    @Column(name = "publish_date")
    LocalDate publishDate;

    // gettersetterは省略
}

リポゞトリヌ。Spring Data JPAに埓い、JpaRepositoryむンタヌフェヌスを拡匵しお甚意。

src/main/java/org/littlewings/spring/batch/repository/BookRepository.java

package org.littlewings.spring.batch.repository;

import org.littlewings.spring.batch.entity.Book;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface BookRepository extends JpaRepository<Book, String> {
}

Spring Bootの蚭定。

src/main/resources/application.properties

spring.datasource.url=jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&characterSetResults=utf-8&connectionCollation=utf8mb4_0900_bin
spring.datasource.username=kazuhira
spring.datasource.password=password

spring.sql.init.mode=always
spring.batch.jdbc.initialize-schema=always

デヌタベヌス接続ず、schema.sqlの実行spring.sql.init.modeおよび
Spring Batchのメタデヌタ甚のテヌブルも䜜成spring.batch.jdbc.initialize-schemaするようにしたす。

取り蟌むCSVファむルは、ヘッダヌなしのものず有りのものの2぀を䜜成。

ヘッダヌなし。

src/main/resources/book.csv

978-4798142470,Spring培底入門 Spring FrameworkによるJavaアプリケヌション開発,4400,2016-07-21
978-4774182179,[改蚂新版]Spring入門 ――Javaフレヌムワヌク・より良い蚭蚈ずアヌキテクチャ,4180,2016-06-14
978-1492076988,Spring Boot: Up and Running: Building Cloud Native Java and Kotlin Applications,6265,2021-03-23
978-1484237236,The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud,7361,2019-07-09
978-4798161488,MySQL培底入門 第4版 MySQL 8.0察応,4180,2020-07-06
978-4797393118,基瀎からのMySQL 第3版 (基瀎からシリヌズ),6038,2017-09-22
978-4873116389,実践ハむパフォヌマンスMySQL 第3版,5280,2013-11-25
978-4295000198,やさしく孊べるMySQL運甚・管理入門【5.7察応】,2860,2016-12-15
978-4798147406,詳解MySQL 5.7 止たらぬ進化に乗り遅れないためのテクニカルガむド (NEXT ONE),3960,2016-08-26
978-4774170206,MariaDB&MySQL党機胜バむブル,3850,2014-12-18

ヘッダヌあり。

src/main/resources/book_with_header.csv

isbn,title,price,publishDate
978-4798142470,Spring培底入門 Spring FrameworkによるJavaアプリケヌション開発,4400,2016-07-21
978-4774182179,[改蚂新版]Spring入門 ――Javaフレヌムワヌク・より良い蚭蚈ずアヌキテクチャ,4180,2016-06-14
978-1492076988,Spring Boot: Up and Running: Building Cloud Native Java and Kotlin Applications,6265,2021-03-23
978-1484237236,The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud,7361,2019-07-09
978-4798161488,MySQL培底入門 第4版 MySQL 8.0察応,4180,2020-07-06
978-4797393118,基瀎からのMySQL 第3版 (基瀎からシリヌズ),6038,2017-09-22
978-4873116389,実践ハむパフォヌマンスMySQL 第3版,5280,2013-11-25
978-4295000198,やさしく孊べるMySQL運甚・管理入門【5.7察応】,2860,2016-12-15
978-4798147406,詳解MySQL 5.7 止たらぬ進化に乗り遅れないためのテクニカルガむド (NEXT ONE),3960,2016-08-26
978-4774170206,MariaDB&MySQL党機胜バむブル,3850,2014-12-18

チャンクStepを䜜成する

では、チャンク甚のStepを持぀Jobから䜜成しおいきたす。

今回のお題だず自分でItemReaderやItemWriterを䜜成しなくおもよいのですが、たずは自分でも曞いおみるこずにしたす。

ItemReader。

src/main/java/org/littlewings/spring/batch/chunk/CsvFileItemReader.java

package org.littlewings.spring.batch.chunk;

import java.io.BufferedReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

import org.littlewings.spring.batch.entity.Book;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.item.file.DefaultBufferedReaderFactory;
import org.springframework.batch.item.file.ResourceAwareItemReaderItemStream;
import org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader;
import org.springframework.core.io.Resource;

public class CsvFileItemReader extends AbstractItemCountingItemStreamItemReader<Book> implements ResourceAwareItemReaderItemStream<Book> {
    Logger logger = LoggerFactory.getLogger(CsvFileItemReader.class);

    Resource resource;
    BufferedReader reader;

    DateTimeFormatter publishDateFormatter;

    @Override
    protected void doOpen() throws Exception {
        DefaultBufferedReaderFactory bufferedReaderFactory = new DefaultBufferedReaderFactory();
        reader = bufferedReaderFactory.create(resource, "UTF-8");
        publishDateFormatter = DateTimeFormatter.ofPattern("uuuu-MM-dd");
    }


    @Override
    protected Book doRead() throws Exception {
        String line = reader.readLine();

        if (line != null) {
            String[] tokens = line.split(",");
            Book book = new Book();
            book.setIsbn(tokens[0]);
            book.setTitle(tokens[1]);
            book.setPrice(Integer.parseInt(tokens[2]));
            book.setPublishDate(LocalDate.parse(tokens[3], publishDateFormatter));

            logger.info("[reader] read book:  isbn = {}, title = {}", book.getIsbn(), book.getTitle());

            return book;
        }

        logger.info("[reader] readed books");

        return null;
    }

    @Override
    protected void doClose() throws Exception {
        reader.close();
    }

    @Override
    public void setResource(Resource resource) {
        this.resource = resource;
    }
}

1行のCSV芁玠をBookクラスにマッピングする箇所は、愚盎に䜜成したした。ヘッダヌの読み飛ばしには察応しおいたせん。

ItemReaderの䜜成には、AbstractItemCountingItemStreamItemReaderクラスを継承するず良さそうです。

Abstract base class that provides basic restart capabilities by counting the number of items returned from an ItemReader.

List of ItemReaders and ItemWriters

たた、ResourceAwareItemReaderItemStreamむンタヌフェヌスを実装するこずで、Spring Batchの他のItemReaderず同じように
読み蟌み察象のファむルをResourceで衚珟するようにしおおきたす。

ちなみに、このItemReaderはクラスの定矩時にはSpringのBeanずしおは定矩せず、Java ConfigでSpringのBeanずしお登録したす。

ItemProcessor。こちらは、ログ出力するだけにしたした。

LoggingBookProcessor.java

package org.littlewings.spring.batch.chunk;

import org.littlewings.spring.batch.entity.Book;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.item.ItemProcessor;

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

    @Override
    public Book process(Book item) throws Exception {
        logger.info("[processor] process book: isbn = {}, title = {}", item.getIsbn(), item.getTitle());

        return item;
    }
}

ItemWriter。

src/main/java/org/littlewings/spring/batch/chunk/BookJpaItemWriter.java

package org.littlewings.spring.batch.chunk;

import java.util.List;

import org.littlewings.spring.batch.entity.Book;
import org.littlewings.spring.batch.repository.BookRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.item.support.AbstractItemStreamItemWriter;
import org.springframework.beans.factory.annotation.Autowired;

public class BookJpaItemWriter extends AbstractItemStreamItemWriter<Book> {
    Logger logger = LoggerFactory.getLogger(BookJpaItemWriter.class);

    @Autowired
    BookRepository bookRepository;

    @Override
    public void write(List<? extends Book> items) throws Exception {
        logger.info("[writer] write items: size = {}", items.size());

        items.forEach(book -> {
            logger.info("[writer] write item: isbn = {}, title = {}", book.getIsbn(), book.getTitle());
            bookRepository.save(book);
        });
    }
}

先ほど䜜成した、Book甚のリポゞトリヌを䜿っおデヌタをテヌブルに保存したす。

継承しおいるAbstractItemStreamItemWriterクラスは、ItemWriterおよびItemStreamを実装した抜象クラスで、今回は䜿甚しおいたせんが
openメ゜ッドやcloseメ゜ッドをオヌバヌラむドしお初期化、終了凊理を実装するこずができたす。

Abstract base class that combines the ItemStream and ItemWriter interfaces.

List of ItemReaders and ItemWriters

では、JobおよびStepの定矩を行っおいきたす。

参照するのは、以䞋あたりですね。

Configuring and Running a Job

Configuring and Running a Job / Configuring a Job

Configuring a Step

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

今回は、こんな感じで䜜成。

src/main/java/org/littlewings/spring/batch/config/FileLoadToDatabaseJobConfig.java

package org.littlewings.spring.batch.config;

import org.littlewings.spring.batch.chunk.BookJpaItemWriter;
import org.littlewings.spring.batch.chunk.CsvFileItemReader;
import org.littlewings.spring.batch.chunk.LoggingBookProcessor;
import org.littlewings.spring.batch.entity.Book;
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.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;

@Configuration
public class FileLoadToDatabaseJobConfig {
    @Autowired
    JobBuilderFactory jobBuilderFactory;

    @Autowired
    StepBuilderFactory stepBuilderFactory;

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

    @Bean
    public Step fileLoadToDatabaseStep() {
        return stepBuilderFactory
                .get("fileLoadToDatabaseStep")
                .<Book, Book>chunk(3)
                .reader(csvFileItemReader(null))
                .processor(loggingBookProcessor())
                .writer(bookJpaItemWriter())
                .build();
    }

    @StepScope
    @Bean
    public CsvFileItemReader csvFileItemReader(@Value("#{jobParameters['filePath']}") String filePath) {
        Resource fileResource = new FileSystemResource(filePath);

        CsvFileItemReader itemReader = new CsvFileItemReader();
        itemReader.setResource(fileResource);
        itemReader.setName("csvFileItemReader");
        itemReader.setSaveState(false);
        return itemReader;
    }

    @StepScope
    @Bean
    public LoggingBookProcessor loggingBookProcessor() {
        return new LoggingBookProcessor();
    }

    @StepScope
    @Bean
    public BookJpaItemWriter bookJpaItemWriter() {
        return new BookJpaItemWriter();
    }
}

ここたでに䜜成した、ItemReader、ItemProcessor、ItemWriterのBean定矩。いずれも@StepScopeずしおいたす。

    @StepScope
    @Bean
    public CsvFileItemReader csvFileItemReader(@Value("#{jobParameters['filePath']}") String filePath) {
        Resource fileResource = new FileSystemResource(filePath);

        CsvFileItemReader itemReader = new CsvFileItemReader();
        itemReader.setResource(fileResource);
        itemReader.setName("csvFileItemReader");
        itemReader.setSaveState(false);
        return itemReader;
    }

    @StepScope
    @Bean
    public LoggingBookProcessor loggingBookProcessor() {
        return new LoggingBookProcessor();
    }

    @StepScope
    @Bean
    public BookJpaItemWriter bookJpaItemWriter() {
        return new BookJpaItemWriter();
    }

読み蟌むファむルは、JobParameterずしお指定するこずにしたした。JobParameterたたはJobParametersを䜿うには、
Beanが@StepScopdeでスコヌプ定矩されおいる必芁がありたす。

    public CsvFileItemReader csvFileItemReader(@Value("#{jobParameters['filePath']}") String filePath) {
        Resource fileResource = new FileSystemResource(filePath);

Stateずいうのは、コミットされる前たでにどこたで読み取ったか凊理したかを保存するもので、再起動時に続きから始められる
仕掛けになるものですが。今回はこういった考慮はしないこずにするので、falseにしおおきたす。

        itemReader.setSaveState(false);

ItemReaders and ItemWriters / Preventing State Persistence

あずはJobずStepの定矩。

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

    @Bean
    public Step fileLoadToDatabaseStep() {
        return stepBuilderFactory
                .get("fileLoadToDatabaseStep")
                .<Book, Book>chunk(3)
                .reader(csvFileItemReader(null))
                .processor(loggingBookProcessor())
                .writer(bookJpaItemWriter())
                .build();
    }

読み蟌むファむル内のデヌタが10件しかないので、チャンクサむズは3にしおいたす。

JobBuilderFactory#getやStepBuilderFactory#getで指定しおいる名前がJob名やStep名になるようなのですが、Bean名は別になるので
他ず衝突しないようにBean定矩のメ゜ッド名を合わせるなり@Beanアノテヌションで指定するなりした方がよいでしょう。

たた、incrementerで指定しおいるRunIdIncrementerですが、これはJobParameterずしおrun.idを远加するものです。

RunIdIncrementer (Spring Batch 4.3.5 API)

run.idは自動でむンクリメントされおいくのですが、Spring BatchのJobは同じパラメヌタヌ指定では起動できなくなるので、
こちらを远加しおおきたす。

では、ここで1床動䜜確認しおおきたす。

パッケヌゞング。

$ mvn package

起動。

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

Spring Batchでは、デフォルトですべおのJobを起動しようずしたす。これは、spring.batch.job.namesプロパティで起動するJobを
指摘できたす耇数の堎合はカンマ区切り。

この時点ではひず぀しかJobがないのですが、最初から指定しおおきたしょう。

ログは、こんな感じに出力されたす。

2022-04-24 20:32:03.650  INFO 36238 --- [           main] o.s.b.a.b.JobLauncherApplicationRunner   : Running default command line with: [filePath=src/main/resources/book.csv]
2022-04-24 20:32:03.794  INFO 36238 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=fileLoadToDatabaseJob]] launched with the following parameters: [{run.id=1, filePath=src/main/resources/book.csv}]
2022-04-24 20:32:03.885  INFO 36238 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [fileLoadToDatabaseStep]
2022-04-24 20:32:04.006  INFO 36238 --- [           main] o.l.s.batch.chunk.CsvFileItemReader      : [reader] read book:  isbn = 978-4798142470, title = Spring培底入門 Spring FrameworkによるJavaアプリケヌション開発
2022-04-24 20:32:04.009  INFO 36238 --- [           main] o.l.s.batch.chunk.CsvFileItemReader      : [reader] read book:  isbn = 978-4774182179, title = [改蚂新版]Spring入門 ――Java フレヌムワヌク・より良い蚭蚈ずアヌキテクチャ
2022-04-24 20:32:04.009  INFO 36238 --- [           main] o.l.s.batch.chunk.CsvFileItemReader      : [reader] read book:  isbn = 978-1492076988, title = Spring Boot: Up and Running: Building Cloud Native Java and Kotlin Applications
2022-04-24 20:32:04.014  INFO 36238 --- [           main] o.l.s.batch.chunk.LoggingBookProcessor   : [processor] process book: isbn = 978-4798142470, title = Spring培底入門 Spring FrameworkによるJavaアプリケヌション開発
2022-04-24 20:32:04.014  INFO 36238 --- [           main] o.l.s.batch.chunk.LoggingBookProcessor   : [processor] process book: isbn = 978-4774182179, title = [改蚂新版]Spring入門 ――Javaフレヌムワヌク・より良い蚭蚈ずアヌキテクチャ
2022-04-24 20:32:04.014  INFO 36238 --- [           main] o.l.s.batch.chunk.LoggingBookProcessor   : [processor] process book: isbn = 978-1492076988, title = Spring Boot: Up and Running: Building Cloud Native Java and Kotlin Applications
2022-04-24 20:32:04.015  INFO 36238 --- [           main] o.l.s.batch.chunk.BookJpaItemWriter      : [writer] write items: size = 3
2022-04-24 20:32:04.016  INFO 36238 --- [           main] o.l.s.batch.chunk.BookJpaItemWriter      : [writer] write item: isbn = 978-4798142470, title = Spring培底入門 Spring FrameworkによるJavaアプリケヌション開発
2022-04-24 20:32:04.064  INFO 36238 --- [           main] o.l.s.batch.chunk.BookJpaItemWriter      : [writer] write item: isbn = 978-4774182179, title = [改蚂新版]Spring入門 ――Java フレヌムワヌク・より良い蚭蚈ずアヌキテクチャ
2022-04-24 20:32:04.066  INFO 36238 --- [           main] o.l.s.batch.chunk.BookJpaItemWriter      : [writer] write item: isbn = 978-1492076988, title = Spring Boot: Up and Running: Building Cloud Native Java and Kotlin Applications
2022-04-24 20:32:04.111  INFO 36238 --- [           main] o.l.s.batch.chunk.CsvFileItemReader      : [reader] read book:  isbn = 978-1484237236, title = The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud
2022-04-24 20:32:04.111  INFO 36238 --- [           main] o.l.s.batch.chunk.CsvFileItemReader      : [reader] read book:  isbn = 978-4798161488, title = MySQL培底入門 第4版 MySQL 8.0察応
2022-04-24 20:32:04.111  INFO 36238 --- [           main] o.l.s.batch.chunk.CsvFileItemReader      : [reader] read book:  isbn = 978-4797393118, title = 基瀎からのMySQL 第3版 (基瀎 からシリヌズ)
2022-04-24 20:32:04.112  INFO 36238 --- [           main] o.l.s.batch.chunk.LoggingBookProcessor   : [processor] process book: isbn = 978-1484237236, title = The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud
2022-04-24 20:32:04.112  INFO 36238 --- [           main] o.l.s.batch.chunk.LoggingBookProcessor   : [processor] process book: isbn = 978-4798161488, title = MySQL培底入門 第4版 MySQL 8.0察応
2022-04-24 20:32:04.112  INFO 36238 --- [           main] o.l.s.batch.chunk.LoggingBookProcessor   : [processor] process book: isbn = 978-4797393118, title = 基瀎からのMySQL 第3版 (基瀎からシリヌズ)
2022-04-24 20:32:04.112  INFO 36238 --- [           main] o.l.s.batch.chunk.BookJpaItemWriter      : [writer] write items: size = 3
2022-04-24 20:32:04.112  INFO 36238 --- [           main] o.l.s.batch.chunk.BookJpaItemWriter      : [writer] write item: isbn = 978-1484237236, title = The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud
2022-04-24 20:32:04.114  INFO 36238 --- [           main] o.l.s.batch.chunk.BookJpaItemWriter      : [writer] write item: isbn = 978-4798161488, title = MySQL培底入門 第4版 MySQL 8.0察応
2022-04-24 20:32:04.116  INFO 36238 --- [           main] o.l.s.batch.chunk.BookJpaItemWriter      : [writer] write item: isbn = 978-4797393118, title = 基瀎からのMySQL 第3版 (基瀎 からシリヌズ)
2022-04-24 20:32:04.145  INFO 36238 --- [           main] o.l.s.batch.chunk.CsvFileItemReader      : [reader] read book:  isbn = 978-4873116389, title = 実践ハむパフォヌマンスMySQL 第3版
2022-04-24 20:32:04.145  INFO 36238 --- [           main] o.l.s.batch.chunk.CsvFileItemReader      : [reader] read book:  isbn = 978-4295000198, title = やさしく孊べるMySQL運甚・管 理入門【5.7察応】
2022-04-24 20:32:04.146  INFO 36238 --- [           main] o.l.s.batch.chunk.CsvFileItemReader      : [reader] read book:  isbn = 978-4798147406, title = 詳解MySQL 5.7 止たらぬ進化に乗り遅れないためのテクニカルガむド (NEXT ONE)
2022-04-24 20:32:04.146  INFO 36238 --- [           main] o.l.s.batch.chunk.LoggingBookProcessor   : [processor] process book: isbn = 978-4873116389, title = 実践ハむパフォヌマンスMySQL 第3版
2022-04-24 20:32:04.146  INFO 36238 --- [           main] o.l.s.batch.chunk.LoggingBookProcessor   : [processor] process book: isbn = 978-4295000198, title = やさしく孊べるMySQL運甚・管理入門【5.7察応】
2022-04-24 20:32:04.146  INFO 36238 --- [           main] o.l.s.batch.chunk.LoggingBookProcessor   : [processor] process book: isbn = 978-4798147406, title = 詳解MySQL 5.7 止たらぬ 進化に乗り遅れないためのテクニカルガむド (NEXT ONE)
2022-04-24 20:32:04.147  INFO 36238 --- [           main] o.l.s.batch.chunk.BookJpaItemWriter      : [writer] write items: size = 3
2022-04-24 20:32:04.147  INFO 36238 --- [           main] o.l.s.batch.chunk.BookJpaItemWriter      : [writer] write item: isbn = 978-4873116389, title = 実践ハむパフォヌマンスMySQL 第3版
2022-04-24 20:32:04.149  INFO 36238 --- [           main] o.l.s.batch.chunk.BookJpaItemWriter      : [writer] write item: isbn = 978-4295000198, title = やさしく孊べるMySQL運甚・管 理入門【5.7察応】
2022-04-24 20:32:04.150  INFO 36238 --- [           main] o.l.s.batch.chunk.BookJpaItemWriter      : [writer] write item: isbn = 978-4798147406, title = 詳解MySQL 5.7 止たらぬ進化に乗り遅れないためのテクニカルガむド (NEXT ONE)
2022-04-24 20:32:04.189  INFO 36238 --- [           main] o.l.s.batch.chunk.CsvFileItemReader      : [reader] read book:  isbn = 978-4774170206, title = MariaDB&MySQL党機胜バむブル
2022-04-24 20:32:04.189  INFO 36238 --- [           main] o.l.s.batch.chunk.CsvFileItemReader      : [reader] readed books
2022-04-24 20:32:04.189  INFO 36238 --- [           main] o.l.s.batch.chunk.LoggingBookProcessor   : [processor] process book: isbn = 978-4774170206, title = MariaDB&MySQL党機胜バむブル
2022-04-24 20:32:04.189  INFO 36238 --- [           main] o.l.s.batch.chunk.BookJpaItemWriter      : [writer] write items: size = 1
2022-04-24 20:32:04.190  INFO 36238 --- [           main] o.l.s.batch.chunk.BookJpaItemWriter      : [writer] write item: isbn = 978-4774170206, title = MariaDB&MySQL党機胜バむブル
2022-04-24 20:32:04.251  INFO 36238 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [fileLoadToDatabaseStep] executed in 365ms
2022-04-24 20:32:04.311  INFO 36238 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=fileLoadToDatabaseJob]] completed with the following parameters: [{run.id=1, filePath=src/main/resources/book.csv}] and the following status: [COMPLETED] in 464ms

ItemReaderチャンクサむズ分繰り返し → ItemProcessorチャンクサむズ分繰り返し → ItemWriterチャンクサむズのデヌタを䞀気に
匕き枡しを取埗したチャンクの数だけ繰り返すずいう挙動になっおいたす。

このあたりの呌び出しは、以䞋を芋るず良さそうです。

https://github.com/spring-projects/spring-batch/blob/4.3.5/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/ChunkOrientedTasklet.java#L64-L95

https://github.com/spring-projects/spring-batch/blob/4.3.5/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/ChunkOrientedTasklet.java#L71

https://github.com/spring-projects/spring-batch/blob/4.3.5/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/SimpleChunkProcessor.java#L210

https://github.com/spring-projects/spring-batch/blob/4.3.5/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/SimpleChunkProcessor.java#L217

run.idも指定されおいたすね。

2022-04-24 20:32:03.794  INFO 36238 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=fileLoadToDatabaseJob]] launched with the following parameters: [{run.id=1, filePath=src/main/resources/book.csv}]

デヌタベヌスの方は、Jobを起動するたではなにもテヌブルがありたせんが

mysql> use practice;
Database changed
mysql> show tables;
Empty set (0.00 sec)

Job実行埌は以䞋のようになっおいたす。

mysql> show tables;
+------------------------------+
| Tables_in_practice           |
+------------------------------+
| BATCH_JOB_EXECUTION          |
| BATCH_JOB_EXECUTION_CONTEXT  |
| BATCH_JOB_EXECUTION_PARAMS   |
| BATCH_JOB_EXECUTION_SEQ      |
| BATCH_JOB_INSTANCE           |
| BATCH_JOB_SEQ                |
| BATCH_STEP_EXECUTION         |
| BATCH_STEP_EXECUTION_CONTEXT |
| BATCH_STEP_EXECUTION_SEQ     |
| book                         |
+------------------------------+
10 rows in set (0.00 sec)

デヌタが入っおいるこずの確認。

mysql> select * from book;
+----------------+---------------------------------------------------------------------------------------------------------+-------+--------------+
| isbn           | title                                                                                                   | price | publish_date |
+----------------+---------------------------------------------------------------------------------------------------------+-------+--------------+
| 978-1484237236 | The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud                       |  7361 | 2019-07-09   |
| 978-1492076988 | Spring Boot: Up and Running: Building Cloud Native Java and Kotlin Applications                         |  6265 | 2021-03-23   |
| 978-4295000198 | やさしく孊べるMySQL運甚・管理入門【5.7察応】                                                            |  2860 | 2016-12-15   |
| 978-4774170206 | MariaDB&MySQL党機胜バむブル                                                                             |  3850 | 2014-12-18   |
| 978-4774182179 | [改蚂新版]Spring入門 ――Javaフレヌムワヌク・より良い蚭蚈ずアヌキテクチャ                                 |  4180 | 2016-06-14   |
| 978-4797393118 | 基瀎からのMySQL 第3版 (基瀎からシリヌズ)                                                                |  6038 | 2017-09-22   |
| 978-4798142470 | Spring培底入門 Spring FrameworkによるJavaアプリケヌション開発                                           |  4400 | 2016-07-21   |
| 978-4798147406 | 詳解MySQL 5.7 止たらぬ進化に乗り遅れないためのテクニカルガむド (NEXT ONE)                               |  3960 | 2016-08-26   |
| 978-4798161488 | MySQL培底入門 第4版 MySQL 8.0察応                                                                       |  4180 | 2020-07-06   |
| 978-4873116389 | 実践ハむパフォヌマンスMySQL 第3版                                                                       |  5280 | 2013-11-25   |
+----------------+---------------------------------------------------------------------------------------------------------+-------+--------------+
10 rows in set (0.01 sec)

他のテヌブルの内容は、こんな感じになっおいたす。

mysql> select * from BATCH_JOB_EXECUTION;
+------------------+---------+-----------------+----------------------------+----------------------------+----------------------------+-----------+-----------+--------------+----------------------------+----------------------------+
| JOB_EXECUTION_ID | VERSION | JOB_INSTANCE_ID | CREATE_TIME                | START_TIME                 | END_TIME                   | STATUS    | EXIT_CODE | EXIT_MESSAGE | LAST_UPDATED               | JOB_CONFIGURATION_LOCATION |
+------------------+---------+-----------------+----------------------------+----------------------------+----------------------------+-----------+-----------+--------------+----------------------------+----------------------------+
|                1 |       2 |               1 | 2022-04-24 20:32:03.720000 | 2022-04-24 20:32:03.814000 | 2022-04-24 20:32:04.278000 | COMPLETED | COMPLETED |              | 2022-04-24 20:32:04.279000 | NULL                       |
+------------------+---------+-----------------+----------------------------+----------------------------+----------------------------+-----------+-----------+--------------+----------------------------+----------------------------+
1 row in set (0.00 sec)

mysql> select * from BATCH_JOB_EXECUTION_CONTEXT;
+------------------+--------------------------------+--------------------+
| JOB_EXECUTION_ID | SHORT_CONTEXT                  | SERIALIZED_CONTEXT |
+------------------+--------------------------------+--------------------+
|                1 | {"@class":"java.util.HashMap"} | NULL               |
+------------------+--------------------------------+--------------------+
1 row in set (0.00 sec)

mysql> select * from BATCH_JOB_EXECUTION_PARAMS;
+------------------+---------+----------+-----------------------------+----------------------------+----------+------------+-------------+
| JOB_EXECUTION_ID | TYPE_CD | KEY_NAME | STRING_VAL                  | DATE_VAL                   | LONG_VAL | DOUBLE_VAL | IDENTIFYING |
+------------------+---------+----------+-----------------------------+----------------------------+----------+------------+-------------+
|                1 | LONG    | run.id   |                             | 1970-01-01 09:00:00.000000 |        1 |          0 | Y           |
|                1 | STRING  | filePath | src/main/resources/book.csv | 1970-01-01 09:00:00.000000 |        0 |          0 | Y           |
+------------------+---------+----------+-----------------------------+----------------------------+----------+------------+-------------+
2 rows in set (0.00 sec)

mysql> select * from BATCH_JOB_EXECUTION_SEQ;
+----+------------+
| ID | UNIQUE_KEY |
+----+------------+
|  1 | 0          |
+----+------------+
1 row in set (0.00 sec)

mysql> select * from BATCH_JOB_INSTANCE;
+-----------------+---------+-----------------------+----------------------------------+
| JOB_INSTANCE_ID | VERSION | JOB_NAME              | JOB_KEY                          |
+-----------------+---------+-----------------------+----------------------------------+
|               1 |       0 | fileLoadToDatabaseJob | 2247a3263300ca04598c73ac33b0fb7e |
+-----------------+---------+-----------------------+----------------------------------+
1 row in set (0.00 sec)

mysql> select * from BATCH_JOB_SEQ;
+----+------------+
| ID | UNIQUE_KEY |
+----+------------+
|  1 | 0          |
+----+------------+
1 row in set (0.00 sec)

mysql> select * from BATCH_STEP_EXECUTION;
+-------------------+---------+------------------------+------------------+----------------------------+----------------------------+-----------+--------------+------------+--------------+-------------+-----------------+------------------+--------------------+----------------+-----------+--------------+----------------------------+
| STEP_EXECUTION_ID | VERSION | STEP_NAME              | JOB_EXECUTION_ID | START_TIME                 | END_TIME                   | STATUS    | COMMIT_COUNT | READ_COUNT | FILTER_COUNT | WRITE_COUNT | READ_SKIP_COUNT | WRITE_SKIP_COUNT | PROCESS_SKIP_COUNT | ROLLBACK_COUNT | EXIT_CODE | EXIT_MESSAGE | LAST_UPDATED               |
+-------------------+---------+------------------------+------------------+----------------------------+----------------------------+-----------+--------------+------------+--------------+-------------+-----------------+------------------+--------------------+----------------+-----------+--------------+----------------------------+
|                 1 |       6 | fileLoadToDatabaseStep |                1 | 2022-04-24 20:32:03.885000 | 2022-04-24 20:32:04.250000 | COMPLETED |            4 |         10 |            0 |          10 |               0 |                0 |                  0 |              0 | COMPLETED |              | 2022-04-24 20:32:04.252000 |
+-------------------+---------+------------------------+------------------+----------------------------+----------------------------+-----------+--------------+------------+--------------+-------------+-----------------+------------------+--------------------+----------------+-----------+--------------+----------------------------+
1 row in set (0.00 sec)

mysql> select * from BATCH_STEP_EXECUTION_CONTEXT;
+-------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------+
| STEP_EXECUTION_ID | SHORT_CONTEXT                                                                                                                                                                                 | SERIALIZED_CONTEXT |
+-------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------+
|                 1 | {"@class":"java.util.HashMap","batch.taskletType":"org.springframework.batch.core.step.item.ChunkOrientedTasklet","batch.stepType":"org.springframework.batch.core.step.tasklet.TaskletStep"} | NULL               |
+-------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------+
1 row in set (0.00 sec)

mysql> select * from BATCH_STEP_EXECUTION_SEQ;
+----+------------+
| ID | UNIQUE_KEY |
+----+------------+
|  1 | 0          |
+----+------------+
1 row in set (0.01 sec)

RunIdIncrementerを䜿甚しおいるので、同じ起動パラメヌタヌでもう1床Jobを起動できたす。

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

この時、run.idは2になっおいたす。

2022-04-24 20:33:56.256  INFO 36352 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=fileLoadToDatabaseJob]] launched with the following parameters: [{run.id=2, filePath=src/main/resources/book.csv}]

もし、RunIdIncrementerを削陀しお

    @Bean
    public Job fileLoadToDatabaseJob() {
        return jobBuilderFactory
                .get("fileLoadToDatabaseJob")
                // .incrementer(new RunIdIncrementer())
                .start(fileLoadToDatabaseStep())
                .build();
    }

同じJobParameter定矩で起動しようずするず、すでに完了枈みずいうこずで゚ラヌになりたす。

Caused by: org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException: A job instance already exists and is complete for parameters={filePath=src/main/resources/book.csv}.  If you want to run this job again, change the parameters.

今回はこちらの定矩ではないですが、JobParameterをそもそも䜿っおいない堎合は、完了したJobは以䞋の様に空振りしたす。

2022-04-24 21:09:56.925  INFO 38225 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Step already complete or not restartable, so no action to execute: StepExecution: id=8, version=6, name=fileLoadToDatabaseStep, status=COMPLETED, exitStatus=COMPLETED, readCount=10, filterCount=0, writeCount=10 readSkipCount=0, writeSkipCount=0, processSkipCount=0, commitCount=4, rollbackCount=0, exitDescription=

Spring Batchが提䟛するItemReader、ItemWriterを䜿う

先ほどは、ItemReaderおよびItemWriterを自分で䜜成したした。

ですが、Spring BatchがItemReaderずItemWriterの実装をいく぀か提䟛しおいるので、こちらを䜿甚すれば自分でItemReaderやItemWriterを
実装せずにJobを構築できたりもしたす。

List of ItemReaders and ItemWriters

今回であれば、FlatFileItemReaderずJpaItemWriterが䜿えたすね。

ItemReaders and ItemWriters / Flat Files

FlatFileItemReader (Spring Batch 4.3.5 API)

JpaItemWriter (Spring Batch 4.3.5 API)

JobおよびStepの定矩はこちら。

src/main/java/org/littlewings/spring/batch/config/FileLoadToDatabaseSimplyJobConfig.java

package org.littlewings.spring.batch.config;

import java.time.format.DateTimeFormatter;
import javax.persistence.EntityManagerFactory;

import org.littlewings.spring.batch.chunk.LoggingBookProcessor;
import org.littlewings.spring.batch.entity.Book;
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.database.JpaItemWriter;
import org.springframework.batch.item.database.builder.JpaItemWriterBuilder;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;
import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;
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.format.datetime.standard.DateTimeFormatterRegistrar;
import org.springframework.format.support.DefaultFormattingConversionService;

@Configuration
public class FileLoadToDatabaseSimplyJobConfig {
    @Autowired
    JobBuilderFactory jobBuilderFactory;

    @Autowired
    StepBuilderFactory stepBuilderFactory;

    @Autowired
    EntityManagerFactory entityManagerFactory;

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

    @Bean
    public Step fileLoadToDatabaseSimplyStep() {
        return stepBuilderFactory
                .get("fileLoadToDatabaseSimplyStep")
                .<Book, Book>chunk(3)
                .reader(flatFileBookItemReader(null, false))
                .processor(simpleLoggingBookProcessor())
                .writer(jpaBookItemWriter())
                .build();
    }

    @StepScope
    @Bean
    public FlatFileItemReader<Book> flatFileBookItemReader(
            @Value("#{jobParameters['filePath']}") String filePath,
            @Value("#{jobParameters['hasHeader'] ?: true}") boolean hasHeader
    ) {
        Resource fileResource = new FileSystemResource(filePath);

        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false);
        DateTimeFormatterRegistrar dateTimeFormatterRegistrar = new DateTimeFormatterRegistrar();
        dateTimeFormatterRegistrar.setDateFormatter(DateTimeFormatter.ofPattern("uuuu-MM-dd"));
        dateTimeFormatterRegistrar.registerFormatters(conversionService);

        BeanWrapperFieldSetMapper<Book> fieldSetMapper = new BeanWrapperFieldSetMapper<>();
        fieldSetMapper.setConversionService(conversionService);
        fieldSetMapper.setTargetType(Book.class);

        try {
            fieldSetMapper.afterPropertiesSet();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        return new FlatFileItemReaderBuilder<Book>()
                .name("flatFileBookItemReader")
                .resource(fileResource)
                .encoding("UTF-8")
                .delimited()
                .names(new String[]{"isbn", "title", "price", "publishDate"})
                .linesToSkip(hasHeader ? 1 : 0)
                .fieldSetMapper(fieldSetMapper)
                .saveState(false)
                .build();
    }

    @StepScope
    @Bean
    public LoggingBookProcessor simpleLoggingBookProcessor() {
        return new LoggingBookProcessor();
    }

    @StepScope
    @Bean
    public JpaItemWriter<Book> jpaBookItemWriter() {
        return new JpaItemWriterBuilder<Book>()
                .entityManagerFactory(entityManagerFactory)
                .build();
    }
}

ItemProcessorの方は、先ほどのものをそのたた䜿っおいたす。

FlatFileItemReaderの構築はこちら。

    @StepScope
    @Bean
    public FlatFileItemReader<Book> flatFileBookItemReader(
            @Value("#{jobParameters['filePath']}") String filePath,
            @Value("#{jobParameters['hasHeader'] ?: true}") boolean hasHeader
    ) {
        Resource fileResource = new FileSystemResource(filePath);

        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false);
        DateTimeFormatterRegistrar dateTimeFormatterRegistrar = new DateTimeFormatterRegistrar();
        dateTimeFormatterRegistrar.setDateFormatter(DateTimeFormatter.ofPattern("uuuu-MM-dd"));
        dateTimeFormatterRegistrar.registerFormatters(conversionService);

        BeanWrapperFieldSetMapper<Book> fieldSetMapper = new BeanWrapperFieldSetMapper<>();
        fieldSetMapper.setConversionService(conversionService);
        fieldSetMapper.setTargetType(Book.class);

        try {
            fieldSetMapper.afterPropertiesSet();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        return new FlatFileItemReaderBuilder<Book>()
                .name("flatFileBookItemReader")
                .resource(fileResource)
                .encoding("UTF-8")
                .delimited()
                .names(new String[]{"isbn", "title", "price", "publishDate"})
                .linesToSkip(hasHeader ? 1 : 0)
                .fieldSetMapper(fieldSetMapper)
                .saveState(false)
                .build();
    }

FlatFileItemReaderは、FlatFileItemReaderBuilderで構築したす。

        return new FlatFileItemReaderBuilder<Book>()
                .name("flatFileBookItemReader")
                .resource(fileResource)
                .encoding("UTF-8")
                .delimited()
                .names(new String[]{"isbn", "title", "price", "publishDate"})
                .linesToSkip(hasHeader ? 1 : 0)
                .fieldSetMapper(fieldSetMapper)
                .saveState(false)
                .build();

namesで各項目の名前を定矩しおおき、BeanWrapperFieldSetMapperでBookにマッピングしたす。BookのプロパティにLocalDateを
䜿甚したものもあるので、DefaultFormattingConversionServiceずDateTimeFormatterRegistrarを䜿っお日付をパヌスできるように
しおいたす。

        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false);
        DateTimeFormatterRegistrar dateTimeFormatterRegistrar = new DateTimeFormatterRegistrar();
        dateTimeFormatterRegistrar.setDateFormatter(DateTimeFormatter.ofPattern("uuuu-MM-dd"));
        dateTimeFormatterRegistrar.registerFormatters(conversionService);

        BeanWrapperFieldSetMapper<Book> fieldSetMapper = new BeanWrapperFieldSetMapper<>();
        fieldSetMapper.setConversionService(conversionService);
        fieldSetMapper.setTargetType(Book.class);

読み蟌むCSVファむルのヘッダヌの有無は、JobParameterで指定できるようにしたした。

    public FlatFileItemReader<Book> flatFileBookItemReader(
            @Value("#{jobParameters['filePath']}") String filePath,
            @Value("#{jobParameters['hasHeader'] ?: true}") boolean hasHeader
    ) {

デフォルトでヘッダヌ有りにしおいたす。

JpaItemWriterは、JpaItemWriterBuilderで構築したす。この時、EntityManagerFactoryが必芁になりたす。

    @StepScope
    @Bean
    public JpaItemWriter<Book> jpaBookItemWriter() {
        return new JpaItemWriterBuilder<Book>()
                .entityManagerFactory(entityManagerFactory)
                .build();
    }

アプリケヌションができたので、実行しおみたしょう。

最初に、デヌタを削陀しおおきたす。

mysql> truncate table book;
Query OK, 0 rows affected (0.34 sec)

mysql> select * from book;
Empty set (0.00 sec)

パッケヌゞングしお

$ mvn package

実行しおみたす。先ほどずはspring.batch.job.namesに指定するJob名を倉曎したす。

$ java -Dspring.batch.job.names=fileLoadToDatabaseSimplyJob -jar target/batch-example-0.0.1-SNAPSHOT.jar filePath=src/main/resources/book_with_header.csv

ログ。

2022-04-24 20:48:35.470  INFO 37031 --- [           main] o.s.b.a.b.JobLauncherApplicationRunner   : Running default command line with: [filePath=src/main/resources/book_with_header.csv]
2022-04-24 20:48:35.682  INFO 37031 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=fileLoadToDatabaseSimplyJob]] launched with the following parameters: [{run.id=1, filePath=src/main/resources/book_with_header.csv}]
2022-04-24 20:48:35.817  INFO 37031 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [fileLoadToDatabaseSimplyStep]
2022-04-24 20:48:35.981  INFO 37031 --- [           main] o.l.s.batch.chunk.LoggingBookProcessor   : [processor] process book: isbn = 978-4798142470, title = Spring培底入門 Spring FrameworkによるJavaアプリケヌション開発
2022-04-24 20:48:35.982  INFO 37031 --- [           main] o.l.s.batch.chunk.LoggingBookProcessor   : [processor] process book: isbn = 978-4774182179, title = [改蚂新版]Spring入門 ――Javaフレヌムワヌク・より良い蚭蚈ずアヌキテクチャ
2022-04-24 20:48:35.982  INFO 37031 --- [           main] o.l.s.batch.chunk.LoggingBookProcessor   : [processor] process book: isbn = 978-1492076988, title = Spring Boot: Up and Running: Building Cloud Native Java and Kotlin Applications
2022-04-24 20:48:36.078  INFO 37031 --- [           main] o.l.s.batch.chunk.LoggingBookProcessor   : [processor] process book: isbn = 978-1484237236, title = The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud
2022-04-24 20:48:36.078  INFO 37031 --- [           main] o.l.s.batch.chunk.LoggingBookProcessor   : [processor] process book: isbn = 978-4798161488, title = MySQL培底入門 第4版 MySQL 8.0察応
2022-04-24 20:48:36.078  INFO 37031 --- [           main] o.l.s.batch.chunk.LoggingBookProcessor   : [processor] process book: isbn = 978-4797393118, title = 基瀎からのMySQL 第3版 (基瀎からシリヌズ)
2022-04-24 20:48:36.118  INFO 37031 --- [           main] o.l.s.batch.chunk.LoggingBookProcessor   : [processor] process book: isbn = 978-4873116389, title = 実践ハむパフォヌマンスMySQL 第3版
2022-04-24 20:48:36.118  INFO 37031 --- [           main] o.l.s.batch.chunk.LoggingBookProcessor   : [processor] process book: isbn = 978-4295000198, title = やさしく孊べるMySQL運甚・管理入門【5.7察応】
2022-04-24 20:48:36.118  INFO 37031 --- [           main] o.l.s.batch.chunk.LoggingBookProcessor   : [processor] process book: isbn = 978-4798147406, title = 詳解MySQL 5.7 止たらぬ 進化に乗り遅れないためのテクニカルガむド (NEXT ONE)
2022-04-24 20:48:36.160  INFO 37031 --- [           main] o.l.s.batch.chunk.LoggingBookProcessor   : [processor] process book: isbn = 978-4774170206, title = MariaDB&MySQL党機胜バむブル
2022-04-24 20:48:36.208  INFO 37031 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [fileLoadToDatabaseSimplyStep] executed in 391ms
2022-04-24 20:48:36.298  INFO 37031 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=fileLoadToDatabaseSimplyJob]] completed with the following parameters: [{run.id=1, filePath=src/main/resources/book_with_header.csv}] and the following status: [COMPLETED] in 514ms

デヌタが入りたした。

mysql> select * from book;
+----------------+---------------------------------------------------------------------------------------------------------+-------+--------------+
| isbn           | title                                                                                                   | price | publish_date |
+----------------+---------------------------------------------------------------------------------------------------------+-------+--------------+
| 978-1484237236 | The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud                       |  7361 | 2019-07-09   |
| 978-1492076988 | Spring Boot: Up and Running: Building Cloud Native Java and Kotlin Applications                         |  6265 | 2021-03-23   |
| 978-4295000198 | やさしく孊べるMySQL運甚・管理入門【5.7察応】                                                            |  2860 | 2016-12-15   |
| 978-4774170206 | MariaDB&MySQL党機胜バむブル                                                                             |  3850 | 2014-12-18   |
| 978-4774182179 | [改蚂新版]Spring入門 ――Javaフレヌムワヌク・より良い蚭蚈ずアヌキテクチャ                                 |  4180 | 2016-06-14   |
| 978-4797393118 | 基瀎からのMySQL 第3版 (基瀎からシリヌズ)                                                                |  6038 | 2017-09-22   |
| 978-4798142470 | Spring培底入門 Spring FrameworkによるJavaアプリケヌション開発                                           |  4400 | 2016-07-21   |
| 978-4798147406 | 詳解MySQL 5.7 止たらぬ進化に乗り遅れないためのテクニカルガむド (NEXT ONE)                               |  3960 | 2016-08-26   |
| 978-4798161488 | MySQL培底入門 第4版 MySQL 8.0察応                                                                       |  4180 | 2020-07-06   |
| 978-4873116389 | 実践ハむパフォヌマンスMySQL 第3版                                                                       |  5280 | 2013-11-25   |
+----------------+---------------------------------------------------------------------------------------------------------+-------+--------------+
10 rows in set (0.00 sec)

今回のJobでヘッダヌなしのファむルを読む時は、JobParameterであるhasHeaderをfalseにしたす。

$ java -Dspring.batch.job.names=fileLoadToDatabaseSimplyJob -jar target/batch-example-0.0.1-SNAPSHOT.jar filePath=src/main/resources/book.csv hasHeader=false

Taskletを䜜成する

最埌に、Taskletを䜜成しおみたす。

今回は、ログ出力のみなのでこんな感じで䜜成。

src/main/java/org/littlewings/spring/batch/tasklet/LoggingBookTasklet.java

package org.littlewings.spring.batch.tasklet;

import java.time.format.DateTimeFormatter;

import org.littlewings.spring.batch.repository.BookRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;

public class LoggingBookTasklet implements Tasklet {
    Logger logger = LoggerFactory.getLogger(LoggingBookTasklet.class);

    @Autowired
    BookRepository bookRepository;

    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
        DateTimeFormatter publishDateFormatter = DateTimeFormatter.ofPattern("uuuu-MM-dd");

        bookRepository
                .findAll(Sort.by(Sort.Direction.DESC, "price"))
                .forEach(book -> logger.info(
                        "[tasklet] isbn = {}, title = {}, price = {}, publishDate = {}",
                        book.getIsbn(),
                        book.getTitle(),
                        book.getPrice(),
                        book.getPublishDate().format(publishDateFormatter)
                ));

        return RepeatStatus.FINISHED;
    }
}

JobおよびStep定矩。

src/main/java/org/littlewings/spring/batch/config/LoggingDatabaseJobConfig.java

package org.littlewings.spring.batch.config;

import org.littlewings.spring.batch.tasklet.LoggingBookTasklet;
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.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class LoggingDatabaseJobConfig {
    @Autowired
    JobBuilderFactory jobBuilderFactory;

    @Autowired
    StepBuilderFactory stepBuilderFactory;

    @Bean
    public Job loggingDatabaseJog() {
        return jobBuilderFactory
                .get("loggingDatabaseJob")
                .incrementer(new RunIdIncrementer())
                .start(loggingDatabaseStep())
                .build();
    }

    @Bean
    public Step loggingDatabaseStep() {
        return stepBuilderFactory
                .get("loggingDatabaseStep")
                .tasklet(loggingBookTasklet())
                .build();
    }

    @StepScope
    @Bean
    public LoggingBookTasklet loggingBookTasklet() {
        return new LoggingBookTasklet();
    }
}

特城的なずころは、Stepを構築する際にtaskletを指定しおいるこずですね。

    @Bean
    public Step loggingDatabaseStep() {
        return stepBuilderFactory
                .get("loggingDatabaseStep")
                .tasklet(loggingBookTasklet())
                .build();
    }

パッケヌゞングしお

$ mvn package

実行。

$ java -Dspring.batch.job.names=loggingDatabaseJob -jar target/batch-example-0.0.1-SNAPSHOT.jar

ログ。

2022-04-24 21:00:08.593  INFO 37435 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=loggingDatabaseJob]] launched with the following parameters: [{run.id=1}]
2022-04-24 21:00:08.697  INFO 37435 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [loggingDatabaseStep]
2022-04-24 21:00:08.990  INFO 37435 --- [           main] o.l.s.batch.tasklet.LoggingBookTasklet   : [tasklet] isbn = 978-1484237236, title = The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud, price = 7361, publishDate = 2019-07-09
2022-04-24 21:00:08.990  INFO 37435 --- [           main] o.l.s.batch.tasklet.LoggingBookTasklet   : [tasklet] isbn = 978-1492076988, title = Spring Boot: Up and Running: Building Cloud Native Java and Kotlin Applications, price = 6265, publishDate = 2021-03-23
2022-04-24 21:00:08.990  INFO 37435 --- [           main] o.l.s.batch.tasklet.LoggingBookTasklet   : [tasklet] isbn = 978-4797393118, title = 基瀎からのMySQL 第3版 (基瀎からシリヌズ), price = 6038, publishDate = 2017-09-22
2022-04-24 21:00:08.990  INFO 37435 --- [           main] o.l.s.batch.tasklet.LoggingBookTasklet   : [tasklet] isbn = 978-4873116389, title = 実践ハむパフォヌマンスMySQL 第3版, price = 5280, publishDate = 2013-11-25
2022-04-24 21:00:08.990  INFO 37435 --- [           main] o.l.s.batch.tasklet.LoggingBookTasklet   : [tasklet] isbn = 978-4798142470, title = Spring培底入門 Spring FrameworkによるJavaアプリケヌション開発, price = 4400, publishDate = 2016-07-21
2022-04-24 21:00:08.991  INFO 37435 --- [           main] o.l.s.batch.tasklet.LoggingBookTasklet   : [tasklet] isbn = 978-4774182179, title = [改蚂新版]Spring入門 ――Javaフレヌムワヌク・より良い蚭蚈ずアヌキテクチャ, price = 4180, publishDate = 2016-06-14
2022-04-24 21:00:08.991  INFO 37435 --- [           main] o.l.s.batch.tasklet.LoggingBookTasklet   : [tasklet] isbn = 978-4798161488, title = MySQL培底入門 第4版 MySQL 8.0察応, price = 4180, publishDate = 2020-07-06
2022-04-24 21:00:08.991  INFO 37435 --- [           main] o.l.s.batch.tasklet.LoggingBookTasklet   : [tasklet] isbn = 978-4798147406, title = 詳解MySQL 5.7 止たらぬ進化に乗り遅れな いためのテクニカルガむド (NEXT ONE), price = 3960, publishDate = 2016-08-26
2022-04-24 21:00:08.991  INFO 37435 --- [           main] o.l.s.batch.tasklet.LoggingBookTasklet   : [tasklet] isbn = 978-4774170206, title = MariaDB&MySQL党機胜バむブル, price = 3850, publishDate = 2014-12-18
2022-04-24 21:00:08.991  INFO 37435 --- [           main] o.l.s.batch.tasklet.LoggingBookTasklet   : [tasklet] isbn = 978-4295000198, title = やさしく孊べるMySQL運甚・管理入門【5.7 察応】, price = 2860, publishDate = 2016-12-15
2022-04-24 21:00:09.041  INFO 37435 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [loggingDatabaseStep] executed in 343ms
2022-04-24 21:00:09.101  INFO 37435 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=loggingDatabaseJob]] completed with the following parameters: [{run.id=1}] and the following status: [COMPLETED] in 454ms

これで、Taskletの方もOKですね。

JobRepositoryを䜜成するSQLに぀いお

今回、spring.batch.jdbc.initialize-schemaをalwaysにしおいたのでSpring BatchのJobRepositoryで䜿うテヌブルを自動で䜜成しお
くれおいたしたが、そのSQLは以䞋のディレクトリにあるようです。

https://github.com/spring-projects/spring-batch/tree/4.3.5/spring-batch-core/src/main/resources/org/springframework/batch/core

ちなみに、ここたで実行した埌の各テヌブルの状態は、こんな感じになりたした。

mysql> select * from BATCH_JOB_EXECUTION;
+------------------+---------+-----------------+----------------------------+----------------------------+----------------------------+-----------+-----------+--------------+----------------------------+----------------------------+
| JOB_EXECUTION_ID | VERSION | JOB_INSTANCE_ID | CREATE_TIME                | START_TIME                 | END_TIME                   | STATUS    | EXIT_CODE | EXIT_MESSAGE | LAST_UPDATED               | JOB_CONFIGURATION_LOCATION |
+------------------+---------+-----------------+----------------------------+----------------------------+----------------------------+-----------+-----------+--------------+----------------------------+----------------------------+
|                1 |       2 |               1 | 2022-04-24 20:32:03.720000 | 2022-04-24 20:32:03.814000 | 2022-04-24 20:32:04.278000 | COMPLETED | COMPLETED |              | 2022-04-24 20:32:04.279000 | NULL                       |
|                2 |       2 |               2 | 2022-04-24 20:33:56.195000 | 2022-04-24 20:33:56.274000 | 2022-04-24 20:33:56.720000 | COMPLETED | COMPLETED |              | 2022-04-24 20:33:56.720000 | NULL                       |
|                3 |       2 |               3 | 2022-04-24 20:48:35.586000 | 2022-04-24 20:48:35.731000 | 2022-04-24 20:48:36.245000 | COMPLETED | COMPLETED |              | 2022-04-24 20:48:36.246000 | NULL                       |
|                4 |       2 |               4 | 2022-04-24 20:49:52.658000 | 2022-04-24 20:49:52.761000 | 2022-04-24 20:49:53.241000 | COMPLETED | COMPLETED |              | 2022-04-24 20:49:53.242000 | NULL                       |
|                5 |       2 |               5 | 2022-04-24 21:00:08.514000 | 2022-04-24 21:00:08.618000 | 2022-04-24 21:00:09.072000 | COMPLETED | COMPLETED |              | 2022-04-24 21:00:09.073000 | NULL                       |
+------------------+---------+-----------------+----------------------------+----------------------------+----------------------------+-----------+-----------+--------------+----------------------------+----------------------------+
5 rows in set (0.00 sec)

mysql> select * from BATCH_JOB_EXECUTION_CONTEXT;
+------------------+--------------------------------+--------------------+
| JOB_EXECUTION_ID | SHORT_CONTEXT                  | SERIALIZED_CONTEXT |
+------------------+--------------------------------+--------------------+
|                1 | {"@class":"java.util.HashMap"} | NULL               |
|                2 | {"@class":"java.util.HashMap"} | NULL               |
|                3 | {"@class":"java.util.HashMap"} | NULL               |
|                4 | {"@class":"java.util.HashMap"} | NULL               |
|                5 | {"@class":"java.util.HashMap"} | NULL               |
+------------------+--------------------------------+--------------------+
5 rows in set (0.01 sec)

mysql> select * from BATCH_JOB_EXECUTION_PARAMS;
+------------------+---------+-----------+-----------------------------------------+----------------------------+----------+------------+-------------+
| JOB_EXECUTION_ID | TYPE_CD | KEY_NAME  | STRING_VAL                              | DATE_VAL                   | LONG_VAL | DOUBLE_VAL | IDENTIFYING |
+------------------+---------+-----------+-----------------------------------------+----------------------------+----------+------------+-------------+
|                1 | LONG    | run.id    |                                         | 1970-01-01 09:00:00.000000 |        1 |          0 | Y           |
|                1 | STRING  | filePath  | src/main/resources/book.csv             | 1970-01-01 09:00:00.000000 |        0 |          0 | Y           |
|                2 | LONG    | run.id    |                                         | 1970-01-01 09:00:00.000000 |        2 |          0 | Y           |
|                2 | STRING  | filePath  | src/main/resources/book.csv             | 1970-01-01 09:00:00.000000 |        0 |          0 | Y           |
|                3 | LONG    | run.id    |                                         | 1970-01-01 09:00:00.000000 |        1 |          0 | Y           |
|                3 | STRING  | filePath  | src/main/resources/book_with_header.csv | 1970-01-01 09:00:00.000000 |        0 |          0 | Y           |
|                4 | LONG    | run.id    |                                         | 1970-01-01 09:00:00.000000 |        2 |          0 | Y           |
|                4 | STRING  | hasHeader | false                                   | 1970-01-01 09:00:00.000000 |        0 |          0 | Y           |
|                4 | STRING  | filePath  | src/main/resources/book.csv             | 1970-01-01 09:00:00.000000 |        0 |          0 | Y           |
|                5 | LONG    | run.id    |                                         | 1970-01-01 09:00:00.000000 |        1 |          0 | Y           |
+------------------+---------+-----------+-----------------------------------------+----------------------------+----------+------------+-------------+
10 rows in set (0.00 sec)

mysql> select * from BATCH_JOB_EXECUTION_SEQ;
+----+------------+
| ID | UNIQUE_KEY |
+----+------------+
|  5 | 0          |
+----+------------+
1 row in set (0.00 sec)

mysql> select * from BATCH_JOB_INSTANCE;
+-----------------+---------+-----------------------------+----------------------------------+
| JOB_INSTANCE_ID | VERSION | JOB_NAME                    | JOB_KEY                          |
+-----------------+---------+-----------------------------+----------------------------------+
|               1 |       0 | fileLoadToDatabaseJob       | 2247a3263300ca04598c73ac33b0fb7e |
|               2 |       0 | fileLoadToDatabaseJob       | 1062f6824a3419e049c6d95f6e5e99c2 |
|               3 |       0 | fileLoadToDatabaseSimplyJob | 22f79c44e978cc2d028116a60ed9fdd5 |
|               4 |       0 | fileLoadToDatabaseSimplyJob | 27f07cdb7c5117f846c1f6eec03d26de |
|               5 |       0 | loggingDatabaseJob          | 853d3449e311f40366811cbefb3d93d7 |
+-----------------+---------+-----------------------------+----------------------------------+
5 rows in set (0.00 sec)

mysql> select * from BATCH_JOB_SEQ;
+----+------------+
| ID | UNIQUE_KEY |
+----+------------+
|  5 | 0          |
+----+------------+
1 row in set (0.00 sec)

mysql> select * from BATCH_STEP_EXECUTION;
+-------------------+---------+------------------------------+------------------+----------------------------+----------------------------+-----------+--------------+------------+--------------+-------------+-----------------+------------------+--------------------+----------------+-----------+--------------+----------------------------+
| STEP_EXECUTION_ID | VERSION | STEP_NAME                    | JOB_EXECUTION_ID | START_TIME                 | END_TIME                   | STATUS    | COMMIT_COUNT | READ_COUNT | FILTER_COUNT | WRITE_COUNT | READ_SKIP_COUNT | WRITE_SKIP_COUNT | PROCESS_SKIP_COUNT | ROLLBACK_COUNT | EXIT_CODE | EXIT_MESSAGE | LAST_UPDATED               |
+-------------------+---------+------------------------------+------------------+----------------------------+----------------------------+-----------+--------------+------------+--------------+-------------+-----------------+------------------+--------------------+----------------+-----------+--------------+----------------------------+
|                 1 |       6 | fileLoadToDatabaseStep       |                1 | 2022-04-24 20:32:03.885000 | 2022-04-24 20:32:04.250000 | COMPLETED |            4 |         10 |            0 |          10 |               0 |                0 |                  0 |              0 | COMPLETED |              | 2022-04-24 20:32:04.252000 |
|                 2 |       6 | fileLoadToDatabaseStep       |                2 | 2022-04-24 20:33:56.364000 | 2022-04-24 20:33:56.690000 | COMPLETED |            4 |         10 |            0 |          10 |               0 |                0 |                  0 |              0 | COMPLETED |              | 2022-04-24 20:33:56.692000 |
|                 3 |       6 | fileLoadToDatabaseSimplyStep |                3 | 2022-04-24 20:48:35.817000 | 2022-04-24 20:48:36.208000 | COMPLETED |            4 |         10 |            0 |          10 |               0 |                0 |                  0 |              0 | COMPLETED |              | 2022-04-24 20:48:36.209000 |
|                 4 |       6 | fileLoadToDatabaseSimplyStep |                4 | 2022-04-24 20:49:52.851000 | 2022-04-24 20:49:53.207000 | COMPLETED |            4 |         10 |            0 |          10 |               0 |                0 |                  0 |              0 | COMPLETED |              | 2022-04-24 20:49:53.208000 |
|                 5 |       3 | loggingDatabaseStep          |                5 | 2022-04-24 21:00:08.698000 | 2022-04-24 21:00:09.041000 | COMPLETED |            1 |          0 |            0 |           0 |               0 |                0 |                  0 |              0 | COMPLETED |              | 2022-04-24 21:00:09.042000 |
+-------------------+---------+------------------------------+------------------+----------------------------+----------------------------+-----------+--------------+------------+--------------+-------------+-----------------+------------------+--------------------+----------------+-----------+--------------+----------------------------+
5 rows in set (0.00 sec)

mysql> select * from BATCH_STEP_EXECUTION_CONTEXT;
+-------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------+
| STEP_EXECUTION_ID | SHORT_CONTEXT                                                                                                                                                                                                            | SERIALIZED_CONTEXT |
+-------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------+
|                 1 | {"@class":"java.util.HashMap","batch.taskletType":"org.springframework.batch.core.step.item.ChunkOrientedTasklet","batch.stepType":"org.springframework.batch.core.step.tasklet.TaskletStep"}                            | NULL               |
|                 2 | {"@class":"java.util.HashMap","batch.taskletType":"org.springframework.batch.core.step.item.ChunkOrientedTasklet","batch.stepType":"org.springframework.batch.core.step.tasklet.TaskletStep"}                            | NULL               |
|                 3 | {"@class":"java.util.HashMap","batch.taskletType":"org.springframework.batch.core.step.item.ChunkOrientedTasklet","batch.stepType":"org.springframework.batch.core.step.tasklet.TaskletStep"}                            | NULL               |
|                 4 | {"@class":"java.util.HashMap","batch.taskletType":"org.springframework.batch.core.step.item.ChunkOrientedTasklet","batch.stepType":"org.springframework.batch.core.step.tasklet.TaskletStep"}                            | NULL               |
|                 5 | {"@class":"java.util.HashMap","batch.taskletType":"org.littlewings.spring.batch.tasklet.LoggingBookTasklet$$EnhancerBySpringCGLIB$$7acedf7a","batch.stepType":"org.springframework.batch.core.step.tasklet.TaskletStep"} | NULL               |
+-------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------+
5 rows in set (0.00 sec)

mysql> select * from BATCH_STEP_EXECUTION_SEQ;
+----+------------+
| ID | UNIQUE_KEY |
+----+------------+
|  5 | 0          |
+----+------------+
1 row in set (0.00 sec)

たずめ

Spring Batchを初めお䜿っおみたした。

jBatchを䜿ったり、情報は芋たこずがあったのでざっくりず知っおいる぀もりでしたが、実際に゜ヌスコヌドを曞いお動かそうずするず
いろいろハマりたしお 。

今回詊しおみお、少しは感芚がわかったかなず思いたす。

Spring Web MVCアプリケヌションのテスト方法を芋おいきたいモックでBeanのテスト

これは、なにをしたくお曞いたもの

Spring Web MVCのテスト方法を、ドキュメントをしっかり眺めお芋たこずないなぁず。
時々扱っおいる時もかなり雰囲気で䜿っおいたので、ちょっず芋おみるこずにしたした。

あず、オマケでモックMockitoでふ぀うにBeanのテストをするものも぀けおおきたす。

この゚ントリヌは、こちらの続き的な感じですね。

Spring Boot/Spring Frameworkのテストに関するドキュメントをざっくり眺めてみる - CLOVER🍀

参照するドキュメントずSpring Web MVCのテスト

今回、Spring Web MVCのテストを芋おいくにあたり、参照するドキュメントは䞻に次の2぀です。

Spring Bootのテストに関するドキュメント。

Core Features / Testing

さらに絞り蟌むず、Spring Boot Applicationのテストに関するセクションですね。

Core Features / Testing / Testing Spring Boot Applications

Spring Frameworkのテストに関するドキュメント。

Testing

こちらも絞り蟌むず、むンテグレヌションテストに関するセクションになりたす。

Testing / Integration Testing

基本的には、Spring Bootのドキュメントを芋おいき、テスト自䜓を曞く時に䜿うクラスに関しおはSpring Frameworkのドキュメントを
必芁に応じお芋おいく、ずいう感じになりたす。

Spring Web MVCのテストをする方法ですが、Spring BootやSpring Frameworkのテストに関するドキュメントを芋おいくず
耇数あるこずに気づきたす。

読み進め方ですが、たずはSpring Bootプロゞェクトを䜜成しおサンプルプログラムを䜜り、そこからテストのバリ゚ヌションを倉え぀぀
テストコヌドを曞いおいくこずにしたす。

順番は、以䞋にしたす。

䞋に行くに埓っお時間がかかるようになりたすが、より本物の環境に近づいおいきたす。ずいうか、最埌のパタヌンはテスト䞭に
組み蟌みTomcatを起動するこずになりたす。

このあたりを、適宜ドキュメントの内容を絡めながら曞いおいきたいず思いたす。HtmlUnitやSeleniumを䜿ったテストは扱いたせん。

ちなみに、Spring Frameworkのテストのセクションには、HtmlUnitやWebDriver、GebずMockMvcを合わせお䜿う方法に぀いお曞かれたり
しおいたす。興味があれば、こちらをどうぞ。

Testing / Integration / MockMvc / Testing HtmlUnit Integration

では、進めおいきたす。

環境

今回の環境は、こちら。

$ java --version
openjdk 17.0.2 2022-01-18
OpenJDK Runtime Environment (build 17.0.2+8-Ubuntu-120.04)
OpenJDK 64-Bit Server VM (build 17.0.2+8-Ubuntu-120.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.5 (3599d3414f046de2324203b78ddcf9b5e4388aa0)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.2, 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-107-generic", arch: "amd64", family: "unix"

準備

たずはSpring Bootプロゞェクトを䜜成したす。

$ curl -s https://start.spring.io/starter.tgz \
  -d bootVersion=2.6.6 \
  -d javaVersion=17 \
  -d name=webmvc-testing \
  -d groupId=org.littlewings \
  -d artifactId=webmvc-testing \
  -d version=0.0.1-SNAPSHOT \
  -d packageName=org.littlewings \
  -d dependencies=web,webflux \
  -d baseDir=webmvc-testing | tar zxvf -

䟝存関係にweb以倖にwebfluxが入っおいたすが、この理由は埌で曞きたす。

プロゞェクト内ぞ移動。

$ cd webmvc-testing

ディレクトリ構成。

$ tree
.
├── HELP.md
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │   └── org
    │   │       └── littlewings
    │   │           └── WebmvcTestingApplication.java
    │   └── resources
    │       ├── application.properties
    │       ├── static
    │       └── templates
    └── test
        └── java
            └── org
                └── littlewings
                    └── WebmvcTestingApplicationTests.java

12 directories, 7 files

Mavenでの䟝存関係およびプラグむン蚭定。

        <dependencies>
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-web</artifactId>
                </dependency>
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-webflux</artifactId>
                </dependency>

                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-test</artifactId>
                        <scope>test</scope>
                </dependency>
                <dependency>
                        <groupId>io.projectreactor</groupId>
                        <artifactId>reactor-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/WebmvcTestingApplication.java

package org.littlewings;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class WebmvcTestingApplication {

        public static void main(String[] args) {
                SpringApplication.run(WebmvcTestingApplication.class, args);
        }

}

src/test/java/org/littlewings/WebmvcTestingApplicationTests.java

package org.littlewings;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class WebmvcTestingApplicationTests {

        @Test
        void contextLoads() {
        }

}

テストコヌドには、@SpringBootTestアノテヌションが付䞎されおいたす。

このあたりは、いったん削陀したす。

$ rm src/main/java/org/littlewings/WebmvcTestingApplication.java src/test/java/org/littlewings/WebmvcTestingApplicationTests.java

@SpringBootTestアノテヌションに぀いお

@SpringBootTestアノテヌションに぀いおの説明を少し芋おみたす。

@SpringBootTestアノテヌションは、Spring Bootの機胜が必芁な堎合に@ContextConfigurationアノテヌションの代わりに䜿甚したす。

Spring Boot provides a @SpringBootTest annotation, which can be used as an alternative to the standard spring-test @ContextConfiguration annotation when you need Spring Boot features. The annotation works by creating the ApplicationContext used in your tests through SpringApplication. In addition to @SpringBootTest a number of other annotations are also provided for testing more specific slices of an application.

Core Features / Testing / Testing Spring Boot Applications

SpringApplicationを䜿い、ApplicationContextの䜜成を行いたす。たた、テストでの機胜が䞍足する堎合、Auto Configurationを行うための
機胜も提䟛しおいたすスラむスず読んでいたす。

Core Features / Testing / Testing Spring Boot Applications / https://docs.spring.io/spring-boot/docs/2.6.6/reference/html/features.html#features.testing.spring-boot-applications.autoconfigured-tests

そしお、Web環境の方に目を向けおみるず、@SpringBootTestアノテヌションはサヌバヌを開始しないず曞いおいたす。
ですが、webEnvironment属性を䜿っおテストの実行方法を決められるようです。

By default, @SpringBootTest will not start a server. You can use the webEnvironment attribute of @SpringBootTest to further refine how your tests run:

Core Features / Testing / Testing Spring Boot Applications

@SpringBootTestアノテヌションのwebEnvironment属性には、以䞋の4皮類が指定できたす。

  • MOCK
    • デフォルトの挙動で、Web甚のApplicationContextをロヌドしお、モックWeb環境を提䟛する
    • 組み蟌みサヌバヌは起動しない
    • @AutoConfigureMockMvcアノテヌションたたは@AutoConfigureWebTestClientアノテヌションず組み合わせるこずで、Webアプリケヌションのモックベヌスのテストが可胜になる
  • RANDOM_PORT
    • WebServerApplicationContextをロヌドしお、本物のWeb環境を提䟛する
    • 組み蟌みサヌバヌが起動し、ランダムなポヌトでリッスンする
  • DEFINED_PORT
    • WebServerApplicationContextをロヌドしお、本物のWeb環境を提䟛する
    • 組み蟌みサヌバヌが起動し、application.propertiesで定矩されたポヌトたたはデフォルトの8080ポヌトでリッスンする
  • NONE
    • SpringApplicationによっおApplicationContextはロヌドするものの、モックやそれ以倖のいずれのWeb環境も提䟛しない

テストをモックWeb環境で行うか、組み蟌みサヌバヌで行うか、ずいう感じになりたすたったくWeb環境を䜿わないずいう遞択肢もありたすが。

Spring Web MVCが䜿甚可胜な堎合は、Spring Web MVCベヌスのApplicationContextが構成されるようです。

If Spring MVC is available, a regular MVC-based application context is configured.

Core Features / Testing / Testing Spring Boot Applications / Detecting Web Application Type

サンプルアプリケヌションを䜜成する

テスト察象のサンプルアプリケヌションを䜜成したしょう。お題は、ちょっずしたechoプログラムにしたす。

mainクラス。

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

package org.littlewings.spring.webmvc;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

RestController。GETずPOST甚のメ゜ッドを甚意しお、POSTの堎合はJSONでリク゚ストを受け取るようにしたす。

src/main/java/org/littlewings/spring/webmvc/EchoController.java

package org.littlewings.spring.webmvc;

import java.util.Optional;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("echo")
public class EchoController {
    MessageDecorationService messageDecorationService;

    public EchoController(MessageDecorationService messageDecorationService) {
        this.messageDecorationService = messageDecorationService;
    }

    @GetMapping("get")
    public EchoResponse get(@RequestParam String message) {
        Thread.dumpStack();

        return EchoResponse
                .create(
                        messageDecorationService
                                .decorate(Optional.ofNullable(message).orElse("Hello World[get]!!")),
                        Thread.currentThread().getName()
                );
    }

    @PostMapping("post")
    public EchoResponse post(@RequestBody EchoRequest echoRequest) {
        Thread.dumpStack();

        return EchoResponse
                .create(
                        messageDecorationService
                                .decorate(Optional.ofNullable(echoRequest.getMessage()).orElse("Hello World[post]!!")),
                        Thread.currentThread().getName()
                );
    }
}

レスポンスには受け取ったメッセヌゞをServiceで加工しお返し、この時に動䜜しおいるスレッド名を返すようにしたす。
それから、RestControllerのメ゜ッド呌び出し時にはスタックトレヌスを出力するようにしおいたす。

リク゚スト甚のクラス。

src/main/java/org/littlewings/spring/webmvc/EchoRequest.java

package org.littlewings.spring.webmvc;

public class EchoRequest {
    String message;

    public static EchoRequest create(String message) {
        EchoRequest echoRequest = new EchoRequest();
        echoRequest.setMessage(message);

        return echoRequest;
    }

    // gettersetterは省略
}

レスポンス甚のクラス。

src/main/java/org/littlewings/spring/webmvc/EchoResponse.java

package org.littlewings.spring.webmvc;

public class EchoResponse {
    String message;
    String threadName;

    public static EchoResponse create(String message, String threadName) {
        EchoResponse echoResponse = new EchoResponse();
        echoResponse.setMessage(message);
        echoResponse.setThreadName(threadName);

        return echoResponse;
    }

    // gettersetterは省略
}

RestControllerが䜿うServiceクラス。

src/main/java/org/littlewings/spring/webmvc/MessageDecorationService.java

package org.littlewings.spring.webmvc;

import org.springframework.stereotype.Service;

@Service
public class MessageDecorationService {
    public String decorate(String message) {
        return String.format("★★★ %s !!★★★", message);
    }
}

動䜜確認。

$ mvn spring-boot:run

OKですね。

$ curl localhost:8080/echo/get?message=Hello
{"message":"★★★ Hello !!★★★","threadName":"http-nio-8080-exec-1"}


$ curl -H 'Content-Type: application/json' localhost:8080/echo/post -d '{"message": "Hello"}'
{"message":"★★★ Hello !!★★★","threadName":"http-nio-8080-exec-2"}

では、こちらに察しおテストを曞いおいきたしょう。なお、䟝存関係にspring-boot-starter-webfluxが含たれおいたしたが、特に明瀺的に
説明しない限りはspring-boot-starter-webずspring-boot-starter-testが䟝存関係に含たれおいればテストは動䜜したす。

spring-boot-starter-webfluxに぀いおは、必芁な郚分で説明したす。

デフォルトの@SpringBootTestで組み蟌みサヌバヌが起動しないこずを確認する

実際のテストに行く前に、@SpringBootTestアノテヌションだけでは組み蟌みサヌバヌが起動しないこずを確認しおおきたす。

こんなテストで確認。

src/test/java/org/littlewings/spring/webmvc/SpringBootTestDefaultTest.java

package org.littlewings.spring.webmvc;

import java.io.IOException;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.URI;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import static org.assertj.core.api.Assertions.assertThatThrownBy;

@SpringBootTest
public class SpringBootTestDefaultTest {
    @Test
    public void test() throws IOException {
        HttpURLConnection conn = (HttpURLConnection) URI.create("http://localhost:8080").toURL().openConnection();
        assertThatThrownBy(() -> conn.connect())
                .isInstanceOf(ConnectException.class)
                .hasMessage("接続を拒吊されたした");
    }
}

ドキュメントどおりサヌバヌが起動しおいないので、接続が拒吊されたす。

では、先に進めおいきたしょう。

@WebMvcTestを䜿っおテストを曞く

ドキュメントの順番ずはだいぶ異なりたすが、最初に@WebMvcTestアノテヌションを䜿ったテストを曞くこずにしたす。

Core Features / Testing / Testing Spring Boot Applications / Auto-configured Spring MVC Tests

こちらは、Spring Web MVCを䜿っお実装した、特定のControllerが動䜜するかをテストしたす。

@WebMvcTestアノテヌションを䜿甚するず、Spring Web MVCを構成するずずもに限られたBeanをスキャンしたす。

  • @Controller
  • @ControllerAdvice
  • @JsonComponent
  • Converter
  • GenericConverter
  • Filter
  • HandlerInterceptor
  • WebMvcConfigurer
  • WebMvcRegistrations
  • HandlerMethodArgumentResolver

通垞の@Componentや@ConfigurationPropertiesずいったBeanは、@WebMvcTestアノテヌションを䜿甚しおもスキャンされたせん。
䜿甚したい堎合は、@EnableConfigurationPropertiesアノテヌションを付䞎するこずになりたす。

なお、@WebMvcTestアノテヌションを䜿甚した時に有効になるAuto Configurationは、以䞋にたずめられおいたす。

Test Auto-configuration Annotations

@WebMvcTestアノテヌションを䜿甚しお䜜成したテストは、こちら。

src/test/java/org/littlewings/spring/webmvc/WebMvcTestTest.java

package org.littlewings.spring.webmvc;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest(EchoController.class)
public class WebMvcTestTest {
    @Autowired
    MockMvc mvc;

    @MockBean
    MessageDecorationService messageDecorationService;

    @Autowired
    ObjectMapper objectMapper;

    @Test
    public void getTest() throws Exception {
        given(messageDecorationService.decorate("WebMvcTest[get]"))
                .willReturn("*** WebMvcTest[get] !!***");

        // queryParam
        mvc
                .perform(
                        get("/echo/get")
                                .queryParam("message", "WebMvcTest[get]")

                )
                .andExpect(status().isOk())
                .andExpect(
                        content()
                                .json(
                                        objectMapper.writeValueAsString(
                                                EchoResponse.create("*** WebMvcTest[get] !!***", "main")
                                        )
                                )
                );

        // querystring
        mvc
                .perform(
                        get("/echo/get?message=WebMvcTest[get]")

                )
                .andExpect(status().isOk())
                .andExpect(jsonPath("message", Matchers.is("*** WebMvcTest[get] !!***")))
                .andExpect(jsonPath("threadName", Matchers.is("main")));
    }

    @Test
    public void postTest() throws Exception {
        given(messageDecorationService.decorate("WebMvcTest[post]"))
                .willReturn("*** WebMvcTest[post] !!***");

        mvc
                .perform(
                        post("/echo/post")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(
                                        objectMapper.writeValueAsString(
                                                EchoRequest.create("WebMvcTest[post]")
                                        )
                                )

                )
                .andExpect(status().isOk())
                .andExpect(header().string("content-type", MediaType.APPLICATION_JSON_VALUE))
                .andExpect(jsonPath("message", Matchers.is("*** WebMvcTest[post] !!***")))
                .andExpect(jsonPath("threadName", Matchers.is("main")));
    }
}

テストクラスに@WebMvcTestアノテヌションを付䞎したす。

@WebMvcTest(EchoController.class)
public class WebMvcTestTest {

ドキュメントに習っおControllerを@WebMvcTestアノテヌションに指定しおいたすが、実は指定しなくおも動きたす。
スキャン察象に@Controllerが入っおいるからですね。

Controllerが䟝存するBeanは、モックにしたす。

    @MockBean
    MessageDecorationService messageDecorationService;

ずいうか、最初に曞いたようにスキャンされるBeanが限定されおいるので、今回のように別のBeanに䟝存しおいるControllerはモックで
䟝存関係をなんずかするなりBeanを登録するなりしないず動䜜したせん。

もし今回の゜ヌスコヌドからモックに関するコヌドを削陀するず、䜜成したRestControllerの䟝存関係が解決できずにテストに倱敗したす。

Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'echoController' defined in file [/path/to/webmvc-testing/target/classes/org/littlewings/spring/webmvc/EchoController.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.littlewings.spring.webmvc.MessageDecorationService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:800)
    at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:229)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1372)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1222)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:953)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:740)
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:415)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:303)
    at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:136)
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:99)
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:124)
    ... 72 more
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.littlewings.spring.webmvc.MessageDecorationService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1799)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1355)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1309)
    at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:887)
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
    ... 90 more

テストにはMockMvcを䜿甚したす。

    @Autowired
    MockMvc mvc;

MockMvc自䜓に぀いおは、こちらを参照。

Testing / Integration Testing / MockMvc

今回は、最初に䟝存するServiceのモックを蚭定しお、それからMockMvcを䜿っおテストしおいたす。

    @Test
    public void getTest() throws Exception {
        given(messageDecorationService.decorate("WebMvcTest[get]"))
                .willReturn("*** WebMvcTest[get] !!***");

        // queryParam
        mvc
                .perform(
                        get("/echo/get")
                                .queryParam("message", "WebMvcTest[get]")

                )
                .andExpect(status().isOk())
                .andExpect(
                        content()
                                .json(
                                        objectMapper.writeValueAsString(
                                                EchoResponse.create("*** WebMvcTest[get] !!***", "main")
                                        )
                                )
                );

        // querystring
        mvc
                .perform(
                        get("/echo/get?message=WebMvcTest[get]")

                )
                .andExpect(status().isOk())
                .andExpect(jsonPath("message", Matchers.is("*** WebMvcTest[get] !!***")))
                .andExpect(jsonPath("threadName", Matchers.is("main")));
    }

今回は、なんずなくQueryStringをqueryParamで蚭定したり、盎接URLに付けたりしおみたした。たた、JSONレスポンスのアサヌション方法も
分けおいたす。

アサヌション結果を芋るずわかるのですが、今回のテストではRestControllerはmainスレッドで動䜜しおいたすね。

                .andExpect(jsonPath("message", Matchers.is("*** WebMvcTest[get] !!***")))
                .andExpect(jsonPath("threadName", Matchers.is("main")));

RestControllerのメ゜ッド呌び出し時のスタックトレヌスはこちら。

java.lang.Exception: Stack trace
        at java.base/java.lang.Thread.dumpStack(Thread.java:1380)
        at org.littlewings.spring.webmvc.EchoController.get(EchoController.java:23)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:568)
        at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
        at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150)
        at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)
        at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
        at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
        at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
        at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1067)
        at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)
        at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
        at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:655)
        at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
        at org.springframework.test.web.servlet.TestDispatcherServlet.service(TestDispatcherServlet.java:72)
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:764)
        at org.springframework.mock.web.MockFilterChain$ServletFilterProxy.doFilter(MockFilterChain.java:167)
        at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134)
        at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
        at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134)
        at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
        at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134)
        at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
        at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134)
        at org.springframework.test.web.servlet.MockMvc.perform(MockMvc.java:199)
        at org.littlewings.spring.webmvc.WebMvcTestTest.getTest(WebMvcTestTest.java:35)
        
        〜省略〜

この少し埌に曞くモック環境でのテストに぀いお曞いた内容を芋るずわかりたすが、@WebMvcTestアノテヌションを䜿った時もモック環境で
動䜜しおいるこずになりたす。

MockMvcに぀いお

MockMvcに぀いお、もう少し深堀しおみたしょう。

MockMvcにはスタンドアロンモヌドずSpringのConfigurationを䜿ったモヌドの2぀のセットアップ方法がありたす。

Testing / Integration Testing / MockMvc / Setup Choices

Spring Bootでは、SpringのConfigurationを䜿ったモヌドでセットアップするようです。

https://github.com/spring-projects/spring-boot/blob/v2.6.6/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcAutoConfiguration.java#L78-L87

https://github.com/spring-projects/spring-boot/blob/v2.6.6/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcAutoConfiguration.java#L95-L99

MockMvcのセットアップ自䜓は完了しおいるので、あずはリク゚ストを蚭定したり、レスポンスをアサヌションしたりずいった感じの
䜿い方になりたす。

Testing / Integration Testing / MockMvc / Performing Requests

Testing / Integration Testing / MockMvc / Defining Expectations

今回は通垞のControllerで䜿いたくなるようなフォワヌド先やモデルのアサヌションは行いたせんが、サンプルがあるのでこちらを
参考にするずよいでしょう。

https://github.com/spring-projects/spring-framework/tree/v5.3.18/spring-test/src/test/java/org/springframework/test/web/servlet/samples

Servlet Filterを登録する機胜もありそうですが

Testing / Integration Testing / MockMvc / Filter Registrations

こちらは、@WebMvcTestアノテヌションの属性で衚珟するこずになりそうです。

WebMvcTest (Spring Boot 2.6.6 API)

ドキュメントを芋るず、@AutoConfigureMockMvcアノテヌションを䜿うずよい、ずは曞かれおいたすが。

Core Features / Testing / Testing Spring Boot Applications / Auto-configured Spring MVC Tests

たた、MockMvcずE2Eテストの比范に぀いおもドキュメントに曞かれおいたす。

Testing / Integration Testing / MockMvc / MockMvc vs End-to-End Tests

MockMvcはServletのモック実装に基づいお構築されおいお、実際のサヌブレットコンテナず比べるず異なる郚分もありたす。

jsessoinid Cookieがない、フォワヌドやリダむレクト、サヌブレットの䟋倖ハンドリングは行われず、Spring Bootではあたり関係ないですが
JSPのレンダリングも行われたせん。
※Theymeleafなどのテンプレヌト゚ンゞンでのレンダリングや、JSON、XMLなどでのレスポンスは行えるようです

MockMvcを䜿甚したテストではControllerに䟝存するBeanをモック化したりしお、Webレむダヌに限定したテストがしやすくなるずころが
ポむントです。SpringずしおはMockMvcの利甚もむンテグレヌションテストに䜍眮づけおいたすが、単䜓テストに近いものではありたす。

このため、E2Eテストのようなブラックボックス的なテストず異なり、MockMvcではHandlerControllerが䜿われたこず、䟋倖凊理で
HandlerExceptionResolverが䜿われたこず、モデルの属性やバむンディング゚ラヌなどを確認できたす。

このような特性を螏たえ぀぀、どのテストを䜿うかを決めおいくこずになるんでしょうね。

モック環境でテストする

次は、モック環境でのテストを行いたす。

Core Features / Testing / Testing Spring Boot Applications / Testing with a mock environment

具䜓的には、@SpringBootTestアノテヌションず@AutoConfigureMockMvcアノテヌションの組み合わせになりたす。

䜜成したテストコヌドはこちら。

src/test/java/org/littlewings/spring/webmvc/AutoConfigureMockMvcTest.java

package org.littlewings.spring.webmvc;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest
@AutoConfigureMockMvc
public class AutoConfigureMockMvcTest {
    @Test
    public void getTest(@Autowired MockMvc mvc) throws Exception {
        mvc
                .perform(
                        get("/echo/get")
                                .queryParam("message", "MockMvc[get]")

                )
                .andExpect(status().isOk())
                .andExpect(jsonPath("message", Matchers.is("★★★ MockMvc[get] !!★★★")))
                .andExpect(jsonPath("threadName", Matchers.is("main")));
    }

    @Test
    public void postTest(@Autowired MockMvc mvc, @Autowired ObjectMapper objectMapper) throws Exception {
        mvc
                .perform(
                        post("/echo/post")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(
                                        objectMapper.writeValueAsString(
                                                EchoRequest.create("MockMvc[post]")
                                        )
                                )

                )
                .andExpect(status().isOk())
                .andExpect(header().string("content-type", MediaType.APPLICATION_JSON_VALUE))
                .andExpect(jsonPath("message", Matchers.is("★★★ MockMvc[post] !!★★★")))
                .andExpect(jsonPath("threadName", Matchers.is("main")));
    }
}

@WebMvcTestアノテヌションを䜿った時ず同じようにMockMvcを䜿ったテストになりたすが、先ほどずの違いはRestControllerが
䟝存するBeanをモック化しおいないこずですね。

たたテストクラスに付䞎するアノテヌションで、明瀺的なControllerの指定もなくなりたした。

@SpringBootTest
@AutoConfigureMockMvc
public class AutoConfigureMockMvcTest {

RestControllerのメ゜ッド呌び出し時のスタックトレヌスは、こちら。

java.lang.Exception: Stack trace
    at java.base/java.lang.Thread.dumpStack(Thread.java:1380)
    at org.littlewings.spring.webmvc.EchoController.get(EchoController.java:23)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1067)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:655)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
    at org.springframework.test.web.servlet.TestDispatcherServlet.service(TestDispatcherServlet.java:72)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:764)
    at org.springframework.mock.web.MockFilterChain$ServletFilterProxy.doFilter(MockFilterChain.java:167)
    at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134)
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
    at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134)
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
    at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134)
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
    at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134)
    at org.springframework.test.web.servlet.MockMvc.perform(MockMvc.java:199)
    at org.littlewings.spring.webmvc.AutoConfigureMockMvcTest.getTest(AutoConfigureMockMvcTest.java:22)

    〜省略〜

よく芋るず、@WebMvcTestアノテヌションを䜿った時のスタックトレヌスずたったく同じです。

そしお、スレッド名もmainであるこずが確認できたす。

                .andExpect(jsonPath("message", Matchers.is("★★★ MockMvc[post] !!★★★")))
                .andExpect(jsonPath("threadName", Matchers.is("main")));

ずいうこずは、この2぀のテスト方法では、同じモック環境で動䜜しおいるこずになりたすね。違いはBeanのスキャン範囲ずいったずころ
でしょうか@SpringBootTestアノテヌションを䜿っおいる差かず。

ちなみに、このテストの䞭に@MockBeanアノテヌションを䜿ったコヌドを远加するこずで、䟝存するBeanをモック化するこずは
ふ぀うにできたす。

    @MockBean
    MessageDecorationService messageDecorationService;

組み蟌みサヌバヌを起動しおテストする

最埌は、組み蟌みサヌバヌを起動しおテストを行いたす。

Core Features / Testing / Testing Spring Boot Applications / Testing with a running server

こちらは、@SpringBootTestアノテヌションのwebEnvironment属性にRANDOM_PORTたたはDEFINED_PORTを指定するこずで
実珟できたす。今回は、RANDOM_PORTを䜿甚したす。

䜜成したテストはこちら。

src/test/java/org/littlewings/spring/webmvc/SpringBootTestWebEnvironmentTest.java

package org.littlewings.spring.webmvc;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
        properties = "server.tomcat.threads.max=1"
)
public class SpringBootTestWebEnvironmentTest {
    @Test
    public void getTest(@Autowired WebTestClient webClient) {
        webClient
                .get()
                .uri(uriBuilder -> uriBuilder
                        .path("/echo/get")
                        .queryParam("message", "WebEnvironment Test[get]")
                        .build()
                )
                .exchange()
                .expectStatus().isOk()
                .expectBody(EchoResponse.class)
                .consumeWith(result -> {
                    EchoResponse response = result.getResponseBody();
                    assertThat(response.getMessage()).isEqualTo("★★★ WebEnvironment Test[get] !!★★★");
                    assertThat(response.getThreadName()).isEqualTo("http-nio-auto-1-exec-1");
                });
    }

    @Test
    public void postTest(@Autowired WebTestClient webClient) {
        webClient
                .post()
                .uri("/echo/post")
                .contentType(MediaType.APPLICATION_JSON)
                .bodyValue(EchoRequest.create("WebEnvironment Test[post]"))
                .exchange()
                .expectStatus().isOk()
                .expectBody(EchoResponse.class)
                .consumeWith(result -> {
                    EchoResponse response = result.getResponseBody();
                    assertThat(response.getMessage()).isEqualTo("★★★ WebEnvironment Test[post] !!★★★");
                    assertThat(response.getThreadName()).isEqualTo("http-nio-auto-1-exec-1");
                });
    }
}

@SpringBootTestアノテヌションのwebEnvironment属性はRANDOM_PORTずし、たたRestControllerがスレッド名を返すので
固定になるように組み蟌みTomcatのスレッドプヌルは1にしたした。

@SpringBootTest(
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
        properties = "server.tomcat.threads.max=1"
)
public class SpringBootTestWebEnvironmentTest {

テストには、WebTestClientを䜿うようです。

    @Test
    public void getTest(@Autowired WebTestClient webClient) {
        webClient
                .get()
                .uri(uriBuilder -> uriBuilder
                        .path("/echo/get")
                        .queryParam("message", "WebEnvironment Test[get]")
                        .build()
                )
                .exchange()
                .expectStatus().isOk()
                .expectBody(EchoResponse.class)
                .consumeWith(result -> {
                    EchoResponse response = result.getResponseBody();
                    assertThat(response.getMessage()).isEqualTo("★★★ WebEnvironment Test[get] !!★★★");
                    assertThat(response.getThreadName()).isEqualTo("http-nio-auto-1-exec-1");
                });
    }

このために、䟝存関係にspring-boot-starter-webfluxを远加しおいたす。

Core Features / Testing / Testing Spring Boot Applications / Testing with a running server

Testing / Integration Testing / WebTestClient

ちなみに、WebTestClientはモック環境でも利甚できるようです。

Spring WebFluxを䟝存関係に远加したくない堎合は、TestRestTemplateを䜿っおテストしおもOKです。

RestController呌び出し時のスタックトレヌスはこちら。

java.lang.Exception: Stack trace
    at java.base/java.lang.Thread.dumpStack(Thread.java:1380)
    at org.littlewings.spring.webmvc.EchoController.get(EchoController.java:23)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1067)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:655)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:764)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:360)
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:399)
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:889)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1743)
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
    at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
    at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.base/java.lang.Thread.run(Thread.java:833)

Tomcatのスレッドプヌル内で動䜜しおいるこずがわかりたすね。

たた、アサヌション時に確認しおいたスレッド名も、mainではなくなっおいたした。

                .consumeWith(result -> {
                    EchoResponse response = result.getResponseBody();
                    assertThat(response.getMessage()).isEqualTo("★★★ WebEnvironment Test[get] !!★★★");
                    assertThat(response.getThreadName()).isEqualTo("http-nio-auto-1-exec-1");
                });

Beanのモック化も行わなず、Spring Web MVCアプリケヌションずしお完党な圢で動䜜したす。E2Eテストずいった甚途のむメヌゞで
䜿うこずになるんでしょうね。

これでひずずおりSpring Web MVCのテスト方法の確認はできた感じでしょうか。

オマケモックを䜿ったBeanのテスト

ここたでSpring Web MVCのテストを芋おきたしたが、ふ぀うのBeanのテストのパタヌンを完党に飛ばしおいたす。

軜くやっおおきたしょう。単䜓テストな感じであればむンスタンスをnewしおも良いず思うのですが、今回はモックを䜿っおみたす。

Core Features / Testing / Testing Spring Boot Applications / Mocking and Spying Beans

たずは、Serviceをもうひず぀远加したしょう。

src/main/java/org/littlewings/spring/webmvc/WordsJoinService.java

package org.littlewings.spring.webmvc;

import java.util.Arrays;
import java.util.stream.Collectors;

import org.springframework.stereotype.Service;

@Service
public class WordsJoinService {
    MessageDecorationService messageDecorationService;

    public WordsJoinService(MessageDecorationService messageDecorationService) {
        this.messageDecorationService = messageDecorationService;
    }

    public String join(String... words) {
        return messageDecorationService.decorate(Arrays.stream(words).collect(Collectors.joining(" ")));
    }
}

このServiceでは、最初に䜜成したServiceを䜿っおいたす。

テストはこちら。

src/test/java/org/littlewings/spring/webmvc/MockingBeansTest.java

package org.littlewings.spring.webmvc;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.mockito.BDDMockito.given;

@SpringBootTest
public class MockingBeansTest {
    @Autowired
    WordsJoinService wordsJoinService;

    @MockBean
    MessageDecorationService messageDecorationService;

    @Test
    public void serviceTest() {
        given(messageDecorationService.decorate("Hello World"))
                .willReturn("*** Hello World !!***");

        assertThat(wordsJoinService.join("Hello", "World"), is("*** Hello World !!***"));
    }
}

テストクラスは@SpringBootTestアノテヌションを䜿っお䜜成しお、テスト察象のBeanを@Autowiredし぀぀、䟝存するBeanは
@MockBeanアノテヌションでモック化すればOKです。

たずめ

Spring Web MVCで䜜成したアプリケヌションのテスト方法を芋おみたした。

あたり理解できおいなかったので、時間を取っお今回敎理しおみお良かったかなぁず思いたす。