Java Platform, Enterprise Edition (Java EE) 8
Учебник по Java EE

Назад Вперёд Содержание

Пример phonebilling

Пример приложения phonebilling, расположенный в каталоге tut-install/examples/batch/phonebilling/, демонстрирует, как использовать фреймворк пакетной обработки в Java EE для реализации системы телефонного биллинга. В этом примере приложение обрабатывает файл журнала телефонных звонков и создаёт счёт для каждого клиента.

Здесь рассматриваются следующие темы:

Архитектура phonebilling

Приложение phonebilling состоит из следующих элементов.

  • Файл определения задания (phonebilling.xml), который использует язык спецификации задания (JSL) для определения пакетного задания с двумя шагами фрагмента. Первый шаг читает записи вызовов из файла журнала и связывает их со счётом. Второй шаг вычисляет сумму к оплате и записывает каждый счёт в текстовый файл.

  • Класс Java (CallRecordLogCreator), который создаёт файл журнала для пакетного задания. Это вспомогательный бин, который не содержит никаких значимых для этого примера функций.

  • Две сущности Java Persistence API (JPA) (CallRecord и PhoneBill), которые представляют записи вызовов и счета клиентов. Приложение использует entity manager JPA для хранения объектов этих сущностей в базе данных.

  • Три пакетных артефакта (CallRecordReader, CallRecordProcessor и CallRecordWriter), которые реализуют первый шаг приложения. Этот шаг считывает записи вызовов из файла журнала, связывает их со счётом и сохраняет их в базе данных.

  • Четыре пакетных артефакта (BillReader, BillProcessor, BillWriter и BillPartitionMapper), которые реализуют второй шаг приложения. Этот шаг является разделённым и получает каждый счёт из базы данных, рассчитывает сумму к оплате и записывает её в текстовый файл.

  • Две страницы Facelets (index.xhtml и jobstarted.xhtml), которые предоставляют внешний интерфейс пакетного приложения. Первая страница показывает файл журнала, который будет обработан пакетным заданием, а вторая страница позволяет пользователю проверить статус задания и показывает итоговый счёт для каждого клиента.

  • Managed-бин (JsfBean), доступ к которому осуществляется со страниц Facelets. Компонент отправляет пакетное задание на выполнение, проверяет статус задания и считывает текстовые файлы для каждого счёта.

Файл определения задания

Файл определения задания phonebilling.xml находится в каталоге WEB-INF/classes/META-INF/batch-jobs/. Файл определяет три свойства уровня задания и два шага:


<job id="phonebilling" xmlns="http://xmlns.jcp.org/xml/ns/javaee"
     version="1.0">
    <properties>
        <property name="log_file_name" value="log1.txt"/>
        <property name="airtime_price" value="0.08"/>
        <property name="tax_rate" value="0.07"/>
    </properties>
    <step id="callrecords" next="bills"> ... </step>
    <step id="bills"> ... </step>
</job>

Первый шаг определяется следующим образом:

<step id="callrecords" next="bills">
    <chunk checkpoint-policy="item" item-count="10">
        <reader ref="CallRecordReader"></reader>
        <processor ref="CallRecordProcessor"></processor>
        <writer ref="CallRecordWriter"></writer>
    </chunk>
</step>

Этот шаг является обычным шагом фрагмента, указывающего пакетные артефакты, реализующие каждую фазу шага. Имена пакетных артефактов не являются полностью квалифицированными именами классов, потому что пакетные артефакты являются бинами CDI, аннотированными @Named.

Второй шаг определяется следующим образом:

<step id="bills">
    <chunk checkpoint-policy="item" item-count="2">
        <reader ref="BillReader">
            <properties>                <property name="firstItem" value="#{partitionPlan['firstItem']}"/>                <property name="numItems" value="#{partitionPlan['numItems']}"/>            </properties>        </reader>
        <processor ref="BillProcessor"></processor>
        <writer ref="BillWriter"></writer>
    </chunk>
    <partition>
        <mapper ref="BillPartitionMapper"/>
    </partition>
    <end on="COMPLETED"/>
