春のバッチ - タスクレット対チャンク

Spring Batch –タスクレットとチャンク

1. 前書き

Spring Batch provides two different ways for implementing a job: using tasklets and chunks

この記事では、簡単な実際の例を使用して、両方のメソッドを構成および実装する方法を学習します。

2. 依存関係

adding the required dependenciesから始めましょう:


    org.springframework.batch
    spring-batch-core
    4.0.0.RELEASE


    org.springframework.batch
    spring-batch-test
    4.0.0.RELEASE
    test

spring-batch-coreおよびspring-batch-testの最新バージョンを入手するには、MavenCentralを参照してください。

3. ユースケース

次の内容のCSVファイルについて考えてみましょう。

Mae Hodges,10/22/1972
Gary Potter,02/22/1953
Betty Wise,02/17/1968
Wayne Rose,04/06/1977
Adam Caldwell,09/27/1995
Lucille Phillips,05/14/1992

first position of each line represents a person’s name and the second position represents his/her date of birth

私たちのユースケースはgenerate another CSV file that contains each person’s name and ageです:

Mae Hodges,45
Gary Potter,64
Betty Wise,49
Wayne Rose,40
Adam Caldwell,22
Lucille Phillips,25

ドメインが明確になったので、先に進み、両方のアプローチを使用してソリューションを構築しましょう。 タスクレットから始めましょう。

4. タスクレットアプローチ

4.1. はじめにとデザイン

タスクレットは、ステップ内で単一のタスクを実行するためのものです。 私たちの仕事は、次々に実行されるいくつかのステップで構成されます。 Each step should perform only one defined task

私たちの仕事は3つのステップから成ります:

  1. 入力CSVファイルから行を読み取ります。

  2. 入力CSVファイルですべての人の年齢を計算します。

  3. 各個人の名前と年齢を新しい出力CSVファイルに書き込みます。

全体像の準備ができたので、ステップごとに1つのクラスを作成しましょう。

LinesReaderは、入力ファイルからのデータの読み取りを担当します。

public class LinesReader implements Tasklet {
    // ...
}

LinesProcessorは、ファイル内のすべての人の年齢を計算します。

public class LinesProcessor implements Tasklet {
    // ...
}

最後に、LinesWriterは、出力ファイルに名前と年齢を書き込む責任があります。

public class LinesWriter implements Tasklet {
    // ...
}

この時点で、all our steps implement Tasklet interface。 これにより、executeメソッドを実装する必要があります。

@Override
public RepeatStatus execute(StepContribution stepContribution,
  ChunkContext chunkContext) throws Exception {
    // ...
}

この方法では、各ステップのロジックを追加します。 そのコードを開始する前に、ジョブを構成しましょう。

4.2. 設定

add some configuration to Spring’s application contextにする必要があります。 前のセクションで作成したクラスの標準Bean宣言を追加したら、ジョブ定義を作成する準備が整います。

@Configuration
@EnableBatchProcessing
public class TaskletsConfig {

    @Autowired
    private JobBuilderFactory jobs;

    @Autowired
    private StepBuilderFactory steps;

    @Bean
    protected Step readLines() {
        return steps
          .get("readLines")
          .tasklet(linesReader())
          .build();
    }

    @Bean
    protected Step processLines() {
        return steps
          .get("processLines")
          .tasklet(linesProcessor())
          .build();
    }

    @Bean
    protected Step writeLines() {
        return steps
          .get("writeLines")
          .tasklet(linesWriter())
          .build();
    }

    @Bean
    public Job job() {
        return jobs
          .get("taskletsJob")
          .start(readLines())
          .next(processLines())
          .next(writeLines())
          .build();
    }

    // ...

}

これは、“taskletsJob”が3つのステップで構成されることを意味します。 最初のもの(readLines)は、BeanlinesReaderで定義されたタスクレットを実行し、次のステップに移動します。processLines. ProcessLinesは、BeanlinesProcessorで定義されたタスクレットを実行します。最後のステップへ:writeLines

