Статьи

Обзор пакетной обработки в Java EE 7.0

Этот пост Махеш Каннан появляется через  Oracle .

Пакетная обработка используется во многих отраслях промышленности для задач, начиная от расчета заработной платы; генерация выписок; задания на конец дня, такие как расчет процентов и ETL (извлечение, загрузка и преобразование) в хранилище данных; и многое другое. Как правило, пакетная обработка является объемно-ориентированной, неинтерактивной и длительной — и может требовать больших объемов данных или вычислений. Пакетные задания могут выполняться по расписанию или инициироваться по требованию. Кроме того, поскольку пакетные задания, как правило, являются длительными, указание и перезапуск являются общими функциями, встречающимися в пакетных заданиях.

JSR 352 (Пакетная обработка для платформы Java), часть недавно представленной платформы Java EE 7, определяет модель программирования для пакетных приложений, а также среду выполнения для запуска и управления пакетными заданиями. В этой статье рассматриваются некоторые ключевые концепции, включая основные функции, обзор выбранных API-интерфейсов, структуру языка планирования заданий и образец пакетного приложения. В статье также описывается, как запускать пакетные приложения с помощью GlassFish Server Open Source Edition 4.0.

Архитектура пакетной обработки

В этом разделе и на рисунке 1 описаны основные компоненты архитектуры пакетной обработки.

фигура 1

фигура 1

  • Работа  инкапсулирует весь процесс пакетной обработки. Задание содержит один или несколько шагов. Задание составляется с использованием языка спецификации заданий (JSL), который определяет последовательность, в которой должны выполняться шаги. В JSR 352 JSL указывается в XML-файле, который называется XML-файлом задания. Короче говоря, задание (с JSR 352) — это, по сути, контейнер для шагов.
  • Шаг  является объектом домена , который инкапсулирует независимый, последовательный этап работы. Шаг содержит всю необходимую логику и данные для выполнения фактической обработки. Спецификация пакета намеренно оставляет определение шага расплывчатым, потому что содержание шага является чисто специфическим для приложения и может быть настолько сложным или простым, насколько того пожелает разработчик. Есть два вида шагов: чанк и пакетный.
    • Кусок шаг -стиль содержит ровно один  ItemReader, один  ItemProcessorи один  ItemWriter. В этом шаблоне  ItemReaderвыполняется чтение по одному элементу за раз,  ItemProcessor обработка элемента на основе бизнес-логики (например, «вычисление баланса счета») и передача его во время выполнения пакета для агрегации. После считывания и обработки количества элементов «размера куска» они передаются объекту  ItemWriter, который записывает данные (например, в таблицу базы данных или плоский файл). Затем транзакция совершается.
    • В JSR 352 также определен вид шага, который сам по себе катится, называемый  пакетом . Пакет может использовать все, что нужно для выполнения этого шага, например, отправку электронной почты.
  • JobOperator предоставляет интерфейс для управления всеми аспектами обработки заданий, включая рабочие команды, такие как запуск, перезапуск и остановка, а также команды репозитория заданий, такие как извлечение заданий и выполнение шагов. См. Раздел 10.4 спецификации JSR 352 для более подробной информации о  JobOperator.
  • JobRepository содержит информацию о работах, которые выполняются в настоящее время, и работах, которые выполнялись в прошлом JobOperator предоставляет API для доступа к этому хранилищу. JobRepository может быть реализован с использованием, скажем, базы данных или файловой системы.

Разработка простого приложения для расчета заработной платы

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

SimplePayrollJob Пакетное задание включает в себя чтение входных данных для обработки заработной платы из файла значения , разделенные запятыми (CSV). Каждая строка в файле содержит идентификатор сотрудника и базовый оклад (в месяц) для одного сотрудника. Затем пакетное задание рассчитывает удерживаемый налог, бонус и чистую зарплату. Наконец, задание должно записать обработанные записи заработной платы в таблицу базы данных.

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

Язык спецификации заданий для приложения по обработке заработной платы

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

В JSR 352 JSL в основном определяет порядок, в котором должны выполняться шаги для выполнения задания. JSL достаточно мощный, чтобы разрешить условное выполнение шагов, и он также позволяет каждому шагу иметь свои собственные свойства, слушателей и так далее.

