Quartz Scheduler — одна из самых популярных библиотек планирования в мире Java. В прошлом я работал с Quartz в основном в приложениях Spring. Недавно я исследовал планирование в приложении JEE 6, работающем на JBoss 7.1.1, которое будет развернуто в облаке. Одним из вариантов, которые я рассматриваю, является Quartz Scheduler, поскольку он предлагает кластеризацию с базой данных. В этой статье я покажу, как легко настроить Quartz в приложении JEE и запустить его на JBoss 7.1.1 или WildFly 8.0.0, использовать MySQL в качестве хранилища заданий и использовать CDI для использования внедрения зависимостей в заданиях. Все будет сделано в IntelliJ. Давайте начнем.
Создать проект Maven
Я использовал org.codehaus.mojo.archetypes:webapp-javaee6 archetype для начальной загрузки приложения, а затем я немного изменил pom.xml . Я также добавил зависимость slf4J , поэтому результирующий pom.xml выглядит следующим образом:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <modelVersion>4.0.0</modelVersion> <groupId>pl.codeleak</groupId> <artifactId>quartz-jee-demo</artifactId> <version>1.0</version> <packaging>war</packaging> <name>quartz-jee-demo</name> <properties> <endorsed.dir>${project.build.directory}/endorsed</endorsed.dir> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>6.0</version> <scope>provided</scope> </dependency> <!-- Logging --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.7</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-jdk14</artifactId> <version>1.7.7</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.3.2</version> <configuration> <source>1.7</source> <target>1.7</target> <compilerArguments> <endorseddirs>${endorsed.dir}</endorseddirs> </compilerArguments> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>2.1.1</version> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <version>2.1</version> <executions> <execution> <phase>validate</phase> <goals> <goal>copy</goal> </goals> <configuration> <outputDirectory>${endorsed.dir}</outputDirectory> <silent>true</silent> <artifactItems> <artifactItem> <groupId>javax</groupId> <artifactId>javaee-endorsed-api</artifactId> <version>6.0</version> <type>jar</type> </artifactItem> </artifactItems> </configuration> </execution> </executions> </plugin> </plugins> </build></project> |
Следующим шагом был импорт проекта в IDE. В моем случае это IntelliJ и создайте конфигурацию запуска с JBoss 7.1.1.
Одно замечание: в настройках виртуальной машины в конфигурации запуска я добавил две переменные:
|
1
2
|
-Djboss.server.default.config=standalone-custom.xml-Djboss.socket.binding.port-offset=100 |
standalone-custom.xml является копией стандартного standalone.xml , так как необходимо будет изменить конфигурацию (см. ниже).
Настроить сервер JBoss
В моем демонстрационном приложении я хотел использовать базу данных MySQL с Quartz, поэтому мне нужно было добавить источник данных MySQL в мою конфигурацию. Это можно быстро сделать за два шага.
Добавить модуль драйвера
Я создал папку JBOSS_HOME/modules/com/mysql/main . В эту папку я добавил два файла: module.xml и mysql-connector-java-5.1.23.jar . Файл модуля выглядит следующим образом:
|
1
2
3
4
5
6
7
8
9
|
<?xml version="1.0" encoding="UTF-8"?> <module xmlns="urn:jboss:module:1.0" name="com.mysql"> <resources> <resource-root path="mysql-connector-java-5.1.23.jar"/> </resources> <dependencies> <module name="javax.api"/> </dependencies> </module> |
Настроить источник данных
В файле standalone-custom.xml в подсистеме datasources я добавил новый источник данных:
|
1
2
3
4
5
6
7
8
|
<datasource jta="false" jndi-name="java:jboss/datasources/MySqlDS" pool-name="MySqlDS" enabled="true" use-java-context="true"> <connection-url>jdbc:mysql://localhost:3306/javaee</connection-url> <driver>com.mysql</driver> <security> <user-name>jeeuser</user-name> <password>pass</password> </security></datasource> |
И водитель:
|
1
2
3
|
<drivers> <driver name="com.mysql" module="com.mysql"/></drivers> |
Примечание: Для целей этой демонстрации источник данных не JTA удалось упростить настройку.
Настройте кварц с кластеризацией
Я использовал официальное руководство для настройки Quarts с кластеризацией: http://quartz-scheduler.org/documentation/quartz-2.2.x/configuration/ConfigJDBCJobStoreClustering
Добавить Quartz-зависимости в pom.xml
|
01
02
03
04
05
06
07
08
09
10
|
<dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.2.1</version></dependency><dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz-jobs</artifactId> <version>2.2.1</version></dependency> |
Добавьте quartz.properties в src/main/resources
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
#============================================================================# Configure Main Scheduler Properties #============================================================================org.quartz.scheduler.instanceName = MySchedulerorg.quartz.scheduler.instanceId = AUTO#============================================================================# Configure ThreadPool #============================================================================org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPoolorg.quartz.threadPool.threadCount = 1#============================================================================# Configure JobStore #============================================================================org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTXorg.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegateorg.quartz.jobStore.useProperties = falseorg.quartz.jobStore.dataSource=MySqlDSorg.quartz.jobStore.isClustered = trueorg.quartz.jobStore.clusterCheckinInterval = 5000org.quartz.dataSource.MySqlDS.jndiURL=java:jboss/datasources/MySqlDS |
Создание таблиц MySQL для использования в Quartz
Файл схемы находится в дистрибутиве Quartz: quartz-2.2.1\docs\dbTables .
Демо-код
Имея конфигурацию на месте, я хотел проверить, работает ли Quartz, поэтому я создал планировщик без заданий и триггеров.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
package pl.codeleak.quartzdemo;import org.quartz.JobKey;import org.quartz.Scheduler;import org.quartz.SchedulerException;import org.quartz.TriggerKey;import org.quartz.impl.StdSchedulerFactory;import org.quartz.impl.matchers.GroupMatcher;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import javax.annotation.PostConstruct;import javax.annotation.PreDestroy;import javax.ejb.Singleton;import javax.ejb.Startup;@Startup@Singletonpublic class SchedulerBean { private Logger LOG = LoggerFactory.getLogger(SchedulerBean.class); private Scheduler scheduler; @PostConstruct public void scheduleJobs() { try { scheduler = new StdSchedulerFactory().getScheduler(); scheduler.start(); printJobsAndTriggers(scheduler); } catch (SchedulerException e) { LOG.error("Error while creating scheduler", e); } } private void printJobsAndTriggers(Scheduler scheduler) throws SchedulerException { LOG.info("Quartz Scheduler: {}", scheduler.getSchedulerName()); for(String group: scheduler.getJobGroupNames()) { for(JobKey jobKey : scheduler.getJobKeys(GroupMatcher.<JobKey>groupEquals(group))) { LOG.info("Found job identified by {}", jobKey); } } for(String group: scheduler.getTriggerGroupNames()) { for(TriggerKey triggerKey : scheduler.getTriggerKeys(GroupMatcher.<TriggerKey>groupEquals(group))) { LOG.info("Found trigger identified by {}", triggerKey); } } } @PreDestroy public void stopJobs() { if (scheduler != null) { try { scheduler.shutdown(false); } catch (SchedulerException e) { LOG.error("Error while closing scheduler", e); } } }} |
Когда вы запустите приложение, вы сможете увидеть отладочную информацию из Quartz:
|
1
2
3
4
5
6
|
Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally. NOT STARTED. Currently in standby mode. Number of jobs executed: 0 Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 1 threads. Using job-store 'org.quartz.impl.jdbcjobstore.JobStoreTX' - which supports persistence. and is clustered. |
Пусть Кварц использует CDI
В Quartz задания должны реализовывать интерфейс org.quartz.Job .
|
01
02
03
04
05
06
07
08
09
10
11
12
13
|
package pl.codeleak.quartzdemo;import org.quartz.Job;import org.quartz.JobExecutionContext;import org.quartz.JobExecutionException;public class SimpleJob implements Job { @Override public void execute(JobExecutionContext context) throws JobExecutionException { // do something }} |
Затем для создания вакансии мы используем JobBuilder:
|
1
2
3
4
5
|
JobKey job1Key = JobKey.jobKey("job1", "my-jobs");JobDetail job1 = JobBuilder .newJob(SimpleJob.class) .withIdentity(job1Key) .build(); |
В моем примере мне нужно было внедрить EJB в мои задания, чтобы повторно использовать существующую логику приложения. Так что на самом деле мне нужно было вставить ссылку на EJB. Как это можно сделать с помощью кварца? Легко. В Quartz Scheduler есть метод для предоставления JobFactory, который будет отвечать за создание экземпляров Job.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
package pl.codeleak.quartzdemo;import org.quartz.Job;import org.quartz.JobDetail;import org.quartz.Scheduler;import org.quartz.SchedulerException;import org.quartz.spi.JobFactory;import org.quartz.spi.TriggerFiredBundle;import javax.enterprise.inject.Any;import javax.enterprise.inject.Instance;import javax.inject.Inject;import javax.inject.Named;public class CdiJobFactory implements JobFactory { @Inject @Any private Instance<Job> jobs; @Override public Job newJob(TriggerFiredBundle triggerFiredBundle, Scheduler scheduler) throws SchedulerException { final JobDetail jobDetail = triggerFiredBundle.getJobDetail(); final Class<? extends Job> jobClass = jobDetail.getJobClass(); for (Job job : jobs) { if (job.getClass().isAssignableFrom(jobClass)) { return job; } } throw new RuntimeException("Cannot create a Job of type " + jobClass); }} |
На данный момент все задания могут использовать внедрение зависимостей и внедрять другие зависимости, включая EJB.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
package pl.codeleak.quartzdemo.ejb;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import javax.ejb.Stateless;@Statelesspublic class SimpleEjb { private static final Logger LOG = LoggerFactory.getLogger(SimpleEjb.class); public void doSomething() { LOG.info("Inside an EJB"); }}package pl.codeleak.quartzdemo;import org.quartz.Job;import org.quartz.JobExecutionContext;import org.quartz.JobExecutionException;import pl.codeleak.quartzdemo.ejb.SimpleEjb;import javax.ejb.EJB;import javax.inject.Named;public class SimpleJob implements Job { @EJB // @Inject will work too private SimpleEjb simpleEjb; @Override public void execute(JobExecutionContext context) throws JobExecutionException { simpleEjb.doSomething(); }} |
Последний шаг — изменить SchedulerBean:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
package pl.codeleak.quartzdemo;import org.quartz.*;import org.quartz.impl.StdSchedulerFactory;import org.quartz.impl.matchers.GroupMatcher;import org.quartz.spi.JobFactory;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import javax.annotation.PostConstruct;import javax.annotation.PreDestroy;import javax.ejb.Singleton;import javax.ejb.Startup;import javax.inject.Inject;@Startup@Singletonpublic class SchedulerBean { private Logger LOG = LoggerFactory.getLogger(SchedulerBean.class); private Scheduler scheduler; @Inject private JobFactory cdiJobFactory; @PostConstruct public void scheduleJobs() { try { scheduler = new StdSchedulerFactory().getScheduler(); scheduler.setJobFactory(cdiJobFactory); JobKey job1Key = JobKey.jobKey("job1", "my-jobs"); JobDetail job1 = JobBuilder .newJob(SimpleJob.class) .withIdentity(job1Key) .build(); TriggerKey tk1 = TriggerKey.triggerKey("trigger1", "my-jobs"); Trigger trigger1 = TriggerBuilder .newTrigger() .withIdentity(tk1) .startNow() .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(10)) .build(); scheduler.scheduleJob(job1, trigger1); scheduler.start(); printJobsAndTriggers(scheduler); } catch (SchedulerException e) { LOG.error("Error while creating scheduler", e); } } private void printJobsAndTriggers(Scheduler scheduler) throws SchedulerException { // not changed } @PreDestroy public void stopJobs() { // not changed }} |
Примечание. Перед запуском приложения добавьте файл beans.xml в каталог WEB-INF.
|
1
2
3
4
5
6
7
8
9
|
<?xml version="1.0" encoding="UTF-8"?><beans xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee bean-discovery-mode="all"></beans> |
Теперь вы можете запустить сервер и наблюдать за результатами. Сначала была создана работа и триггер:
|
1
2
3
|
12:08:19,592 INFO (MSC service thread 1-3) Quartz Scheduler: MyScheduler12:08:19,612 INFO (MSC service thread 1-3) Found job identified by my-jobs.job112:08:19,616 INFO (MSC service thread 1-3) Found trigger identified by m |
Наша работа выполняется (примерно каждые 10 секунд):
|
1
2
|
12:08:29,148 INFO (MyScheduler_Worker-1) Inside an EJB12:08:39,165 INFO (MyScheduler_Worker-1) Inside an EJB |
Посмотрите также внутри таблиц Quartz, и вы увидите, что они заполнены данными.
Протестируйте приложение
Последнее, что я хотел проверить, было то, как задания запускаются в нескольких случаях. Для моего теста я просто дважды клонировал конфигурацию сервера в IntelliJ и назначил различное смещение порта для каждой новой копии.

Дополнительные изменения, которые мне нужно было сделать, это изменить создание рабочих мест и триггеров. Поскольку все объекты Quartz хранятся в базе данных, создание одного и того же задания и триггера (с одинаковыми ключами) вызовет исключение:
|
1
|
Error while creating scheduler: org.quartz.ObjectAlreadyExistsException: Unable to store Job : 'my-jobs.job1', because one already exists with this identification. |
Мне нужно было изменить код, чтобы убедиться, что если задание / триггер существует, я его обновлю. Окончательный код метода scheduleJobs для этого теста регистрирует три триггера для одной и той же работы.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
@PostConstructpublic void scheduleJobs() { try { scheduler = new StdSchedulerFactory().getScheduler(); scheduler.setJobFactory(cdiJobFactory); JobKey job1Key = JobKey.jobKey("job1", "my-jobs"); JobDetail job1 = JobBuilder .newJob(SimpleJob.class) .withIdentity(job1Key) .build(); TriggerKey tk1 = TriggerKey.triggerKey("trigger1", "my-jobs"); Trigger trigger1 = TriggerBuilder .newTrigger() .withIdentity(tk1) .startNow() .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(10)) .build(); TriggerKey tk2 = TriggerKey.triggerKey("trigger2", "my-jobs"); Trigger trigger2 = TriggerBuilder .newTrigger() .withIdentity(tk2) .startNow() .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(10)) .build(); TriggerKey tk3 = TriggerKey.triggerKey("trigger3", "my-jobs"); Trigger trigger3 = TriggerBuilder .newTrigger() .withIdentity(tk3) .startNow() .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(10)) .build(); scheduler.scheduleJob(job1, newHashSet(trigger1, trigger2, trigger3), true); scheduler.start(); printJobsAndTriggers(scheduler); } catch (SchedulerException e) { LOG.error("Error while creating scheduler", e); }} |
В дополнение к вышесказанному я добавил запись JobExecutionContext в SimpleJob, чтобы я мог лучше проанализировать результат.
|
01
02
03
04
05
06
07
08
09
10
|
@Overridepublic void execute(JobExecutionContext context) throws JobExecutionException { try { LOG.info("Instance: {}, Trigger: {}, Fired at: {}", context.getScheduler().getSchedulerInstanceId(), context.getTrigger().getKey(), sdf.format(context.getFireTime())); } catch (SchedulerException e) {} simpleEjb.doSomething();} |
После запуска всех трех экземпляров сервера я наблюдал результаты.
Выполнение работы
Я наблюдал выполнение trigger2 на всех трех узлах, и оно было выполнено на трех из них следующим образом:
|
1
2
3
4
5
|
Instance: kolorobot1399805959393 (instance1), Trigger: my-jobs.trigger2, Fired at: 13:00:09Instance: kolorobot1399805989333 (instance3), Trigger: my-jobs.trigger2, Fired at: 13:00:19Instance: kolorobot1399805963359 (instance2), Trigger: my-jobs.trigger2, Fired at: 13:00:29Instance: kolorobot1399805959393 (instance1), Trigger: my-jobs.trigger2, Fired at: 13:00:39Instance: kolorobot1399805959393 (instance1), Trigger: my-jobs.trigger2, Fired at: 13:00:59 |
Аналогично для других триггеров.
восстановление
После того, как я отключил kolorobot1399805989333 (instance3), через некоторое время я увидел следующее в логах:
|
1
2
|
ClusterManager: detected 1 failed or restarted instances.ClusterManager: Scanning for instance "kolorobot1399805989333"'s failed in-progress jobs. |
Затем я отключил kolorobot1399805963359 (instance2), и снова это то, что я видел в журналах:
|
1
2
3
|
ClusterManager: detected 1 failed or restarted instances.ClusterManager: Scanning for instance "kolorobot1399805963359"'s failed in-progress jobs.ClusterManager: ......Freed 1 acquired trigger(s). |
На данный момент все триггеры были выполнены kolorobot1399805959393 (instance1)
Бег на Wildfly 8
Без каких-либо изменений я мог бы развернуть то же приложение на WildFly 8.0.0. Аналогично JBoss 7.1.1 я добавил модуль MySQL (расположение папки модулей отличается в WildFly 8 — modules/system/layers/base/com/mysql/main . Источник данных и драйвер были определены точно так же, как показано выше. Я создал конфигурацию запуска для WildFly 8:

И я запустил приложение, получая те же результаты, что и с JBoss 7.
Я обнаружил, что WildFly предлагает хранилище на основе базы данных для постоянных таймеров EJB , но я еще не исследовал его. Может быть, что-то для другого сообщения в блоге.
Исходный код
- Пожалуйста, найдите исходный код этого сообщения в блоге на GitHub: https://github.com/kolorobot/quartz-jee-demo
| Ссылка: | Как: Кварцевый планировщик с кластеризацией в приложении JEE с MySQL от нашего партнера по JCG Рафаля Боровца в блоге Codeleak.pl . |

