Планирование заданий с помощью Quartz

Quartz API использует многосторонний подход к планированию заданий в приложениях Java

Quartz представляет собой проект с открытым исходным кодом, который предлагает исчерпывающий набор функциональных возможностей для планирования заданий. В этой статье инженер-программист Майкл Липтон и архитектор информационных технологий Субэк Джанг представляют Quartz API, начиная с общего обзора среды разработки и заканчивая набором примеров кода, которые показывают ее основные функциональные возможности. После прочтения этой статьи и изучения примеров кода вы сможете внедрить базовые функциональные возможности Quartz в любое приложение Java™.

Майкл Липтон, инженер-программист, IBM

Майкл Липтон (Michael Lipton) работает инженером-программистом в лаборатории IBM WebAhead, где он занимается приложениями J2EE и J2SE. В свободное время Майкл любит развлекаться.



Субэк Джанг, IT-архитектор и интегратор, IBM

Субэк Джанг (Soobaek Jang) является IT-архитектором в лаборатории IBM WebAhead, где он занимается приложениями, обеспечивающими производственное сотрудничество и внедрение инноваций.



20.04.2007

Так как число и сложность современных Web-приложений продолжают возрастать, возрастает и сложность всех лежащих в основе приложения компонентов. Планирование заданий является общим требованием для приложений Java в современных системах, и потому оно - предмет постоянной заботы для разработчиков Java. Хотя современные технологии планирования заданий усовершенствовались по сравнению с примитивными триггерами баз данных и отдельными потоками планировщиков, планирование заданий остается нетривиальной проблемой. Одно из наиболее эффективных решений данной проблемы предлагается Quartz API, разработанный OpenSymphony.

Quartz представляет собой среду разработки для планирования заданий с открытым исходным кодом, которая обеспечивает простой, но эффективный механизм планирования заданий в приложениях Java. Quartz представляет разработчикам возможность планировать задания по временному интервалу или по времени суток. В ней реализованы отношения типа "многие-ко-многим" для заданий и триггеров, что позволяет соединять множественные задания с различными триггерами. Приложения, в которые входит Quartz, могут повторно использовать задания из разных событий и группировать множественные задания для одного события. Вы можете настроить Quartz через конфигурационный файл (в котором вы можете задать источник данных для JDBC-транзакций, для глобального подписчика на задания и/или триггеры, плагинов, пулов потоков и т. д.), при этом не происходит интеграции с контекстом или ссылками сервера приложений. Положительным результатом здесь является то, что задания не получают доступа к внутренним функциям Web-сервера; например, в случае с сервером приложений WebSphere задания, запланированные с помощью Quartz, не мешают источникам данных сервера и кэшированию (Dyna-cache).

В статье рассматривается Quartz API с использованием набора примеров кода, которые показывают работу таких механизмов, как задания, триггеры, хранилища заданий и свойства.

Начало работы

Чтобы начать работу с Quartz, вы должны сконфигурировать ваш проект для работы с Quartz API. Данная процедура выглядит следующим образом:

  1. Загрузите Quartz API.
  2. Распакуйте quartz-x.x.x.jar и поместите его в папку вашего проекта или в папку, на которую указывает путь к классам вашего проекта.
  3. Поместите jar-файлы из базовой или дополнительной папки в папку вашего проекта или в папку, на которую указывает путь к классам вашего проекта.
  4. Если вы используете JDBCJobStore, поместите все jar-файлы JDBC в папку вашего проекта или в папку, на которую указывает путь к классам вашего проекта.

Для вашего удобства мы скомпилировали все необходимые файлы, включая JDBC-файлы для DB2, в единый zip-файл. Обратитесь к разделу Загрузка, чтобы загрузить код.

Теперь давайте посмотрим на основные компоненты Quartz API.


Задания и триггеры

Два основные единицы пакета планирования Quartz - это задания и триггеры. Задание представляет собой исполняемую задачу, которая может быть запланирована, а триггер обеспечивает график выполнения задания. Хотя эти две сущности можно было бы объединить, в Quartz они разделены намеренно, что приносит определенную пользу.

