Java 任务调度 - Quartz

打算为自己的项目加上一些定时调度功能,使用Timer不能完全满足需要(很弱= =),所以决定试一下Quartz(本文笔记源于慕课网相关课程)。

Quartz具备以下特点:

  • 强大的调度功能
  • 灵活的应用方式
  • 分布式和集群能力

基本概念

设计模式:

  • Builder模式
  • Factory模式
  • 组件模式
  • 链式写法

核心概念:

  • 调度器(Scheduler)
  • 任务(JobDetail)
  • 触发器(SimpleTrigger,CronTrigger)

重要组成:

  • Job(定义运行任务的接口)
  • JobDetail(描述Job实现类和相关静态信息)
  • JobBuilder(定义和创建JobDetail实例)
  • JobStore(存放Job数据)
  • Trigger(Job的时间触发规则)
  • TriggerBuilder(创建Trigger)
  • ThreadPool(提供运行任务的线程)
  • Scheduler(独立运行容器,分组和名称注册JobDetail、Trigger)
  • Calendar(一个Trigger可以和多个Calendar关联,排除或包含某时间点)
  • Listener(JobListener,TriggerListener,SchedulerListener)

Demo

pom.xml

    <dependency>
        <groupId>org.quartz-scheduler</groupId>
        <artifactId>quartz</artifactId>
        <version>2.2.3</version>
    </dependency>

HelloJob.java

package com.imooc.demo.HelloQuartz.job;

import java.text.SimpleDateFormat;
import java.util.Date;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class HelloJob implements Job {
    public void execute(JobExecutionContext context) throws JobExecutionException {
        Date date = new Date();
        SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("Current Exec Time Is : " + sf.format(date));
        System.out.println("Hello World!");
    }
}

HelloScheduler.java

package com.imooc.demo.HelloQuartz.scheduler;

import com.imooc.demo.HelloQuartz.job.HelloJob;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

    public class HelloScheduler {
    public static void main(String[] args) throws SchedulerException {

        // 创建JobDetail实例,将该实例与HelloJob Class绑定
        JobDetail jobDetail = JobBuilder.newJob(HelloJob.class)
            .withIdentity("myJob", "group1").build();    // 指定Job名称和组名称

        // 创建Trigger实例
        Trigger trigger = TriggerBuilder
            .newTrigger()
            .withIdentity("myTrigger", "group1")    // 指定trigger名称和组名称
            .startNow()    // 立即执行
            .withSchedule(
                SimpleScheduleBuilder.simpleSchedule()
                    .withIntervalInSeconds(1).repeatForever())    // 定义执行频度:每隔2s执行一次,不停止
            .build();

        // 创建Scheduler实例
        SchedulerFactory sfact = new StdSchedulerFactory();    // 创建SchedulerFactory
        Scheduler scheduler = sfact.getScheduler();    // 获取Scheduler实例
        scheduler.start();

        scheduler.scheduleJob(jobDetail, trigger);    // 绑定JobDetail和Trigger
    }
}

Job

public interface Job {
    void execute(JobExecutionContext context)
        throws JobExecutionException;
}

生命周期

每次调度器执行job时,调用execute方法前会创建一个新的Job实例。调用完成后,关联的Job对象会被释放、GC。

JobDetail

为Job提供设置属性,以及JobDataMap成员变量属性,用来存储指定Job实例的状态信息,调度器需要借助JobDetail对象添加Job实例。

  • name
  • group
  • jobClass
  • jobDataMap
  • ...

JobExecutionContext

  • 当Scheduler调用一个Job,会将JobExecutionContext传递给Job的execute()方法;
  • Job能通过JobExecutionContext对象访问到Quartz运行时的环境以及Job本身的明细数据。

JobDataMap

  • 在执行任务调度时JobDataMap存储在JobExecutionContext中,方便获取;
  • JobDataMap可以装在任何可序列化的数据对象,当Job实例对象呗执行时这些参数对象会传递给它;
  • JobDataMap实现了JDK的Map接口,并添加了非常方便的方法来存取基本数据类型。

