starzware

ITスキル

Java

TIPS
xxxx

JOB: 複数のStepをまとめたもので、バッチ処理全体の流れを定義する単位。
STEP: JOB内で実行される処理の単位。1つのSTEPは、処理ロジックを1つ持ち、chunk型またはtasklet型で実装される。
  
[Chunk(Job-Step)]一定件数ずつ繰り返すバッチ処理に向いた構成。
StepBuilderFactoryで以下を指定する。
 .chunk(件数):1回のトランザクションで処理する件数を指定
 ItemReader: データの読み取り
 ItemProcessor: データの変換・加工(任意)
 ItemWriter: データの書き込み
トランザクション単位: Chunk単位(指定した件数ごと)
(向いている処理)
大量データの分割処理、同じ処理を繰り返すタイプ(CSV→DB登録など)、トランザクション効率の最適化が必要な処理

[Tasklet]一括で処理を行う単純なバッチ処理に向いた構成。
StepBuilderFactoryで以下を指定する。
 .tasklet(): Tasklet実装クラスを指定
Tasklet.execute() 内で処理全体を記述(chunk型のReader/Processor/Writerをすべて一体化)
トランザクション単位: Tasklet全体で1トランザクション
(向いている処理)
単発・シンプルな処理(ファイル削除、フラグ更新など)、前後処理(初期化、後片付け)、API呼び出し、ログ出力など
設計時に気をつけること

(カテゴリ)	(気をつけること)
・再実行設計	冪等性の確保、フラグ・チェックを入れる
・トランザクション	Chunk単位で処理されるので、巻き戻しを意識
・パラメータ設計	JobInstanceが正しく一意になるようにする
・排他制御	多重実行を避ける工夫(ジョブ名やキーによる排他など)
・エラーハンドリング	ログ出力と例外処理、失敗時の処理も設計しておく
・管理テーブル	肥大化に注意。運用ルールを決めておく
・通知・監視	成功/失敗時の通知、ジョブ監視の導入
管理テーブル

BATCH_JOB_INSTANCE	バッチジョブの「インスタンス」(入力パラメータに応じた単位)
BATCH_JOB_EXECUTION	ジョブの1回ごとの実行履歴
BATCH_JOB_EXECUTION_PARAMS	ジョブに渡したパラメータ
BATCH_STEP_EXECUTION	各Stepの実行履歴
BATCH_STEP_EXECUTION_CONTEXT	Stepの実行コンテキストデータ(状態)
BATCH_JOB_EXECUTION_CONTEXT	ジョブ全体の実行コンテキストデータ
BATCH_JOB_INSTANCE	ジョブインスタンスの定義
[Spring Batch]ジョブ実行時にリターンコード(終了コード)

正常終了(COMPLETED) → 0
異常終了(FAILEDなど) → 1以上の値

(参考)
Spring Bootの場合、ExitCodeGeneratorにより終了コードが決定される。
Sring Boot以外(Spring)の場合でも、CommandLineJobRunnerにより、異常終了コードは0以外になる。
構成

  project-root/
  ├── src/
  │   └── main/
  │       ├── java/com/example/batch/
  │       │   ├── config/                   ← Job/Step定義
  │       │   ├── domain/                   ← エンティティやDTO
  │       │   ├── processor/                ← ItemProcessor
  │       │   ├── reader/                   ← ItemReader
  │       │   ├── writer/                   ← ItemWriter
  │       │   ├── runner/                   ← 手動起動系(任意)
  │       │   └── BatchApplication.java     ← メインクラス
  │       └── resources/
  │           ├── application.yaml          ← ★ これが設定の心臓部!
  │           └── logback-spring.xml        ← ログ設定(任意)
  └── pom.xml
PGサンプル(chunk)


// Job => Step
// Step = reader → processor(I→O) → writer

// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
//  JobConfig JobとStepの定義を作成
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
@Configuration
//@RequiredArgsConstructor(byLombok)だと@Autowired(ただしfinal付き)しなくても良いらしい。
public class UserImportJobConfig {

  @Autowired
  private final JobBuilderFactory jobBuilderFactory;
  @Autowired
  private final StepBuilderFactory stepBuilderFactory;

  private final UserImportReader reader;
  private final UserImportProcessor processor;
  private final UserWriter writer;

  @Bean
  public Job userImportJob() {
    return jobBuilderFactory.get("userImportJob")
            .start(userImportStep())
            .build();
  }

  @Bean
  public Step userImportStep() {
    return stepBuilderFactory.get("userImportStep")
            .<UserImport, User>chunk(10)  // 10件ずつCommitする設定
            .reader(reader)
            .processor(processor)
            .writer(writer)
            .build();
  }
}

// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
//  DTO
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
@Data
public class UserImport {
  private Long id;
  private String name;
  private String email;
  private boolean imported;
}