Пакетное приложение может иметь столько JSL, сколько нужно, что позволяет ему запускать столько пакетных заданий, сколько требуется. Например, приложение может иметь два JSL, один для обработки платежной ведомости, а другой для генерации отчетов. Каждый JSL должен иметь уникальное имя и помещаться в  META-INF/batch-jobs каталог. Подкаталоги под  META-INF/batch-jobs игнорируются.

Наш JSL для обработки заработной платы помещен в файл с именем  SimplePayrollJob.xml и выглядит как в листинге 1:

<job id="SimplePayrollJob" xmlns=http://xmlns.jcp.org/xml/ns/javaee version="1.0">
    <step id="process">
        <chunk item-count="2">
            <reader ref="simpleItemReader/>
            <processor ref="simpleItemProcessor/>
            <writer ref="simpleItemWriter/>
        </chunk>
    </step>
</job>

Листинг 1

Наша  SimplePayrollJob пакетная работа имеет только один шаг (называется «процесс»). Это шаг в стиле чанка, имеющий (как требуется для шага в стиле чанка), an  ItemReader, an  ItemProcessorи an  ItemWriter. Реализации для  ItemReaderItemProcessorи ItemWriter для этого шага задаются с помощью  ref атрибута в  <reader><processor>и  <writer> элементах.

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

item-count Атрибут, который определяется как  2 в листинге 1, определяет размер порции порции.

Вот общий обзор того, как выполняются шаги в стиле чанков. Пожалуйста, обратитесь к разделу 11.6 («Обычная обработка фрагментов») спецификации JSR 352 для более подробной информации.

  1. Начать транзакцию.
  2. Вызовите  ItemReader и передайте элемент, прочитанный с  ItemReader помощью  ItemProcessorItemProcessor обрабатывает элемент и возвращает обработанный элемент во время выполнения пакета.
  3. Пакетная среда выполнения повторяет шаг 2  item-count раза и ведет список обработанных элементов.
  4. Пакетная среда выполнения вызывает  ItemWriter запись  item-count количества обработанных элементов.
  5. Если исключения выброшены из  ItemReaderItemProcessorили  ItemWriter, сделке не удается , и шаг отмечен как «FAILED» . Пожалуйста, обратитесь к Разделу 5.2.1.2.1 («Пропуск исключений») в спецификации JSR 352.
  6. Если исключений нет, пакетная среда выполнения получает данные контрольной точки из  ItemReader и  ItemWriter (см. Раздел 2.5 в спецификации JSR 352 для получения более подробной информации). Пакетная среда выполнения фиксирует транзакцию.
  7. Шаги с 1 по 6 повторяются, если  ItemReader имеется больше данных для чтения.

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

Записывая  ItemReaderItemProcessorи ItemWriter

Написание ItemReader

Наш пакет обработки JSL для расчета заработной платы определяет один стиль шага и указывает, что этот шаг использует  ItemReader именованный simpleItemReader. Наше приложение содержит реализацию  ItemReader для чтения входных данных CSV. В листинге 2 показан фрагмент нашего  ItemReader:

@Named
public class SimpleItemReader
       extends AbstractItemReader {

       @Inject
       private JobContext jobContext; 
       ...
}

Перечисление 2

Обратите внимание, что класс помечен  @Named аннотацией. Поскольку в  @Named аннотации используется значение по умолчанию, для этого компонента используется имя «Контексты и внедрение зависимостей» (CDI)  simpleItemReader. JSL определяет имя CDI из  ItemReader в <reader> элементе. Это позволяет пакетной среде выполнения (через CDI) создавать наш экземпляр  ItemReader при выполнении шага.

Наш  ItemReader также вводит  JobContextJobContext позволяет артефакту пакета ( ItemReaderв данном случае) считывать значения, которые были переданы во время передачи задания.

Наша платежная ведомость  SimpleItemReader переопределяет  open() метод, чтобы открыть ввод, из которого считываются входные данные заработной платы. Как мы увидим позже, параметр  prevCheckpointInfo не будет нулевым, если задание перезапускается.

В нашем примере  open() метод, показанный в листинге 3, открывает входной файл заработной платы (который был упакован вместе с приложением).