ジョブフローが定義され、ロジックを追加する準備が整いました。

4.3. モデルとユーティリティ

CSVファイルの行を操作するので、クラスLine:を作成します。

public class Line implements Serializable {

    private String name;
    private LocalDate dob;
    private Long age;

    // standard constructor, getters, setters and toString implementation

}

LineSerializable.を実装することに注意してください。これは、Lineがステップ間でデータを転送するためのDTOとして機能するためです。 Spring Batchによると、objects that are transferred between steps must be serializable

一方、行の読み書きについて考えることもできます。

そのために、OpenCSVを利用します。


    com.opencsv
    opencsv
    4.1

Maven Centralで最新のOpenCSVバージョンを探します。

OpenCSVが含まれると、we’re also going to create a FileUtils classになります。 CSV行を読み書きするメソッドを提供します:

public class FileUtils {

    public Line readLine() throws Exception {
        if (CSVReader == null)
          initReader();
        String[] line = CSVReader.readNext();
        if (line == null)
          return null;
        return new Line(
          line[0],
          LocalDate.parse(
            line[1],
            DateTimeFormatter.ofPattern("MM/dd/yyyy")));
    }

    public void writeLine(Line line) throws Exception {
        if (CSVWriter == null)
          initWriter();
        String[] lineStr = new String[2];
        lineStr[0] = line.getName();
        lineStr[1] = line
          .getAge()
          .toString();
        CSVWriter.writeNext(lineStr);
    }

    // ...
}

readLineはOpenCSVのreadNextメソッドのラッパーとして機能し、Lineオブジェクトを返すことに注意してください。

同様に、writeLineは、Lineオブジェクトを受信するOpenCSVのwriteNextをラップします。 このクラスの完全な実装は、the GitHub Projectにあります。

この時点で、各ステップの実装から始める準備が整いました。

4.4. LinesReader

先に進んで、LinesReaderクラスを完了しましょう。

public class LinesReader implements Tasklet, StepExecutionListener {

    private final Logger logger = LoggerFactory
      .getLogger(LinesReader.class);

    private List lines;
    private FileUtils fu;

    @Override
    public void beforeStep(StepExecution stepExecution) {
        lines = new ArrayList<>();
        fu = new FileUtils(
          "taskletsvschunks/input/tasklets-vs-chunks.csv");
        logger.debug("Lines Reader initialized.");
    }

    @Override
    public RepeatStatus execute(StepContribution stepContribution,
      ChunkContext chunkContext) throws Exception {
        Line line = fu.readLine();
        while (line != null) {
            lines.add(line);
            logger.debug("Read line: " + line.toString());
            line = fu.readLine();
        }
        return RepeatStatus.FINISHED;
    }

    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        fu.closeReader();
        stepExecution
          .getJobExecution()
          .getExecutionContext()
          .put("lines", this.lines);
        logger.debug("Lines Reader ended.");
        return ExitStatus.COMPLETED;
    }
}

LinesReader’s executeメソッドは、入力ファイルパス上にFileUtilsインスタンスを作成します。 次に、adds lines to a list until there’re no more lines to read

2つの追加メソッドを提供するクラスalso implements StepExecutionListenerbeforeStepafterStep。 これらのメソッドを使用して、executeの実行の前後に物事を初期化して閉じます。

afterStepコードを見ると、結果リスト(lines)がジョブのコンテキストに配置されて次のステップで使用できるようになっている行がわかります。

stepExecution
  .getJobExecution()
  .getExecutionContext()
  .put("lines", this.lines);

この時点で、最初のステップはすでにその責任を果たしています。CSV行をメモリ内のListにロードします。 2番目のステップに移動して、それらを処理しましょう。

4.5. LinesProcessor

LinesProcessor will also implement StepExecutionListener and of course, Tasklet.これは、beforeStepexecute、およびafterStepメソッドも実装することを意味します。

public class LinesProcessor implements Tasklet, StepExecutionListener {