Отделив выполнение работы от графика ее выполнения, Quartz дает вам возможность изменить запланированный триггер для отдельного задания без потери самого задания или его контекста. Кроме того, любое отдельное задание может быть связано с множеством триггеров.


Пример 1: Задания

Вы можете сделать Java-класс исполняемым, реализовав интерфейс org.quartz.job. Пример задания в Quartz приводится в Листинге 1. Этот класс замещает метод execute(JobExecutionContext context) очень простым оператором вывода. Этот метод может содержать любой код, который мы хотели бы выполнить. (Все примеры кода основаны на Quartz 1.5.2, стабильной версии на момент написания этой статьи.)

Листинг 1. SimpleQuartzJob.java
package com.ibm.developerworks.quartz;

import java.util.Date;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class SimpleQuartzJob implements Job {

    public SimpleQuartzJob() {
    }

    public void execute(JobExecutionContext context) throws JobExecutionException {
        System.out.println("In SimpleQuartzJob - executing its JOB at " 
                + new Date() + " by " + context.getTrigger().getName());
    }
}

Обратите внимание, что метод execute принимает объект JobExecutionContext в качестве аргумента. Этот объект обеспечивает контекст выполнения экземпляра задания. В частности он представляет доступ к планировщику и триггеру, которые совместно использовались для начала выполнения задания, а также объекта JobDetail задания. Quartz отделяет выполнение задания от состояния окружения задания, помещая состояние в объект JobDetail и используя конструктор JobDetail для запуска экземпляра задания. В объекте JobDetail хранятся подписчики на задания, группа, отображение данных, описание и другие свойства задания.


Пример 2: Простые триггеры

Триггер создает график исполнения задания. Quartz предлагает для триггеров несколько различных опций различной степени сложности. SimpleTrigger в Листинге 2 знакомит с основами триггеров:

Листинг 2. SimpleTriggerRunner.java
    public void task() throws SchedulerException
    {
        // Запускаем Schedule Factory
        SchedulerFactory schedulerFactory = new StdSchedulerFactory();
        // Извлекаем планировщик из schedule factory
        Scheduler scheduler = schedulerFactory.getScheduler();
        
        // текущее время
        long ctime = System.currentTimeMillis(); 
        
        // Запускаем JobDetail с именем задания,
        // группой задания и классом выполняемого задания
        JobDetail jobDetail = 
        	new JobDetail("jobDetail-s1", "jobDetailGroup-s1", SimpleQuartzJob.class);
        // Запускаем SimpleTrigger с его именем и именем группы
        SimpleTrigger simpleTrigger = 
        	new SimpleTrigger("simpleTrigger", "triggerGroup-s1");
        // Устанавливаем время запуска
        simpleTrigger.setStartTime(new Date(ctime));
        // Устанавливаем интервал, с которым задание должно запускаться (здесь 10 секунд) 
        simpleTrigger.setRepeatInterval(10000);
        // Устанавливаем число выполнений этого задания, (10 раз) 
        // после 10 раз его выполнение прекратится.
        simpleTrigger.setRepeatCount(100);
        // Устанавливаем время завершения задания. 
        // Устанавливаем на 60 секунд со времени запуска,
        // Даже если мы установим число повторов на 10, выполнение прекратится
        // после 6 повторов, так как к этому времени наступит время завершения.
        //simpleTrigger.setEndTime(new Date(ctime + 60000L));
        // Устанавливаем приоритет для триггера.
        // Если не установлен, он по умолчанию равен 5.
        //simpleTrigger.setPriority(10);
        // Планируем задание с JobDetail и Trigger
        scheduler.scheduleJob(jobDetail, simpleTrigger);
        
        // Запускаем планировщик
        scheduler.start();
    }

Листинг 2 начинается с создания экземпляра SchedulerFactory и получения планировщика. Как уже обсуждалось ранее, объект JobDetail создается приписыванием Job в качестве аргумента к его конструктору. Как видно из названия, экземпляр SimpleTrigger достаточно примитивен. После создания объекта, мы устанавливаем несколько основных свойств, планируя задание на немедленное выполнение, а затем на повторное выполнение каждые 10 секунд до тех пор, пока задание не будет выполнено 100 раз.

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


