`

Quartz

 
阅读更多

Configuring Quartz with JDBCJobStore in Spring

I am starting a little series about Quartz scheduler internals, tips and tricks, this is a chapter 0 - how to configure persistent job store. In Quartz you essentially have a choice between storing jobs and triggers in memory and in a relation database ( Terracotta is a recent addition to the mix). I would say in 90% of the cases when you use RAMJobStore with Quartz you don't really need Quartz at all. Obviously this storage backend is transient and all your pending jobs and triggers are lost between restarts. If you are fine with that, much simpler and more lightweight solutions are available, including ScheduledExecutorService built into JDK and @Scheduled(cron="*/5 * * * * MON-FRI") in Spring. Can you justify using extra 0,5MiB JAR in this scenario?

This changes dramatically when you need clustering, fail-over, load-balancing and few other buzz-words. There are several use-cases for that:
  • single server cannot handle required number of concurrent, long running jobs and the executions need to be split into several machines - but each task must be executed exactly ones
  • we cannot afford to run jobs too late - if one server is down, another should run the job on time
  • ...or less strictly - the job needs to run eventually - even if the one and only server was down for maintenance, delayed jobs need to be run as soon as possible after restart
In all cases above we need some sort of non-transient global storage to keep track which jobs were executed, so that they are run exactly ones by one machine. Relational database as a shared memory works good in this scenario.

So if you think you need to schedule jobs and have some of the requirements above, keep reading. I will show you how to configure Quartz with Spring and fully integrate them. First of all we need a DataSource:
1
2
3
4
5
6
7
8
9
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
import org.apache.commons.dbcp.BasicDataSource
import com.googlecode.flyway.core.Flyway
import org.jdbcdslog.DataSourceProxy
import org.springframework.jdbc.datasource.{DataSourceTransactionManager, LazyConnectionDataSourceProxy}
import org.h2.Driver
  
@Configuration
@EnableTransactionManagement
class Persistence {
  
    @Bean
    def transactionManager() = new DataSourceTransactionManager(dataSource())
  
    @Bean
    @Primary
    def dataSource() = {
        val proxy = new DataSourceProxy()
        proxy.setTargetDSDirect(dbcpDataSource())
        new LazyConnectionDataSourceProxy(proxy)
    }
  
    @Bean(destroyMethod = "close")
    def dbcpDataSource() = {
        val dataSource = new BasicDataSource
        dataSource.setDriverClassName(classOf[Driver].getName)
        dataSource.setUrl("jdbc:h2:mem:quartz-demo;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MVCC=TRUE")
        dataSource.setUsername("sa")
        dataSource.setPassword("")
        dataSource.setMaxActive(20)
        dataSource.setMaxIdle(20)
        dataSource.setMaxWait(10000)
        dataSource.setInitialSize(5)
        dataSource.setValidationQuery("SELECT 1")
        dataSource
    }
  
}
As you might have guessed, Quartz needs some database tables to work with. It does not create them automatically, but SQL scripts for several databases are provided, including H2 which as you can see I am using. I think Flyway is the easiest way to run database scripts on startup:

1
2
3
4
5
6
@Bean(initMethod = "migrate")
def flyway() = {
 val fly = new Flyway()
 fly.setDataSource(dataSource())
 fly
}
BTW in case you haven't noticed: no, there is no XML in our sample application and yes, we are using Spring.

Let's move on to Quartz:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
class Scheduling {
  
    @Resource
    val persistence: Persistence = null
  
    @Bean
    @DependsOn(Array("flyway"))
    def schedulerFactory() = {
        val schedulerFactoryBean = new SchedulerFactoryBean()
        schedulerFactoryBean.setDataSource(persistence.dataSource())
        schedulerFactoryBean.setTransactionManager(persistence.transactionManager())
        schedulerFactoryBean.setConfigLocation(new ClassPathResource("quartz.properties"))
        schedulerFactoryBean.setJobFactory(jobFactory())
        schedulerFactoryBean.setApplicationContextSchedulerContextKey("applicationContext")
        schedulerFactoryBean.setSchedulerContextAsMap(Map().asJava)
        schedulerFactoryBean.setWaitForJobsToCompleteOnShutdown(true)
        schedulerFactoryBean
    }
  
    @Bean
    def jobFactory() = new SpringBeanJobFactory
  
}
It is nice to know you can inject instance of @Configuration annotated classes into another such class for convenience. Except that - nothing fancy. Note that we need @DependsOn(Array("flyway")) on Quartz scheduler factory - otherwise the scheduler might start before Flyway fired the migration script with Quartz database tables causing unpleasant errors on startup. The essential bits are SpringBeanJobFactory and schedulerContextAsMap. The special factory makes Spring responsible for creating Job instances. Unfortunately this factory is quite limited which we will see shortly in the following example. First we need a Spring bean and a Quartz job:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
class Printer extends Logging {
  
    def print(msg: String) {
        logger.info(msg)
    }
  
}
  
class PrintMessageJob extends Job with Logging {
  
    @BeanProperty
    var printer: Printer = _
  
    @BeanProperty
    var msg = ""
  
    def execute(context: JobExecutionContext) {
        printer print msg
    }
}
First unexpected input is @BeanProperty instead of @Autowired or @Resource. Turns out that Job is not really a Spring bean, even though Spring creates an instance of it. Instead Spring discovers required dependencies using available setters. So where does the msg string come from? Keep going:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
val trigger = newTrigger().
        withIdentity("Every-few-seconds", "Demo").
        withSchedule(
            simpleSchedule().
                    withIntervalInSeconds(4).
                    repeatForever()
        ).
        build()
  
val job = newJob(classOf[PrintMessageJob]).
        withIdentity("Print-message", "Demo").
        usingJobData("msg", "Hello, world!").
        build()
  
scheduler.scheduleJob(job, trigger)
Quartz 2.0 ships with a nice internal DSL for creating jobs and triggers in a readable manner. As you can see I am passing an extra "Hello, world!" parameter to the job. This parameter is stored in so called JobData in the database per job or per trigger. It will be provided to the job when it is executed. This way you can parametrize your jobs. However when executed our job throws NullPointerException... Apparently printer reference was not set and silently ignored. Turns out Spring won't simply look through all the beans available in the ApplicationContext. The SpringBeanJobFactory only looks into Jobs' and Triggers' JobData and into so called scheduler context (already mentioned). If you want to inject any Spring bean into Job you must explicitly place a reference to that bean in schedulerContext:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
class Scheduling {
  
    @Resource
    val printer: Printer = null
  
    @Bean
    def schedulerFactory() = {
        val schedulerFactoryBean = new SchedulerFactoryBean()
        //...
        schedulerFactoryBean.setSchedulerContextAsMap(schedulerContextMap().asJava)
        //...
        schedulerFactoryBean
    }
  
    def schedulerContextMap() =
        Map(
            "printer" -> printer
        )
  
}
Unfortunately each and every Spring bean you want to inject to job has to be explicitly referenced in schedulerContextMap. Even worse, if you forget about it, Quartz will silently log NPE at runtime. In the future we will write more robust job factory. But for starters we have a working Spring + Quartz application ready for further experiments, sources as always available under my GitHub account.

You might ask yourself way can't we simply use MethodInvokingJobDetailFactoryBean? Well, first of all because it does not work with persistent job stores. Secondly - because it is unable to pass JobData to Job - so we can no longer distinguish between different job runs. For instance our job printing message would have to always print the same message hard-coded in the class.

BTW if anyone asks you: How many classes does a Java enterprise developer need to print “Hello world!" you can proudly reply: 4 classes, 30 JARs taking 20 MiB of space and a relational database with 10+ tables. Seriously, this is an output of our article here...

 

 

Quartz scheduler plugins - hidden treasure

Although briefly described in the official documentation, I believe Quartz plugins aren't known enough, looking at how useful they are.

Essentially plugins in Quartz are convenient classes wrapping registration of underlying listeners. You are free to write your own plugins but we will focus on existing ones shipped with Quartz.

LoggingTriggerHistoryPlugin

First some background. Two main abstractions in Quartz are jobs and triggers. Job is a piece of code that we would like to schedule. Trigger instructs the scheduler when this code should run. CRON (e.g. run every Friday between 9 AM and 5 PM until November) and simple (run 100 times every 2 hours) triggers are most commonly used. You associate any number of triggers to a single job.

Believe it or not, Quartz by default provides no logging or monitoring whatsoever of executed jobs and triggers. There is an API, but no built-in logging is implemented. It won't show you that it now executes this particular job due to this trigger firing. So the first thing you should do is adding the following lines to your quartz.properties:

1
2
3
4
5
6
7
org.quartz.plugin.triggerHistory.class=org.quartz.plugins.history.LoggingTriggerHistoryPlugin
  
org.quartz.plugin.triggerHistory.triggerFiredMessage=Trigger [{1}.{0}] fired job [{6}.{5}] scheduled at: {2, date, dd-MM-yyyy HH:mm:ss.SSS}, next scheduled at: {3, date, dd-MM-yyyy HH:mm:ss.SSS}
  
org.quartz.plugin.triggerHistory.triggerCompleteMessage=Trigger [{1}.{0}] completed firing job [{6}.{5}] with resulting trigger instruction code: {9}. Next scheduled at: {3, date, dd-MM-yyyy HH:mm:ss.SSS}
  
org.quartz.plugin.triggerHistory.triggerMisfiredMessage=Trigger [{1}.{0}] misfired job [{6}.{5}]. Should have fired at: {3, date, dd-MM-yyyy HH:mm:ss.SSS}
The first line (and the only required) loads the plugin class LoggingTriggerHistoryPlugin. The remaining lines are configuring the plugin, customizing the logging messages. I found the built-in defaults not very well thought, e.g. they display current time which is already part of the logging framework message. You are free to construct any logging message, see the API for details. Adding these extra few lines makes debugging and monitoring much easier:
1
2
3
LoggingTriggerHistoryPlugin | Trigger [Demo.Every-few-seconds] fired job [Demo.Print-message] scheduled at:  04-04-2012 23:23:47.036, next scheduled at:  04-04-2012 23:23:51.036
//...job output
LoggingTriggerHistoryPlugin | Trigger [Demo.Every-few-seconds] completed firing job [Demo.Print-message] with resulting trigger instruction code: DO NOTHING. Next scheduled at:  04-04-2012 23:23:51.036
You see now why naming your triggers (Demo.Every-few-seconds) and jobs (Demo.Print-message) is so important.

LoggingJobHistoryPlugin

There is another handy plugin related to logging:
1
2
3
4
5
org.quartz.plugin.jobHistory.class=org.quartz.plugins.history.LoggingJobHistoryPlugin
org.quartz.plugin.jobHistory.jobToBeFiredMessage=Job [{1}.{0}] to be fired by trigger [{4}.{3}], re-fire: {7}
org.quartz.plugin.jobHistory.jobSuccessMessage=Job [{1}.{0}] execution complete and reports: {8}
org.quartz.plugin.jobHistory.jobFailedMessage=Job [{1}.{0}] execution failed with exception: {8}
org.quartz.plugin.jobHistory.jobWasVetoedMessage=Job [{1}.{0}] was vetoed. It was to be fired by trigger [{4}.{3}] at: {2, date, dd-MM-yyyy HH:mm:ss.SSS}
The rule is the same - plugin + extra configuration. See JavaDoc of LoggingJobHistoryPlugin for details and possible placeholders. Quick look at logs reveals very descriptive output:
1
2
3
4
5
Trigger [Demo.Every-few-seconds] fired job [Demo.Print-message] scheduled at:  04-04-2012 23:34:53.739, next scheduled at:  04-04-2012 23:34:57.739
Job [Demo.Print-message] to be fired by trigger [Demo.Every-few-seconds], re-fire: 0
//...job output
Job [Demo.Print-message] execution complete and reports: null
Trigger [Demo.Every-few-seconds] completed firing job [Demo.Print-message] with resulting trigger instruction code: DO NOTHING. Next scheduled at:  04-04-2012 23:34:57.739
I have no idea why these plugins aren't enabled by default. After all, if you don't want such a verbose output, you can turn it off in your logging framework. Never mind, I think it is a good idea to have them in place when troubleshooting Quartz execution.

XMLSchedulingDataProcessorPlugin

This is a pretty comprehensive plugin. It reads XML file (by default named quartz_data.xml) containing jobs and triggers definitions and adds them to the scheduler. This is especially useful when you have a global job that you need to add once. Plugin can either update the existing jobs/triggers or ignore the XML file if they already exist - very useful when JDBCJobStore is used.
1
org.quartz.plugin.xmlScheduling.class=org.quartz.plugins.xml.XMLSchedulingDataProcessorPlugin
In the aforementioned article we have been manually adding job to the scheduler:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
val trigger = newTrigger().
        withIdentity("Every-few-seconds", "Demo").
        withSchedule(
            simpleSchedule().
                    withIntervalInSeconds(4).
                    repeatForever()
        ).
        build()
   
val job = newJob(classOf[PrintMessageJob]).
        withIdentity("Print-message", "Demo").
        usingJobData("msg", "Hello, world!").
        build()
   
scheduler.scheduleJob(job, trigger)
The same can be achieved with XML configuration, just place the following quartz_data.xml in your CLASSPATH:
1
2
3
4
5
6
7
8
9
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
<?xml version="1.0" encoding="UTF-8"?>
                     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  
    <processing-directives>
        <overwrite-existing-data>false</overwrite-existing-data>
        <ignore-duplicates>true</ignore-duplicates>
    </processing-directives>
  
    <schedule>
        <trigger>
            <simple>
                <name>Every-few-seconds</name>
                <group>Demo</group>
                <job-name>Print-message</job-name>
                <job-group>Demo</job-group>
                <repeat-count>-1</repeat-count>
                <repeat-interval>4000</repeat-interval>
            </simple>
        </trigger>
  
        <job>
            <name>Print-message</name>
            <group>Demo</group>
            <job-class>com.blogspot.nurkiewicz.quartz.demo.PrintMessageJob</job-class>
            <job-data-map>
                <entry>
                    <key>msg</key>
                    <value>Hello, World!</value>
                </entry>
            </job-data-map>
        </job>
  
    </schedule>
  
  
</job-scheduling-data>
The file supports both simple and CRON triggers and is well described using XML Schema. It is even possible to point out to an XML files somewhere in the file system and periodically scan them for changes (!) (see: XMLSchedulingDataProcessorPlugin.setScanInterval(). Guess what is Quartz using to schedule periodic scanning?
1
2
org.quartz.plugin.xmlScheduling.fileNames=/etc/quartz/system-jobs.xml,/home/johnny/my-jobs.xml
org.quartz.plugin.xmlScheduling.scanInterval=60

ShutdownHookPlugin

Last but not least, ShutdownHookPlugin. Small but probably useful plugin that register shutdown hook in the JVM in order to gently stop the scheduler. However I recommend turning cleanShutdown off - if the system already tries to abruptly stop the application (typically scheduler shutdown is called by Spring via SchedulerFactoryBean) or the user hit Ctrl+C - waiting for currently running jobs seems like a bad idea. After all, maybe we are killing the application because some jobs are running for too long/hunging?
1
2
org.quartz.plugin.shutdownHook.class=org.quartz.plugins.management.ShutdownHookPlugin
org.quartz.plugin.shutdownHook.cleanShutdown=false
As you can see Qurtz ships with few quite interesting plugins. For some reason they aren't described in detail in the official documentation, but they work pretty well and are a valuable addition to scheduler.

The source code with applied plugins is available on GitHub.

 

分享到:
评论

相关推荐

    quartz-2.3.2-API文档-中文版.zip

    赠送jar包:quartz-2.3.2.jar; 赠送原API文档:quartz-2.3.2-javadoc.jar; 赠送源代码:quartz-2.3.2-sources.jar; 赠送Maven依赖信息文件:quartz-2.3.2.pom; 包含翻译后的API文档:quartz-2.3.2-javadoc-API...

    quartz-2.3.0-API文档-中文版.zip

    赠送jar包:quartz-2.3.0.jar; 赠送原API文档:quartz-2.3.0-javadoc.jar; 赠送源代码:quartz-2.3.0-sources.jar; 赠送Maven依赖信息文件:quartz-2.3.0.pom; 包含翻译后的API文档:quartz-2.3.0-javadoc-API...

    quartz指南,Quartz 工程

    文件里面包括 1:Quartz开发指南.pdf 2:Quartz从入门到进阶.pdf 3:QuartzBeginnerExample一个附带的工程例子 4:quartz-1.6.1.zip Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,它...

    自开发实现Quartz Web管理工具

    网上能找到的Quartz Web管理的资料都是使用的一个国外人写的Quartz WebApp的东东,功能也很全面。但是作为自己的应用其实用不了那么多功能,一般我们只要可以定义一个job,指定一个Cron表达式完成工作即可,附带的...

    quartz quartz-1.8.6 dbTables 建表sql

    quartz quartz-1.8.6 dbTables quartz动态任务调度需要的数据库脚本。

    Quartz.NET-2.0

    Quartz.NET框架的核心是调度器。调度器负责管理Quartz.NET应用运行时环境。Quartz不仅仅是线程和线程管理。为确保可伸缩性,Quartz.NET采用了基于多线程的架构。启动时,框架初始化一套worker线程,这套线程被调度器...

    quartz-1.6.0.jar和quartz-all-1.6.0.jar

    该压缩包内包含两个quartz的jar包, 分别是quartz-1.6.0.jar和quartz-all-1.6.0.jar

    quartz-2.2.3版本的quartz初始化sql语句

    quartz-2.2.3版本的quartz初始化sql语句

    quartz-2.1.1 完整源码

    Quartz是一个开源的作业调度框架,它完全由Java写成,并设计用于J2SE和J2EE应用中。它提供了巨大的灵 活性而不牺牲简单性。你能够用它来为执行一个作业而创建简单的或复杂的调度 What's New In Quartz Scheduler ...

    Quartz原理及实例

    Quartz原理及实例,spring4.x+Quartz.2.2.1结合的开发,静态和动态实例

    Quartz-2.0.2 CSDN下载

    Quartz框架的核心是调度器。调度器负责管理Quartz应用运行时环境。调度器不是靠自己做所有的工作,而是依赖框架内一些非常重要的部件。 Quartz不仅仅是线程和线程管理。为确保可伸缩性,Quartz采用了基于多线程的...

    quartz-2.3.0-API文档-中英对照版.zip

    赠送jar包:quartz-2.3.0.jar; 赠送原API文档:quartz-2.3.0-javadoc.jar; 赠送源代码:quartz-2.3.0-sources.jar; 赠送Maven依赖信息文件:quartz-2.3.0.pom; 包含翻译后的API文档:quartz-2.3.0-javadoc-API...

    quartz-1.6.1-API文档-中文版.zip

    赠送jar包:quartz-1.6.1.jar; 赠送原API文档:quartz-1.6.1-javadoc.jar; 包含翻译后的API文档:quartz-1.6.1-javadoc-API文档-中文(简体)版.zip 对应Maven信息:groupId:org.opensymphony.quartz,...

    quartz官方数据库大全

    Quartz是一个功能丰富的开源作业调度库,几乎可以集成在任何Java应用程序中 - 从最小的独立应用程序到最大的电子商务系统。Quartz可用于创建简单或复杂的计划,以执行数十,数百甚至数万个作业; 将任务定义为标准...

    Quartz-1.8.6 CSDN 下载

    Quartz框架的核心是调度器。调度器负责管理Quartz应用运行时环境。调度器不是靠自己做所有的工作,而是依赖框架内一些非常重要的部件。 Quartz不仅仅是线程和线程管理。为确保可伸缩性,Quartz采用了基于多线程的...

    Quartz-2.2.3 CSDN 下载

    Quartz框架的核心是调度器。调度器负责管理Quartz应用运行时环境。调度器不是靠自己做所有的工作,而是依赖框架内一些非常重要的部件。 Quartz不仅仅是线程和线程管理。为确保可伸缩性,Quartz采用了基于多线程的...

    Quartz.NET 调度系统 demo

    前言:8月份翻译了Quartz.NET的官方课程:开源的作业调度框架 - Quartz.NET, 有的朋友抱怨难用,确实,目前Qiartz.NET的最新版本还是0.6,还存在很多bug和不完善的地方。本文使用一系列代码示例介绍 Quartz.NET API...

    深入解读Quartz的原理

    深入解读Quartz的原理,定时任务框架是web开发过程中使用很多的框架之一

    quartz-1.4.5.zip

    Quartz is a richly featured, open source job scheduling library that can be integrated within virtually any Java application - from the smallest stand-alone application to the largest e-commerce ...

    C# 控制台Quartz定时任务.doc

    Quartz定时任务

Global site tag (gtag.js) - Google Analytics