Spring Batchを基本からまとめてみた【入門】

## Spring Bootの概要及び必要知識

(1)Maven

- javaプログラムをコンパイル・テスト・パッケージング・デプロイを行うツール
- 使用するJarライブラリ群の依存性を管理
- pom.xmlという設定ファイルに、プロジェクトの情報、ライブラリ、プラグイン等を記述する

(2)Javaアノテーション

- 『注釈』の意、アノテーション型
- 補足として機能したり、フレームワークに意図を伝える（処理を指示）できる
- フレームワークで用意されたアノテーションを使えるようになることが重要
- アノテーションには引数を渡すことができる

(3)Dependency Injection

- 一般的なjavaプログラムでは、あるクラスから他のクラスを呼び出す時にはnew(インスタンス化)する必要があり、これを依存性のある状態と呼ぶ
-　インスタンスの生成をSpring Boot（DIコンテナ）にまかせ、インスタンスの生成、参照の解決を自動で行ってもらう事を指す。依存性をDIコンテナが管理し、適用してくれる
-　@Autowaired　というアノテーションを記述するだけで、Spring Boot管理下にある他のクラスを呼び出せる 


## Spring Batchの概要

- 大規模なデータ処理を行う、バッチ処理向けのフレームワーク
- Spring Frameworkの一部として提供され、Springの機能を利用して効率的にバッチ処理を開発出来る
- javaでバッチを開発する場合のデファクトスタンダート

※ バッチ処理とは、あらかじめ一連の処理手順を登録しておき、自動的に処理を行う方式のこと

## Spring Batch 全体概要
```
JobLauncher  ⇨ 　Job Repository　⇨　Database

      ⇩
      
Job (一連のステップの集合)  ⇨   Job Repository　⇨　Database
    　
      ⇩
      
Step (バッチ処理の単位)    ⇨   Tasklet            
                        ⇨   Platform Transaction Manager　⇨　Database

      ⇩       
      
Chunk 『Item Reader』『Item Processor』『Item Writer』
```
- JobLauncher : ジョブの実行を管理(ジョブを起動して、ジョブ全体の成功または失敗を判定)
- Job : バッチ処理の単位(顧客データの取り込みや月次レポートの作成)
- Step : ジョブ内で実行される処理の単位(処理内容を記述するタスクレッドとチャンクと呼び出す)
- tasklet : タスクレッドはステップ内で実行される処理の最小単位(具体的な処理内容を記述)
- Chunk : 『Item Reader』『Item Processor』『Item Writer』を組み合わせてデータ処理を行う
- Item Reader : データを読み込む
- Item Processor　: 読み込んだデータを処理する
- Item Writer : 処理を書き込む
- Job Repository　: ジョブの実行履歴を保存する
- Platform Transaction Manager : トランザクションの境界を定義してロールバックコミットを制御する

## Spring Batchを設定する
(1)プロジェクトを作成する