@Data
public class User {
  private Long id;
  private String name;
  private String email;
}

// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
//  Reader
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
@Component
public class UserImportReader implements ItemReader<UserImport> {

  @Autowired
  private final UserImportMapper userImportMapper;
  @Autowired
  private Iterator<UserImport> userIterator;

  @Override
  public UserImport read() {
    if (userIterator == null) {
        List<UserImport> users = userImportMapper.findUnimportedUsers();
        userIterator = users.iterator();
    }
    return userIterator.hasNext() ? userIterator.next() : null;
  }
}

// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
//  Processor Inputデータを変換する役割
//    以下の例ではUserImport型からUser型に変換
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
@Component
public class UserImportProcessor implements ItemProcessor<UserImport, User> {

  @Override
  public User process(UserImport item) {
    User user = new User();
    user.setName(item.getName());
    user.setEmail(item.getEmail());
    return user;
  }
}

// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
//  Writer chunkの指定数のListが渡される
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

@Component
public class UserWriter implements ItemWriter {

  @Autowired
  private final UserMapper userMapper;
  @Autowired
  private final UserImportMapper userImportMapper;

  @Override
  public void write(List<? extends User> users) {
    for (User user : users) {
        // 本登録
        userMapper.insert(user);

        // フラグ更新(処理中から処理済みに更新)
        userImportMapper.markAsImported(user.getEmail());
    }
  }
}

// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
//  Mapper(MyBatis)
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

@Mapper
public interface UserImportMapper {
  List<UserImport> findUnimportedUsers();
  void markAsImported(String email);
}

@Mapper
public interface UserMapper {
  void insert(User user);
}

// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
//  起動クラス
//    SpringApplication.run(...)
//      ApplicationContext起動
//        JobLauncherCommandLineRunner実行
//          @BeanのJOBが起動
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

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

@SpringBootApplication
public class BatchApplication {
    // application.propertiesに以下の設定を起動する
    // spring.batch.job.names=userImportJob
    public static void main(String[] args) {
        SpringApplication.run(BatchApplication.class, args);
    }
}
PGサンプル(tasklet)

// Job => Step
// Step = Tasklet

// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
//  JobConfig JobとStepの定義を作成
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.beans.factory.annotation.Autowired;

@Configuration
public class FileDeleteJobConfig {

    @Autowired
    private JobBuilderFactory jobBuilderFactory;

    @Autowired
    private StepBuilderFactory stepBuilderFactory;

    @Autowired
    private FileDeleteTasklet fileDeleteTasklet;

    @Bean
    public Job fileDeleteJob() {
        return jobBuilderFactory.get("fileDeleteJob")
                .start(fileDeleteStep())
                .build();
    }

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

// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
//  Tasklet
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
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.stereotype.Component;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

@Component
public class FileDeleteTasklet implements Tasklet {

    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
        Path filePath = Paths.get("/tmp/temp.txt");

        if (Files.exists(filePath)) {
            Files.delete(filePath);
            System.out.println("ファイルを削除しました。");
        } else {
            System.out.println("ファイルが存在しません。");
        }

        return RepeatStatus.FINISHED;
    }
}

// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
//  起動クラス
//    SpringApplication.run(...)
//      ApplicationContext起動
//        JobLauncherCommandLineRunner実行
//          @BeanのJOBが起動
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

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

@SpringBootApplication
public class TaskletBatchApplication {
    public static void main(String[] args) {
        // application.propertiesに以下の設定を起動する
        // spring.batch.job.names=fileDeleteJob
        SpringApplication.run(TaskletBatchApplication.class, args);
    }
}
起動チェック

1.@SpringBootApplicationがある
  メインクラスに定義されていること
2.Job が @Bean で定義されている
  JobBuilderFactory.get("jobName") を使って作成
3.application.yaml で spring.batch.job.enabled: true
  省略してもtrue(デフォルト)
4.Job
  Jobが1つの場合:明示的にjob名指定しなくても起動される
  Jobが複数ある場合:--spring.batch.job.names=jobName で明示必要
maven(pom.xml)


<dependencies>
    <!-- Spring Boot Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-batch</artifactId>
    </dependency>

    <!-- JDBC経由でDBアクセスするなら(例:H2 or PostgreSQLなど) -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- Lombok(@RequiredArgsConstructorなど) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <!-- テスト用(任意) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
[リリース時]logback-spring.xmlの場所を指定する

set LOGGING_CONFIG=file:C:\path\to\logback-spring.xml
java -jar my-batch.jar
[リリース時]application.yaml/application.propertiesの場所

設定ファイル(application.yaml/.properties)を探す順番
1.jarと同じディレクトリ
2.jarのあるディレクトリのconfigサブディレクトリ	
3.[--spring.config.location]で指定する