    private Logger logger = LoggerFactory.getLogger(
      LinesProcessor.class);

    private List lines;

    @Override
    public void beforeStep(StepExecution stepExecution) {
        ExecutionContext executionContext = stepExecution
          .getJobExecution()
          .getExecutionContext();
        this.lines = (List) executionContext.get("lines");
        logger.debug("Lines Processor initialized.");
    }

    @Override
    public RepeatStatus execute(StepContribution stepContribution,
      ChunkContext chunkContext) throws Exception {
        for (Line line : lines) {
            long age = ChronoUnit.YEARS.between(
              line.getDob(),
              LocalDate.now());
            logger.debug("Calculated age " + age + " for line " + line.toString());
            line.setAge(age);
        }
        return RepeatStatus.FINISHED;
    }

    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        logger.debug("Lines Processor ended.");
        return ExitStatus.COMPLETED;
    }
}

it loads lines list from the job’s context and calculates the age of each personを理解するのは簡単です。

前の手順と同じオブジェクトで変更が行われるため、コンテキストに別の結果リストを配置する必要はありません。

そして、最後のステップの準備が整いました。

4.6. LinesWriter

LinesWriter‘s task is to go over lines list and write name and age to the output file

public class LinesWriter implements Tasklet, StepExecutionListener {

    private final Logger logger = LoggerFactory
      .getLogger(LinesWriter.class);

    private List lines;
    private FileUtils fu;

    @Override
    public void beforeStep(StepExecution stepExecution) {
        ExecutionContext executionContext = stepExecution
          .getJobExecution()
          .getExecutionContext();
        this.lines = (List) executionContext.get("lines");
        fu = new FileUtils("output.csv");
        logger.debug("Lines Writer initialized.");
    }

    @Override
    public RepeatStatus execute(StepContribution stepContribution,
      ChunkContext chunkContext) throws Exception {
        for (Line line : lines) {
            fu.writeLine(line);
            logger.debug("Wrote line " + line.toString());
        }
        return RepeatStatus.FINISHED;
    }

    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        fu.closeWriter();
        logger.debug("Lines Writer ended.");
        return ExitStatus.COMPLETED;
    }
}

これで、ジョブの実装は完了です。 テストを作成して実行し、結果を確認しましょう。

4.7. ジョブの実行

ジョブを実行するために、テストを作成します。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = TaskletsConfig.class)
public class TaskletsTest {

    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;

    @Test
    public void givenTaskletsJob_whenJobEnds_thenStatusCompleted()
      throws Exception {

        JobExecution jobExecution = jobLauncherTestUtils.launchJob();
        assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus());
    }
}

ContextConfigurationアノテーションは、ジョブ定義を持つSpringコンテキスト構成クラスを指しています。

テストを実行する前に、Beanをいくつか追加する必要があります。

@Bean
public JobLauncherTestUtils jobLauncherTestUtils() {
    return new JobLauncherTestUtils();
}

@Bean
public JobRepository jobRepository() throws Exception {
    MapJobRepositoryFactoryBean factory
      = new MapJobRepositoryFactoryBean();
    factory.setTransactionManager(transactionManager());
    return (JobRepository) factory.getObject();
}

@Bean
public PlatformTransactionManager transactionManager() {
    return new ResourcelessTransactionManager();
}

@Bean
public JobLauncher jobLauncher() throws Exception {
    SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
    jobLauncher.setJobRepository(jobRepository());
    return jobLauncher;
}

すべて準備完了です! さあ、テストを実行してください!

ジョブが終了すると、output.csvに期待されるコンテンツが表示され、ログに実行フローが表示されます。