公式：[spring initializr](https://start.spring.io/)

① ブラウザを開き、(https://start.spring.io/) を入力し、開く
② 下記を入力し、Dependenciesで下記を選択し、『GENERATE』ボタンを押す

- Project: Maven
- Language: Java
- SpringBoot: 3.2.9
- jar
- 21

- Group: com.udemy
- Artifact: hello-spring-batch
- Name : hello-spring-batch
- Description: Demo projects for Spring Boot
- Package name: com.udemy.hello

Dependencies

- Spring Batch
- PostgreSQL Driver
- Lombok

(2)Spring Batch設定Configクラスを作成する

①config/SpringConfig.javaを作成する
- Spring Batchでは、＠Configurationを付与をした設定用のコンフィグクラスを作成する
- バッチ処理の全体構成・設定を記述する

```config/SpringConfig.java

package com.udemy.hello_spring_batch_2.config;

import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;

// @Configurationアノテーションを付与する
@Configuration
public class SpringConfig {

    // JobLauncher/JobRepository/PlatformTransactionManagerのクラス変数及びコンストラクタによる設定を記載する
    private final JobLauncher jobLauncher;
    private final JobRepository jobRepository;
    private final PlatformTransactionManager transactionManager;

    public SpringConfig(JobLauncher jobLauncher, JobRepository jobRepository,
            PlatformTransactionManager transactionManager) {
        this.jobLauncher = jobLauncher;
        this.jobRepository = jobRepository;
        this.transactionManager = transactionManager;

    }
}
```

(3)tasklet/HelloTasklet1.javaを作成する

①tasklet/HelloTasklet1.javaを作成する

- Taskletインターフェースを実装し、Taskletを定義する

 ※Taskletとは、Step内で実行される処理を記述するためのインターフェースで、execute()メソッドを実装することで、具体的な処理を記述する

 ```tasklet/HelloTasklet1.java
package com.udemy.hello_spring_batch_2.tasklet;

import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.configuration.annotation.StepScope;
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.stereotype.Component;

//@Componentアノテーションによって、Spring Beanとして登録される

@Component("Hello Tasklet1")
// StepScopeアノテーションによって、インスタンスをステップごとに生成・破棄する
@StepScope
// Tasklet インターフェイスexecuteメソッドを実装する
public class HelloTasklet1 implements Tasklet {

    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
        System.out.println("Hello Tasklet1!!!");
        // RepeatStatus.FINISHEDは、executeメソッドが正常終了したことを示す
        return RepeatStatus.FINISHED;
    }
}       
```

②Taskletをバッチ設定クラスに定義する
- 作成したTaskletはバッチ設定クラスに定義し、上位のStepから呼び出されるように準備する

```config/SpringConfig.java
package com.udemy.hello_spring_batch_2.config;

import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;

// @Configurationアノテーションを付与する
@Configuration
public class SpringConfig {

    // JobLauncher/JobRepository/PlatformTransactionManagerのクラス変数及びコンストラクタによる設定を記載する
    private final JobLauncher jobLauncher;
    private final JobRepository jobRepository;
    private final PlatformTransactionManager transactionManager;

    // @Autowiredアノテーション、@Qualifierアノテーションを使用して、『HelloTasklet1』と名前がついたBeanのインスタンスを自動で注入する
    @Autowired
    @Qualifier("HelloTasklet1")
    private Tasklet helloTasklet1;

    public SpringConfig(JobLauncher jobLauncher, JobRepository jobRepository,
            PlatformTransactionManager transactionManager) {
        this.jobLauncher = jobLauncher;
        this.jobRepository = jobRepository;
        this.transactionManager = transactionManager;

    }
}
```



 

③config/SpringConfig.javaにStepとjobを定義する

※ Jobとは、①バッチ処理の単位（顧客データ取込、月次レポート作成等）を表し、複数のStepをまとめて実行できる。②実行するステップの情報・バッチ処理にパラメータを設定した場合のパラメータチェックを設定できる。
※ Stepとは、Job内で実行される処理の単位を表し、Taskletもしくは、Chunkにおける各処理（読取・加工・書込）を設定する。

## Spring Batchを実行する

- 実行時にはSpring Batchの内部機能として、ジョブの実行状態・履歴等の情報がデータベースに格納される。

①application.propertiesにDB接続情報を記載する

```application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/BatchDB?createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.batch.jdbc.initialize-schema = ALWAYS
```

②tasklet/HelloTasklet1.javaにジョブに対してパラメータ(引数)を与える
- JobExecutionContextは、Job実行中に共有されるデータストアで、Tasklet間で値を受け渡したい場合は、JobExecutionContextに値を保存し、別のTaskletで取得する。

```tasklet/HelloTasklet1.java
package com.udemy.hello.tasklet;

import org.springframework.batch.item.ExecutionContext;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.configuration.annotation.StepScope;
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.Value;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Component("HelloTasklet1")
@StepScope
@Slf4j
public class HelloTasklet1 implements Tasklet {
    // @Valueアノテーションを指定
    @Value("#{jobParameters['param1']}")
    private String param1;

    @Value("#{jobParameters['param2']}")
    private Integer param2;

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

        log.info("Hello Tasklet1");
        log.info("param1={}", param1); // パラメータ埋め込み修正
        log.info("param2={}", param2); // パラメータ埋め込み修正

        // System.out.println("Hello Tasklet1");
        // System.out.println("param1=" + param1);
        // System.out.println("param2=" + param2);

        // JobExecutionContextを取得
        ExecutionContext jobContext = contribution.getStepExecution()
                .getJobExecution()
                .getExecutionContext();
        jobContext.put("jobKey1", "jobValue1");

        return RepeatStatus.FINISHED;
    }
}
```


③validator/HelloJobParametersValidator.javaを作成する
- バラメータ検証を行うクラスを作成し、JobParametersValidatorインターフェイスを実装することで実現できる

```validator/HelloJobParametersValidator.java
package com.udemy.hello.validator;

import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersInvalidException;
import org.springframework.batch.core.JobParametersValidator;

public class HelloJobParametersValidator implements JobParametersValidator {

    @Override
    public void validate(JobParameters parameters)
            throws JobParametersInvalidException {
        // パラメータ1の値チェック
        String param1 = parameters.getString("param1");
        if (!param1.equals("DEV") &&
                !param1.equals("TEST") &&
                !param1.equals("PROD")) {
            throw new JobParametersInvalidException("param1:" + param1
                    + "DEV/TEST/PRODのいずれかを指定してください");
        }
        // パラメータ2の値チェック
        String param2 = parameters.getString("param2");
        // 数値であることをチェック
        try {
            Integer.parseInt(param2);
        } catch (Exception e) {
            throw new JobParametersInvalidException("param2=" + param2
                    + "paramsは数値を指定してください。");
        }
    }
}
```


④tasklet/HelloTasklet2.javaを作成する

```tasklet/HelloTasklet2.java
package com.udemy.hello.tasklet;

import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.configuration.annotation.StepScope;
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.Value;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;

@Component("HelloTasklet2")
@StepScope
@Slf4j
public class HelloTasklet2 implements Tasklet {

    @Value("#{JobExecutionContext['jobKey1']}")
    private String jobValue1;

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

        log.info("Hello Tasklet2");
        log.info("jobValue1={}" + jobValue1);

        // System.out.println("Hello Tasklet2");
        // System.out.println("jobValue1=" + jobValue1);

        return RepeatStatus.FINISHED;
    }
}
```


⑤resourcesにlogback.xmlを作成する

- Lombokとは、Javaのコードを簡潔にし、生産性を高め、可読性を高めるオープンソースのライブラリ
- @Dataアノテーションで、Getter/Setterを書かなくても使用できる
- @Slf4jのアノテーションをつけることでログ出力の実装が可能となる

## Chunkを作成する

- Chunkとは、ItemReader、ItemProcessor、ItemWriterを組み合わせて、データ処理を行う単位で、ItemReaderからデータを読み込み、ItemProcessorで処理し、ItemWriterで書き込む

- ItemProcessorとは、ItemReaderから受け取ったデータ項目を処理し、ItemWriterを渡す

- ItemWriterとは、ItemProcessorから受け取った処理結果を書き込む

①chunk/HelloReader.javaを作成する

```chunk/HelloReader.java
package com.udemy.hello.chunk;

import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.NonTransientResourceException;
import org.springframework.batch.item.ParseException;
import org.springframework.batch.item.UnexpectedInputException;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

@Component
@StepScope
@Slf4j
public class HelloReader implements ItemReader<String> {

    private int intIndex = 0;

    @Override
    public String read() throws Exception,
            UnexpectedInputException, ParseException,
            NonTransientResourceException {
        String[] readDataList = { "associate", "consultant", "manager",
                "director", "president", null };

        log.info("Read Data={}", readDataList[intIndex]);

        return readDataList[intIndex++];
    }
}
```

②chunk/HelloProcessor.javaを作成する

```chunk/HelloProcessor.javaを作成する
package com.udemy.hello.chunk;

import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

@Component
@StepScope
@Slf4j
public class HelloProcessor implements ItemProcessor<String, String> {

    @Override
    public String process(String item) throws Exception {

        item = item.toUpperCase();
        log.info("Processor item={}", item);
        return item;
    }
}
```

③chunk/HelloWriter.javaを作成する

```chunk/HelloWriter.java
package com.udemy.hello.chunk;

import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.item.Chunk;
import org.springframework.batch.item.ItemWriter;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

@Component
@StepScope
@Slf4j
public class HelloWriter implements ItemWriter<String> {

    @Override
    public void write(Chunk<? extends String> chunk)
            throws Exception {
        log.info("Writer chunk={}", chunk);
        log.info("-----------------");
    }

}
```

## EventListenerを作成する

①listener/HelloJobExecutionListener.javaを作成する

```listener/HelloJobExecutionListener.java
package com.udemy.hello.listener;

import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobExecutionListener;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

@Component
@Slf4j
public class HelloJobExecutionListener implements JobExecutionListener {

    @Override
    public void beforeJob(JobExecution jobExecution) {
        log.info("Job スタート　at:{}", jobExecution.getStartTime());
    }

    @Override
    public void afterJob(JobExecution jobExecution) {
        log.info("Job 終了　at:{}", jobExecution.getEndTime());
    }
}
```

## CSVファイルを読み込みDBへ登録するバッチを作成する

(1)プロジェクトを作成する

公式：[spring initializr](https://start.spring.io/)

① ブラウザを開き、(https://start.spring.io/) を入力し、開く
② 下記を入力し、Dependenciesで下記を選択し、『GENERATE』ボタンを押す

- Project: Maven
- Language: Java
- SpringBoot: 3.2.9
- jar
- 21

- Group: com.udemy
- Artifact: hello-spring-batch
- Name : hello-spring-batch
- Description: Demo projects for Spring Boot
- Package name: com.udemy.hello

Dependencies

- Spring Batch
- PostgreSQL Driver
- lombock

## CSVファイルを読み込む

①src/main/resoucesにinputData/employee.csvを作成する

```src/main/resouces/inputData/employee.csv
従業員コード,従業員名,ジョブタイトル,上司,入社日
5000,山田　太郎,manager,5003,2001/01/01
5001,佐藤　二郎,salesman,5000,2002/05/02
5002,田中　三郎,associate,5001,2010/09/20
5003,鈴木　一郎,srmgr,,2020/01/01
```

②resouces/application.propertiesにDB接続文字列を追加する

```resouces/application.properties
# csvパスのエントリを追加する
csv.path=inputData/employee.csv

spring.datasource.url=jdbc:mysql://localhost:3306/BatchDB?createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.batch.jdbc.initialize-schema = ALWAYS
```

③resouces/logback.xmlを追加する

```resouces/logback.xml
<configuration>
  
  <property name="logDir" value="C:\pleiades-2023\workspace\hello-spring-batch\log\" />
  <property name="fileName" value="hellolog" />

  <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>${logDir}${fileName}.%d{yyyy-MM-dd}.log</fileNamePattern>
      <maxHistory>10</maxHistory>
    </rollingPolicy>
    
    <encoder>
      <pattern>%d %level %m %n</pattern>
    </encoder>
   </appender>

  <root level="INFO">
    <appender-ref ref="FILE" />
  </root>
  
</configuration>

```

## CSVデータを保持するクラスを作成する

①src/main/resoucesにmodel/Employee.javaを作成する

```src/main/resouces/model/Employee.java

package com.udemy.hello.model;

import java.util.Date;
import lombok.Data

public class Employee {

    private Integer empNumber;
    private String empName;
    private String jobTitle;
    private Integer mgrNumber;
    private Date hireDate;
}
```

②configCvs/SpringConfig.javaを作成する

```configCvs/SpringConfig.java
package com.udemy.hello.configCsv;

import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.database.BeanPropertyItemSqlParameterSourceProvider;
import org.springframework.batch.item.database.JdbcBatchItemWriter;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.core.io.ClassPathResource;
import java.nio.charset.StandardCharsets;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.job.builder.JobBuilder;

import javax.sql.DataSource;

import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;
import org.springframework.batch.item.file.mapping.DefaultLineMapper;
import org.springframework.batch.item.file.transform.DelimitedLineTokenizer;
import com.udemy.hello.model.Employee;

@Configuration
public class SpringConfig {
    private final JobLauncher jobLauncher;
    private final JobRepository jobRepository;
    private final PlatformTransactionManager transactionManager;

    @Value("${csv.path}")
    private Resource inputCSV;

    @Autowired
    private DataSource dataSource;

    private static final String INSERT_EMP_SQL = "INSERT INTO employee (empNumber, empName, jobTitle, mgrNumber, hireDate) "
            +
            "VALUES (empNumber, empName, jobTitle, mgrNumber, hireDate)";

    public SpringConfig(JobLauncher jobLauncher, JobRepository jobRepository,
            PlatformTransactionManager transactionManager) {
        this.jobLauncher = jobLauncher;
        this.jobRepository = jobRepository;
        this.transactionManager = transactionManager;
    }

    @Bean
    @StepScope
    public FlatFileItemReader<Employee> csvItemReader() {

        FlatFileItemReader<Employee> reader = new FlatFileItemReader<Employee>();
        reader.setResouce(inputCSV);

        reader.setLinesToSkip(1);
        reader.setEncoding(StandardCharsets.UTF_8.name());

        BeanWrapperFieldSetMapper<Employee> beanWrapperFieldSetMapper = new BeanWrapperFieldSetMapper<Employee>();
        beanWrapperFieldSetMapper.setTargetType(Employee.class);

        DelimitedLineTokenizer tokenizer = new DelimitedLineTokenizer();
        String[] csvTitleArray = new String[] { "EmpNumber", "EmpName", "JobTitle", "mgrNumber", "HireDate" };
        tokenizer.setNames(csvTitleArray);

        DefaultLineMapper<Employee> lineMapper = new DefaultLineMapper<Employee>();
        lineMapper.setFieldSetMapper(beanWrapperFieldSetMapper);
        lineMapper.setLineTokenizer(tokenizer);

        reader.setLineMapper(lineMapper);
        return reader;
    }

    @Autowired
    @Qualifier("empItemProcessor")
    public ItemProcessor<Employee, Employee> empItemProcessor;

    @Bean
    @StepScope
    public JdbcBatchItemWriter<Employee> jdbcBatchItemWriter() {
        BeanPropertyItemSqlParameterSourceProvider<Employee> provider = new BeanPropertyItemSqlParameterSourceProvider<Employee>();

        JdbcBatchItemWriter<Employee> writer = new JdbcBatchItemWriter<Employee>();
        writer.setDataSource(dataSource);
        writer.setItemSqlParameterSourceProvider(provider);
        writer.setSql(INSERT_EMP_SQL);
        return writer;
    }

    @Bean
    public Step chunkStep1() {
        return new StepBuilder("EmpImportStep1", jobRepository)
                .<Employee, Employee>chunk(1, transactionManager)
                .reader(csvItemReader())
                .processor(empItemProcessor)
                .writer(jdbcItemWriter())
                .build();
    }

    @Bean
    public Job chunkJob() {
        return new JobBuilder("chunkJob", jobRepository)
                .incrementer(new RunIdIncrementer())
                .start(chunkStep1())
                .build();
    }
}
```


③chuckCsv/EmpItemProcessor.javaを作成する

```chuckCsv/EmpItemProcessor.java
package com.udemy.hello.chuckCsv;

import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.stereotype.Component;

import com.udemy.hello.model.Employee;

import lombok.extern.slf4j.Slf4j;

@Component("EmpItemProcessor")
@StepScope
@Slf4j
public class EmpItemProcessor implements ItemProcessor<Employee, Employee> {

    @Override
    public Employee process(Employee item) throws Exception {
        log.info("Proceess...{}", item);
        item.setJobTitle(item.getJobTitle().toUpperCase());
        return item;
    }
}
```

## Rest APIからSpring Batchを実行する



## 参考サイト
[【ゼロからスタート】Spring Batchで始めるJavaバッチ開発入門トレーニング
](https://www.udemy.com/course/spring-batchjava/learn/lecture/43564712#overview)