获取JobDataMap:

  • 从Map中直接获取;
  • Job实现类中添加与JobDataMap的key同名的成员变量,并实现其Setter方法(Quartz框架默认的JobFactory实现类在初始化Job实例对象时会自动调用这些Setter方法)。

Trigger

告知调度程序触发时间来执行Job。

  • JobKey:Job实例的标识;
  • StartTime:首次执行的时间(java.util.Date);
  • EndTime:最后一次触发的时间。

SimpleTrigger

  • 在指定时间段内执行一次作业任务,或指定时间间隔内多次执行作业任务;
  • 重复次数可传入0,正整数,SimpleTrigger.REPEAT_INDEFINITELY常量值;
  • 重复执行间隔必须为0或长整数。

CronTrigger

  • 基于日历的触发器,比SimpleTrigger更常用;
  • 使用Cron表达式配置:[秒] [分] [时] [日] [月] [周] [年]:
字段 是否必填 允许值 允许特殊字符
0~59 ,-*/
0~59 ,-*/
0~23 ,-*/
1-31 ,-*?/LWC
1-12或JAN-DEC ,-*/
1-7或SUN-SAT ,-*/LC#
empty, 1970-2099 ,-*/

通配符

通配符 含义
* 表示所有值。例如在分的字段上设置"*",表示每分钟都会触发。
? 表示不指定值。使用的场景为不需要关心当前设置这个字段的值。例如要在每月的10号触发一个操作,但不关心是周几,所以需要周位置的那个字段设置为"?",具体设置为 0 0 0 10 * ?
- 表示区间。例如在小时上设置"10-12",表示10,11,12点都会触发。
, 表示指定多个值,例如在周字段上设置 "MON,WED,FRI" 表示周一、周三和周五触发
/ 用于递增触发。如在秒上面设置"5/15" 表示从5秒开始,每增15秒触发5,20,35,50。在月字段上设置“1/3”所示每月1号开始,每隔三天触发一次。
L 表示最后的意思。在日字段设置上,表示当月的最后一天(依据当前月份,如果是二月还会依据是否是润年[leap]), 在周字段上表示星期六,相当于"7"或"SAT"。如果在"L"前加上数字,则表示该数据的最后一个。例如在周字段上设置"6L"这样的格式,则表示“本月最后一个星期五"
W 表示离指定日期的最近那个工作日(周一至周五)。 例如在日字段上设置"15W",表示离每月15号最近的那个工作日触发。如果15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发。如果15号正好在工作日(周一至周五),则就在该天触发。如果指定格式为"1W",它则表示每月1号往后最近的工作日触发。如果1号正是周六,则将在3号下周一触发。(注:"W"前只能设置具体的数字,不允许区间"-")。
# 序号(表示每月的第几个周几),例如在周字段上设置“6#3”表示在每月的第三个周六。注意如果指定“#5”,正好第五周没有周六,则不会触发该配置(用在母亲节和父亲节再合适不过了)。

“L”和“W”可以一组合使用。如果在日字段上设置"LW",则表示在本月的最后一个工作日触发(一般指发工资 )

一些实例

表达式 含义
0 15 10 ? * * 每天10点15分
0 0/5 14 * * ? 每天下午的2点-2点59分,每5分钟
0 15 10 ? * MON-FRI 周一到周五每天10点15分
0 15 10 ? * 6#3 每月第三周的周五
0 15 10 ? * 6L 2016-2017 2016-2017每月最后一周的周五10点15分

Scheduler

所有Scheduler实由有SchedulerFactory创建;

StdScheduler

  • 使用一组参数java.util.Properties创建和初始化Quartz调度器(quartz.properties);
  • 调用getScheduler方法创建和初始化调度器对象。

主要函数:

  • scheduleJob
  • start
  • standby(挂起,调用start恢复)
  • shutdown(是否等待job完成后再关闭)

DirectScheduler

quartz.properties

声明式配置,默认读取工程下的配置文件,不存在则读取Jar包中的。

配置文件详解

  • 调度器属性(instanceName,instanceId)
  • 线程池属性(threadCount,threadPriority,threadPool.class)
  • 作业存储配置(Job、Trigger信息存储方式)
  • 插件配置
