Java EE 7 Batch Processing

Пакетная обработка Java EE 7

1. Вступление

Представьте, что мы должны были вручную выполнить такие задачи, как обработка платежных ведомостей, расчет процентов и создание счетов. Это стало бы довольно скучным, подверженным ошибкам и бесконечным списком ручных задач!

В этом руководстве мы рассмотрим пакетную обработку Java (JSR 352), часть платформы Jakarta EE, а также отличную спецификацию для автоматизации подобных задач. It offers application developers a model for developing robust batch processing systems so that they can focus on the business logic.

2. Maven Зависимости

Поскольку JSR 352 - это всего лишь спецификация, нам нужно включитьits API иimplementation, напримерjberet:


    javax.batch
    javax.batch-api
    1.0.1


    org.jberet
    jberet-core
    1.0.2.Final


    org.jberet
    jberet-support
    1.0.2.Final


    org.jberet
    jberet-se
    1.0.2.Final

Мы также добавим базу данных в памяти, чтобы мы могли рассмотреть более реалистичные сценарии.

3. Ключевые идеи

JSR 352 вводит несколько концепций, которые мы можем посмотреть следующим образом:

image

Давайте сначала определим каждую деталь:

  • Начиная слева, у нас естьJobOperator. Этоmanages all aspects of job processing such as starting, stopping, and restarting

  • Далее у нас естьJob. Работа - это логическая коллекция шагов; заключает в себе весь пакетный процесс

  • Задание будет содержать от 1 до nSteps. Каждый шаг - это независимая, последовательная единица работы. Шаг состоит изreading input,processing на входе иwriting output.

  • И последнее, но не менее важное: у нас естьJobRepository w, в котором хранится текущая информация о заданиях. Это помогает отслеживать рабочие места, их состояние и результаты их выполнения

В шагах немного больше деталей, поэтому давайте рассмотрим их дальше. Сначала мы рассмотрим шагиChunk, а затемBatchlets.

4. Создание чанка

Как было сказано ранее, фрагмент - это своего рода шаг.. Мы часто будем использовать фрагмент для выражения операции, которая выполняется снова и снова, скажем, над набором элементов. Это что-то вроде промежуточных операций из Java Streams.

При описании чанка нам необходимо указать, откуда брать элементы, как их обрабатывать и куда отправлять позже.

4.1. Чтение предметов

Чтобы читать элементы, нам нужно реализоватьItemReader.

В этом случае мы создадим считыватель, который будет просто выводить числа от 1 до 10:

@Named
public class SimpleChunkItemReader extends AbstractItemReader {
    private Integer[] tokens;
    private Integer count;

    @Inject
    JobContext jobContext;

    @Override
    public Integer readItem() throws Exception {
        if (count >= tokens.length) {
            return null;
        }

        jobContext.setTransientUserData(count);
        return tokens[count++];
    }

    @Override
    public void open(Serializable checkpoint) throws Exception {
        tokens = new Integer[] { 1,2,3,4,5,6,7,8,9,10 };
        count = 0;
    }
}

Теперь мы просто читаем внутреннее состояние класса. Но, конечно,readItem could pull from a database из файловой системы или другого внешнего источника.

Обратите внимание, что мы сохраняем часть этого внутреннего состояния с помощьюJobContext#setTransientUserData(), который пригодится позже.

Also, note the checkpoint parameter. Мы тоже возьмем это снова.

4.2. Обработка предметов

Конечно, причина, по которой мы разбиваемся на части, состоит в том, что мы хотим выполнить какую-то операцию с нашими элементами!

Каждый раз, когда мы возвращаемnull от обработчика элементов, мы удаляем этот элемент из пакета.

Итак, допустим, здесь мы хотим оставить только четные числа. Мы можем использоватьItemProcessor, который отклоняет нечетные, возвращаяnull:

@Named
public class SimpleChunkItemProcessor implements ItemProcessor {
    @Override
    public Integer processItem(Object t) {
        Integer item = (Integer) t;
        return item % 2 == 0 ? item : null;
    }
}

processItem будет вызываться один раз для каждого элемента, который испускает нашItemReader.

4.3. Написание предметов

Наконец, задание вызоветItemWriter, чтобы мы могли написать наши преобразованные элементы:

@Named
public class SimpleChunkWriter extends AbstractItemWriter {
    List processed = new ArrayList<>();
    @Override
    public void writeItems(List items) throws Exception {
        items.stream().map(Integer.class::cast).forEach(processed::add);
    }
}


How long is items? Через мгновение мы определим размер блока, который будет определять размер списка, отправляемого вwriteItems.

4.4. Определение куска в работе