Пример 3: Триггеры cron

CronTrigger дает возможность более точного планирования, чем SimpleTrigger, но также не является слишком сложным. Основываясь на выражениях cron, CronTrigger дает возможность устанавливать интервалы повторов в календарном виде, а не в стандартном, что является значительным улучшением по сравнению с SimpleTriggers.

Выражение cron состоит из следующих семи полей:

  • Секунды
  • Минуты
  • Часы
  • День месяца
  • Месяц
  • День недели
  • Год (необязательное поле)

Специальные символы

Триггеры cron используют серию специальных символов, например:

  • Символ косая черта (/) обозначает приращение значения. Например, "5/15" в поле "секунды" означает каждые 15 секунд, начиная с пятой секунды.
  • Знак вопроса (?) и букву L (L) разрешается использовать только в полях "день месяца" и "день недели". Знак вопроса означает, что в поле не должно быть указанной величины. Таким образом, если вы устанавливаете день недели, вы можете вставить "?" в поле "день недели" для обозначения того, что значение "день недели" несущественно. Буква L - это сокращение от last (последний). Если она помещается в поле "день месяца", задание будет запланировано на последний день месяца. В поле "день недели" "L" равнозначно "7", если помещается само по себе, или означает последний экземпляр "дня недели" в этом месяце. Так, "0L" запланирует выполнение задания на последнее воскресенье данного месяца.
  • Буква W (W) в поле "день месяца" планирует выполнение задания на ближайший к заданному значению рабочий день. Введя "1W" в поле "день месяца" вы планируете выполнение задания на рабочий день, ближайший к первому числу месяца.
  • Знак фунта (#) устанавливает конкретный рабочий день данного месяца. Ввод "MON#2" в поле "день недели" планирует задание на второй понедельник месяца.
  • Знак астериска (*) является подстановочным знаком и обозначает, что любое возможное значение может быть принято для данного отдельного поля.

Все эти определения могут привести в уныние, но выражения cron становятся простыми после нескольких минут тренировки.

Листинг 3 показывает пример CronTrigger. Отметьте, что создание экземпляров SchedulerFactory, Scheduler и JobDetail идентичны примеру, SimpleTrigger. В этом случае мы только поменяли триггер. Выражение cron, которое мы задали ("0/5 * * * * ?") планирует выполнение задания каждые 5 секунд.

Листинг 3. CronTriggerRunner.java
    public void () throws SchedulerException
    {
        // Запускаем Schedule Factory
        SchedulerFactory schedulerFactory = new StdSchedulerFactory();
        // Извлекаем планировщик из schedule factory
        Scheduler scheduler = schedulerFactory.getScheduler();
        
        // текущее время
        long ctime = System.currentTimeMillis(); 
        
        // Запускаем JobDetail с именем задания,
        // группой задания и классом выполняемого задания
        JobDetail jobDetail = 
        	new JobDetail("jobDetail2", "jobDetailGroup2", SimpleQuartzJob.class);
        // Запускаем CronTrigger с его именем и именем группы
        CronTrigger cronTrigger = new CronTrigger("cronTrigger", "triggerGroup2");
        try {
            // Устанавливаем CronExpression
            CronExpression cexp = new CronExpression("0/5 * * * * ?");
            // Присваиваем CronExpression CronTrigger'у
            cronTrigger.setCronExpression(cexp);
        } catch (Exception e) {
            e.printStackTrace();
        }
        // Планируем задание с помощью JobDetail и Trigger
        scheduler.scheduleJob(jobDetail, cronTrigger);
        
        // Запускаем планировщик
        scheduler.start();
    }

Продвинутые возможности Quartz

Вы получаете доступ к большому числу функциональных возможностей, используя только задания и триггеры, как описано выше. Quartz представляет собой лёгкий в освоении и гибкий пакет планирования, однако, он может предложить еще больше функциональных возможностей тем, кто попытается изучить его глубже. В следующем разделе представлены некоторые продвинутые возможности Quartz.


Хранилища заданий

Quartz предлагает два различных средства, с помощью которых можно хранить связанные с заданиями и триггерами данные в памяти или в базе данных. Первое средство, экземпляр класса RAMJobStore, является настройкой по умолчанию. Это самое простое в использовании хранилище заданий, дающее к тому же наибольшую производительность, поскольку все данные хранятся в памяти. Главным недостатком этого метода является недостаточная сохраняемость данных. Поскольку все данные сохраняются в RAM, вся информация будет утеряна после "падения" приложения или системы.

Чтобы исправить эти недостатки, Quartz предлагает JDBCJobStore. Как следует из названия, этот способ сохранения заданий помещает данные в базу данных через JDBC. Ценой более надёжного хранения данных является более низкая производительность, а также большая сложность.

Настройка JDBCJobStore

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

Настройка JDBCJobStore для использования в ваших приложениях происходит в два этапа: во-первых, вы должны создать таблицы базы данных для использования при хранилищами заданий. JDBCJobStore совместимо со всеми основными базами данных, а Quartz предлагает для создания таблиц серию SQL-скриптов, облегчающих процесс установки. Вы найдете SQL-скрипты для создания таблиц в папке "docs/dbTables" дистрибутива Quartz. Во вторых, вы должны определить некоторые свойства, показанные в Таблице 1:

Таблица 1. Свойства JDBCJobStore
Имя свойстваЗначение
org.quartz.jobStore.classorg.quartz.impl.jdbcjobstore.JobStoreTX (or JobStoreCMT)
org.quartz.jobStore.tablePrefixQRTZ_ (дополнительный, настраиваемый)
org.quartz.jobStore.driverDelegateClassorg.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.dataSourceqzDS (настраиваемый)
org.quartz.dataSource.qzDS.drivercom.ibm.db2.jcc.DB2Driver (может быть любой другой драйвер базы данных)
org.quartz.dataSource.qzDS.urljdbc:db2://localhost:50000/QZ_SMPL (настраиваемый)
org.quartz.dataSource.qzDS.userdb2inst1 (помещает userid в вашу базу данных)
org.quartz.dataSource.qzDS.passwordpass4dbadmin (помещает ваш пароль для пользователя)
org.quartz.dataSource.qzDS.maxConnections30

Листинг 4 иллюстрирует надежность сохранения данных, предлагаемую JDBCJobStore. Как показано в предыдущих примерах, мы начали с инициализации SchedulerFactory и Scheduler. Затем вместо того, чтобы инициализировать задание и триггер, мы вызвали список имен группы триггера и далее список имен триггера для каждого имени группы триггера. Обратите внимание, что каждое задание должно быть перепланировано с использованием метода Scheduler.reschedule(). Просто повторная инициализация задания, которое было завершено при предыдущем выполнении приложения не позволяет правильно загрузить свойства триггера.

Листинг 4. JDBCJobStoreRunner.java
    public void task() throws SchedulerException
    {
        // Запускаем Schedule Factory
        SchedulerFactory schedulerFactory = new StdSchedulerFactory();
        // Извлекаем планировщик из schedule factory
        Scheduler scheduler = schedulerFactory.getScheduler();
        
        String[] triggerGroups;
        String[] triggers;

        triggerGroups = scheduler.getTriggerGroupNames();
        for (int i = 0; i < triggerGroups.length; i++) {
           triggers = scheduler.getTriggerNames(triggerGroups[i]);
           for (int j = 0; j < triggers.length; j++) {
              Trigger tg = scheduler.getTrigger(triggers[j], triggerGroups[i]);
              
              if (tg instanceof SimpleTrigger & tg.getName().equals("simpleTrigger")) {
                  ((SimpleTrigger)tg).setRepeatCount(100);
                  // перепланируем задание
                  scheduler.rescheduleJob(triggers[j], triggerGroups[i], tg);
                  // убираем задание из плана
                  //scheduler.unscheduleJob(triggersInGroup[j], triggerGroups[i]);
              }
           }
        }        

        // запускаем планировщик
        scheduler.start();
    }

Запуск JDBCJobStore

Когда мы запускаем наш пример в первый раз, триггер инициализируется в базе данных. Рисунок 1 показывает базу данных после того, как триггер был инициализирован, но до того, как он запустился. Таким образом, REPEAT_COUNT установлен на 100, основываясь на методе setRepeatCount() в Листинге 4, а TIMES_TRIGGERED равен 0. После того, как приложение поработало какое-то время, оно останавливается.

Рисунок 1. Данные в базе данных с использованием JDBCJobStore (до запуска)
Данные в базе данных с использованием JDBCJobStore (до запуска)

Рисунок 2 показывает базу данных после того, как приложение было остановлено. На этом рисунке TIMES_TRIGGERED был установлен на 19, что обозначает число запусков данного задания.

Рисунок 2. Те же самые данные после 19 повторов
Те же самые данные после 19 повторов

Когда мы снова запускаем приложение, происходит обновление REPEAT_COUNT. Это видно на Рисунке 3. Здесь мы видим, что REPEAT_COUNT изменилось на 81, так что новый REPEAT_COUNT равен предыдущему значению REPEAT_COUNT минус предыдущее значение TIMES_TRIGGERED. Далее мы видим, что на Рисунке 3 новое значение TIMES_TRIGGERED равно 7, что указывает, что задание было запущено ещё семь раз с того момента, как приложение было перезапущено.

Рисунок 3. Данные после второго запуска с семью повторами
Данные после второго запуска с семью повторами

После того, как мы снова остановили приложение, значение REPEAT_COUNT снова обновляется. Это показано на Рисунке 4, здесь приложение было остановлено и еще не перезапущено Снова значение REPEAT_COUNT изменено путем вычитания предыдущего значения TIMES_TRIGGERED из предыдущего значения REPEAT_COUNT.

Рисунок 4. Исходные данные перед повторным запуском триггера
Исходные данные перед повторным запуском триггера

Использование свойств

Как мы уже видели JDBCJobStore позволяет вам использовать целый ряд свойств для точной настройки Quartz. Вы должны установить эти свойства в файле quartz.properties. См. листинг конфигурируемых свойств в разделе Ресурсы. Листинг 5 показывает образцы свойств, используемых в примере JDBCJobStore:

Листинг 5. quartz.properties
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 10
org.quartz.threadPool.threadPriority = 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true

# Используем RAMJobStore
## Используя RAMJobStore, пожалуйста, не забудьте закомментировать следующее
## - org.quartz.jobStore.tablePrefix, 
## - org.quartz.jobStore.driverDelegateClass, 
## - org.quartz.jobStore.dataSource
#org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore

# Используем JobStoreTX
## Не забудьте вначале запустить соответствующий скрипт
## (под docs/dbTables), чтобы создать таблицы
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX

# Конфигурируем JDBCJobStore с Table Prefix
org.quartz.jobStore.tablePrefix = QRTZ_

# Используем DriverDelegate
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate

# Используем источник данных
org.quartz.jobStore.dataSource = qzDS

# Определяем используемый источник данных
org.quartz.dataSource.qzDS.driver = com.ibm.db2.jcc.DB2Driver
org.quartz.dataSource.qzDS.URL = jdbc:db2://localhost:50000/dbname
org.quartz.dataSource.qzDS.user = dbuserid
org.quartz.dataSource.qzDS.password = password
org.quartz.dataSource.qzDS.maxConnections = 30

Заключение

Среда планирования заданий Quartz предлагает мощный, и в то же время простой в применении API. Quartz можно использовать для запуска простых заданий, а также и для постоянного хранения и выполнения сложных заданий JDBC. OpenSymphony успешно заполнил нишу среди программных продуктов с открытым исходным кодом, сделав обычно нудную для разработчиков работу по планированию заданий тривиальной.


Загрузка

ОписаниеИмяРазмер
Sample Java Code on Quartz with dependent jarsj-quartz-withJars.zip3173KB
Sample Java Code on Quartz without dependent jarsj-quartz-noJars.zip10KB

Ресурсы

Научиться

Получить продукты и технологии

  • Загрузите Quartz: Решение с открытым исходным кодом для планирования заданий в Java-приложениях.

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



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

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java, Open source
ArticleID=211042
ArticleTitle=Планирование заданий с помощью Quartz
publish-date=04202007