Quartz Tutorial 2 - More About Jobs and Job Details
就像是在上一节看到的,Job
相当容易去实现,毕竟接口中只有一个execute
方法。这里只需要再了解一些别的事情你就能理解作业的本质,关于Job
接口中的execute
方法,关于JobDetail
类。
当一个实现了Job
接口的类定义完成,它要实际进行的工作也就很清晰了,但Quartz还需要知道你希望一个作业实例所具有的其它属性。那些属性是通过JobDetail
类来告知Quartz的,前面已经简单介绍过。
JobDetail
实例是用JobBuilder
类来构建的,可以使用静态导入该类中所有的静态方法,使代码具有DSL风格。
|
|
首先花点时间讨论一下Job
的本质和一个Job
实例在Quartz内的生命周期。首先回顾一下第一节中的代码
|
|
作业的实体类HelloJob
是这样定义的
|
|
注意到,我们给调度器传递了一个JobDetail
实例,它知道我们要执行的作业的实际类型,因为我们构造它的时候传递进去了。每次调度器执行这个作业之前,它都会先创建一个新的HelloJob
实例,然后调用execute
方法。作业一旦执行完毕,调度器对这个对象实例的引用便会丢弃,这个实例就会被垃圾回收。能这样做的一个条件是,Job
的实现类必须有一个公共的无参构造器(当然,此时使用的是默认的JobFactory
);另一个就是这个实现类不需要有什么表示状态的数据成员,这没什么意义,一旦作业执行完毕,这些数据成员也就不再保留了。
那么我们如何为作业实例提供属性或者配置?或者怎样在执行过程中继续跟踪它的状态呢?这里提供了JobDataMap
,它也是JobDetail
对象的一部分。
JobDataMap
JobDataMap
可以持有你想让作业执行期间能访问到的(可序列化的)数据对象,它是Map接口的一种实现,此外还添加了一些方法使之能方便存取基本类型的数据。
如下一段代码就是在定义/构建JobDetail
阶段,将一些数据放置到JobDataMap
对象中。
|
|
如下代码就是在作业执行期间,去JobDataMap
中获取数据
|
|
如果你使用不变的JobStore
(将会在本系列的JobStore
一节讨论),你应当慎重决定将什么放入到JobDataMap
,因为该对象将会被序列化,因此同意出现类版本问题。当然标准的Java类型应该很安全,但如果有人修改了类型定义而你又序列化了实例,要小心,免得破坏了兼容性。你也可以考虑将JDBC-JobStore
和JobDataMap
置于只能放基本类型和字符串的模式,这样就能消除未来可能的序列化问题。
如果在你的作业类中添加了set
访问器,而且它的名字与JobDataMap
中某个键的名字是符合的(例如setJobSays(String val)
与jobSays
),那么Quartz的默认JobFactory
类会在实例化这个作业的时候自动调用这些方法,这样就不用在代码中显式获取了。
触发器也可以拥有与之关联的JobDataMap
。当你有个储存在调度器中的作业要被多个触发器常规/重复使用的时候,它就非常有用了。对于每次互相独立的触发,你可以给作业提供不同的输入数据。
在作业执行期间可以在JobExecutionContext
找到一个JobDataMap
,它合并了在JobDetail
和Trigger
中的JobDataMap
,而且,对于相同名字的值,后者将会覆盖掉前者。
下面是一个从JobExecutionContext
中找到并使用合并后的JobDataMap
的例子:
|
|
或者也可以用到JobFactory
的注入功能,如下例所示:
|
|
这段代码确实更长了,但是execute()
方法也确实清晰了。而且像是set
构造器可以用IDE自动生成。
Job “Instance”
有很多用户对于一个“作业实例”的确切形式感到疑惑,我们在这一节和后面两小节将之解释清楚。
你可以创建一个单独的作业类,然后通过在调度器内创建多个JobDetail
实例来保存它的“实例定义”,每个JobDetail
都有他自己的属性集合及JobDataMap
,然后将它们都放到调度器中。
例如,你可以创建一个实现了Job
接口的类,叫做SalesReportJob
。这个作业类需要接受一个参数,来得知消费者的名字然后生成针对他的消费报告。因此可能需要创建多个作业的定义,例如“SalesReportForJoe
”或“SalesReportForMike
”。
当一个触发器被触发,其关联的JobDetail
实例将会被加载,然后它用到的作业类将会通过调度器配置的JobFactory
来实例化。默认的JobFactory
只是简单的调用newInstance()
,然后尝试调用名字和JobDataMap中名字匹配的set
访问器来赋值。或许你会需要构造你自己的JobFactory
的实现来做一些事,例如你自己的控制反转或依赖注入容器来生成/初始化作业实例。
我们将每个JobDetail
称为一个“作业定义”或者“作业明细的实例”;我们还将每个正在执行的作业称为一个“作业实例”或“作业定义的一个实例”。通常,如果我们只是简单用到了“作业”一词,这指的是一个命名好的定义,或者作业明细。如果我们要说Job
接口的实现类,我们会使用“作业类”这个词语。
Job State and Concurrency
现在我们将解释一些关于作业的状态数据(JobDataMap
)和并发性。它们是能加到你的作业类定义上的注解,它们将会影响Quartz的行为。
@DisallowConcurrentExecution
注解是加到作业类上的,它告知Quartz不要并发执行给定作业的多个实例。注意这里的措辞,这是反复推敲过的。在上一节的例子中,如果“SalesReportJob
”有这个注解,在给定时刻,只能有一个“SalesReportForJoe
”运行,但是它可以和一个“SalesReportForMike
”同时运行。这个限制是基于JobDetail
,而不是基于作业类的实例。
@PersistJobDataAfterExecution
也是给作业类加的注解。它告知Quartz,只有execute
方法完全成功执行完毕之后才更新存储JobDetail
的JobDataMap
,因为同一个作业的下一次执行将会使用更新后的值,而不是原来的值。就像上面介绍的@DisallowConcurrentExecution
注解,它也是限制作业定义实例而不是作业类实例的。
如果你使用了@PersistJobDataAfterExecution
注解,那你也应当认真考虑是否也使用@DisallowConcurrentExecution
注解,这是为了避免同一作业的两个实例同时执行时出现可能的竞态条件。
Other Attributes Of Jobs
这里是其它能通过JobDetail对象对作业进行定义的属性:
- Durability - 如果一个作业是非耐用的,那么一旦调度器中没有活动触发器与之关联,他将会被自动删除。换句话说,非持久作业的生命周期受到与它关联的触发器限制。
- RequestsRecovery - 如果一个作业“请求恢复”,而且在它执行期间被调度器“强制关闭”(例如进程运行中崩溃,或者电脑关机),那么当调度器再次启动时,这个作业也将再次执行。这种情况下,
JobExecutionContext.isRecovering()
将返回true
。
JobExecutionException
最后,我们需要介绍Job.execute
方法的一些其它细节。在该方法中,唯一能丢出的异常(包括运行时异常)类型是JobExecutionException
。因此你应该使用try-catch语句块包裹execute
方法的全部内容。你也应当花时间看看这个异常的文档,因为你的作业能够适应该异常来告知调度器具体使用哪种方式来处理这个出现异常的作业。