[main] DEBUG o.b.t.tasklets.LinesReader - Lines Reader initialized.
[main] DEBUG o.b.t.tasklets.LinesReader - Read line: [Mae Hodges,10/22/1972]
[main] DEBUG o.b.t.tasklets.LinesReader - Read line: [Gary Potter,02/22/1953]
[main] DEBUG o.b.t.tasklets.LinesReader - Read line: [Betty Wise,02/17/1968]
[main] DEBUG o.b.t.tasklets.LinesReader - Read line: [Wayne Rose,04/06/1977]
[main] DEBUG o.b.t.tasklets.LinesReader - Read line: [Adam Caldwell,09/27/1995]
[main] DEBUG o.b.t.tasklets.LinesReader - Read line: [Lucille Phillips,05/14/1992]
[main] DEBUG o.b.t.tasklets.LinesReader - Lines Reader ended.
[main] DEBUG o.b.t.tasklets.LinesProcessor - Lines Processor initialized.
[main] DEBUG o.b.t.tasklets.LinesProcessor - Calculated age 45 for line [Mae Hodges,10/22/1972]
[main] DEBUG o.b.t.tasklets.LinesProcessor - Calculated age 64 for line [Gary Potter,02/22/1953]
[main] DEBUG o.b.t.tasklets.LinesProcessor - Calculated age 49 for line [Betty Wise,02/17/1968]
[main] DEBUG o.b.t.tasklets.LinesProcessor - Calculated age 40 for line [Wayne Rose,04/06/1977]
[main] DEBUG o.b.t.tasklets.LinesProcessor - Calculated age 22 for line [Adam Caldwell,09/27/1995]
[main] DEBUG o.b.t.tasklets.LinesProcessor - Calculated age 25 for line [Lucille Phillips,05/14/1992]
[main] DEBUG o.b.t.tasklets.LinesProcessor - Lines Processor ended.
[main] DEBUG o.b.t.tasklets.LinesWriter - Lines Writer initialized.
[main] DEBUG o.b.t.tasklets.LinesWriter - Wrote line [Mae Hodges,10/22/1972,45]
[main] DEBUG o.b.t.tasklets.LinesWriter - Wrote line [Gary Potter,02/22/1953,64]
[main] DEBUG o.b.t.tasklets.LinesWriter - Wrote line [Betty Wise,02/17/1968,49]
[main] DEBUG o.b.t.tasklets.LinesWriter - Wrote line [Wayne Rose,04/06/1977,40]
[main] DEBUG o.b.t.tasklets.LinesWriter - Wrote line [Adam Caldwell,09/27/1995,22]
[main] DEBUG o.b.t.tasklets.LinesWriter - Wrote line [Lucille Phillips,05/14/1992,25]
[main] DEBUG o.b.t.tasklets.LinesWriter - Lines Writer ended.

タスクレットは以上です。 これで、チャンクアプローチに進むことができます。

5. チャンクアプローチ

5.1. はじめにとデザイン

名前が示すように、このアプローチはperforms actions over chunks of dataです。 つまり、すべての行を一度に読み取り、処理、および書き込むのではなく、一度に一定量のレコード(チャンク)を読み取り、処理し、書き込みます。

次に、ファイルにデータがなくなるまでこのサイクルを繰り返します。

その結果、フローはわずかに異なります。

  1. 行がありますが:

    • X行の量に対して行う:

      • 一行読む

      • 1行処理する

    • X行分書き込みます。

したがって、three beans for chunk oriented approachも作成する必要があります。

public class LineReader {
     // ...
}
public class LineProcessor {
    // ...
}
public class LinesWriter {
    // ...
}

実装に移る前に、ジョブを構成しましょう。

5.2. 設定

ジョブ定義も異なって見えます:

@Configuration
@EnableBatchProcessing
public class ChunksConfig {

    @Autowired
    private JobBuilderFactory jobs;

    @Autowired
    private StepBuilderFactory steps;

    @Bean
    public ItemReader itemReader() {
        return new LineReader();
    }

    @Bean
    public ItemProcessor itemProcessor() {
        return new LineProcessor();
    }

    @Bean
    public ItemWriter itemWriter() {
        return new LinesWriter();
    }

    @Bean
    protected Step processLines(ItemReader reader,
      ItemProcessor processor, ItemWriter writer) {
        return steps.get("processLines"). chunk(2)
          .reader(reader)
          .processor(processor)
          .writer(writer)
          .build();
    }

