Quartz Job Scheduling Framework

О чем и зачем эта статья

В статье рассказывается зачем нужно планировать задания и почему для этого стоит использовать Quartz. Статья написана как этап подготовки к открытому семинару по Java-технологиям, проводимому сообществом разработчиков iForge.

Зачем нужно планировать задания?

Планирование заданий уходит корнями во времена мейнфреймов, когда компьютеров было мало, а желающих ими воспользоваться — много. В то, безусловно тёмное, время люди были вынуждены заранее подготавливать задания на обработку в виде перфокарт и отдавать их оператору, который отвечал за выполнение заданий и распределял когда какая задача должна быть выполнена. Тогда и появилось понятие планирования заданий (Job Scheduling).

Сейчас Job Scheduling связывают в первую очередь с такими задачами, которые обычно не требуют участия человека и выполняются с некоторой периодичностью: резервное копирование данных, отправка уведомлений по расписанию, выполнение сервисных задач и т.п. Конечно же, все эти задачи можно попросить выполнять и человека. Он даже будет с ними справляться, до определённого момента — пока задач не станет слишком много или они не станут слишком сложными для него (например, нам нужно выгрести информацию из пары-тройки баз данных, сформировать на её основе отчёт и разослать его, скажем, трём сотням получателей). Людям свойственно ошибаться, поэтому чем сложнее задача, тем выше вероятность, что человек, который её выполняет, допустит ошибку.

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

Программистов часто просят автоматизировать выполнение подобных задач.. Даже не так, в процессе работы практически над любым приложением программист сталкивается с необходимостью планирования заданий. Или нет, лучше так.. Жил был один программист, которому постоянно приходилось заново придумывать и писать, как бы так управлять периодическими заданиями. Программиста звали Джеймс Хаус (James House). Однажды ему надоело каждый раз заново изобретать велосипед и он написал Quartz и запостил его на Sourceforge.

Встречайте, Quartz.

Проект быстро стал популярным, обзавёлся собственным коммунити, оброс новыми возможностями. В 2006 году Quartz стал одним из компонентов OpenSymphony.

Сейчас Quartz — это полнофункциональный инструмент для планирования заданий, который может быть интегрирован в приложение любой сложности, начиная с простейших локальных приложений и закачивая крупнейшими системами уровня предприятий. Quartz может быть использован для планирования простых и сложных заданий, для управления выполнением десятков, сотен и даже тысяч заданий. Quartz используют в своих продуктах такие компании как Apache Software Foundation, JBoss, Cisco и многие другие.

Пример задачи, решаемой с помощью Quartz

Пожалуй закончим с философией и прочей нудятиной и попробуем уже что-нибудь автоматизировать при помощи Quartz. В качестве простого примера предлагаю написать решение вот такой задачки:

Необходимо следить за изменением содержимого заданного каталога в файловой системе, при изменении содержимого нужно отправить уведомление администратору по почте.

Такая вот несложная задачка, но на её основе я надеюсь показать вам некоторую часть возможностей Quartz. От постановки задачи предлагаю сразу перейти к решению. Для того, чтобы запустить примеры из этой статьи вам нужно прежде всего скачать сам quartz. Также убедитесь, что у вас в classpath доступны следующие библиотеки: commons-io, commons-logging, jta, commons-collections, javax.mail, javax.activation — в архиве с примером есть файл конфигурации maven (pom.xml), в котором они все перечислены. Если у вас установлен maven, просто запустите mvn install из корня проекта, необходимые библиотеки будут скачаны автоматически.

Задания (Jobs)

Для работы с заданиями в Quartz определён интерфейс Job (у него есть несколько потомков, но о них позднее), в котором объявлен лишь один метод:

public void execute(JobExecutionContext context) throws JobExecutionException

Таким образом для того, чтобы определить все действия которые мы хотим выполнять, нам достаточно объявить класс, реализующий этот интерфейс, поместив всю логику работы в метод execute. Обратите внимание на объект передаваемый в качестве параметра. JobExecutionContext — это объект, который содержит информацию, которая была передана заданию, перед тем как оно было запущено на исполнение. Проще говоря, если нам нужно передать какие-то данные в задание, перед тем как оно будет запущено, мы должны использовать JobExecutionContext. Давайте быстренько напишем, классик, который будет сканировать заданную папку и отправлять уведомления:
public class DirectoryScanJob implements StatefulJob {
 