</step>

Этот шаг является разделённым шагом. План разделения указывается с помощью артефакта BillPartitionMapper вместо использования элемента plan.

Сущности CallRecord и PhoneBill

Сущность CallRecord определяется следующим образом:

@Entity
public class CallRecord implements Serializable {
    @Id @GeneratedValue
    private Long id;
    @Temporal(TemporalType.DATE)
    private Date datetime;
    private String fromNumber;
    private String toNumber;
    private int minutes;
    private int seconds;
    private BigDecimal price;

    public CallRecord() { }

    public CallRecord(String datetime, String from,
            String to, int min, int sec)             throws ParseException { ... }

    public CallRecord(String jsonData) throws ParseException { ... }

    /* ... Геттеры и сеттеры ... */
}

Поле id автогенерируется реализацией JPA для сохранения и извлечения объектов CallRecord в базу данных и из неё.

Второй конструктор создаёт объект CallRecord из записи данных JSON в файле журнала, используя API обработки JSON. Записи в журнале выглядят следующим образом:

{"datetime":"03/01/2013 04:03","from":"555-0101",
"to":"555-0114","length":"03:39"}

Сущность PhoneBill определяется следующим образом:

@Entity
public class PhoneBill implements Serializable {
    @Id
    private String phoneNumber;
    @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST)
    @OrderBy("datetime ASC")
    private List<CallRecord> calls;
    private BigDecimal amountBase;
    private BigDecimal taxRate;
    private BigDecimal tax;
    private BigDecimal amountTotal;

    public PhoneBill() { }

    public PhoneBill(String number) {
        this.phoneNumber = number;
        calls = new ArrayList<>();
    }

    public void addCall(CallRecord call) {
        calls.add(call);
    }

    public void calculate(BigDecimal taxRate) { ... }

    /* ... Геттеры и сеттеры ... *
}

Аннотация @OneToMany определяет связь между счётом и его записями вызовов. Атрибут FetchType.EAGER требует раннего (eager) извлечения коллекции. Атрибут CascadeType.PERSIST указывает, что элементы в списке вызовов должны автоматически сохраняться при сохранении счёта за телефон. Аннотация @OrderBy определяет порядок извлечения элементов списка вызовов из базы данных.

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

Для получения дополнительной информации об API персистентности Java см. главу 40 «Введение в API персистентности Java». Для получения дополнительной информации об API обработки JSON см. главу 20 «Обработка JSON».

Шаг фрагментов записи вызовов

Первый шаг состоит из артефактов пакета CallRecordReader, CallRecordProcessor и CallRecordWriter.

Артефакт CallRecordReader считывает записи вызовов из файла журнала:

@Dependent
@Named("CallRecordReader")
public class CallRecordReader implements ItemReader {
    private ItemNumberCheckpoint checkpoint;
    private String fileName;
    private BufferedReader breader;
    @Inject
    JobContext jobCtx;

    /* ... Переопределение методов open, close, readItem
     * и checkpointInfo ... * /
}

Метод open считывает свойство log_filename и открывает файл журнала с буферизованным читателем:

fileName = jobCtx.getProperties().getProperty("log_file_name");
breader = new BufferedReader(new FileReader(fileName));

Если предоставляется объект контрольной точки, метод open продвигает читателя до последней контрольной точки. В противном случае этот метод создаёт новый объект контрольной точки. Объект контрольной точки отслеживает номер строки из последнего подтверждённого фрагмента.

Метод readItem возвращает новый объект CallRecord или null в конце файла журнала:

@Override
public Object readItem() throws Exception {
    /* Считывание строки из файла журнала и
     * создание CallRecord из JSON */
    String callEntryJson = breader.readLine();
    if (callEntryJson != null) {
        checkpoint.nextItem();
        return new CallRecord(callEntryJson);
    } else
        return null;
}

Артефакт CallRecordProcessor получает цену эфирного времени из свойств задания, вычисляет цену каждого вызова и возвращает объект вызова. Этот артефакт переопределяет только метод processItem.