public void open(Serializable prevCheckpointInfo) throws Exception {
        JobOperator jobOperator = BatchRuntime.getJobOperator();
        Properties jobParameters = jobOperator.getParameters(jobContext.getExecutionId());
        String resourceName = (String) jobParameters.get("payrollInputDataFileName");
        inputStream = new FileInputStream(resourceName);        
        br = new BufferedReader(new InputStreamReader(inputStream));

        if (prevCheckpointInfo != null)
            recordNumber = (Integer) prevCheckpointInfo;
        for (int i=1; i<recordNumber; i++) {   //Skip upto recordNumber
            br.readLine();
        } 
       System.out.println("[SimpleItemReader] Opened Payroll file for reading from record number: "              + recordNumber);
    }

Перечисление 3

readItem() Метод в основном считывает одну строку данных из входного файла и определяет , содержит ли строка два целых числа (один для работника ID и один для базовой заработной платы). Если есть два целых числа, он создает и возвращает новый экземпляр  PayrollInputRecord и возвращается во время выполнения пакета (которое затем передается  ItemWriter).

public Object readItem() throws Exception {       
       Object record = null;
       if (line != null) {
            String[] fields = line.split("[, \t\r\n]+");
            PayrollInputRecord payrollInputRecord = new PayrollInputRecord();
            payrollInputRecord.setId(Integer.parseInt(fields[0]));
            payrollInputRecord.setBaseSalary(Integer.parseInt(fields[1]));
            record = payrollInputRecord;
            //Now that we could successfully read, Increment the record number
           recordNumber++;
        }
        return record;
}

Листинг 4

Метод  checkpointInfo() вызывается средой выполнения пакета в конце каждой успешной транзакции фрагмента. Это позволяет Читателю проверить последнюю успешную позицию чтения.

В нашем примере  checkpointInfo() возвращается значение,  recordNumber указывающее количество записей, которые были успешно прочитаны, как показано в листинге 5.

@Override
public Serializable checkpointInfo() throws Exception {
        return recordNumber;
}

Листинг 5

Написание ItemProcessor

Наша  SimpleItemProcessor следует шаблону, подобному шаблону для  SimpleItemReader.

processItem() Метод получает (от периодического выполнения)  PayrollInputRecord. Затем он рассчитывает налог и нетто и возвращает в  PayrollRecord качестве вывода. Обратите внимание, что в листинге 6 тип объекта, возвращаемого объектом,  ItemProcessor может сильно отличаться от типа объекта, от которого он получен  ItemReader.

@Named
public class SimpleItemProcessor
    implements ItemProcessor {

    @Inject
    private JobContext jobContext;

    public Object processItem(Object obj) 
        		throws Exception {
        PayrollInputRecord inputRecord =
                (PayrollInputRecord) obj;
        PayrollRecord payrollRecord = 
                new PayrollRecord();

        int base = inputRecord.getBaseSalary();
        float tax = base * 27 / 100.0f;
        float bonus = base * 15 / 100.0f;

        payrollRecord.setEmpID(inputRecord.getId());
        payrollRecord.setBase(base);
        payrollRecord.setTax(tax);
        payrollRecord.setBonus(bonus);
        payrollRecord.setNet(base + bonus - tax);   
        return payrollRecord;
    } 
}

Листинг 6

Написание ItemWriter

К настоящему времени  SimpleItemWriter должны следовать предсказуемым линиям для вас.

Единственное отличие состоит в том, что он внедряет a,  EntityManager чтобы он мог сохранять  PayrollRecord экземпляры (которые являются сущностями JPA) в базе данных, как показано в листинге 7.

@Named
public class SimpleItemWriter
    extends AbstractItemWriter {

    @PersistenceContext
    EntityManager em;

    public void writeItems(List list) throws Exception {
        for (Object obj : list) {
            System.out.println("PayrollRecord: " + obj);
            em.persist(obj);
        }
    }
    
}

Листинг 7

В  writeItems() метод сохраняется все  PayrollRecord экземпляры в таблице базы данных с помощью JPA. В item-count списке будет не более  записей (размер чанка).

Теперь, когда мы имеем наше JSL,  ItemReaderItemProcessor, и  ItemWriter готовы, давайте посмотрим , как пакетное задание может быть представлено.

Запуск пакетной работы с сервлета

Обратите внимание, что простое присутствие XML-файла задания или других артефактов пакета (например,  ItemReader) не означает, что пакетное задание автоматически запускается при развертывании приложения. Пакетное задание должно быть инициировано явно, скажем, из сервлета или из таймера Enterprise JavaBeans (EJB) или бизнес-метода EJB.

