springBoot有一个定时执行某个方法的 注解:
@Scheduled
可以满足挺多的需求,但是到了一些场景,就显得比较麻烦,比如:
机器待机五分钟后执行切换待机状态。如果是按照使用@Scheduled注解,就得持久化一个表,里面存放机器信息,转换时间,然后@Scheduled一个方法,每隔一定时间扫描全表,查询需要转换的机器。
还有一种有趣的解决办法:
它会定义一个小顶堆,存放需要执行的任务(主要还是存放的该任务的执行时间),
小顶堆结构;
添加任务只需要在最后一个位置加上,然后上浮操作(和父级对比,父级更大就交换,父级小就不动,一直对比,直到父级小不动),所以会发现,每次都是操作父子级,如何快速找到父级,成了最大问题,于是,存储结构选择了数组:
只需要将子集下标/2=取整,就是父级下标。
存入:
也就是说,每次只需要判断顶部数据是否可以执行即可,比上面那种直接扫描全表性能更快。
执行一个任务后,就会刷新顶堆,拿出最后一个。
取出:
package com.quxiao;
import java.time.LocalTime;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @program: springBoot
* @author: quxiao
* @create: 2023-11-18 11:00
**/
public class t1 {
public static void main(String[] args) {
Timer timer = new Timer();
for (int i = 0; i < 2; i++) {
TimerTask timerTask = new TimerTest1("" + i);
timer.schedule(timerTask, new Date(), 2000);
}
}
static final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(10, 20, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(60));
static class TimerTest1 extends TimerTask {
String name;
public TimerTest1() {
}
public TimerTest1(String name) {
this.name = name;
}
@Override
public void run() {
//如果不使用线程池运行,会导致延迟执行,因为run方法是阻塞的,无法做到异步
poolExecutor.execute(() -> {
System.out.println(name + ": " + (LocalTime.now().toString()));
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}
}
}
Timer是java自带的一个定时任务,定时执行指定的方法,
还有一种就是时间轮,Cron表达式,每个位置指定日期,这里我们使用quartz框架来进行调度器:
org.springframework.boot
spring-boot-starter-quartz
直接使用和springBoot项目运行
首先定义执行什么,怎么执行,以及储存任务
执行什么:
package com.quartz.jop;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import java.time.LocalTime;
/**
* @program: springBoot
* @author: quxiao
* @create: 2023-11-18 20:00
**/
public class TestJop implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
System.out.println(LocalTime.now().toString());
}
}
只有实现了job接口的类才能执行,我们可以在里面执行任何东西。
然后就是怎么执行:
package com.quartz.controller;
import com.quartz.jop.TestJop;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @program: springBoot
* @author: quxiao
* @create: 2023-11-18 20:04
**/
@RestController
public class t1 {
@RequestMapping("test1")
public void test1() {
//定义需要执行的方法(就是实现类)
JobDetail jobDetail = JobBuilder.newJob(TestJop.class)
.withIdentity("job1", "group1")
.build();
//如何执行,多少秒一次,什么时候执行,都是在这里定义
SimpleTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1", "trigger1")
.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(1).repeatForever())
.build();
try {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
//储存起来
scheduler.scheduleJob(jobDetail, trigger);
//启动
scheduler.start();
} catch (SchedulerException e) {
throw new RuntimeException(e);
} finally {
}
}
}
在定义定时器时,可以将需要的值传入:
package com.quartz.controller;
import com.quartz.jop.TestJop;
import com.quartz.jop.t2;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @program: springBoot
* @author: quxiao
* @create: 2023-11-18 20:04
**/
@RestController
public class t1 {
@RequestMapping("test1")
public void test1() {
JobDetail jobDetail = JobBuilder.newJob(TestJop.class)
.withIdentity("job1", "group1")
.usingJobData("name","你爹")
.build();
SimpleTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1", "trigger1")
.usingJobData("name","ty")
.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(1).withRepeatCount(2))
.build();
try {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
} catch (SchedulerException e) {
throw new RuntimeException(e);
} finally {
}
}
}
其实就是一个map
取出:
package com.quartz.jop;
import org.quartz.*;
import java.time.LocalTime;
/**
* @program: springBoot
* @author: quxiao
* @create: 2023-11-18 20:00
**/
public class TestJop implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
Trigger trigger = context.getTrigger();
JobDataMap map = trigger.getJobDataMap();
System.out.println(map.get("name"));
System.out.println(LocalTime.now().toString());
JobDetail jobDetail = context.getJobDetail();
JobDataMap jobDataMap = jobDetail.getJobDataMap();
System.out.println(jobDataMap.get("name"));
}
}
还可以直接把trigger 和jobDetail取出来:
JobDataMap mergedJobDataMap = context.getMergedJobDataMap();
不过很显然,可是不能重复的,map嘛
还有一点就时,调度器默认是并行的:
package com.quartz.jop;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* @program: springBoot
* @author: quxiao
* @create: 2023-11-18 20:00
**/
public class TestJop implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
System.out.println(1123211);
try {
TimeUnit.SECONDS.sleep(new Random().nextInt(3));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// JobDataMap mergedJobDataMap = context.getMergedJobDataMap();
// System.out.println(mergedJobDataMap.get("name"));
}
}
也就是说,不管上一个任务是否执行完毕,我到了点,本次任务必然执行-。
使用@DisallowConcurrentExecution注解可以变为串行,也就是同步执行
@PersistJobDataAfterExecution可以让jobDetail的jobDataMap一直保存
package com.quartz.jop;
import org.quartz.*;
/**
* @program: springBoot
* @author: quxiao
* @create: 2023-11-18 20:00
**/
@DisallowConcurrentExecution
@PersistJobDataAfterExecution
public class TestJop implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobDetail jobDetail = context.getJobDetail();
JobDataMap jobDataMap = jobDetail.getJobDataMap();
jobDataMap.put("name", jobDataMap.get("name") + "123");
System.out.println(jobDataMap.get("name"));
}
}
cron 模式定时器
前面用的是基础的执行多少次,指定时间执行还是得用cron方式,注意springBoot有自带的cron执行
@Scheduled注解,但是它只能标识在方法上,也就是说只能某个方法,并不能达到前提条件,例如:
设备闲置5分钟,改为待机状态,只要在五分钟内运行,我们就把这个定时任务删掉就好。
package com.quartz.util;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
* @program: springBoot
* @author: quxiao
* @create: 2023-11-26 10:56
**/
public class DateUtil {
/**
* @param date 指定日期
* @return 返回指定日期的cron表达式
*/
public synchronized static String getCron(Date date) {
String dateFormat = "ss mm HH dd MM ? yyyy";
SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);
String formatTimeStr = null;
if (date != null) {
formatTimeStr = sdf.format(date);
}
return formatTimeStr;
}
/**
* @param date 指定日期
* @param unit 时间单位
* @param number 增加多少
* 增加指定时间
*/
public synchronized static Date addTime(Date date, TemporalUnit unit,long number){
LocalDateTime date1 = date.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
LocalDateTime nextWeek = date1.plus(number, unit);
return Date.from(nextWeek.atZone(ZoneId.systemDefault()).toInstant());
}
}
先给一个时间转换的工具类
使用cron方式只需要将触发器改了就行: (接上序的基础springboot代码)
@Autowired
Scheduler scheduler;
@PostMapping("test1")
public void test1() {
JobDetail jobDetail = JobBuilder.newJob(TestJop.class)
.withIdentity("job1", "group1")
.build();
Date date = DateUtil.addTime(new Date(), ChronoUnit.SECONDS, 5);
//cron表达式
String cron = DateUtil.getCron(date);
CronTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1", "trigger1")
.withSchedule(CronScheduleBuilder.cronSchedule(cron))//达到指定时间执行
.build();
try {
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
} catch (SchedulerException e) {
throw new RuntimeException(e);
}
}
我们之所以使用这个框架,就是因为他可以支持删除、暂停、定时任务,所以就能够把定时任务存起来持久化管理。
删除、暂停、恢复定时任务
@PostMapping("test2")
public void t2() {
try {
JobKey key = new JobKey("job1","group1");
scheduler.deleteJob(key);
} catch (SchedulerException e) {
throw new RuntimeException(e);
}
}
在这里我们前面定义的group分组就起到作用了,通过指定定时任务名job金额分组名group。
所以剩余两个操作也是一样:
@PostMapping("test3")
public void t3() {
try {
JobKey key = new JobKey("job1", "group1");
scheduler.pauseJob(key);
for (int i = 0; i < 10; i++) {
System.out.println("暂停中");
TimeUnit.SECONDS.sleep(1);
}
scheduler.resumeJob(key);
} catch (Exception e) {
}
}