    private static final Object DIRECTORY_TO_SCAN = "directoryToScan";
    private static final Object PREV_FILES_COUNT = "prevFilesCount";
    private static final String ADMIN_EMAIL = "adminEmail";
 
    Log log = LogFactory.getLog(DirectoryScanJob.class);
 
    public DirectoryScanJob() {
        super();
    }
 
    public void execute(JobExecutionContext context) throws JobExecutionException {
        JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
        String target = (String) jobDataMap.get(DIRECTORY_TO_SCAN);
        Integer prevFilesCount = Integer.parseInt((String) jobDataMap.get(PREV_FILES_COUNT));
 
        File targetDir = new File(target);
        int fileCount = targetDir.listFiles().length;
        if (prevFilesCount.intValue() != fileCount) {
            try {
                Transport.send(getNotificationMessage(jobDataMap.getString(ADMIN_EMAIL)));
                log.info("Changes found. Notification succesfully sent.");
            }catch (AddressException e) {
                log.error("Invalid admin email address specified.", e);
            } 
            catch (MessagingException e) {
                log.error("Error while trying to send notification.", e);
            }
            jobDataMap.put(PREV_FILES_COUNT, String.valueOf(fileCount));
        } else {
           log.info("No changes found.");
        }
    }
 
    private Message getNotificationMessage(String recipientEmail) throws AddressException, MessagingException {
        Message m = new MimeMessage(Session.getDefaultInstance(System.getProperties()));
        m.addRecipient(RecipientType.TO, new InternetAddress(recipientEmail));
        m.setSubject("Notification");
        m.setText("Directory contents changed.");
        return m;
    }
}

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

JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();

Всю информацию мы получаем из JobDataMap — это специальный объект, который служит для передачи необходимой для выполнения задания информации. Как она туда попадает — немного позднее. Каждый раз, когда задание запускается, этот объект инициализируется заново — т.е. вся информация, которая туда попадает во время выполнения задания, пропадает. Если нам нужно сохранить информацию между запусками одного и того же типа задания, то вместо интерфейса Job нам следует реализовать интерфейс StatefulJob (как и сделано в примере), тогда во все вызовы метода execute данного задания будет передаваться один и тот же JobDataMap. Использование интерфейса StatefulJob накладывает определённые ограничения — в случа если вам будет необходимо запустить одновременно несколько заданий этого типа, может произойти потеря данных, т.к. все задания будут использовать общий JobDataMap. Поэтому используйте StatefulJob только если вы абсолютно уверены, что в каждый момент времени будет запущен только один экземпляр этого задания. Дальше в примере все просто: достаем из мэпа имя директории, которую нужно сканировать, количество файлов и папок определённое во время предыдущего запуска. Потом считаем текущее количество файлов, если оно отличается от предыдущего — отправляем уведомление администратору (адрес достаём из того же мэпа), если нет — просто пишем в лог уведомление, что изменений не найдено.

Триггеры (Triggers)

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

Давайте посмотрим на следующий пример:

public static void main(String[] args) throws SchedulerException, ParseException {
        Scheduler s = StdSchedulerFactory.getDefaultScheduler();
        s.start();
        Trigger t = TriggerUtils.makeImmediateTrigger(SimpleTrigger.REPEAT_INDEFINITELY, 1000);
        t.setName("Minutely trigger");
        JobDetail jobDetail = new JobDetail("SampleJob",Scheduler.DEFAULT_GROUP, DirectoryScanJob.class);
        jobDetail.getJobDataMap().put(DIRECTORY_TO_SCAN, "/home/aectann");
        jobDetail.getJobDataMap().put(PREV_FILES_COUNT, "0");
        jobDetail.getJobDataMap().put(ADMIN_EMAIL, "aectann@mailinator.com");
        s.scheduleJob(jobDetail, t);
    }

В первой строчке метода main мы получаем экземпляр планировщика у StdSchedulerFactory, затем запускаем его — после этого планировщик начинает выполнят все зарегистрированные в нем задания (пока их там, конечно же, нет). Затем создаём триггер, который указывает, что задание должно выполняться постоянно, с периодичностью в 1 секунду. После этого создаём объект JobDetail, содержащий в себе JobDataMap, в который мы складываем, соответственно путь к целевому каталогу, начальное количество файлов в нем, а также адрес для отправки уведомлений. Вот и все, задача решена (исходный код).

