#1.引言前端
Quartz是一个开源的任务调度框架。基于定时、按期的策略来执行任务是它的核心功能,好比2018年除夕夜晚上8点发红包或者打开【木子道】公众号,每隔10分钟发1次红包。Quartz有3个核心要素:调度器(Scheduler)、任务(Job)、触发器(Trigger)。Quartz彻底使用Java开发,能够集成到各类规模的应用程序中。它可以承载成千上万的任务调度,而且支持集群。它支持将数据存储到数据库中以实现持久化,并支持绝大多数的数据库。它将任务与触发设计为松耦合,即一个任务能够对应多个触发器,这样可以轻松构造出极为复杂的触发策略。java
#2.Quartz 核心API介绍(版本2.3.0)数据库
Scheduler :与调度器交互的主要接口。express
Job:实现该接口组件能被调度程序执行的任务类。服务器
JobDetail:用于定义Job的实例。微信
Trigger:触发器,定义一个Job如何被调度器执行。app
TriggerBuilder:触发器建立器,用于建立触发器Trigger实例。框架
JobBuilder :用于声明一个任务实例,也能够定义关于该任务的详情好比任务名、组名等,这个声明的实例将会做为一个实际执行的任务。ide
Schedulersvn
void start() ; 开始启动Scheduler的线程Trigger。 void shutdown() ;关闭Scheduler的Trigger,并清理与调度程序相关联的全部资源。 boolean isShutdown(); Scheduler是否关闭 Date scheduleJob(JobDetail var1, Trigger var2) ;将该给定添加JobDetail到调度程序,并将给定Trigger与它关联 void triggerJob(JobKey var1, JobDataMap var2) ;触发标识JobDetail(当即执行)。 void pauseJob(JobKey var1) ; 暂停任务 void resumeJob(JobKey var1); 恢复任务 boolean deleteJob(JobKey var1); 删除任务 ......
Scheduler由SchedulerFactory建立,并随着shutdown方法的调用而终止。建立后它将可被用来添加、删除或列出Job和Trigger,或执行一些调度相关的工做。Scheduler 建立完成后,处于“待命”模式,而且必须先start()调用其方法才能触发任何Jobs只有经过start()方法启动后它才会真的工做。
Job
//这个接口由表明要执行的“工做”的类来实现。实例Job必须有一个public 无参数的构造函数。 void execute(JobExecutionContext var1) ;
当job的一个trigger被触发后,execute()方法会被scheduler的一个工做线程调用;传递给execute()方法的JobExecutionContext对象中保存着该job运行时的一些信息 ,执行job的scheduler的引用,触发job的trigger的引用,JobDetail对象引用,以及一些其它信息。
JobDetail
JobDataMap getJobDataMap(); //获取实例成员数据 JobBuilder getJobBuilder();//得到JobBuilder对象
JobDetail对象是在将job加入scheduler时,由客户端程序(你的程序)建立的。它包含job的各类属性设置,以及用于存储job实例状态信息的JobDataMap。
Trigger
//获取触发器惟一标示,同分组key惟一,key= key + 组名称,默认组名称DEFAULT。 TriggerKey getKey(); //获取任务惟一标示, JobKey getJobKey(); //获取实例数据。 JobDataMap getJobDataMap(); //触发优先级,默认是5,同时触发才会比较优先级。 int getPriority();
Trigger用于触发Job的执行。当你准备调度一个job时,你建立一个Trigger的实例,而后设置调度相关的属性。Trigger也有一个相关联的JobDataMap,用于给Job传递一些触发相关的参数。Quartz自带了各类不一样类型的Trigger,最经常使用的主要是SimpleTrigger和CronTrigger。
CronTrigger CronTrigger一般比SimpleTrigger更有用,若是您须要一个基于相似日历的概念重复出现的工做调度计划,而不是SimpleTrigger的精确指定时间间隔。
使用CronTrigger,您能够在每周一上午九点打开个人微信公众号【木子道】,查阅相关技术分享文章。还能够每周五晚8点给我回复。
即便如此,就像SimpleTrigger同样,CronTrigger有一个startTime,指定调度什么时候生效,还有一个(可选)endTime,指定什么时候应该中止调度。
Cron表达式 Cron-Expressions用于配置CronTrigger的实例。Cron-Expressions是由七个子表达式组成的字符串,它们描述了计划的各个细节。这些子表达式用空格分隔,表示: 秒 分钟 小时 日 月 星期 年份(可选)
一个完整的cron-expression的例子是字符串“0 0 0 1 * ?“ - 这意味着”每一个每个月1日00:00:00 发布统计上个月数据“。
单独的子表达式能够包含范围和/或列表。例如,能够用“MON-FRI”,“MON,WED,FRI”,甚至“MON-WED,SAT”代替先前(以“WED”)为例的星期几字段。
通配符(“字符”)能够用来表示该字段的“每一个”可能的值。所以,上例中“月”字段中的字符只是“每个月”。所以,“星期几”字段中的“*”显然意味着“每周的每一天”。
全部的字段都有一组能够指定的有效值。这些值应该至关明显 - 例如秒和分钟的数字0到59,以及数小时的值0到23。月的日期能够是1-31的任何值,可是您须要当心给定的月份有多少天!能够将月份指定为0到11之间的值,或者使用字符串JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV和DEC。能够将星期几指定为1到7之间的值(1 =星期日),或者使用字符串SUN,MON,TUE,WED,THU,FRI和SAT。
“/”字符可用于指定增量值。例如,若是在“分钟”字段中输入“0/15”,则表示“每分钟15分钟,从零开始”。若是在“分钟”字段中使用“3/20”,则意味着“从第三分钟开始的每20分钟一小时” - 或者换句话说,就是在分钟中指定“3,23,43”领域。请注意“ / 35”并不意味着“每35分钟” 的微妙含义,即“每分钟35分钟,从零开始”,换句话说就是指定“0,35”。
'?' 字符被容许用于日期和星期几字段。它用来指定“没有具体的价值”。当你须要在两个字段中的一个字段中指定某些内容时,这是很是有用的,而不是其余的。请参阅下面的示例(和CronTrigger JavaDoc)进行说明。
月份和星期几字段容许使用“L”字符。这个角色对于“最后”来讲是短暂的,可是在两个领域中的每个都有不一样的含义。例如,月份字段中的值“L”意味着“月份的最后一天”,即非闰年的2月28日的1月31日。若是单独使用在星期几字段中,则仅表示“7”或“SAT”。可是,若是在星期几字段中使用另外一个值,则表示“本月的最后一个xxx日”,例如“6L”或“FRIL”都表示“本月的最后一个星期五”。您还能够指定月份的最后一天的偏移量,例如“L-3”,表示日历月份的倒数第三天。当使用“L”选项时,不要指定列表或值的范围,由于您会获得使人困惑/意外的结果。
“W”用于指定与指定日期最近的星期几(星期一至星期五)。例如,若是您要指定“15W”做为月份日期字段的值,则其含义是:“最近的星期几到本月15日”。
“#”用于指定该月的第n个“XXX”工做日。例如,星期几字段中的“6#3”或“FRI#3”的值表示“月的第三个星期五”。
示例Cron表达式
CronTrigger示例1 -建立一个触发器的表达式,每5分钟查阅【木子道】公众号 “0 0/5 * * *?”
CronTrigger示例2 - 一个表达式,用于建立在分钟后10秒(即上午10:00:10,上午10:05:10等)每5分钟触发一次的触发器。 “10 0/5 * * *?”
CronTrigger示例3 - 一个表达式,用于建立在每周三和周五的10:30,11:30,12:30和13:30发生的触发器。 “0 30 10-13?* WED,FRI“
CronTrigger示例4 - 一个表达式,用于建立一个触发器,在每月的第5天和第20天的上午8点到上午10点之间每隔半小时触发一次。请注意,触发器不会在上午10点,仅在8点,8点,9点和9点30分 “0 0/30 8-9 5,20 *?”
请注意,一些调度要求过于复杂,没法用一个触发器来表示 - 例如“上午9点至上午10点之间每5分钟一次,下午1点至10点之间每20分钟一次”。在这种状况下解决方案是简单地建立两个触发器,并注册他们两个运行相同的工做。
JobStore JobStore负责跟踪全部给调度器的“工做数据”:做业,触发器,日历等。为您的Quartz调度器实例选择合适的JobStore是很是重要的一步。幸运的是,一旦你了解它们之间的差别,选择应该是一个很是简单的选择。您能够在您提供给SchedulerFactory的属性文件(或对象)中声明您的调度程序应使用哪一个JobStore(以及它的配置设置),以便生成调度程序实例。
切勿在代码中直接使用JobStore实例。出于某种缘由,许多人试图这样作。JobStore用于石英自己的幕后使用。你必须告诉Quartz(经过配置)使用哪一个JobStore,可是你只能在你的代码中使用Scheduler接口。
RAMJobStore RAMJobStore是最简单的JobStore,它也是性能最高的(CPU时间)。RAMJobStore以明显的方式获得它的名字:它将全部的数据保存在RAM中。这就是为何它闪电般快速,也是为何配置如此简单。缺点是,当你的应用程序结束(或崩溃)时,全部的调度信息都将丢失 - 这意味着RAMJobStore没法遵照做业和触发器的“非易失性”设置。对于某些应用程序来讲,这是能够接受的 - 甚至是所需的行为,可是对于其余应用程序来讲,这多是灾难性的。
JDBCJobStore JDBCJobStore也被恰当地命名 - 它经过JDBC将其全部数据保存在数据库中。所以,配置比RAMJobStore要复杂一些,并且速度也不是那么快。可是,性能退化不是很是糟糕,特别是若是您使用主键上的索引构建数据库表时。在至关现代的机器上有一个体面的局域网(在调度器和数据库之间),检索和更新触发器的时间一般小于10毫秒。
JDBCJobStore几乎能够与任何数据库一块儿使用,它已经被普遍地用于Oracle,PostgreSQL,MySQL,MS SQLServer,HSQLDB和DB2。要使用JDBCJobStore,必须先为Quartz建立一组数据库表来使用。您能够在Quartz发行版的“docs / dbTables”目录中找到建立表的SQL脚本。若是您的数据库类型没有脚本,只需查看其中一个脚本,而后以任何须要的方式修改它。有一点须要注意的是,在这些脚本中,全部的表都之前缀“QRTZ_”开头(例如表“QRTZ_TRIGGERS”和“QRTZ_JOB_DETAIL”)。这个前缀实际上能够是任何你想要的,只要你通知JDBCJobStore什么前缀(在你的Quartz属性中)。使用不一样的前缀对于建立多个表集合,对于多个调度器实例,
一旦建立了表,在配置和启动JDBCJobStore以前,还有一个重要的决定。您须要肯定您的应用程序须要什么类型的事务。若是您不须要将调度命令(如添加和删除触发器)与其余事务绑定,那么您可让Quartz使用JobStoreTX做为JobStore(这是最多见的选择)来管理事务。
若是您须要Quartz与其余事务(即在J2EE应用程序服务器中)一块儿工做,那么您应该使用JobStoreCMT--在这种状况下,Quartz将让应用程序服务器容器管理事务。
最后一块难题是设置一个JDBCJobStore能够链接到数据库的DataSource。DataSources是使用几种不一样的方法之一在你的Quartz属性中定义的。一种方法是让Quartz建立和管理DataSource自己 - 经过提供数据库的全部链接信息。另外一种方法是让Quartz使用由Quartz运行的应用程序服务器管理的DataSource - 经过向JDBCJobStore提供DataSource的JNDI名称。
TerracottaJobStore TerracottaJobStore在不使用数据库的状况下提供了扩展性和健壮性的手段。这意味着您的数据库能够从Quartz中免费下载,而且能够将其全部资源保存到您的应用程序的其他部分。
TerracottaJobStore能够运行群集或非群集,而且在任何状况下为应用程序从新启动之间的持续工做数据提供存储介质,由于数据存储在Terracotta服务器中。它的性能比经过JDBCJobStore使用数据库要好得多(大约好一个数量级),可是比RAMJobStore慢得多。
#3.Quartz 的配置与使用
//配置SchedulerFactory @Bean public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) { SchedulerFactoryBean factory = new SchedulerFactoryBean(); factory.setDataSource(dataSource); //quartz参数 Properties prop = new Properties(); prop.put("org.quartz.scheduler.instanceName", "AstaliScheduler"); prop.put("org.quartz.scheduler.instanceId", "AUTO"); //线程池配置 prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool"); prop.put("org.quartz.threadPool.threadCount", "20"); prop.put("org.quartz.threadPool.threadPriority", "5"); //JobStore配置 //SQL脚本。http://svn.terracotta.org/svn/quartz/tags/quartz-2.1.3/docs/dbTables/ prop.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX"); //集群配置 prop.put("org.quartz.jobStore.isClustered", "true"); //开启集群 prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000"); prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "1"); prop.put("org.quartz.jobStore.misfireThreshold", "12000"); prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_"); //表头 factory.setQuartzProperties(prop); factory.setSchedulerName("AstaliScheduler"); //延时启动 factory.setStartupDelay(30); factory.setApplicationContextSchedulerContextKey("applicationContextKey"); //可选,QuartzScheduler 启动时更新己存在的Job, //这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了 factory.setOverwriteExistingJobs(true); //设置自动启动,默认为true factory.setAutoStartup(true); return factory; } }
//构建Job实例 建立任务 //ScheduleJob extents QuartzJobBean,QuartzJobBean implements Job JobDetail jobDetail = JobBuilder.newJob(ScheduleJob.class) .withIdentity(getJobKey(scheduleJobEntity.getJobId())) .build(); //构建cron CronScheduleBuilder scheduleBuilde = CronScheduleBuilder .cronSchedule(scheduleJob.getCronExpression()) .withMisfireHandlingInstructionDoNothing(); //根据cron,构建一个CronTrigger CronTrigger trigger = TriggerBuilder.newTrigger() .withIdentity(getTriggerKey(scheduleJob.getJobId())) .withSchedule(scheduleBuilder) .build(); //调度器的任务类与触发器关联 scheduler.scheduleJob(jobDetail, trigger);
//当即执行任务 JobDataMap dataMap = new JobDataMap(); dataMap.put(ScheduleJobEntity.JOB_PARAM_KEY, new Gson().toJson(scheduleJob)); scheduler.triggerJob(getJobKey(scheduleJob.getJobId()), dataMap);
//暂定任务 scheduler.pauseJob(getJobKey(jobId));
//恢复任务 scheduler.resumeJob(getJobKey(jobId));
//删除任务 scheduler.deleteJob(getJobKey(jobId));
/** * 获取触发器key */ private static TriggerKey getTriggerKey(Long jobId) { return TriggerKey.triggerKey(JOB_NAME + jobId); } /** * 获取jobKey */ private static JobKey getJobKey(Long jobId) { return JobKey.jobKey(JOB_NAME + jobId); } /** * 获取表达式触发器 */ public static CronTrigger getCronTrigger(Scheduler scheduler, Long jobId) { try { return (CronTrigger) scheduler.getTrigger(getTriggerKey(jobId)); } catch (SchedulerException e) { throw new Exception("getCronTrigger异常,请检查qrtz开头的表,是否有脏数据", e); } }
//上篇文章对newSingleThreadExecutor有说明 ExecutorService service = Executors.newSingleThreadExecutor(); //执行定时任务实现Runnable并重写run方法 ScheduleRunnable task = new ScheduleRunnable(scheduleJob.getBeanName(), scheduleJob.getMethodName(), scheduleJob.getParams()); //提交任务并有结果 Future<?> future = service.submit(task); future.get(); @Override public void run() { try { //利用反射执行方法 ReflectionUtils.makeAccessible(method); if(StringUtils.isNotBlank(params)){ method.invoke(target, params); }else{ method.invoke(target); } }catch (Exception e) { throw new Exception("执行定时任务失败", e); } }
创建一个触发器,每隔上午8点到下午5点,每隔一分钟一次: trigger = newTrigger() .withIdentity("trigger3", "group1") .withSchedule(cronSchedule("0 0/2 8-17 * * ?")) .forJob("myJob", "group1") .build(); 创建一个触发器,天天在上午10:42开枪: trigger = newTrigger() .withIdentity("trigger3", "group1") .withSchedule(dailyAtHourAndMinute(10, 42)) .forJob(myJobKey) .build();
效果图 须要源码的请关注公众号【木子道】▼长按如下二维码便可关注▼
2018年请与我一块儿打怪升级