    @Bean
    public Job job() {
        return jobs
          .get("chunksJob")
          .start(processLines(itemReader(), itemProcessor(), itemWriter()))
          .build();
    }

}

この場合、1つのタスクレットのみを実行するステップは1つだけです。

ただし、そのタスクレットdefines a reader, a writer and a processor that will act over chunks of data

commit interval indicates the amount of data to be processed in one chunkに注意してください。 私たちの仕事は一度に2行の読み取り、処理、書き込みを行います。

これで、チャンクロジックを追加する準備が整いました。

5.3. LineReader

LineReaderは、1つのレコードを読み取り、その内容とともにLineインスタンスを返す役割を果たします。

リーダーになるには、our class has to implement ItemReader interface

public class LineReader implements ItemReader {
     @Override
     public Line read() throws Exception {
         Line line = fu.readLine();
         if (line != null)
           logger.debug("Read line: " + line.toString());
         return line;
     }
}

コードは単純で、1行を読み取って返します。 このクラスの最終バージョンには、StepExecutionListenerも実装します。

public class LineReader implements
  ItemReader, StepExecutionListener {

    private final Logger logger = LoggerFactory
      .getLogger(LineReader.class);

    private FileUtils fu;

    @Override
    public void beforeStep(StepExecution stepExecution) {
        fu = new FileUtils("taskletsvschunks/input/tasklets-vs-chunks.csv");
        logger.debug("Line Reader initialized.");
    }

    @Override
    public Line read() throws Exception {
        Line line = fu.readLine();
        if (line != null) logger.debug("Read line: " + line.toString());
        return line;
    }

    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        fu.closeReader();
        logger.debug("Line Reader ended.");
        return ExitStatus.COMPLETED;
    }
}

beforeStepafterStepは、それぞれステップ全体の前後に実行されることに注意してください。

5.4. LineProcessor

LineProcessorは、LineReaderとほぼ同じロジックに従います。

ただし、この場合、we’ll implement ItemProcessor and its method process()

public class LineProcessor implements ItemProcessor {

    private Logger logger = LoggerFactory.getLogger(LineProcessor.class);

    @Override
    public Line process(Line line) throws Exception {
        long age = ChronoUnit.YEARS
          .between(line.getDob(), LocalDate.now());
        logger.debug("Calculated age " + age + " for line " + line.toString());
        line.setAge(age);
        return line;
    }

}

The process() method takes an input line, processes it and returns an output line。 繰り返しになりますが、StepExecutionListener:も実装します

public class LineProcessor implements
  ItemProcessor, StepExecutionListener {

    private Logger logger = LoggerFactory.getLogger(LineProcessor.class);

    @Override
    public void beforeStep(StepExecution stepExecution) {
        logger.debug("Line Processor initialized.");
    }

    @Override
    public Line process(Line line) throws Exception {
        long age = ChronoUnit.YEARS
          .between(line.getDob(), LocalDate.now());
        logger.debug(
          "Calculated age " + age + " for line " + line.toString());
        line.setAge(age);
        return line;
    }

    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        logger.debug("Line Processor ended.");
        return ExitStatus.COMPLETED;
    }
}

5.5. LinesWriter

リーダーやプロセッサーとは異なり、LinesWriter will write an entire chunk of linesは、Lines:Listを受け取るようになっています。

public class LinesWriter implements
  ItemWriter, StepExecutionListener {

    private final Logger logger = LoggerFactory
      .getLogger(LinesWriter.class);

    private FileUtils fu;

    @Override
    public void beforeStep(StepExecution stepExecution) {
        fu = new FileUtils("output.csv");
        logger.debug("Line Writer initialized.");
    }

    @Override
    public void write(List lines) throws Exception {
        for (Line line : lines) {
            fu.writeLine(line);
            logger.debug("Wrote line " + line.toString());
        }
    }

    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        fu.closeWriter();
        logger.debug("Line Writer ended.");
        return ExitStatus.COMPLETED;
    }
}

