springboot实现定时任务的四种方式小结
六六的小帅 人气:0因为某些需求,要在特定的时间执行一些任务,比如定时删除服务器存储的数据缓存,定时获取数据以及定时发送推送等等,这时就需要用到定时任务了。定时任务,指的是在编程过程中无须做复杂控制的前提下执行简单的定时操作。
Timer
在java中一个完整的定时任务可以用Timer和TimerTask两个类配合完成。
Timer是一种工具,线程用其安排在后台线程中执行的任务,可安排任务执行一次或者定期重复执行。
TimerTask是由Timer安排执行一次或者重复执行的任务。
Timer中提供了四个方法:
(1)schedule(TimerTask task,Date time)——安排在指定的时间执行指定的任务
(2)schedule(TimerTask task,Date firstTime,long period)——安排指定的任务在指定的时间开始进行重复的固定延迟执行
(3)schedule(TimerTask task,long delay)——安排在指定延迟后执行指定的任务
(4)schedule(TimerTask task,long delay,long period)——安排指定的任务在指定的延迟后开始进行重复的固定速率执行
示例:
首先需要创建一个类作为定时任务,该类需要继承TimerTask
public class TimerTask extends java.util.TimerTask{ @Override public void run() { //这里执行定时任务内容 System.out.println("+++++++"); } }
然后创建Timer调用之前创建的定时任务
public class TimerTest { public static void main(String[] args) { Timer timer = new Timer(); TimerTask noticeTask = new TimerTask(); timer.schedule(noticeTask,0,2000); timer.cancel(); System.out.println("结束"); } }
这样定时执行任务的功能就实现了,但Timer有着一定的缺陷:
Timer对于系统时间的改变非常敏感,它对调度的支持是基于绝对时间而不是相对时间。
Timer线程是不会捕获异常的,多线程并行处理定时任务时,Timer运行多个TimerTask时,只要其中之一没有捕获抛出的异常,其他任务便会自动终止运行。同时Timer也不会重新恢复线程的执行,它会错误的认为整个Timer线程都会取消,已经被安排但尚未执行的TimerTask也不会再执行了,新的任务也不能被调度。因此,如果TimerTask抛出未检查的异常,Timer将会产生无法预料的行为。
ScheduledExecutor
Timer是基于绝对时间的,对系统时间比较敏感,而ScheduledExecutor则是基于相对时间。
Timer的内部只有一个线程,如果有多个任务的话就会顺序执行,这样我们的延迟时间和循环时间就会出现问题。而ScheduledThreadPoolExecutor内部是个线程池,可以支持多个任务并发执行,在对延迟任务和循环任务要求严格的时候,就需要考虑使用ScheduledExecutor了。
针对Timer类存在的缺陷,Java 5 推出了基于线程池设计的 ScheduledExecutor,ScheduledExecutor的设计思想是每一个被调度的任务都会由线程池中一个线程去执行,因此任务是并发的,相互之间不会受到干扰,只有当任务的时间到来时,ScheduledExecutor才会真正启动一个线程,其余时间ScheduledExecutor都是处于轮询任务的状态。如果我们设定的调度周期小于任务运行时间,该任务会被重复添加到一个延时任务队列,所以同一时间任务队列中会有多个任务待调度,线程池会首先获取优先级高的任务执行。效果就是任务运行多长时间,调度时间就会变为多久,因为添加到任务队列的任务的延时时间每次都是负数,所以会被立刻执行。
示例:
public class MyScheduledExecutor implements Runnable{ private String jobName; MyScheduledExecutor() { } MyScheduledExecutor(String jobName) { this.jobName = jobName; } @Override public void run() { System.out.println(jobName + " is running"); } }
public class MyScheduledExecutorService { public static void main(String[] args) { long initialDelay = 3; long period = 1; /** * 创建一个线程池,它可安排在给定延迟后运行任务或者定期地执行任务 * 参数:corePoolSize - 池中所保存的线程数,即使线程是空闲的也包括在内 */ ScheduledExecutorService service = Executors.newScheduledThreadPool(2); /** * 从现在开始3秒钟之后,每隔1秒钟执行一次job1,ScheduleAtFixedRate是基于固定时间间隔进行任务调度 * 参数:1、任务体 2、首次执行的延时时间 * 3、任务执行间隔 4、间隔时间单位 */ service.scheduleAtFixedRate(new MyScheduledExecutor("job1"), initialDelay, period, TimeUnit.SECONDS); /** * 从现在开始3秒钟之后,每隔1秒钟执行一次job2,ScheduleWithFixedDelay 取决于每次任务执行的时间长短,基于不固定时间间隔进行任务调度 */ service.scheduleWithFixedDelay(new MyScheduledExecutor("job2"), initialDelay, period, TimeUnit.SECONDS); } }
ScheduledExecutor 配合 Calendar 实现复杂任务调度
示例:设置每星期二的 18:30:00 执行任务
使用 ScheduledExcetuor 和 Calendar 进行任务调度
public class ScheduledExceutorTest2 extends TimerTask { private String jobName = ""; public ScheduledExceutorTest2(String jobName) { super(); this.jobName = jobName; } @Override public void run() { System.out.println("Date = " + new Date() + ", execute " + jobName); } /** * 计算从当前时间currentDate开始,满足条件dayOfWeek, hourOfDay, minuteOfHour, * secondOfMinite的最近时间 */ public Calendar getEarliestDate(Calendar currentDate, int dayOfWeek, int hourOfDay, int minuteOfHour, int secondOfMinite) { // 计算当前时间的WEEK_OF_YEAR,DAY_OF_WEEK, HOUR_OF_DAY, MINUTE,SECOND等各个字段值 int currentWeekOfYear = currentDate.get(Calendar.WEEK_OF_YEAR); int currentDayOfWeek = currentDate.get(Calendar.DAY_OF_WEEK); int currentHour = currentDate.get(Calendar.HOUR_OF_DAY); int currentMinute = currentDate.get(Calendar.MINUTE); int currentSecond = currentDate.get(Calendar.SECOND); // 如果输入条件中的dayOfWeek小于当前日期的dayOfWeek,则WEEK_OF_YEAR需要推迟一周 boolean weekLater = false; if (dayOfWeek < currentDayOfWeek) { weekLater = true; } else if (dayOfWeek == currentDayOfWeek) { // 当输入条件与当前日期的dayOfWeek相等时,如果输入条件中的 // hourOfDay小于当前日期的 // currentHour,则WEEK_OF_YEAR需要推迟一周 if (hourOfDay < currentHour) { weekLater = true; } else if (hourOfDay == currentHour) { // 当输入条件与当前日期的dayOfWeek, hourOfDay相等时, // 如果输入条件中的minuteOfHour小于当前日期的 // currentMinute,则WEEK_OF_YEAR需要推迟一周 if (minuteOfHour < currentMinute) { weekLater = true; } else if (minuteOfHour == currentSecond) { // 当输入条件与当前日期的dayOfWeek, hourOfDay, // minuteOfHour相等时,如果输入条件中的 // secondOfMinite小于当前日期的currentSecond, // 则WEEK_OF_YEAR需要推迟一周 if (secondOfMinite < currentSecond) { weekLater = true; } } } } if (weekLater) { // 设置当前日期中的WEEK_OF_YEAR为当前周推迟一周 currentDate.set(Calendar.WEEK_OF_YEAR, currentWeekOfYear + 1); } // 设置当前日期中的DAY_OF_WEEK,HOUR_OF_DAY,MINUTE,SECOND为输入条件中的值。 currentDate.set(Calendar.DAY_OF_WEEK, dayOfWeek); currentDate.set(Calendar.HOUR_OF_DAY, hourOfDay); currentDate.set(Calendar.MINUTE, minuteOfHour); currentDate.set(Calendar.SECOND, secondOfMinite); return currentDate; } public static void main(String[] args) throws Exception { ScheduledExceutorTest2 test = new ScheduledExceutorTest2("job1"); // 获取当前时间 Calendar currentDate = Calendar.getInstance(); long currentDateLong = currentDate.getTime().getTime(); System.out.println("Current Date = " + currentDate.getTime().toString()); // 计算满足条件的最近一次执行时间 Calendar earliestDate = test.getEarliestDate(currentDate, 3, 18, 30, 00); long earliestDateLong = earliestDate.getTime().getTime(); System.out.println("Earliest Date = " + earliestDate.getTime().toString()); // 计算从当前时间到最近一次执行时间的时间间隔 long delay = earliestDateLong - currentDateLong; // 计算执行周期为一星期 long period = 7 * 24 * 60 * 60 * 1000; ScheduledExecutorService service = Executors.newScheduledThreadPool(10); // 从现在开始delay毫秒之后,每隔一星期执行一次job1 service.scheduleAtFixedRate(test, delay, period, TimeUnit.MILLISECONDS); } }
其核心在于根据当前时间推算出最近一个星期二 18:30:00 的绝对时间,然后计算与当前时间的时间差,作为调用 ScheduledExceutor 函数的参数,计算最近时间要用到 java.util.calendar 的功能。
注解@Scheduled
Spring提供的注解,优点就是配置简单,依赖少,缺点是同一个task,如果前一个还没跑完后面一个就不会触发,不同的task也不能同时运行。因为scheduler的默认线程数为1,配置pool-size为2的话,会导致同一个task前一个还没跑完后面又被触发的问题,不支持集群等。
示例:
yml文件配置:
time: cron: 0/5 * * * * * interval: 5
启动类添加@EnableScheduling
定时任务:
@Component public class TimeTask { @Scheduled(cron = "${time.cron}") public void flush1() throws Exception{ System.out.println("Execute1"); } @Scheduled(cron = "0/${time.interval} * * * * ?") public void flush2() throws Exception{ System.out.println("Execute2"); } @Scheduled(cron = "0/5 * * * * ?") public void flush3() throws Exception{ System.out.println("Execute3"); } }
参数介绍:
fixedDelay
上一次执行完毕时间点之后多长时间再执行。如:
@Scheduled(fixedDelay = 5000) //上一次执行完毕时间点之后5秒再执行
fixedDelayString
与 fixedDelay 意思相同,只是使用字符串的形式。唯一不同的是支持占位符。如:
@Scheduled(fixedDelayString = "5000") //上一次执行完毕时间点之后5秒再执行
占位符的使用:
@Scheduled(fixedDelayString = "${time.fixedDelay}") void testFixedDelayString() { System.out.println("Execute”); }
fixedRate
上一次开始执行时间点之后多长时间再执行。如:
@Scheduled(fixedRate = 5000) //上一次开始执行时间点之后5秒再执行
fixedRateString
与 fixedRate 意思相同,只是使用字符串的形式。唯一不同的是支持占位符。
initialDelay
第一次延迟多长时间后再执行。如:
@Scheduled(initialDelay=1000, fixedRate=5000) //第一次延迟1秒后执行,之后按fixedRate的规则每5秒执行一次
initialDelayString
与 initialDelayString 意思相同,只是使用字符串的形式。唯一不同的是支持占位符。
cron表达式
cron表达式是一个字符串,字符串以5或6个空格隔开,分成6或7个域,每一个域代表一个含义。
cron表达式语法:
[秒] [分] [小时] [日] [月] [周] [年]
注:[年]不是必须的域,可以省略[年],则一共6个域
其中各个域的定义如下:
序号 | 说明 | 必填 | 允许填写的值 | 允许的通配符 |
1 | 秒 | 是 | 0-59 | , - * / |
2 | 分 | 是 | 0-59 | , - * / |
3 | 时 | 是 | 0-23 | , - * / |
4 | 日 | 是 | 1月31日 | , - * ? / L W |
5 | 月 | 是 | 1-12 / JAN-DEC | , - * / |
6 | 周 | 是 | 1-7 / SUN-SAT | , - * ? / L # |
7 | 年 | 否 | 1970-2099 | , - * / |
Cron表达式对特殊字符的大小写不敏感,对代表星期的缩写英文大小写也不敏感。星号(*):可用在所有字段中,表示对应时间域的每一个时刻,例如, 在分钟字段时,表示“每分钟”;
问号(?):该字符只在日期和星期字段中使用,它通常指定为“无意义的值”,相当于点位符;
- 减号(-):表达一个范围,如在小时字段中使用“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在星期字段中相当于星期日后的第一天。
示例:
表示式 | 说明 |
0 0 12 * * ? | 每天12点运行 |
0 15 10 ? * * | 每天10:15运行 |
0 15 10 * * ? | 每天10:15运行 |
0 15 10 * * ? * | 每天10:15运行 |
0 15 10 * * ? 2008 | 在2008年的每天10:15运行 |
0 * 14 * * ? | 每天14点到15点之间每分钟运行一次,开始于14:00,结束于14:59。 |
0 0/5 14 * * ? | 每天14点到15点每5分钟运行一次,开始于14:00,结束于14:55。 |
0 0/5 14,18 * * ? | 每天14点到15点每5分钟运行一次,此外每天18点到19点每5钟也运行一次。 |
0 0-5 14 * * ? | 每天14:00点到14:05,每分钟运行一次。 |
0 10,44 14 ? 3 WED | 3月每周三的14:10分到14:44,每分钟运行一次。 |
0 15 10 ? * MON-FRI | 每周一,二,三,四,五的10:15分运行。 |
0 15 10 15 * ? | 每月15日10:15分运行。 |
0 15 10 L * ? | 每月最后一天10:15分运行。 |
0 15 10 ? * 6L | 每月最后一个星期五10:15分运行。 |
0 15 10 ? * 6L 2007-2009 | 在2007,2008,2009年每个月的最后一个星期五的10:15分运行。 |
0 15 10 ? * 6#3 | 每月第三个星期五的10:15分运行。 |
Quartz
Quartz 是一个完全由 Java 编写的开源作业调度框架,它可以集成在几乎任何Java应用程序中进行作业调度。
Quartz 可以与 J2EE 与 J2SE 应用程序相结合也可以单独使用。
Quartz 允许程序开发人员根据时间的间隔来调度作业。
Quartz 实现了作业和触发器的多对多的关系,还能把多个作业与不同的触发器关联。
Quartz的运行环境
Quartz 可以运行嵌入在另一个独立式应用程序。
Quartz 可以在应用程序服务器(或 servlet 容器)内被实例化,并且参与 XA 事务。
Quartz 可以作为一个独立的程序运行(其自己的 Java 虚拟机内),可以通过 RMI 使用。
Quartz 可以被实例化,作为独立的项目集群(负载平衡和故障转移功能),用于作业的执行。
Job
代表一个工作,要执行的具体内容。此接口中只有一个方法,如下:
public class QuartzJob implements Job { @Override public void execute(JobExecutionContext context) throws JobExecutionException { System.out.println(new Date()); } }
可以通过实现该接口来定义需要执行的任务。
JobDetail
用于定义作业的实例,代表一个具体的可执行的调度程序,Job 是这个可执行程调度程序所要执行的内容,另外 JobDetail 还包含了这个任务调度的方案和策略。
Trigger
代表一个调度参数的配置,什么时候去调。
1> SimpleTrigger:在某个时间段内实现定时任务的重复执行。
参数:startTime(开始时间)、endTime(结束时间)、repeatCount(重复数次)、repeatInterval(重复执行间隔)
2> CronTrigger:基于日历的概念执行计划,这个trigger是最常用的。
参数:startTime(开始时间)、endTime(结束时间)、cronExpression(定时表达式)、timeZone(时区,默认获取jvm所在时区)
Scheduler
代表一个调度容器,一个调度容器中可以注册多个 JobDetail 和 Trigger。当 Trigger 与 JobDetail 组合,就可以被 Scheduler 容器调度了。
Calendar
是一些日历特定时间的集合。一个Trigger可以和多个calendar关联,可以通过calendar在指定时间不执行任务。
示例:服务启动5秒后执行,任务间隔2秒,服务启动15秒后关闭
依赖
<dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.3.0</version> </dependency>
任务调度
@Component //SpringBoot服务启动执行 public class CronScheduler implements CommandLineRunner { @Override public void run(String... args) throws Exception { JobDetail build = JobBuilder.newJob(QuartzJob.class) .withIdentity("myJob", "group1") .build(); Date date = new Date(); long startTime = date.getTime() + 5000; long endTime = date.getTime() + 15000; CronTrigger c = TriggerBuilder.newTrigger() .startAt(new Date(startTime)) .endAt(new Date(endTime)) .withIdentity("CronTrigger1", "t1") .withSchedule(CronScheduleBuilder.cronSchedule("0/2 * * * * ?")) .build(); Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); //设置调度的job和trigger scheduler.scheduleJob(build, c); //开启调度 scheduler.start(); //暂停,可以重新启动 //scheduler.standby(); //停止调度程序触发触发器,并清除与调度程序关联的所有资源 //scheduler.shushutdown(); } }
具体执行的任务
public class QuartzJob implements Job { @Override public void execute(JobExecutionContext context) throws JobExecutionException { System.out.println(new Date()); } }
加载全部内容