В нашем приложении для расчета заработной платы мы используем сервлет (именованный  PayrollJobSubmitterServlet) для отправки пакетного задания. Сервлет отображает HTML-страницу, которая представляет пользователю форму, содержащую две кнопки. Когда нажимается первая кнопка с именем  Calculate Payroll , сервлет вызывает  startNewBatchJob метод, показанный в листинге 8, который запускает новое пакетное задание.

private long startNewBatchJob()
throws Exception {
        JobOperator jobOperator = BatchRuntime.getJobOperator();
        Properties props = new Properties();
        props.setProperty("payrollInputDataFileName", payrollInputDataFileName);
        return jobOperator.start(JOB_NAME, props);
}

Листинг 8

Первым шагом является получение экземпляра  JobOperator. Это можно сделать, вызвав следующее:

JobOperator jobOperator = BatchRuntime.getJobOperator(); 

Затем сервлет создает  Properties объект и сохраняет в нем имя входного файла. Наконец, новое пакетное задание запускается путем вызова следующего:

jobOperator.start(jobName, properties) 

Это  jobname не что иное, как имя файла JSL XML (без  .xml расширения). properties Параметр служит для передачи каких — либо исходных данных для работы. Properties Объект (содержащее имя входного файла заработной платы) становятся доступным для других пакетных артефактов (например  ItemReaderItemProcessorи так далее) через  JobContext интерфейс.

Пакетная среда выполнения назначает уникальный идентификатор, называемый идентификатором выполнения, чтобы идентифицировать каждое выполнение задания, независимо от того, является ли оно вновь отправленным или перезапущенным заданием. Многие из  JobOperator методов принимают идентификатор выполнения в качестве параметра. Используя идентификатор выполнения, программа может получить текущий (и прошлый) статус выполнения и другую статистику о задании. JobOperator.start() Метод возвращает идентификатор выполнения задания , которое было начато.

Получение сведений о пакетных заданиях

Когда пакетное задание отправлено, пакетная среда выполнения создает экземпляр  JobExecution для его отслеживания. JobExecution имеет методы для получения различных деталей, таких как время начала работы, время завершения работы, состояние завершения работы и так далее. Чтобы получить  JobExecution идентификатор выполнения, вы можете использовать  JobOperator.getJobExecution(executionId) метод. В листинге 9 показано определение  JobExecution:

package javax.batch.runtime;
public interface JobExecution {
    long getExecutionId();
    java.lang.String getJobName();
    javax.batch.runtime.BatchStatus getBatchStatus();
    java.util.Date getStartTime();
    java.util.Date getEndTime();
    java.lang.String getExitStatus();
    java.util.Date getCreateTime();
    java.util.Date getLastUpdatedTime();
    java.util.Properties getJobParameters();
}

Листинг 9

Упаковка заявки

Теперь, когда у нас есть JSL,  ItemReaderItemProcessorItemWriter, и наш сервлет готов, настало время , чтобы упаковать их и получить готовый к развертыванию.

Вы можете развернуть пакетное приложение , как и любой из поддерживаемых архивов Java EE (например,  .war.jarили  .ear). Вы можете связать свои классы артефактов пакета вместе с другими классами Java EE (такими как EJB-компоненты и сервлеты).

Единственное специальное требование — вам нужно поместить JSL вашей работы в  META-INF/batch-jobs каталог для  .jar файлов. Для .war типов архивов поместите JSL вашей работы в  WEB-INF/classes/META-INF/batch-jobs каталог.

Развертывание и запуск примера приложения для расчета заработной платы в GlassFish 4.0

Давайте развернем разработанное нами приложение для расчета заработной платы на сервере приложений GlassFish 4.0. GlassFish 4.0 является эталонной реализацией (RI) для спецификации Java EE 7.0 и также содержит RI для JSR 352. Вы можете найти больше информации о GlassFish 4.0 на  http://glassfish.org  и о Java Batch 1.0 RI на  https://java.net/projects/jbatch/ .

Установка и запуск GlassFish 4.0

Вы можете скачать GlassFish 4.0 с  https://glassfish.java.net/public/downloadsindex.html#top  и затем установить его. Запустите GlassFish 4.0, открыв окно командной строки и выполнив следующую команду:

<GlassFish Install Dir>/bin/asadmin start-domain

