Spring Batch - тасклеты против кусков

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, обратитесь к Maven Central.

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.

Наша работа будет состоять из трех этапов:

  1. Чтение строк из входного файла CSV.

  2. Рассчитайте возраст каждого человека во входном CSV-файле.

  3. Запишите имя и возраст каждого человека в новый выходной файл CSV.

Теперь, когда общая картина готова, давайте создадим по одному классу на шаг.

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” будет состоять из трех шагов. Первый (readLines) выполнит тасклет, определенный в bean-компонентеlinesReader, и перейдет к следующему шагу:processLines. ProcessLines выполнит тасклет, определенный в bean-компонентеlinesProcessor, и пойдет до последнего шага: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

}

Обратите внимание, чтоLine реализуетSerializable.. Это потому, чтоLine будет действовать как DTO для передачи данных между шагами. Согласно Spring Batch,objects that are transferred between steps must be serializable.

С другой стороны, мы можем начать думать о чтении и написании строк.

Для этого мы воспользуемся OpenCSV:


    com.opencsv
    opencsv
    4.1

Найдите последнюю версиюOpenCSV в Maven Central.

После включения OpenCSVwe’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 действует как оболочка для методаreadNext OpenCSV и возвращает объектLine.

Таким же образомwriteLine обертываетwriteNext OpenCSV, получая объектLine. Полную реализацию этого класса можно найти в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.

Наш классalso implements StepExecutionListener, который предоставляет два дополнительных метода:beforeStep иafterStep. Мы будем использовать эти методы для инициализации и закрытия вещей до и после выполненияexecute.

Если мы посмотрим на кодafterStep, то заметим строку, где список результатов (lines) помещается в контекст задания, чтобы сделать его доступным для следующего шага:

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

На данный момент наш первый шаг уже выполнил свои обязанности: загрузить строки CSV вList в памяти. Перейдем ко второму шагу и обработаем их.

4.5. LinesProcessorс

LinesProcessor will also implement StepExecutionListener and of course, Tasklet. Это означает, что он также будет реализовывать методыbeforeStep,execute и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
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 количество строк.

Итак, нам также нужно создать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();
    }

}

В этом случае есть только один шаг, выполняющий только один тасклет.

Однако этот тасклет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. Наша работа будет читать, обрабатывать и писать две строки одновременно.

Теперь мы готовы добавить логику фрагментов!

5.3. LineReaderс

LineReader будет отвечать за чтение одной записи и возврат экземпляра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;
     }
}

Код прост, он просто читает одну строку и возвращает ее. Мы также реализуем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;
    }
}

Следует отметить, чтоbeforeStep иafterStep выполняются до и после всего шага соответственно.

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, так что он получаетList изLines:

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());
    }
}

После настройкиChunksConfig, как описано выше дляTaskletsConfig, мы готовы к запуску теста!

Как только работа будет выполнена, мы увидим, что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.