# Default Properties file for use by StdSchedulerFactory
# to create a Quartz Scheduler Instance, if a different
# properties file is not explicitly specified.

# ===========================================================================
# 调度器属性:Configure Main Scheduler Properties 
# ===========================================================================
org.quartz.scheduler.instanceName: DefaultQuartzScheduler
org.quartz.scheduler.instanceid:AUTO
org.quartz.scheduler.rmi.export: false
org.quartz.scheduler.rmi.proxy: false
org.quartz.scheduler.wrapJobExecutionInUserTransaction: false

# ===========================================================================  
# 线程池属性:Configure ThreadPool 
# ===========================================================================
# 线程池的实现类(一般使用SimpleThreadPool即可满足几乎所有用户的需求)
org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
# 指定线程数,至少为1(无默认值)(一般设置为1-100直接的整数合适)
org.quartz.threadPool.threadCount: 10
# 设置线程的优先级(最大为java.lang.Thread.MAX_PRIORITY 10,最小为Thread.MIN_PRIORITY 1,默认为5)
org.quartz.threadPool.threadPriority: 5
# 设置SimpleThreadPool的一些属性
# 设置是否为守护线程
# org.quartz.threadpool.makethreadsdaemons = false
# org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true
# org.quartz.threadpool.threadsinheritgroupofinitializingthread=false
# 线程前缀默认值是:[Scheduler Name]_Worker
# org.quartz.threadpool.threadnameprefix=swhJobThead;
# 配置全局监听(TriggerListener,JobListener) 则应用程序可以接收和执行 预定的事件通知

# ===========================================================================
# 全局的Trigger监听器:Configuring a Global TriggerListener
# MyTriggerListenerClass 类必须有一个无参数的构造函数,和 属性的set方法,目前2.2.x只支持原始数据类型的值(包括字符串)
# ===========================================================================
# org.quartz.triggerListener.NAME.class = com.swh.MyTriggerListenerClass
# org.quartz.triggerListener.NAME.propName = propValue
# org.quartz.triggerListener.NAME.prop2Name = prop2Value

# ===========================================================================
# 全局的Job监听器:Configuring a Global JobListener
# MyJobListenerClass 类必须有一个无参数的构造函数,和 属性的set方法,目前2.2.x只支持原始数据类型的值(包括字符串)
# ===========================================================================
# org.quartz.jobListener.NAME.class = com.swh.MyJobListenerClass
# org.quartz.jobListener.NAME.propName = propValue
# org.quartz.jobListener.NAME.prop2Name = prop2Value

# ===========================================================================  
# 存储调度信息(工作,触发器和日历等):Configure JobStore 
# ===========================================================================
# 信息保存时间 默认值60秒
org.quartz.jobStore.misfireThreshold: 60000
# 保存job和Trigger的状态信息到内存中的类
org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore

# ===========================================================================  
# 插件属性配置:Configure SchedulerPlugins
# ===========================================================================
# 自定义插件  
# org.quartz.plugin.NAME.class = com.swh.MyPluginClass
# org.quartz.plugin.NAME.propName = propValue
# org.quartz.plugin.NAME.prop2Name = prop2Value
# 配置trigger执行历史日志(可以看到类的文档和参数列表)
org.quartz.plugin.triggHistory.class = org.quartz.plugins.history.LoggingTriggerHistoryPlugin  
org.quartz.plugin.triggHistory.triggerFiredMessage = Trigger {1}.{0} fired job {6}.{5} at: {4, date, HH:mm:ss MM/dd/yyyy}  
org.quartz.plugin.triggHistory.triggerCompleteMessage = Trigger {1}.{0} completed firing job {6}.{5} at {4, date, HH:mm:ss MM/dd/yyyy} with resulting trigger instruction code: {9}  
# 配置job调度插件  quartz_jobs(jobs and triggers内容)的XML文档  
# 加载 Job 和 Trigger 信息的类   (1.8之前用:org.quartz.plugins.xml.JobInitializationPlugin)
org.quartz.plugin.jobInitializer.class = org.quartz.plugins.xml.XMLSchedulingDataProcessorPlugin
# 指定存放调度器(Job 和 Trigger)信息的xml文件,默认是classpath下quartz_jobs.xml
org.quartz.plugin.jobInitializer.fileNames = my_quartz_job2.xml  
# org.quartz.plugin.jobInitializer.overWriteExistingJobs = false  
org.quartz.plugin.jobInitializer.failOnFileNotFound = true  
# 自动扫描任务单并发现改动的时间间隔,单位为秒
org.quartz.plugin.jobInitializer.scanInterval = 10
# 覆盖任务调度器中同名的jobDetail,避免只修改了CronExpression所造成的不能重新生效情况
org.quartz.plugin.jobInitializer.wrapInUserTransaction = false