Теперь мы собрали все это в XML-файл, используя JSL или Job Specification Language. Обратите внимание, что мы перечислим наш читатель, процессор, блокировку, а также размер блока:


    
        
            
            
            
        
    

The chunk size is how often progress in the chunk is committed to the job repository, что важно для гарантии завершения, если часть системы выйдет из строя.

Нам нужно будет поместить этот файл вMETA-INF/batch-jobs для файлов.jar s и вWEB-INF/classes/META-INF/batch-jobs для файлов.war.

Мы присвоили нашей задаче id“simpleChunk”, , поэтому давайте попробуем это в модульном тесте.

Now, jobs are executed asynchronously, which makes them tricky to test. В этом примере не забудьте проверить наши опросыBatchTestHelper which и дождаться завершения задания:

@Test
public void givenChunk_thenBatch_completesWithSuccess() throws Exception {
    JobOperator jobOperator = BatchRuntime.getJobOperator();
    Long executionId = jobOperator.start("simpleChunk", new Properties());
    JobExecution jobExecution = jobOperator.getJobExecution(executionId);
    jobExecution = BatchTestHelper.keepTestAlive(jobExecution);
    assertEquals(jobExecution.getBatchStatus(), BatchStatus.COMPLETED);
}

Вот что такое куски. Теперь давайте посмотрим на пакеты.

5. Создание Пакета

Не все четко вписывается в итеративную модель. Например, у нас может быть задача, которую нам просто нужноinvoke once, run to completion, and return an exit status.

Контракт на пакет довольно прост:

@Named
public class SimpleBatchLet extends AbstractBatchlet {

    @Override
    public String process() throws Exception {
        return BatchStatus.COMPLETED.toString();
    }
}

Как и JSL:


    
        
    

И мы можем проверить это, используя тот же подход, что и раньше:

@Test
public void givenBatchlet_thenBatch_completeWithSuccess() throws Exception {
    JobOperator jobOperator = BatchRuntime.getJobOperator();
    Long executionId = jobOperator.start("simpleBatchLet", new Properties());
    JobExecution jobExecution = jobOperator.getJobExecution(executionId);
    jobExecution = BatchTestHelper.keepTestAlive(jobExecution);
    assertEquals(jobExecution.getBatchStatus(), BatchStatus.COMPLETED);
}

Итак, мы рассмотрели несколько различных способов реализации шагов.

Теперь давайте посмотрим на механизмы дляmarking and guaranteeing progress.

6. Пользовательская контрольная точка

Неудачи обязательно произойдут в середине работы. Should we just start over the whole thing, or can we somehow start where we left off?с

Как следует из названия,checkpoints помогает нам периодически устанавливать закладку в случае сбоя.

By default, the end of chunk processing is a natural checkpoint.

Однако мы можем настроить его с помощью нашего собственногоCheckpointAlgorithm:

@Named
public class CustomCheckPoint extends AbstractCheckpointAlgorithm {

    @Inject
    JobContext jobContext;

    @Override
    public boolean isReadyToCheckpoint() throws Exception {
        int counterRead = (Integer) jobContext.getTransientUserData();
        return counterRead % 5 == 0;
    }
}

Помните счет, который мы поместили в переходные данные ранее? Здесьwe can pull it out with JobContext#getTransientUserData to указывает, что мы хотим зафиксировать каждое 5-е обработанное число.

Без этого фиксация произошла бы в конце каждого чанка, или в нашем случае, каждого 3-го числа.

And then, we match that up with the checkout-algorithm directive in our XML underneath our chunk:


    
        
            
            
            
            
        
    

Давайте протестируем код, снова отметив, что некоторые стандартные шаги скрыты вBatchTestHelper:

@Test
public void givenChunk_whenCustomCheckPoint_thenCommitCountIsThree() throws Exception {
    // ... start job and wait for completion

    jobOperator.getStepExecutions(executionId)
      .stream()
      .map(BatchTestHelper::getCommitCount)
      .forEach(count -> assertEquals(3L, count.longValue()));
    assertEquals(jobExecution.getBatchStatus(), BatchStatus.COMPLETED);
}

Таким образом, мы могли бы ожидать количество коммитов 2, так как у нас есть десять элементов и мы настроили коммиты на каждый 5-й элемент. Ноthe framework does one more final read commit at the end, чтобы убедиться, что все было обработано, что подводит нас к 3.

Далее давайте посмотрим, как обрабатывать ошибки.

7. Обработка исключений

По умолчаниюthe job operator will mark our job as FAILED in case of an exception.

Давайте изменим программу чтения элементов, чтобы убедиться, что она не работает:

@Override
public Integer readItem() throws Exception {
    if (tokens.hasMoreTokens()) {
        String tempTokenize = tokens.nextToken();
        throw new RuntimeException();
    }
    return null;
}

