Spring Batch 5でtaskletジョブの実装

技術

Spring Batch5でtaskletジョブの実装をしてみました。

Spring Batch4から5でのバージョンアップに伴い、JobBuilderFactoryなどが非推奨となってたので勉強も兼ねて作成してみました。

開発環境

私が構築した時の開発環境は以下の通りとなります。

環境バージョン
Java17
SpringBatch5.1.0

ソース構成

デバッグ環境を作った際のソース構成は以下の通りです。

  • springBatchSample
    • src
      • main
        • java
          • com.example.batchprocessing
            • BatchConfiguration.java
            • JobCompletionNotificationListener.java
            • Person.java
            • PersonTasklet.java
            • SpringBatchSampleApplication.java
        • resource
          • application.yml
          • sample-data.csv
          • schema-all.sql
    • pom.xml

pom.xml

必要なライブラリを導入するためにpom.xmlを以下のように記述します。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>springBatchSample</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springBatchSample</name>
    <description>springBatchSample</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-batch</artifactId>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>

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

</project>

spring-boot-starter-batchを指定することでSpringBatch5系と必要なライブラリを導入してます。

またデータベースのはh2を使いますのでh2のライブラリを導入してます。

resources配下資材の準備

resources配下資材の作成をします。

アプリの設定を行うapplication.ymlを以下のように記述します。

spring:
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:testdb
    username: sa
    password:
sql:
  init:
    schema-locations: classpath:sql/schema-all.sql

passwordは未入力で大丈夫です。

サンプル用のCSVファイルであるsample-data.csvを以下のように記述します。

Jill,Doe
Joe,Doe
Justin,Doe
Jane,Doe
John,Doe

データベース内にテーブルを作成するためのschema-all.sqlを以下のように記述します。

DROP TABLE people IF EXISTS;

CREATE TABLE people
(
    person_id  BIGINT AUTO_INCREMENT PRIMARY KEY,
    first_name VARCHAR(20),
    last_name  VARCHAR(20)
);

ビジネスクラスの作成

データを保持するためのクラスであるPersonクラスを作成します。

package com.example.batchprocessing;

public record Person(String firstName, String lastName) {
}

recordクラスとして作成しているため、コンパイル時は以下のようなコードが生成されるのでコンストラクタやgetterを記述する必要がありません。

package com.example.batchprocessing;

public record Person(String firstName, String lastName) {
    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String firstName() {
        return this.firstName;
    }

    public String lastName() {
        return this.lastName;
    }
}

taskletクラスの作成

Taskletの実装クラスであるPersonTaskletを作成します。

package com.example.batchprocessing;

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.item.Chunk;
import org.springframework.batch.item.ItemStreamReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.stereotype.Component;

@Component
public class PersonTasklet implements Tasklet {

    private static final Logger log = LoggerFactory.getLogger(PersonTasklet.class);

    final ItemStreamReader<Person> reader;

    final ItemWriter<Person> writer;

    public PersonTasklet(ItemStreamReader<Person> reader, ItemWriter<Person> writer) {
        this.reader = reader;
        this.writer = writer;
    }

    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {

        Person person;

        Chunk<Person> personList = new Chunk<>();

        try {
            reader.open(chunkContext.getStepContext().getStepExecution().getExecutionContext());

            while ((person = reader.read()) != null) {
                String firstName = person.firstName().toUpperCase();
                String lastName = person.lastName().toUpperCase();
                Person transformedPerson = new Person(firstName, lastName);
                personList.add(person);
                log.info("Converting ({}) into ({})", person, transformedPerson);
            }

            writer.write(personList);

        } finally {
            reader.close();
        }
        return RepeatStatus.FINISHED;
    }
}

PersonクラスのfirstNameやlastNameにCSVファイルの各レコードが格納されます。

それらの値をUpperCaseに変換した上でListに格納し、writer.writeに引数として渡します。

ジョブ構成クラスの作成

バッチのジョブを構成するBatchConfigurationクラスを作成します。

package com.example.batchprocessing;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.item.database.JdbcBatchItemWriter;
import org.springframework.batch.item.database.builder.JdbcBatchItemWriterBuilder;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

@Configuration
public class BatchConfiguration {

    @Bean
    public Job job(JobRepository jobRepository, Step step1,
                   JobCompletionNotificationListener listener) {
        return new JobBuilder("taskletJob", jobRepository)
                .listener(listener)
                .start(step1)
                .build();
    }

    @Bean
    public Step step1(JobRepository jobRepository,
                      PlatformTransactionManager transactionManager, PersonTasklet personTasklet) {
        return new StepBuilder("personTasklet", jobRepository)
                .tasklet(personTasklet, transactionManager)
                .build();
    }

    @Bean
    public FlatFileItemReader<Person> reader() {
        return new FlatFileItemReaderBuilder<Person>()
                .name("personItemReader")
                .resource(new ClassPathResource("sample-data.csv"))
                .delimited()
                .names("firstName", "lastName")
                .targetType(Person.class)
                .build();
    }

    @Bean
    public JdbcBatchItemWriter<Person> writer(DataSource dataSource) {
        return new JdbcBatchItemWriterBuilder<Person>()
                .sql("INSERT INTO people (first_name, last_name) VALUES (:firstName, :lastName)")
                .dataSource(dataSource)
                .beanMapped()
                .build();
    }
}

jobメソッドでジョブを構成してます。

startでstep1を指定することで最初にstep1を実行するように指定してます。

またlistenerにJobCompletionNotificationListenerを登録することでジョブ完了後に指定した処理が行えるようにしてます。

step1メソッドではPersonTaskletをtaskletとして実行するように登録を行ってます。

readerメソッドとwriterメソッドはPersonTaskletで使用される処理になります。

readerメソッドはsample-data.csv読み込んでPersonクラスに値を設定します。

writerメソッドはpeopleテーブルのfirst_nameとlast_nameをインサートする処理になります。

ジョブ完了クラスの作成

ジョブ完了後に実行するJobCompletionNotificationListenerを作成します。

package com.example.batchprocessing;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobExecutionListener;
import org.springframework.jdbc.core.DataClassRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;

@Component
public class JobCompletionNotificationListener implements JobExecutionListener {

    private static final Logger log = LoggerFactory.getLogger(JobCompletionNotificationListener.class);

    private final JdbcTemplate jdbcTemplate;

    public JobCompletionNotificationListener(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public void afterJob(JobExecution jobExecution) {
        if (jobExecution.getStatus() == BatchStatus.COMPLETED) {
            log.info("!!! JOB FINISHED! Time to verify the results");

            jdbcTemplate
                    .query("SELECT first_name, last_name FROM people", new DataClassRowMapper<>(Person.class))
                    .forEach(person -> log.info("Found <{{}}> in the database.", person));
        }
    }
}

SpringApplication実行クラスの作成

最後にSpringBatchを実行するためのSpringBatchSampleApplicationを作成します。

package com.example.batchprocessing;

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

@SpringBootApplication
public class SpringBatchSampleApplication {

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

}

実行すると以下の赤枠の通り、名字と名前がUpperCaseに変換されてます。

コメント

タイトルとURLをコピーしました