Ещё немного про триггеры

Вообще в Quartz определено три основных типа триггеров:

  • org.quartz.SimpleTrigger — простейший вариант, предназначен для работы с заданиями, которые должны быть запущены в определённое время, определенное количество раз с некоторым интервалом между выполнениями.
  • org.quartz.CronTrigger — более мощный и гибкий триггер, основан на выражениях UNIX-ового планировщика cron. Допустим, вам требуется запускать задание каждые пять минут с 8 до 9 утра по понедельникам и пятницам. Если вы попытаетесь реализовать подобное расписание при помощи простых триггеров, возможно у вас это и получится, но вам придётся создать несколько триггеров. В тоже время можно обойтись всего одним cron-триггером, создав его на основе подобного выражения: 0 0/5 8 ? * MON,FRI. Тогда код для планирования нашего задания будет выглядеть примерно так (как видите, мы только заменили триггер, в остальном порядок работы не изменился):
public static void main(String[] args) throws SchedulerException, ParseException {
        Scheduler s = StdSchedulerFactory.getDefaultScheduler();
        s.start();
        Trigger t = new CronTrigger("Cron trigger", Scheduler.DEFAULT_GROUP,"0 0/5 8 ? * MON,FRI");
        JobDetail jobDetail = new JobDetail("SampleJob",Scheduler.DEFAULT_GROUP, DirectoryScanJob.class);
        jobDetail.getJobDataMap().put(DIRECTORY_TO_SCAN, "/home/admin");
        jobDetail.getJobDataMap().put(PREV_FILES_COUNT, "0");
        jobDetail.getJobDataMap().put(ADMIN_EMAIL, "admin@gmail.com");
        s.scheduleJob(jobDetail, t);
    }
  • org.quartz.NthIncludedDayTrigger — предназначен для планирования заданий на определённый момент внутри периода. Например если вам нужно запускать задание 15 числа каждого месяца — это триггер, то, что вам нужно. Триггеры этого типа могут быть связаны с календарями (не путать со стандартным календарём Java) — объектами позволяющими более гибко настраивать время выполнения задания. Например, HolidayCalendar позволяет определить праздничные дни в году, когда вам не нужно выполнять задание.

В показанном выше примере для создания триггера используется статический метод класса TriggerUtils — этот класс содержит множество заготовок-настроек триггеров. Использование этого класса действительно облегчает жизнь :) Хотя, вы конечно же вольны создавать нужные вам триггеры вручную.

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

Планировщик Quartz хранит информацию о запланированных заданиях в специальных хранилищах (Storages). Существует два основных типа хранилищ:

  • memory-storage — используется по умолчанию, все данные хранятся в памяти, информация о заданиях и расписании теряется после перезапуска компьютера.
  • persistent-storage — позволяет хранить информацию в базе данных, доступ к которой осуществляется через JDBC. Для использования этого типа хранилища вам потребуется сконфигурировать Quartz (это делается через специальный файл свойств), указав ему тип базы данных, путь к ней и т.п. Также вам будет необходимо создать в вашей базе таблицы, которые необходимы Quartz для работы.

Дополнительные возможности

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

  • Listeners — вы можете расширить функциональность Quartz написав обработчики событий, поступающих от заданий, триггеров и планировщика. Например, можно выполнять запись в лог файл по завершении задания, используя Listener.
  • Plugins — компоненты, расширяющие функциональность фреймворка. Quartz поставлятся с набором стандартных плагинов. К примеру, JobInitializationPlugin может использоваться для инициализации заданий из xml-файла. Вы также можете написать свой плагин.
  • Кластеризация — Quatrz — масштабируемая система, вы можете настроить его так, что несколько экземпляров программы будут работать с одной базой данных, обеспечивая безотказную работу и балансировку нагрузки сервиса планирования заданий.
  • Интеграция с веб-приложениями — Quartz может быть легко интегрирован в ваше веб-приложение, вы можете объявить отдельный сервлет для него (org.quartz.ee.servlet. QuartzInitializerServlet).

Заключение

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

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License