А потом тест:

@Test
public void whenChunkError_thenBatch_CompletesWithFailed() throws Exception {
    // ... start job and wait for completion
    assertEquals(jobExecution.getBatchStatus(), BatchStatus.FAILED);
}

Но мы можем изменить это поведение по умолчанию несколькими способами:

  • [.s1]#skip-limit # указывает количество исключений, которые этот шаг проигнорирует перед ошибкой.

  • retry-limit  указывает, сколько раз оператор задания должен повторить шаг перед неудачей.

  • [.s1]#skippable-exception-class # указывает набор исключений, которые будут игнорироваться при обработке фрагментов.

Итак, мы можем отредактировать нашу работу, чтобы она игнорировалаRuntimeException, а также некоторые другие, просто для иллюстрации:


    
        
            
            
            
            
                
                
            
            
                
                
            
        
    

И теперь наш код пройдет:

@Test
public void givenChunkError_thenErrorSkipped_CompletesWithSuccess() throws Exception {
   // ... start job and wait for completion
   jobOperator.getStepExecutions(executionId).stream()
     .map(BatchTestHelper::getProcessSkipCount)
     .forEach(skipCount -> assertEquals(1L, skipCount.longValue()));
   assertEquals(jobExecution.getBatchStatus(), BatchStatus.COMPLETED);
}

8. Выполнение нескольких шагов

Ранее мы упоминали, что у задания может быть любое количество шагов, так что давайте сейчас это посмотрим.

8.1. Увольнение на следующий шаг

По умолчаниюeach step is the last step in the job.

Чтобы выполнить следующий шаг в рамках пакетного задания, нам нужно явно указать, используя атрибутnext в определении шага:


    
        
            
            
            
        
    
    
        
    

Если мы забудем этот атрибут, то следующий шаг в последовательности не будет выполнен.

И мы можем увидеть, как это выглядит в API:

@Test
public void givenTwoSteps_thenBatch_CompleteWithSuccess() throws Exception {
    // ... start job and wait for completion
    assertEquals(2 , jobOperator.getStepExecutions(executionId).size());
    assertEquals(jobExecution.getBatchStatus(), BatchStatus.COMPLETED);
}

8.2. Потоки

Последовательность шагов также может быть заключена вflow. When the flow is finished, it is the entire flow that transitions to the execution element. Кроме того, элементы внутри потока не могут переходить к элементам вне потока.

Мы можем, скажем, выполнить два шага внутри потока, а затем выполнить переход этого потока к изолированному шагу:


    
        
            
            
        
        
        
    
    
        
    
    
    
     
    

И мы все еще можем видеть выполнение каждого шага независимо:

@Test
public void givenFlow_thenBatch_CompleteWithSuccess() throws Exception {
    // ... start job and wait for completion

    assertEquals(3, jobOperator.getStepExecutions(executionId).size());
    assertEquals(jobExecution.getBatchStatus(), BatchStatus.COMPLETED);
}

8.3. Решения

У нас также есть поддержка if / else в видеdecisions. Decisions providea customized way of determining a sequence among steps, flows, and splits.

Как и шаги, он работает с элементами перехода, такими какnext, которые могут направлять или прекращать выполнение задания.

Давайте посмотрим, как можно настроить задание:


     
     
     
     
        
        
     
     
    
     
     
    
     

Любой элементdecision должен быть настроен с классом, реализующимDecider. Его задача - вернуть решение в видеString.

Каждыйnext внутриdecision похож на утверждениеcase in aswitch .

8.4. Разделяет

Splits удобны, так как позволяют нам выполнять потоки одновременно:


   
      
      
              
           
      
      
          
              
      
      
   
   
      
   

Конечно,this means that the order isn’t guaranteed.

Подтверждаем, что они все еще бегут. The flow steps will be performed in an arbitrary order, but the isolated step will always be last:с

@Test
public void givenSplit_thenBatch_CompletesWithSuccess() throws Exception {
    // ... start job and wait for completion
    List stepExecutions = jobOperator.getStepExecutions(executionId);

    assertEquals(3, stepExecutions.size());
    assertEquals("splitJobSequenceStep3", stepExecutions.get(2).getStepName());
    assertEquals(jobExecution.getBatchStatus(), BatchStatus.COMPLETED);
}

9. Разбиение задания

Мы также можем использовать пакетные свойства в нашем Java-коде, которые были определены в нашей работе.

They can be scoped at three levels – the job, the step, and the batch-artifact.

Давайте посмотрим на несколько примеров того, как они потребляли.

Когда мы хотим использовать свойства на уровне работы:

@Inject
JobContext jobContext;
...
jobProperties = jobContext.getProperties();
...

Это также можно использовать на уровне шага:

@Inject
StepContext stepContext;
...
stepProperties = stepContext.getProperties();
...

Когда мы хотим использовать свойства на уровне пакетного артефакта:

@Inject
@BatchProperty(name = "name")
private String nameString;

Это пригодится с разделами.

Видите, с помощью разбиений мы можем запускать потоки одновременно. But we can also partition a step into sets of items or set separate inputs, allowing us another way to split up the work across multiple threads.с

Чтобы понять сегмент работы, который должен выполнять каждый раздел, мы можем комбинировать свойства с разделами:


    
        
    
    
        
            
        
    
        
        
        
    
    
        
        
            
        
        
            
        
        
    
    

10. Остановить и перезапустить

Теперь это определение рабочих мест. А теперь поговорим об управлении ими.

В наших модульных тестах мы уже видели, что можем получить экземплярJobOperator  изBatchRuntime:

JobOperator jobOperator = BatchRuntime.getJobOperator();

И тогда мы можем начать работу:

Long executionId = jobOperator.start("simpleBatchlet", new Properties());

Тем не менее, мы также можем остановить работу:

jobOperator.stop(executionId);

И, наконец, мы можем перезапустить работу:

executionId = jobOperator.restart(executionId, new Properties());

Давайте посмотрим, как остановить выполняющееся задание:

@Test
public void givenBatchLetStarted_whenStopped_thenBatchStopped() throws Exception {
    JobOperator jobOperator = BatchRuntime.getJobOperator();
    Long executionId = jobOperator.start("simpleBatchLet", new Properties());
    JobExecution jobExecution = jobOperator.getJobExecution(executionId);
    jobOperator.stop(executionId);
    jobExecution = BatchTestHelper.keepTestStopped(jobExecution);
    assertEquals(jobExecution.getBatchStatus(), BatchStatus.STOPPED);
}

И если пакет равенSTOPPED, то мы можем перезапустить его:

@Test
public void givenBatchLetStopped_whenRestarted_thenBatchCompletesSuccess() {
    // ... start and stop the job

    assertEquals(jobExecution.getBatchStatus(), BatchStatus.STOPPED);
    executionId = jobOperator.restart(jobExecution.getExecutionId(), new Properties());
    jobExecution = BatchTestHelper.keepTestAlive(jobOperator.getJobExecution(executionId));

    assertEquals(jobExecution.getBatchStatus(), BatchStatus.COMPLETED);
}

11. Получение вакансий

Когда отправляется пакетное задание, тогдаthe batch runtime creates an instance of JobExecution to track it.

Чтобы получитьJobExecution для идентификатора выполнения, мы можем использовать методJobOperator#getJobExecution(executionId).

И,StepExecution provides helpful information for tracking a step’s execution.

Чтобы получитьStepExecution для идентификатора выполнения, мы можем использовать методJobOperator#getStepExecutions(executionId).

Из этого мы можем получитьseveral metrics о шаге черезStepExecution#getMetrics:

@Test
public void givenChunk_whenJobStarts_thenStepsHaveMetrics() throws Exception {
    // ... start job and wait for completion
    assertTrue(jobOperator.getJobNames().contains("simpleChunk"));
    assertTrue(jobOperator.getParameters(executionId).isEmpty());
    StepExecution stepExecution = jobOperator.getStepExecutions(executionId).get(0);
    Map metricTest = BatchTestHelper.getMetricsMap(stepExecution.getMetrics());
    assertEquals(10L, metricTest.get(Metric.MetricType.READ_COUNT).longValue());
    assertEquals(5L, metricTest.get(Metric.MetricType.FILTER_COUNT).longValue());
    assertEquals(4L, metricTest.get(Metric.MetricType.COMMIT_COUNT).longValue());
    assertEquals(5L, metricTest.get(Metric.MetricType.WRITE_COUNT).longValue());
    // ... and many more!
}

12. Недостатки

JSR 352 является мощным, хотя в некоторых областях ему не хватает:

  • Кажется, не хватает читателей и писателей, которые могут обрабатывать другие форматы, такие как JSON

  • Нет поддержки дженериков

  • Разбиение поддерживает только один шаг

  • API не предлагает ничего для поддержки планирования (хотя J2EE имеет отдельный модуль планирования)

  • Из-за асинхронного характера тестирование может быть проблемой

  • API довольно многословен

13. Заключение

В этой статье мы рассмотрели JSR 352 и узнали о порциях, пакетах, разбиениях, потоках и многом другом. Тем не менее, мы почти не коснулись поверхности.

Как всегда, демонстрационный код можно найти вover on GitHub.