LinesWriterコードはそれ自体を物語っています。 繰り返しになりますが、私たちは仕事をテストする準備ができています。

5.6. ジョブの実行

タスクレットアプローチ用に作成したものと同じ新しいテストを作成します。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ChunksConfig.class)
public class ChunksTest {

    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;

    @Test
    public void givenChunksJob_whenJobEnds_thenStatusCompleted()
      throws Exception {

        JobExecution jobExecution = jobLauncherTestUtils.launchJob();

        assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus());
    }
}

上記のTaskletsConfigについて説明したようにChunksConfigを構成すると、テストを実行する準備が整います。

ジョブが完了すると、output.csvに再び期待される結果が含まれ、ログにフローが記述されていることがわかります。

[main] DEBUG o.b.t.chunks.LineReader - Line Reader initialized.
[main] DEBUG o.b.t.chunks.LinesWriter - Line Writer initialized.
[main] DEBUG o.b.t.chunks.LineProcessor - Line Processor initialized.
[main] DEBUG o.b.t.chunks.LineReader - Read line: [Mae Hodges,10/22/1972]
[main] DEBUG o.b.t.chunks.LineReader - Read line: [Gary Potter,02/22/1953]
[main] DEBUG o.b.t.chunks.LineProcessor - Calculated age 45 for line [Mae Hodges,10/22/1972]
[main] DEBUG o.b.t.chunks.LineProcessor - Calculated age 64 for line [Gary Potter,02/22/1953]
[main] DEBUG o.b.t.chunks.LinesWriter - Wrote line [Mae Hodges,10/22/1972,45]
[main] DEBUG o.b.t.chunks.LinesWriter - Wrote line [Gary Potter,02/22/1953,64]
[main] DEBUG o.b.t.chunks.LineReader - Read line: [Betty Wise,02/17/1968]
[main] DEBUG o.b.t.chunks.LineReader - Read line: [Wayne Rose,04/06/1977]
[main] DEBUG o.b.t.chunks.LineProcessor - Calculated age 49 for line [Betty Wise,02/17/1968]
[main] DEBUG o.b.t.chunks.LineProcessor - Calculated age 40 for line [Wayne Rose,04/06/1977]
[main] DEBUG o.b.t.chunks.LinesWriter - Wrote line [Betty Wise,02/17/1968,49]
[main] DEBUG o.b.t.chunks.LinesWriter - Wrote line [Wayne Rose,04/06/1977,40]
[main] DEBUG o.b.t.chunks.LineReader - Read line: [Adam Caldwell,09/27/1995]
[main] DEBUG o.b.t.chunks.LineReader - Read line: [Lucille Phillips,05/14/1992]
[main] DEBUG o.b.t.chunks.LineProcessor - Calculated age 22 for line [Adam Caldwell,09/27/1995]
[main] DEBUG o.b.t.chunks.LineProcessor - Calculated age 25 for line [Lucille Phillips,05/14/1992]
[main] DEBUG o.b.t.chunks.LinesWriter - Wrote line [Adam Caldwell,09/27/1995,22]
[main] DEBUG o.b.t.chunks.LinesWriter - Wrote line [Lucille Phillips,05/14/1992,25]
[main] DEBUG o.b.t.chunks.LineProcessor - Line Processor ended.
[main] DEBUG o.b.t.chunks.LinesWriter - Line Writer ended.
[main] DEBUG o.b.t.chunks.LineReader - Line Reader ended.

We have the same result and a different flow。 ログは、このアプローチに従ってジョブがどのように実行されるかを明らかにします。

6. 結論

異なるコンテキストは、いずれかのアプローチの必要性を示します。 While Tasklets feel more natural for ‘one task after the other' scenarios, chunks provide a simple solution to deal with paginated reads or situations where we don’t want to keep a significant amount of data in memory.

この例の完全な実装は、the GitHub projectにあります。