Поскольку пример приложения для расчета заработной платы использует базу данных (для записи обработанных данных), нам нужно запустить базу данных, прежде чем мы сможем запустить наше приложение. Вы можете запустить базу данных Apache Derby, выполнив следующую команду:

<GlassFish Install Dir>/bin/asadmin start-database

Компиляция, упаковка и развертывание приложения Payroll

Сначала создайте новый каталог с именем  hello-batch. Затем перейдите в  hello-batch каталог:

cd hello-batch

Чтобы скомпилировать и упаковать, запустите следующую команду, которая создается  hello-batch.war в целевом каталоге:

mvn clean package

Для развертывания  hello-batch.warвыполните следующую команду:

<GlassFish Install Dir>/bin/asadmin deploy target/hello-batch.war

Если вы хотите повторно развернуть приложение, вы можете выполнить следующую команду:

<GlassFish Install Dir>/bin/asadmin deploy -force target/hello-batch.war

Запуск приложения для расчета заработной платы

Развернув  hello-batch.war файл, вы можете запустить приложение, открыв его  http://localhost:8080/hello-batch/PayrollJobSubmitterServlet из браузера. Доступ к этому URL должен отображаться на экране, показанном на рисунке 2.

фигура 2

фигура 2

Нажмите кнопку  Calculate Payroll  , и вы увидите новую запись в таблице, как показано на рисунке 3.

Рисунок 3

Рисунок 3

Нажмите   кнопку Refresh , и вы должны увидеть столбцы Exit Status и End Time, обновленные для последней работы (см. Рисунок 4). В столбце «Состояние выхода» показано, было ли задание выполнено неудачно или успешно выполнено. Так как у нас  SimplePayrollJob нет ошибок (по крайней мере, пока!), Статус выхода отображает «ЗАВЕРШЕНО».

Рисунок 4

Рисунок 4

Нажмите кнопки «  Рассчитать зарплату»  и «  Обновить»  еще несколько раз. Обратите внимание, что при каждом запуске задания новый идентификатор выполнения (и идентификатор экземпляра) присваивается заданию, как показано на рисунке 5.

Рисунок 5

Рисунок 5

Перезапуск неудачных заданий

До сих пор мы запускали пакетные задания с использованием этого  jobOperator.start() метода. Допустим, наш входной файл заработной платы имеет некоторые ошибки. Либо тот,  ItemReader либо  ItemProcessor может обнаружить недействительные записи и не выполнить текущий шаг и задание. Администратор или конечный пользователь может исправить ошибку и может перезапустить пакетное задание. Этот подход запуска нового задания, которое начинается с начала после восстановления после ошибок, может не масштабироваться, если объем обрабатываемых данных велик. JobOperator предоставляет другой метод, призванный restart() решить именно эту проблему.

Краткий обзор  JobInstance и JobExecution

Ранее мы видели, что работа по сути является контейнером для шагов. Когда задание запускается, оно должно отслеживаться, поэтому пакетная среда выполнения создает JobInstance. А  JobInstance относится к понятию логического прогона. В нашем примере у нас есть  PayrollJob и, если  PayrollJob запуск проводится каждый месяц, будет январь-2013  JobInstance и еще один февраль-2013  JobInstance, и так далее.

Если обработка расчета заработной платы за январь-2013 не удалась, ее необходимо перезапустить (после предположительного исправления ошибки), но она все еще выполняется в январе-2013, поскольку она все еще обрабатывает записи за январь-2013.

А  JobExecution относится к концепции одной попытки запустить работу. Каждый раз, когда задание запускается или перезапускается, создается новое,  JobExecution которое принадлежит тому же самому  JobInstance. В нашем примере, если январь-2013  JobInstance перезапущен, это все тот же январь-2013, JobInstance но создается новый  JobExecution , принадлежащий тому же  JobInstance.

Таким образом, задание может иметь один или несколько экземпляров,  JobInstance и каждый  JobInstance может иметь один или несколько  JobExecutionэкземпляров. Использование нового  JobInstance означает «начать с начала», а использование существующего  JobInstance обычно означает «начать с того места, где вы остановились».

Возобновление невыполненных работ

Если вы помните, шаг в стиле чанка выполняется в транзакции, в которой  item-count записи читаются, обрабатываются и записываются. После того, как ItemWriter‘ы’  writeItems() вызваны, пакетная среда выполнения вызывает  checkpointInfo() метод для обоих  ItemReader и ItemWriter. Это позволяет как  ItemReader и  ItemWriter в закладки (сохранить) их текущий прогресс. Данные, отмеченные закладкой,  ItemReader могут быть чем угодно, что поможет возобновить чтение. Например, нам  SimpleItemReader нужно сохранить номер строки, до которой он до сих пор успешно прочитал.

