Java实现Web应用中的定时任务(实例讲解)
定时任务,是指定一个未来的时间范围执行一定任务的功能。在当前WEB应用中,多数应用都具备任务调度功能,针对不同的语音,不同的操作系统,都有其自己的语法及解决方案,windows操作系统把它叫做任务计划,linux中cron服务都提供了这个功能,在我们开发业务系统中很多时候会涉及到这个功能。本场chat将使用java语言完成日常开发工作中常用定时任务的使用,希望给大家工作及学习带来帮助。
一、定时任务场景
(1)驱动处理工作流程
作为一个新的预支付订单被初始化放置,如果该订单在指定时间内未进行支付,则将被认为超时订单进行关闭处理;电商系统中应用较多,用户购买商品产生订单,但未进行支付,订单产生30分钟内未支付将关闭订单(且满足该场景数量庞大),不可能采用人工干预。
(2)系统维护
调度工作将获取系统异常日志,及某些关键点数据存储到数据库中,每个工作日(节假日除外平日)在11:30PM转储到数据库,且生成一个XML文件发送至某位员工邮箱。
(3)在应用程序内提供提醒服务。
系统定时提醒登录用户某时间点执行相关工作。
(4)定时对账任务
公司与三方公司(运营商,银行等)业务,每天零点后进行当天业务的对账,将对账信息结果数据发送至相关负责人邮箱,第二天工作时间进行处理不匹配数据。
(5)数据统计
数据记录较多,实时从数据库读取查询会产生一定时间,为客户体验及性能需要,故每周(天,小时)将数据进行汇总,从而在展示数据时能够快速的呈现数据。
使用定时任务的场景还有很多...看来定时任务在我们日常的开发中真的应用很广泛...
二、主流定时任务技术讲解Timer
相信大家都已经非常熟悉java.util.Timer了,它是最简单的一种实现任务调度的方法,下面给出一个具体的例子:
packagecom.ibm.scheduler; importjava.util.Timer; importjava.util.TimerTask; publicclassTimerTestextendsTimerTask{ privateStringjobName=""; publicTimerTest(StringjobName){ super(); this.jobName=jobName; } @Override publicvoidrun(){ System.out.println("execute"+jobName); } publicstaticvoidmain(String[]args){ Timertimer=newTimer(); longdelay1=1*1000; longperiod1=1000; //从现在开始1秒钟之后,每隔1秒钟执行一次job1 timer.schedule(newTimerTest("job1"),delay1,period1); longdelay2=2*1000; longperiod2=2000; //从现在开始2秒钟之后,每隔2秒钟执行一次job2 timer.schedule(newTimerTest("job2"),delay2,period2); } }
/**
输出结果:
executejob1
executejob1
executejob2
executejob1
executejob1
executejob2
*/
使用Timer实现任务调度的核心类是Timer和TimerTask。其中Timer负责设定TimerTask的起始与间隔执行时间。使用者只需要创建一个TimerTask的继承类,实现自己的run方法,然后将其丢给Timer去执行即可。Timer的设计核心是一个TaskList和一个TaskThread。Timer将接收到的任务丢到自己的TaskList中,TaskList按照Task的最初执行时间进行排序。TimerThread在创建Timer时会启动成为一个守护线程。这个线程会轮询所有任务,找到一个最近要执行的任务,然后休眠,当到达最近要执行任务的开始时间点,TimerThread被唤醒并执行该任务。之后TimerThread更新最近一个要执行的任务,继续休眠。
Timer的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务(这点需要注意)。
ScheduledExecutor
鉴于Timer的上述缺陷,Java5推出了基于线程池设计的ScheduledExecutor。其设计思想是,每一个被调度的任务都会由线程池中一个线程去执行,因此任务是并发执行的,相互之间不会受到干扰。需要注意的是,只有当任务的执行时间到来时,ScheduedExecutor才会真正启动一个线程,其余时间ScheduledExecutor都是在轮询任务的状态。
packagecom.ibm.scheduler; importjava.util.concurrent.Executors; importjava.util.concurrent.ScheduledExecutorService; importjava.util.concurrent.TimeUnit; publicclassScheduledExecutorTestimplementsRunnable{ privateStringjobName=""; publicScheduledExecutorTest(StringjobName){ super(); this.jobName=jobName; } @Override publicvoidrun(){ System.out.println("execute"+jobName); } publicstaticvoidmain(String[]args){ ScheduledExecutorServiceservice=Executors.newScheduledThreadPool(10); longinitialDelay1=1; longperiod1=1; //从现在开始1秒钟之后,每隔1秒钟执行一次job1 service.scheduleAtFixedRate( newScheduledExecutorTest("job1"),initialDelay1, period1,TimeUnit.SECONDS); longinitialDelay2=1; longdelay2=1; //从现在开始2秒钟之后,每隔2秒钟执行一次job2 service.scheduleWithFixedDelay( newScheduledExecutorTest("job2"),initialDelay2, delay2,TimeUnit.SECONDS); } }
/** 输出结果: executejob1 executejob1 executejob2 executejob1 executejob1 executejob2 */
上述代码展示了ScheduledExecutorService中两种最常用的调度方法ScheduleAtFixedRate和ScheduleWithFixedDelay。ScheduleAtFixedRate每次执行时间为上一次任务开始起向后推一个时间间隔,即每次执行时间为:initialDelay,initialDelay+period,initialDelay+2*period,…ScheduleWithFixedDelay每次执行时间为上一次任务结束起向后推一个时间间隔,即每次执行时间为:initialDelay,initialDelay+executeTime+delay,initialDelay+2*executeTime+2*delay。由此可见,ScheduleAtFixedRate是基于固定时间间隔进行任务调度,ScheduleWithFixedDelay取决于每次任务执行的时间长短,是基于不固定时间间隔进行任务调度。
用ScheduledExecutor和Calendar实现复杂任务调度
Timer和ScheduledExecutor都仅能提供基于开始时间与重复间隔的任务调度,不能胜任更加复杂的调度需求。比如,设置每星期二的16:38:10执行任务。该功能使用Timer和ScheduledExecutor都不能直接实现,但我们可以借助Calendar间接实现该功能。
packagecom.ibm.scheduler; importjava.util.Calendar; importjava.util.Date; importjava.util.TimerTask; importjava.util.concurrent.Executors; importjava.util.concurrent.ScheduledExecutorService; importjava.util.concurrent.TimeUnit; publicclassScheduledExceutorTest2extendsTimerTask{ privateStringjobName=""; publicScheduledExceutorTest2(StringjobName){ super(); this.jobName=jobName; } @Override publicvoidrun(){ System.out.println("Date="+newDate()+",execute"+jobName); } /** *计算从当前时间currentDate开始,满足条件dayOfWeek,hourOfDay, *minuteOfHour,secondOfMinite的最近时间 *@return */ publicCalendargetEarliestDate(CalendarcurrentDate,intdayOfWeek, inthourOfDay,intminuteOfHour,intsecondOfMinite){ //计算当前时间的WEEK_OF_YEAR,DAY_OF_WEEK,HOUR_OF_DAY,MINUTE,SECOND等各个字段值 intcurrentWeekOfYear=currentDate.get(Calendar.WEEK_OF_YEAR); intcurrentDayOfWeek=currentDate.get(Calendar.DAY_OF_WEEK); intcurrentHour=currentDate.get(Calendar.HOUR_OF_DAY); intcurrentMinute=currentDate.get(Calendar.MINUTE); intcurrentSecond=currentDate.get(Calendar.SECOND); //如果输入条件中的dayOfWeek小于当前日期的dayOfWeek,则WEEK_OF_YEAR需要推迟一周 booleanweekLater=false; if(dayOfWeek /** 输出结果: CurrentDate=WedFeb0217:32:01CST2011 EarliestDate=TueFeb816:38:10CST2011 Date=TueFeb816:38:10CST2011,executejob1 Date=TueFeb1516:38:10CST2011,executejob1 */上述代码实现了每星期二16:38:10调度任务的功能。其核心在于根据当前时间推算出最近一个星期二16:38:10的绝对时间,然后计算与当前时间的时间差,作为调用ScheduledExceutor函数的参数。计算最近时间要用到java.util.calendar的功能。首先需要解释calendar的一些设计思想。Calendar有以下几种唯一标识一个日期的组合方式:
引用
YEAR+MONTH+DAY_OF_MONTH
YEAR+MONTH+WEEK_OF_MONTH+DAY_OF_WEEK
YEAR+MONTH+DAY_OF_WEEK_IN_MONTH+DAY_OF_WEEK
YEAR+DAY_OF_YEAR
YEAR+DAY_OF_WEEK+WEEK_OF_YEAR
上述组合分别加上HOUROFDAY+MINUTE+SECOND即为一个完整的时间标识。
上述DEMO采用了最后一种组合方式。输入为DAY_OF_WEEK,HOUR_OF_DAY,MINUTE,SECOND以及当前日期,输出为一个满足DAY_OF_WEEK,HOUR_OF_DAY,MINUTE,SECOND并且距离当前日期最近的未来日期。计算的原则是从输入的DAY_OF_WEEK开始比较,如果小于当前日期的DAY_OF_WEEK,则需要向WEEK_OF_YEAR进一,即将当前日期中的WEEK_OF_YEAR加一并覆盖旧值;如果等于当前的DAY_OF_WEEK,则继续比较HOUR_OF_DAY;如果大于当前的DAY_OF_WEEK,则直接调用java.util.calenda的calendar.set(field,value)函数将当前日期的DAY_OF_WEEK,HOUR_OF_DAY,MINUTE,SECOND赋值为输入值,依次类推,直到比较至SECOND。我们可以根据输入需求选择不同的组合方式来计算最近执行时间。
用上述方法实现该任务调度比较繁琐,期待需要一个更加完善的任务调度工具来解决这些复杂的调度问题。幸运的是,开源工具包Quartz在这方面展现了强大的能力。
Quartz
OpenSymphony开源组织在Jobscheduling领域又一个开源项目,它可以与J2EE与J2SE应用程序相结合也可以单独使用。Quartz可以用来创建简单或为运行十个,百个,甚至是好几万个Jobs这样复杂的程序。
先来看一个例子吧:
packagecom.test.quartz; importstaticorg.quartz.DateBuilder.newDate; importstaticorg.quartz.JobBuilder.newJob; importstaticorg.quartz.SimpleScheduleBuilder.simpleSchedule; importstaticorg.quartz.TriggerBuilder.newTrigger; importjava.util.GregorianCalendar; importorg.quartz.JobDetail; importorg.quartz.Scheduler; importorg.quartz.Trigger; importorg.quartz.impl.StdSchedulerFactory; importorg.quartz.impl.calendar.AnnualCalendar; publicclassQuartzTest{ publicstaticvoidmain(String[]args){ try{ //创建scheduler Schedulerscheduler=StdSchedulerFactory.getDefaultScheduler(); //定义一个Trigger Triggertrigger=newTrigger().withIdentity("trigger1","group1")//定义name/group .startNow()//一旦加入scheduler,立即生效 .withSchedule(simpleSchedule()//使用SimpleTrigger .withIntervalInSeconds(1)//每隔一秒执行一次 .repeatForever())//一直执行,奔腾到老不停歇 .build(); //定义一个JobDetail JobDetailjob=newJob(HelloQuartz.class)//定义Job类为HelloQuartz类,这是真正的执行逻辑所在 .withIdentity("job1","group1")//定义name/group .usingJobData("name","quartz")//定义属性 .build(); //加入这个调度 scheduler.scheduleJob(job,trigger); //启动之 scheduler.start(); //运行一段时间后关闭 Thread.sleep(10000); scheduler.shutdown(true); }catch(Exceptione){ e.printStackTrace(); } } } packagecom.test.quartz; importjava.util.Date; importorg.quartz.DisallowConcurrentExecution; importorg.quartz.Job; importorg.quartz.JobDetail; importorg.quartz.JobExecutionContext; importorg.quartz.JobExecutionException; publicclassHelloQuartzimplementsJob{ publicvoidexecute(JobExecutionContextcontext)throwsJobExecutionException{ JobDetaildetail=context.getJobDetail(); Stringname=detail.getJobDataMap().getString("name"); System.out.println("sayhelloto"+name+"at"+newDate()); } }通过以上例子:Quartz最重要的3个基本要素:
•Scheduler:调度器。所有的调度都是由它控制。
•Trigger:定义触发的条件。例子中,它的类型是SimpleTrigger,每隔1秒中执行一次(什么是SimpleTrigger下面会有详述)。
•JobDetail&Job:JobDetail定义的是任务数据,而真正的执行逻辑是在Job中,例子中是HelloQuartz。为什么设计成JobDetail+Job,不直接使用Job?这是因为任务是有可能并发执行,如果Scheduler直接使用Job,就会存在对同一个Job实例并发访问的问题。而JobDetail&Job方式,sheduler每次执行,都会根据JobDetail创建一个新的Job实例,这样就可以规避并发访问的问题。
QuartzAPI
Quartz的API的风格在2.x以后,采用的是DSL风格(通常意味着fluentinterface风格),就是示例中newTrigger()那一段东西。它是通过Builder实现的,就是以下几个。(下面大部分代码都要引用这些Builder)
//job相关的builder importstaticorg.quartz.JobBuilder.*; //trigger相关的builder importstaticorg.quartz.TriggerBuilder.*; importstaticorg.quartz.SimpleScheduleBuilder.*; importstaticorg.quartz.CronScheduleBuilder.*; importstaticorg.quartz.DailyTimeIntervalScheduleBuilder.*; importstaticorg.quartz.CalendarIntervalScheduleBuilder.*; //日期相关的builder importstaticorg.quartz.DateBuilder.*;DSL风格写起来会更加连贯,畅快,而且由于不是使用setter的风格,语义上会更容易理解一些。对比一下: JobDetailjobDetail=newJobDetailImpl("jobDetail1","group1",HelloQuartz.class); jobDetail.getJobDataMap().put("name","quartz"); SimpleTriggerImpltrigger=newSimpleTriggerImpl("trigger1","group1"); trigger.setStartTime(newDate()); trigger.setRepeatInterval(1); trigger.setRepeatCount(-1);关于name和group
JobDetail和Trigger都有name和group。
name是它们在这个sheduler里面的唯一标识。如果我们要更新一个JobDetail定义,只需要设置一个name相同的JobDetail实例即可。
group是一个组织单元,sheduler会提供一些对整组操作的API,比如scheduler.resumeJobs()。
Trigger
在开始详解每一种Trigger之前,需要先了解一下Trigger的一些共性。
StartTime&EndTime
startTime和endTime指定的Trigger会被触发的时间区间。在这个区间之外,Trigger是不会被触发的。所有Trigger都会包含这两个属性。
优先级(Priority)
当scheduler比较繁忙的时候,可能在同一个时刻,有多个Trigger被触发了,但资源不足(比如线程池不足)。那么这个时候比剪刀石头布更好的方式,就是设置优先级。优先级高的先执行。需要注意的是,优先级只有在同一时刻执行的Trigger之间才会起作用,如果一个Trigger是9:00,另一个Trigger是9:30。那么无论后一个优先级多高,前一个都是先执行。优先级的值默认是5,当为负数时使用默认值。最大值似乎没有指定,但建议遵循Java的标准,使用1-10,不然鬼才知道看到【优先级为10】是时,上头还有没有更大的值。
Misfire(错失触发)策略
类似的Scheduler资源不足的时候,或者机器崩溃重启等,有可能某一些Trigger在应该触发的时间点没有被触发,也就是MissFire了。这个时候Trigger需要一个策略来处理这种情况。每种Trigger可选的策略各不相同。这里有两个点需要重点注意:
MisFire的触发是有一个阀值,这个阀值是配置在JobStore的。比RAMJobStore是org.quartz.jobStore.misfireThreshold。只有超过这个阀值,才会算MisFire。小于这个阀值,Quartz是会全部重新触发。所有MisFire的策略实际上都是解答两个问题:
•已经MisFire的任务还要重新触发吗?
•如果发生MisFire,要调整现有的调度时间吗?
比如SimpleTrigger的MisFire策略有:
•MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY这个不是忽略已经错失的触发的意思,而是说忽略MisFire策略。它会在资源合适的时候,重新触发所有的MisFire任务,并且不会影响现有的调度时间。比如,SimpleTrigger每15秒执行一次,而中间有5分钟时间它都MisFire了,一共错失了20个,5分钟后,假设资源充足了,并且任务允许并发,它会被一次性触发。这个属性是所有Trigger都适用。
•MISFIRE_INSTRUCTION_FIRE_NOW忽略已经MisFire的任务,并且立即执行调度。这通常只适用于只执行一次的任务。
•MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT将startTime设置当前时间,立即重新调度任务,包括的MisFire的。
•MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT类似MISFIREINSTRUCTIONRESCHEDULENOWWITHEXISTINGREPEAT_COUNT,区别在于会忽略已经MisFire的任务。
•MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT在下一次调度时间点,重新开始调度任务,包括的MisFire的。
•MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT类似于MISFIREINSTRUCTIONRESCHEDULENEXTWITHEXISTINGCOUNT,区别在于会忽略已经MisFire的任务。
•MISFIRE_INSTRUCTION_SMART_POLICY所有的Trigger的MisFire默认值都是这个,大致意思是“把处理逻辑交给聪明的Quartz去决定”。基本策略是。
•如果是只执行一次的调度,使用MISFIRE_INSTRUCTION_FIRE_NOW。
•如果是无限次的调度(repeatCount是无限的),使用MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT。
•否则,使用MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNTMisFire的东西挺繁杂的,可以参考这篇。
Calendar
这里的Calendar不是jdk的java.util.Calendar,不是为了计算日期的。它的作用是在于补充Trigger的时间。可以排除或加入某一些特定的时间点。
以”每月25日零点自动还卡债“为例,我们想排除掉每年的2月25号零点这个时间点(因为有2.14,所以2月一定会破产)。这个时间,就可以用Calendar来实现。
例子:
AnnualCalendarcal=newAnnualCalendar();//定义一个每年执行Calendar,精度为天,即不能定义到2.25号下午2:00 java.util.CalendarexcludeDay=newGregorianCalendar(); excludeDay.setTime(newDate().inMonthOnDay(2,25).build()); cal.setDayExcluded(excludeDay,true);//设置排除2.25这个日期 scheduler.addCalendar("FebCal",cal,false,false);//scheduler加入这个Calendar //定义一个Trigger Triggertrigger=newTrigger().withIdentity("trigger1","group1") .startNow()//一旦加入scheduler,立即生效 .modifiedByCalendar("FebCal")//使用Calendar!! .withSchedule(simpleSchedule() .withIntervalInSeconds(1) .repeatForever()) .build();Quartz体贴地为我们提供以下几种Calendar,注意,所有的Calendar既可以是排除,也可以是包含,取决于:
•HolidayCalendar。指定特定的日期,比如20140613。精度到天。
•DailyCalendar。指定每天的时间段(rangeStartingTime,rangeEndingTime),格式是HH:MM[:SS[:mmm]]。也就是最大精度可以到毫秒。
•WeeklyCalendar。指定每星期的星期几,可选值比如为java.util.Calendar.SUNDAY。精度是天。
•MonthlyCalendar。指定每月的几号。可选值为1-31。精度是天
•AnnualCalendar。指定每年的哪一天。使用方式如上例。精度是天。
•CronCalendar。指定Cron表达式。精度取决于Cron表达式,也就是最大精度可以到秒。
Trigger实现类
Quartz有以下几种Trigger实现:
SimpleTrigger
指定从某一个时间开始,以一定的时间间隔(单位是毫秒)执行的任务。它适合的任务类似于:9:00开始,每隔1小时,执行一次。它的属性有:
•repeatInterval重复间隔
•repeatCount重复次数。实际执行次数是repeatCount+1。因为在startTime的时候一定会执行一次。下面有关repeatCount属性的都是同理。
例子:
CalendarIntervalTrigger
类似于SimpleTrigger,指定从某一个时间开始,以一定的时间间隔执行的任务。但是不同的是SimpleTrigger指定的时间间隔为毫秒,没办法指定每隔一个月执行一次(每月的时间间隔不是固定值),而CalendarIntervalTrigger支持的间隔单位有秒,分钟,小时,天,月,年,星期。相较于SimpleTrigger有两个优势:1、更方便,比如每隔1小时执行,你不用自己去计算1小时等于多少毫秒。2、支持不是固定长度的间隔,比如间隔为月和年。但劣势是精度只能到秒。它适合的任务类似于:9:00开始执行,并且以后每周9:00执行一次。它的属性有:
•interval执行间隔
•intervalUnit执行间隔的单位(秒,分钟,小时,天,月,年,星期)
例子:
calendarIntervalSchedule() .withIntervalInDays(1)//每天执行一次 .build(); calendarIntervalSchedule() .withIntervalInWeeks(1)//每周执行一次 .build();DailyTimeIntervalTrigger
指定每天的某个时间段内,以一定的时间间隔执行任务。并且它可以支持指定星期。它适合的任务类似于:指定每天9:00至18:00,每隔70秒执行一次,并且只要周一至周五执行。它的属性有:
•startTimeOfDay每天开始时间
•endTimeOfDay每天结束时间
•daysOfWeek需要执行的星期
•interval执行间隔
•intervalUnit执行间隔的单位(秒,分钟,小时,天,月,年,星期)
•repeatCount重复次数
例子:
dailyTimeIntervalSchedule() .startingDailyAt(TimeOfDay.hourAndMinuteOfDay(9,0))//第天9:00开始 .endingDailyAt(TimeOfDay.hourAndMinuteOfDay(16,0))//16:00结束 .onDaysOfTheWeek(MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY)//周一至周五执行 .withIntervalInHours(1)//每间隔1小时执行一次 .withRepeatCount(100)//最多重复100次(实际执行100+1次) .build(); dailyTimeIntervalSchedule() .startingDailyAt(TimeOfDay.hourAndMinuteOfDay(9,0))//第天9:00开始 .endingDailyAfterCount(10)//每天执行10次,这个方法实际上根据startTimeOfDay+interval*count算出endTimeOfDay .onDaysOfTheWeek(MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY)//周一至周五执行 .withIntervalInHours(1)//每间隔1小时执行一次 .build();CronTrigger
适合于更复杂的任务,它支持类型于LinuxCron的语法(并且更强大)。基本上它覆盖了以上三个Trigger的绝大部分能力(但不是全部)——当然,也更难理解。它适合的任务类似于:每天0:00,9:00,18:00各执行一次。它的属性只有:
Cron表达式
但这个表示式本身就够复杂了。下面会有说明。例子:
cronSchedule("00/28-17**?")//每天8:00-17:00,每隔2分钟执行一次 .build(); cronSchedule("0309?*MON")//每周一,9:30执行一次 .build(); weeklyOnDayAndHourAndMinute(MONDAY,9,30)//等同于0309?*MON .build();Cron表达式
位置 时间域 允许值 特殊值 1 秒 0-59 ,-*/ 2 分钟 0-59 ,-*/ 3 小时 0-23 ,-*/ 4 日期 1-31 ,-*?/LWC 5 月份 1-12 ,-*/ 6 星期 1-7 ,-*?/LC# 7 年份(可选) 1-31 ,-*/ •星号():可用在所有字段中,表示对应时间域的每一个时刻,例如,在分钟字段时,表示“每分钟”;
•问号(?):该字符只在日期和星期字段中使用,它通常指定为“无意义的值”,相当于点位符;
•减号(-):表达一个范围,如在小时字段中使用“10-12”,则表示从10到12点,即10,11,12;
•逗号(,):表达一个列表值,如在星期字段中使用“MON,WED,FRI”,则表示星期一,星期三和星期五;
•斜杠(/):x/y表达一个等步长序列,x为起始值,y为增量步长值。如在分钟字段中使用0/15,则表示为0,15,30和45秒,而5/15在分钟字段中表示5,20,35,50,你也可以使用*/y,它等同于0/y;
•L:该字符只在日期和星期字段中使用,代表“Last”的意思,但它在两个字段中意思不同。L在日期字段中,表示这个月份的最后一天,如一月的31号,非闰年二月的28号;如果L用在星期中,则表示星期六,等同于7。但是,如果L出现在星期字段里,而且在前面有一个数值X,则表示“这个月的最后X天”,例如,6L表示该月的最后星期五;
•W:该字符只能出现在日期字段里,是对前导日期的修饰,表示离该日期最近的工作日。例如15W表示离该月15号最近的工作日,如果该月15号是星期六,则匹配14号星期五;如果15日是星期日,则匹配16号星期一;如果15号是星期二,那结果就是15号星期二。但必须注意关联的匹配日期不能够跨月,如你指定1W,如果1号是星期六,结果匹配的是3号星期一,而非上个月最后的那天。W字符串只能指定单一日期,而不能指定日期范围;
•LW组合:在日期字段可以组合使用LW,它的意思是当月的最后一个工作日;井号(#):该字符只能在星期字段中使用,表示当月某个工作日。如6#3表示当月的第三个星期五(6表示星期五,#3表示当前的第三个),而4#5表示当月的第五个星期三,假设当月没有第五个星期三,忽略不触发;
•C:该字符只在日期和星期字段中使用,代表“Calendar”的意思。它的意思是计划所关联的日期,如果日期没有被关联,则相当于日历中所有日期。例如5C在日期字段中就相当于日历5日以后的第一天。1C在星期字段中相当于星期日后的第一天。
Cron表达式对特殊字符的大小写不敏感,对代表星期的缩写英文大小写也不敏感。一些例子:
表示式 说明 0012**? 每天12点运行 01510?** 每天10:15运行 01510**? 每天10:15运行 01510**?* 每天10:15运行 01510**?2008 在2008年的每天10:15运行 0*14**? 每天14点到15点之间每分钟运行一次,开始于14:00,结束于14:59。 00/514**? 每天14点到15点每5分钟运行一次,开始于14:00,结束于14:55。 00/514,18**? 每天14点到15点每5分钟运行一次,此外每天18点到19点每5钟也运行一次。 00-514**? 每天14:00点到14:05,每分钟运行一次。 010,4414?3WED 3月每周三的14:10分到14:44,每分钟运行一次。 01510?*MON-FRI 每周一,二,三,四,五的10:15分运行。 0151015*? 每月15日10:15分运行。 01510L*? 每月最后一天10:15分运行。 01510?*6L 每月最后一个星期五10:15分运行。 01510?*6L2007-2009 在2007,2008,2009年每个月的最后一个星期五的10:15分运行。 01510?*6#3 每月第三个星期五的10:15分运行。 JobDetail&Job
JobDetail是任务的定义,而Job是任务的执行逻辑。在JobDetail里会引用一个JobClass定义。一个最简单的例子:
publicclassJobTest{ publicstaticvoidmain(String[]args)throwsSchedulerException,IOException{ JobDetailjob=newJob() .ofType(DoNothingJob.class)//引用JobClass .withIdentity("job1","group1")//设置name/group .withDescription("thisisatestjob")//设置描述 .usingJobData("age",18)//加入属性到ageJobDataMap .build(); job.getJobDataMap().put("name","quertz");//加入属性name到JobDataMap //定义一个每秒执行一次的SimpleTrigger Triggertrigger=newTrigger() .startNow() .withIdentity("trigger1") .withSchedule(simpleSchedule() .withIntervalInSeconds(1) .repeatForever()) .build(); Schedulersche=StdSchedulerFactory.getDefaultScheduler(); sche.scheduleJob(job,trigger); sche.start(); System.in.read(); sche.shutdown(); } } publicclassDoNothingJobimplementsJob{ publicvoidexecute(JobExecutionContextcontext)throwsJobExecutionException{ System.out.println("donothing"); } }从上例我们可以看出,要定义一个任务,需要干几件事:
•创建一个org.quartz.Job的实现类,并实现实现自己的业务逻辑。比如上面的DoNothingJob。
•定义一个JobDetail,引用这个实现类
•加入scheduleJobQuartz调度一次任务,会干如下的事:
•JobClassjobClass=JobDetail.getJobClass()
•JobjobInstance=jobClass.newInstance()。所以Job实现类,必须有一个public的无参构建方法。
•jobInstance.execute(JobExecutionContextcontext)。JobExecutionContext是Job运行的上下文,可以获得Trigger、Scheduler、JobDetail的信息。
也就是说,每次调度都会创建一个新的Job实例,这样的好处是有些任务并发执行的时候,不存在对临界资源的访问问题——当然,如果需要共享JobDataMap的时候,还是存在临界资源的并发访问的问题。
JobDataMap
Job是newInstance的实例,那我怎么传值给它?比如我现在有两个发送邮件的任务,一个是发给"liLei",一个发给"hanmeimei",不能说我要写两个Job实现类LiLeiSendEmailJob和HanMeiMeiSendEmailJob。实现的办法是通过JobDataMap。
每一个JobDetail都会有一个JobDataMap。JobDataMap本质就是一个Map的扩展类,只是提供了一些更便捷的方法,比如getString()之类的。
我们可以在定义JobDetail,加入属性值,方式有二:
•newJob().usingJobData("age",18)//加入属性到ageJobDataMap
•job.getJobDataMap().put("name","quertz");//加入属性name到JobDataMap
然后在Job中可以获取这个JobDataMap的值,方式同样有二:
publicclassHelloQuartzimplementsJob{ privateStringname; publicvoidexecute(JobExecutionContextcontext)throwsJobExecutionException{ JobDetaildetail=context.getJobDetail(); JobDataMapmap=detail.getJobDataMap();//方法一:获得JobDataMap System.out.println("sayhelloto"+name+"["+map.getInt("age")+"]"+"at" +newDate()); } //方法二:属性的setter方法,会将JobDataMap的属性自动注入 publicvoidsetName(Stringname){ this.name=name; } }对于同一个JobDetail实例,执行的多个Job实例,是共享同样的JobDataMap,也就是说,如果你在任务里修改了里面的值,会对其他Job实例(并发的或者后续的)造成影响。
除了JobDetail,Trigger同样有一个JobDataMap,共享范围是所有使用这个Trigger的Job实例。
Job并发
Job是有可能并发执行的,比如一个任务要执行10秒中,而调度算法是每秒中触发1次,那么就有可能多个任务被并发执行。
有时候我们并不想任务并发执行,比如这个任务要去”获得数据库中所有未发送邮件的名单“,如果是并发执行,就需要一个数据库锁去避免一个数据被多次处理。这个时候一个@DisallowConcurrentExecution解决这个问题。就是这样:
publicclassDoNothingJobimplementsJob{ @DisallowConcurrentExecution publicvoidexecute(JobExecutionContextcontext)throwsJobExecutionException{ System.out.println("donothing"); } }注意,@DisallowConcurrentExecution是对JobDetail实例生效,也就是如果你定义两个JobDetail,引用同一个Job类,是可以并发执行的。
JobExecutionException
Job.execute()方法是不允许抛出除JobExecutionException之外的所有异常的(包括RuntimeException),所以编码的时候,最好是try-catch住所有的Throwable,小心处理。
其他属性
•Durability(耐久性?)如果一个任务不是durable,那么当没有Trigger关联它的时候,它就会被自动删除。
•RequestsRecovery如果一个任务是"requestsrecovery",那么当任务运行过程非正常退出时(比如进程崩溃,机器断电,但不包括抛出异常这种情况),Quartz再次启动时,会重新运行一次这个任务实例。
可以通过JobExecutionContext.isRecovering()查询任务是否是被恢复的。
Scheduler
•Scheduler就是Quartz的大脑,所有任务都是由它来设施。
•Schduelr包含一个两个重要组件:JobStore和ThreadPool。
•JobStore是会来存储运行时信息的,包括Trigger,Schduler,JobDetail,业务锁等。它有多种实现RAMJob(内存实现),JobStoreTX(JDBC,事务由Quartz管理),JobStoreCMT(JDBC,使用容器事务),ClusteredJobStore(集群实现)、TerracottaJobStore(什么是Terractta)。
•ThreadPool就是线程池,Quartz有自己的线程池实现。所有任务的都会由线程池执行。
SchedulerFactory
SchdulerFactory,顾名思义就是来用创建Schduler了,有两个实现:DirectSchedulerFactory和StdSchdulerFactory。前者可以用来在代码里定制你自己的Schduler参数。后者是直接读取classpath下的quartz.properties(不存在就都使用默认值)配置来实例化Schduler。通常来讲,我们使用StdSchdulerFactory也就足够了。
SchdulerFactory本身是支持创建RMIstub的,可以用来管理远程的Scheduler,功能与本地一样,可以远程提交个Job什么的。DirectSchedulerFactory的创建接口:
/** *Sameas *{@linkDirectSchedulerFactory#createScheduler(ThreadPoolthreadPool,JobStorejobStore)}, *withtheadditionofspecifyingtheschedulernameandinstanceID.This *schedulercanonlyberetrievedvia *{@linkDirectSchedulerFactory#getScheduler(String)} * *@paramschedulerName *Thenameforthescheduler. *@paramschedulerInstanceId *TheinstanceIDforthescheduler. *@paramthreadPool *Thethreadpoolforexecutingjobs *@paramjobStore *Thetypeofjobstore *@throwsSchedulerException *ifinitializationfailed */ publicvoidcreateScheduler(StringschedulerName, StringschedulerInstanceId,ThreadPoolthreadPool,JobStorejobStore) throwsSchedulerException;StdSchdulerFactory的配置例子,更多配置,参考Quartz配置指南:
org.quartz.scheduler.instanceName=DefaultQuartzScheduler org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool org.quartz.threadPool.threadCount=10 org.quartz.threadPool.threadPriority=5 org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread=true org.quartz.jobStore.class=org.quartz.simpl.RAMJobStore三、Quartz集成Spring
开发一个job类,普通java类,需要有一个执行的方法:
packagecom.tgb.lk.demo.quartz; importjava.util.Date; publicclassMyJob{ publicvoidwork(){ System.out.println("date:"+newDate().toString()); } }把类放到spring容器中,可以使用配置也可以使用注解:
配置jobDetail,指定job对象:
work 配置一个trigger,需要指定一个cron表达式,指定任务的执行时机:
0/3****? 配置调度工厂:
项目启动,定时器开始执行。
四、分析不同定时任务优缺点,寻找一种符合你项目需求的定时任务Timer管理延时任务的缺陷
以前在项目中也经常使用定时器,比如每隔一段时间清理项目中的一些垃圾文件,每隔一段时间进行日志清理;然而Timer是存在一些缺陷的,因为Timer在执行定时任务时只会创建一个线程,所以如果存在多个任务,且任务时间过长,超过了两个任务的间隔时间,会发生一些缺陷
Timer当任务抛出异常时的缺陷
如果TimerTask抛出RuntimeException,Timer会停止所有任务的运行
Timer执行周期任务时依赖系统时间
Timer执行周期任务时依赖系统时间,如果当前系统时间发生变化会出现一些执行上的变化,ScheduledExecutorService基于时间的延迟,不会由于系统时间的改变发生执行变化。
对异常的处理
Quartz的某次执行任务过程中抛出异常,不影响下一次任务的执行,当下一次执行时间到来时,定时器会再次执行任务;而TimerTask则不同,一旦某个任务在执行过程中抛出异常,则整个定时器生命周期就结束,以后永远不会再执行定时器任务。
精确到和功能
Quartz每次执行任务都创建一个新的任务类对象,而TimerTask则每次使用同一个任务类对象。Quartz可以通过cron表达式精确到特定时间执行,而TimerTask不能。Quartz拥有TimerTask所有的功能,而TimerTask则没有上述,基本说明了在以后的开发中尽可能使用ScheduledExecutorService(JDK1.5以后)替代Timer。
五、cron在线表达式生成器http://cron.qqe2.com/附录cron表达式
cron表达式用于配置cronTrigger的实例。cron表达式实际上是由七个子表达式组成。这些表达式之间用空格分隔。
•Seconds(秒)
•Minutes(分)
•Hours(小时)
•Day-of-Month(天)
•Month(月)
•Day-of-Week(周)
•Year(年)
例:"0012?*WED”意思是:每个星期三的中午12点执行。个别子表达式可以包含范围或者列表。例如:上面例子中的WED可以换成"MON-FRI","MON,WED,FRI",甚至"MON-WED,SAT"。子表达式范围:
•Seconds(0~59)
•Minutes(0~59)
•Hours(0~23)
•Day-of-Month(1~31,但是要注意有些月份没有31天)
•Month(0~11,或者"JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC")
•Day-of-Week(1~7,1=SUN或者"SUN,MON,TUE,WED,THU,FRI,SAT”)
•Year(1970~2099)
Cron表达式的格式:秒分时日月周年(可选)。
字段名|允许的值|允许的特殊字符-------|------|------|------秒|0-59|,-*/分|0-59|,-*/小时|0-23|,-*/日|1-31|,-*?/LWC月|1-12orJAN-DEC|,-*/周几|1-7orSUN-SAT|,-*?/LC#年(可选字段)|empty1970-2099|,-*/
字符含义:
•*:代表所有可能的值。因此,“*”在Month中表示每个月,在Day-of-Month中表示每天,在Hours表示每小时
•-:表示指定范围。
•,:表示列出枚举值。例如:在Minutes子表达式中,“5,20”表示在5分钟和20分钟触发。
•/:被用于指定增量。例如:在Minutes子表达式中,“0/15”表示从0分钟开始,每15分钟执行一次。"3/20"表示从第三分钟开始,每20分钟执行一次。和"3,23,43"(表示第3,23,43分钟触发)的含义一样。
•?:用在Day-of-Month和Day-of-Week中,指“没有具体的值”。当两个子表达式其中一个被指定了值以后,为了避免冲突,需要将另外一个的值设为“?”。例如:想在每月20日触发调度,不管20号是星期几,只能用如下写法:00020*?,其中最后以为只能用“?”,而不能用“*”。
•L:用在day-of-month和day-of-week字串中。它是单词“last”的缩写。它在两个子表达式中的含义是不同的。
•在day-of-month中,“L”表示一个月的最后一天,一月31号,3月30号。
•在day-of-week中,“L”表示一个星期的最后一天,也就是“7”或者“SAT”
•如果“L”前有具体内容,它就有其他的含义了。例如:“6L”表示这个月的倒数第六天。“FRIL”表示这个月的最后一个星期五。
•注意:在使用“L”参数时,不要指定列表或者范围,这样会出现问题。
•W:“Weekday”的缩写。只能用在day-of-month字段。用来描叙最接近指定天的工作日(周一到周五)。例如:在day-of-month字段用“15W”指“最接近这个月第15天的工作日”,即如果这个月第15天是周六,那么触发器将会在这个月第14天即周五触发;如果这个月第15天是周日,那么触发器将会在这个月第16天即周一触发;如果这个月第15天是周二,那么就在触发器这天触发。注意一点:这个用法只会在当前月计算值,不会越过当前月。“W”字符仅能在day-of-month指明一天,不能是一个范围或列表。也可以用“LW”来指定这个月的最后一个工作日,即最后一个星期五。
•#:只能用在day-of-week字段。用来指定这个月的第几个周几。例:在day-of-week字段用"6#3"or"FRI#3"指这个月第3个周五(6指周五,3指第3个)。如果指定的日期不存在,触发器就不会触发。
表达式例子:
0****?每1分钟触发一次
00***?每天每1小时触发一次
0010**?每天10点触发一次
0*14**?在每天下午2点到下午2:59期间的每1分钟触发
03091*?每月1号上午9点半
0151015*?每月15日上午10:15触发
*/5****?每隔5秒执行一次
0*/1***?每隔1分钟执行一次
005-15**?每天5-15点整点触发
00/3***?每三分钟触发一次
00-514**?在每天下午2点到下午2:05期间的每1分钟触发
00/514**?在每天下午2点到下午2:55期间的每5分钟触发
00/514,18**?在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
00/309-17**?朝九晚五工作时间内每半小时
0010,14,16**?每天上午10点,下午2点,4点
0012?*WED表示每个星期三中午12点
0017?*TUES,THUR,SAT每周二、四、六下午五点
010,4414?3WED每年三月的星期三的下午2:10和2:44触发
01510?*MON-FRI周一至周五的上午10:15触发
0023L*?每月最后一天23点执行一次
01510L*?每月最后一日的上午10:15触发
01510?*6L每月的最后一个星期五上午10:15触发
01510**?20052005年的每天上午10:15触发
01510?*6L2002-20052002年至2005年的每月的最后一个星期五上午10:15触发
01510?*6#3每月的第三个星期五上午10:15触发
以上这篇Java实现Web应用中的定时任务(实例讲解)就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持毛票票。