Артефакт CallRecordWriter связывает каждую запись о вызове со счётом и сохраняет счёт в базе данных. Этот артефакт переопределяет методы open, close, writeItems и checkpointInfo. Метод writeItems выглядит следующим образом:

@Override
public void writeItems(List<Object> callList) throws Exception {

    for (Object callObject : callList) {
        CallRecord call = (CallRecord) callObject;
        PhoneBill bill = em.find(PhoneBill.class, call.getFromNumber());
        if (bill == null) {
            /* Создание счёта для этого клиента */
            bill = new PhoneBill(call.getFromNumber());
            bill.addCall(call);
            em.persist(bill);
        } else {
            /* Добавление звонка в существующий счёт */
            bill.addCall(call);
        }
    }
}

Шаг фрагмента телефонного биллинга

Второй шаг состоит из артефактов пакетной обработки BillReader, BillProcessor, BillWriter и BillPartitionMapper. Этот шаг получает счета за телефон из базы данных, вычисляет сумму налога и общую сумму задолженности и записывает каждый счёт в текстовый файл. Поскольку обработка каждого счёта не зависит от других, этот этап может быть разделён на несколько потоков (thread).

Артефакт BillPartitionMapper указывает количество разделений и параметры для каждого разделения. В этом примере параметры представляют диапазон элементов, которые должно обрабатывать каждое разделение. Артефакт получает количество счетов в базе данных для расчёта этих диапазонов. Он предоставляет объект плана разделения, который переопределяет методы getPartitions и getPartitionProperties интерфейса PartitionPlan. Метод getPartitions выглядит следующим образом:

@Override
public Properties[] getPartitionProperties() {
    /* Назначаем (приблизительно) равное количество элементов
     * в каждое разделение. */
    long totalItems = getBillCount();
    long partItems = (long) totalItems / getPartitions();
    long remItems = totalItems % getPartitions();

    /* Заполняем массив свойств. Каждый элемент Properties
* в массиве соответствует разделению. */
    Properties[] props = new Properties[getPartitions()];

    for (int i = 0; i < getPartitions(); i++) {
        props[i] = new Properties();
        props[i].setProperty("firstItem",
                String.valueOf(i * partItems));
        /* Последнее разделение получает оставшиеся элементы */
        if (i == getPartitions() - 1) {
            props[i].setProperty("numItems",
                    String.valueOf(partItems + remItems));
        } else {
            props[i].setProperty("numItems",
                    String.valueOf(partItems));
    }
    return props;
}

Артефакт BillReader получает параметры разделения следующим образом:

@Dependent
@Named("BillReader")
public class BillReader implements ItemReader {
    @Inject    @BatchProperty(name = "firstItem")    private String firstItemValue;    @Inject    @BatchProperty(name = "numItems")    private String numItemsValue;
    private ItemNumberCheckpoint checkpoint;    @PersistenceContext    private EntityManager em;    private Iterator iterator;

    @Override
    public void open(Serializable ckpt) throws Exception {
        /* Получение диапазона элементов для обработки в этом разделении */
        long firstItem0 = Long.parseLong(firstItemValue);
        long numItems0 = Long.parseLong(numItemsValue);

        if (ckpt == null) {
            /* Создание контрольной точки для этого разделения */
            checkpoint = new ItemNumberCheckpoint();
            checkpoint.setItemNumber(firstItem0);
            checkpoint.setNumItems(numItems0);
        } else {
            checkpoint = (ItemNumberCheckpoint) ckpt;
        }

        /* Adjust range for this partition from the checkpoint */
        long firstItem = checkpoint.getItemNumber();
        long numItems = numItems0 - (firstItem - firstItem0);
        ...
    }
    ...
}

Этот артефакт также получает итератор для чтения элементов из entity manager-а JPA:

/* Получение итератора для квитанций в этом разделении */
String query = "SELECT b FROM PhoneBill b ORDER BY b.phoneNumber";
Query q = em.createQuery(query).setFirstResult((int) firstItem)
        .setMaxResults((int) numItems);
iterator = q.getResultList().iterator();

Артефакт BillProcessor перебирает список записей вызовов в счёте и рассчитывает налог и общую сумму, причитающуюся за каждый счёт.

Артефакт BillWriter записывает каждый счёт в простой текстовый файл.

Страницы JavaServer Faces

Страница index.xhtml содержит текстовую область, в которой отображается файл журнала записей вызовов. На странице есть кнопка, позволяющая пользователю отправить пакетное задание и перейти на следующую страницу:

<body>
    <h1>The Phone Billing Example Application</h1>
    <h2>Log file</h2>
    <p>The batch job analyzes the following log file:</p>
    <textarea cols="90" rows="25"
              readonly="true">#{jsfBean.createAndShowLog()}</textarea>
    <p> </p>
    <h:form>
        <h:commandButton value="Start Batch Job"
                         action="#{jsfBean.startBatchJob()}" />
    </h:form>
</body>

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

На странице jobstarted.xhtml есть кнопка для проверки текущего статуса пакетного задания и отображения счетов по окончании задания:

<p>Current Status of the Job: <b>#{jsfBean.jobStatus}</b></p>
<h:dataTable var="_row" value="#{jsfBean.rowList}"
             border="1" rendered="#{jsfBean.completed}">
    <!-- ... отображение результатов из jsfBean.rowList ... -->
</h:dataTable>
<!-- Render the check status button if the job has not finished -->
<h:form>
    <h:commandButton value="Check Status"
                     rendered="#{jsfBean.completed==false}"
                     action="jobstarted" />
</h:form>

Managed-бин

Managed-бин JsfBean отправляет задание в пакетную среду выполнения, проверяет статус задания и считывает текстовые файлы для каждого счёта.

Метод startBatchJob компонента отправляет задание в пакетную среду выполнения:

/* Отправка пакетного задания на выполнение.
 * Метод навигации JSF (возвращает название следующей страницы) */
public String startBatchJob() {
    jobOperator = BatchRuntime.getJobOperator();
    execID = jobOperator.start("phonebilling", null);
    return "jobstarted";
}

Метод getJobStatus компонента проверяет статус задания:

/ * Получение статуса задания из пакетной среды выполнения * /
public String getJobStatus() {
    return jobOperator.getJobExecution(execID).getBatchStatus().toString();
}

Метод getRowList компонента создаёт список счетов для отображения на странице JSF jobstarted.xhtml, используя таблицу.

Запуск phonebilling

Вы можете использовать IDE NetBeans или Maven для сборки, упаковки, развёртывания и запуска приложения phonebilling.

Здесь рассматриваются следующие темы:

Запуск phonebilling в IDE NetBeans

  1. Удостоверьтесь, чтобы GlassFish Server был запущен (см. Запуск и остановка сервера GlassFish).

  2. В меню «Файл» выберите «Открыть проект».

  3. В диалоговом окне «Открыть проект» перейдите к:

    tut-install/examples/batch
  4. Выберите каталог phonebilling.

  5. Нажмите Открыть проект.

  6. На вкладке «Проекты» кликните правой кнопкой мыши проект phonebilling и выберите «Выполнить».

    Эта команда собирает и упаковывает приложение в WAR-файл phonebilling.war, расположенный в каталоге target/, развёртывает его на сервере и запускает окно веб-браузера по следующему URL:

    http://localhost:8080/phonebilling/

Запуск phonebilling с использованием Maven

  1. Удостоверьтесь, чтобы GlassFish Server был запущен (см. Запуск и остановка сервера GlassFish).

  2. В окне терминала перейдите в:

    tut-install/examples/batch/phonebilling/
  3. Введите следующую команду для развёртывания приложения:

    mvn install
  4. Откройте окно веб-браузера по следующему URL:

    http://localhost:8080/phonebilling/

Назад Вперёд Содержание
Логотип Oracle  Copyright © 2017, Oracle и/или её дочерних компаний. Все права защищены. Версия перевода 1.0.5 (Java EE Tutorial — русскоязычная версия)