Раздел 10.8 спецификации JSR 352 подробно описывает обработку перезапуска.

Давайте на минутку заглянем в файл журнала, где мы  SimpleItemReader выводим некоторые полезные сообщения от   методов open() и checkpoint()Каждое сообщение имеет префикс строки,  [SimpleItemReader] чтобы вы могли быстро идентифицировать сообщения. Файл журнала находится по адресу  <GlassFish install Dir>/domains/domain1/logs/server.log.

В листинге 10 показаны сообщения с префиксом строки  [SimpleItemReader]:

[SimpleItemReader] Opened Payroll File. Will start reading from record number: 0]]
[SimpleItemReader] checkpointInfo() called. Returning current recordNumber: 2]]
[SimpleItemReader] checkpointInfo() called. Returning current recordNumber: 4]]
[SimpleItemReader] checkpointInfo() called. Returning current recordNumber: 6]]
[SimpleItemReader] checkpointInfo() called. Returning current recordNumber: 8]]
[SimpleItemReader] checkpointInfo() called. Returning current recordNumber: 9]]
[SimpleItemReader] close called.]]

Листинг 10

Примечание : вы также можете использовать команду  tail -f server.log | grep SimpleItemReader.

Поскольку в нашем XML-файле задания ( SimplePayrollJob.xml) в  качестве размера фрагмента указано значение  2 for  item-count, пакетная среда выполнения вызывает checkpointInfo() наши  ItemReader каждые две записи. Пакетная среда выполнения сохраняет эту информацию контрольной точки в  JobRepository. Таким образом, если во время обработки чанка происходит ошибка, пакетное приложение должно быть в состоянии возобновить работу с последней успешной контрольной точки.

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

If you look at our servlet’s output, which is located under <GlassFish install Dir>/domains/domain1/applications/hello-batch/WEB-INF/classes/payroll-data/payroll-data.csv, you see that it displays the location of the input file from where CSV data is read for our payroll application. Listing 11 shows the content of the file:

1, 8100
2, 8200
3, 8300
4, 8400
5, 8500
6, 8600
7, 8700
8, 8800
9, 8900

Listing 11

Open your favorite editor and introduce an error. For example, let’s say we add a few characters to the salary field on the eighth record, as shown in Listing 12:

1, 8100
2, 8200
3, 8300
4, 8400
5, 8500
6, 8600
7, 8700
8, abc8800
9, 8900

Listing 12

Save the file and quit the editor. Go back to your browser and click the Calculate Payroll button followed by the Refresh button. You would see that the recently submitted job failed, as shown in Figure 6. (Look at the Exit Status column.)

Рисунок 6

Figure 6

You will also notice that a Restart button appears next to the execution ID of the job that just failed. If you click Refresh, the job will fail (because we haven’t fixed the issue yet). Figure 7 shows what is displayed after a few clicks of the Refresh button.

Рисунок 7

Figure 7

If you look into the GlassFish server log (located under <GlassFish install Dir>/domains/domain1/logs/server.log), you will see an exception, as shown in Listing 13:

Caught exception executing step: com.ibm.jbatch.container.exception.BatchContainerRuntimeException: 
Failure in Read-Process-Write Loop
...
...
Caused by: java.lang.NumberFormatException: For input string: "abc8800"
        at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
        at java.lang.Integer.parseInt(Integer.java:492)
        at java.lang.Integer.parseInt(Integer.java:527)
        at com.oracle.javaee7.samples.batch.hello.SimpleItemReader.readItem(SimpleItemReader.java:100)

Listing 13

You should also notice that when you click the Restart button, a new job execution is created but its job instance ID remains the same. When you click the Refresh button, our PayrollJobSubmitter servlet calls a method named restartBatchJob(), which is shown in Listing 14:

private long restartBatchJob(long lastExecutionId)
        		throws Exception {
        JobOperator jobOperator = BatchRuntime.getJobOperator();
        Properties props = new Properties();
        props.setProperty("payrollInputDataFileName", payrollInputDataFileName);

        return jobOperator.restart(lastExecutionId, props);
}