# ===========================================================================  
# 插件的配置样例:Sample configuration of ShutdownHookPlugin  ShutdownHookPlugin
# ===========================================================================
# org.quartz.plugin.shutdownhook.class = \org.quartz.plugins.management.ShutdownHookPlugin
# org.quartz.plugin.shutdownhook.cleanShutdown = true
#
# Configure RMI Settings 远程服务调用配置
#
# 如果你想quartz-scheduler出口本身通过RMI作为服务器,然后设置“出口”标志true(默认值为false)。
# org.quartz.scheduler.rmi.export = false
# 主机上rmi注册表(默认值localhost)
# org.quartz.scheduler.rmi.registryhost = localhost
# 注册监听端口号(默认值1099)
# org.quartz.scheduler.rmi.registryport = 1099
# 创建rmi注册,false/never:如果你已经有一个在运行或不想进行创建注册
# true/as_needed:第一次尝试使用现有的注册,然后再回来进行创建
# always:先进行创建一个注册,然后再使用回来使用注册
# org.quartz.scheduler.rmi.createregistry = never
# Quartz Scheduler服务端端口,默认是随机分配RMI注册表
# org.quartz.scheduler.rmi.serverport = 1098
# true:链接远程服务调度(客户端),这个也要指定registryhost和registryport,默认为false
# 如果export和proxy同时指定为true,则export的设置将被忽略
# org.quartz.scheduler.rmi.proxy = false

整合到Spring

pom.xml

    <!-- Spring 上下文 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-support</artifactId>
        <version>${spring.version}</version>
    </dependency>

    <!-- Spring 事务 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-tx</artifactId>
        <version>${spring.version}</version>
    </dependency>

    <!-- Quartz -->
    <dependency>
        <groupId>org.quartz-scheduler</groupId>
        <artifactId>quartz</artifactId>
        <version>2.2.3</version>
    </dependency>

配置作业

MethodInvokingJobDetailFactoryBean

方便调用某个特定Bean。

dispatcher-servlet.xml

    <bean id="simpleJobDetail"
        class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
        <property name="targetObject" ref="myBean" />
        <property name="targetMethod" value="printMessage" />
    </bean>

MyBean.java

@Component("myBean")
public class MyBean {
    public void printMessage() {
        // 打印当前的执行时间,格式为2017-01-01 00:00:00
        Date date = new Date();
        SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("MyBean Executes!" + sf.format(date));
    }
}

JobDetailFactoryBean

便于给作业传递数据,更灵活。

dispatcher-servlet.xml

    <bean id="firstComplexJobDetail" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
        <property name="jobClass"
            value="com.imooc.springquartz.quartz.FirstScheduledJob" />
        <property name="jobDataMap">
            <map>
                <entry key="anotherBean" value-ref="anotherBean" />
            </map>
        </property>
        <property name="Durability" value="true"/>              
    </bean>

FirstScheduledJob.java

public class FirstScheduledJob extends QuartzJobBean{
     private AnotherBean anotherBean;
     
     public void setAnotherBean(AnotherBean anotherBean){
         this.anotherBean = anotherBean;
     }

    @Override
    protected void executeInternal(JobExecutionContext arg0)
            throws JobExecutionException {
        Date date = new Date();
        SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("FirstScheduledJob Executes!" + sf.format(date));
        this.anotherBean.printAnotherMessage();     
    }
}

Spring Boot集成:https://www.cnblogs.com/lic309/p/4089633.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342