Listing 14

The key line in Listing 14 is the call to JobOperator‘s restart() method. This method takes a Properties object just like start(), but instead of passing a job XML file name, it passes the execution ID of the most recently failed job. Using the most recently failed job’s execution ID, the batch runtime can retrieve the previous execution’s last successful checkpoint. The retrieved checkpoint data is passed to the open() method of our SimpleItemReader (and ItemWriter) to enable them to resume reading (and writing) from the last successful checkpoint.

While ensuring that your browser shows the page with a Restart button, edit the file again and remove the extraneous characters from the eighth record. Then click the Restart and Refresh buttons. The latest execution should display a COMPLETED status, as shown in Figure 8.

Рисунок 8

Figure 8

It is time to look into the log file to understand what just happened. Again, looking for messages prefixed with SimpleItemReader, Listing 15 shows what you might see:

[SimpleItemReader] Opened Payroll File. Will start reading from record number: 7]] 
[SimpleItemReader] checkpointInfo() called. Returning current recordNumber: 9]]
[SimpleItemReader] checkpointInfo() called. Returning current recordNumber: 10]]
[SimpleItemReader] close called.]]

Listing 15

As you can see, our SimpleItemReader‘s open() method was called with the previous checkpoint value (which was record number 7) allowing our SimpleItemReader to skip the first six records and resume reading from the seventh record.

Viewing Batch Jobs Using the GlassFish 4.0 Admin Console

You can view the list of all batch jobs in the JobRepository. Fire up a browser window and go to localhost:4848. Then click server (Admin Server) in the left panel, as shown in Figure 9.

Рисунок 9

Figure 9

You can click the Batch tab, which should list all the batch jobs submitted to this GlassFish server. Note that the JobRepository is implemented using a database and, hence, the job details survive GlassFish 4.0 server restarts. Figure 10 shows all the batch jobs in theJobRepository.

Рисунок 10

Figure 10

You can also click one of the IDs listed under Execution IDs. For example, clicking 293 reveals details about just that execution:

Рисунок 11

Figure 11

More details about the execution can be obtained by clicking the Execution Steps tab on the top.

Рисунок 12

Figure 12

Look at the statistics provided by this page. It shows how many reads, writes, and commits were performed during this execution.

Viewing Batch Jobs Using the GlassFish 4.0 CLI

You can also view the details about jobs running in the GlassFish 4.0 server by using the command-line interface (CLI).

To view the list of batch jobs, open a command window and run the following command:

asadmin list-batch-jobs -l

You should see output similar to Figure 13:

Рисунок 13

Figure 13

To view the list of batch JobExecutions, you can run this command:

asadmin list-batch-job-executions -l 

You should see output similar to Figure 14:

Рисунок 14

Figure 14

The command lists the completion status of each execution and also the job parameters passed to each execution.

Finally, in order to see details about each step in a JobExecution, you could use the following command:

asadmin list-batch-job-steps -l 

You should see output similar to Figure 15:

Рисунок 15

Figure 15

Take note of the STEPMETRICS column. It tells how many times ItemReader and ItemWriter were called and also how many commits and rollbacks were done. These are extremely valuable metrics.

The CLI output must match the Admin Console view because they both query the same JobRepository.

You can use asadmin help <command-name> to get more details about the CLI commands.

Conclusion

In this article, we saw how to write, package, and run simple batch applications that use chunk-style steps. We also saw how the checkpoint feature of the batch runtime allows for the easy restart of failed batch jobs. Yet, we have barely scratched the surface of JSR 352. With the full set of Java EE components and features at your disposal, including servlets, EJB beans, CDI beans, EJB automatic timers, and so on, feature-rich batch applications can be written fairly easily.

This article also covered (briefly) the GlassFish 4.0 Admin Console and CLI support for querying the batch JobRepository. Both the Admin Console and the CLI provide valuable details about jobs and steps that can be used to detect potential bottlenecks.

JSR 352 supports many more exciting features such as batchlets, splits, flows, and custom checkpoints, which will be covered in future articles.

See Also

JSR 352

About the Author

Mahesh Kannan is a senior software engineer with Oracle’s Cloud Application Foundation team, and he is the Expert Group Member for the Java Batch JSR. Due to his extensive experience with application servers, containers, and distributed systems, he has served as lead architect and «consultant at large» on many projects that build innovative solutions for Oracle products.