单体项目中定时任务的实现
在企业开发中,遇到的项目无非就两种,单体项目和分布式项目
单体项目中实现定时任务有以下几种方式
1. 使用Timer实现定时任务(不常用)
1.1、JDK1.3推出的定时任务实现工具类java.util.Timer
1.2、API
Timer timer = new Timer();
timer.schedule(任务,延迟时间/执行时间);
timer.schedule(任务,延迟时间,重复频率);
timer.schedule(任务,首次执行时间,重复频率);
示例:
public class MyTimer {
public static void main(String[] args) throws ParseException {
Timer timer = new Timer(); //创建一个定时器对象
String str = "2024-09-23 14:30:00";
Date startDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(str);
timer.schedule(new TimerTask() {
@Override
public void run() {
// 定时任务要执行的功能
System.out.println("hello Timer!");
}
}, startDate);
}
}
因为定时任务类TimerTask是个抽象类,底层实现了Runnable接口,所以在实际开发中,我们会将要定时执行的业务逻辑单独定义在一个类作为定时任务类,让这个类继承TimerTask。
// 定时任务类
public class MyTimerTask extends TimerTask{
@Override
public void run() {
// 定时任务要执行的功能
System.out.println("hello Timer!");
}
}
// 里面的逻辑在实际开发中一般写在service或controller的方法中
public class MyTimer {
public static void main(String[] args) throws ParseException {
Timer timer = new Timer();
String str = "2024-09-23 14:30:00";
Date startDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(str);
MyTimerTask task = new MyTimerTask();
timer.schedule(task, startDate);
}
}
使用Timer实现定时任务的缺点:
- 单线程执行所有任务,效率低;
- 当定义了很多个定时任务时,如果其中一个定时任务发生异常,其他所有定时任务都会终止;
因此,阿里的开发规范中不允许使用这种方式实现定时任务。
2. Spring Task实现定时任务(常用)
2.1、Spring 3.0开始推出定时任务功能,也就是Spring Task,因为Spring Task集成在SpringContext中,所以使用时无需引入依赖。
2.2、@EnableScheduling:SpringBoot启动类上需添加此依赖,表示开启定时任务功能
2.3、@Scheduled(cron = “cron表达式”):定时任务执行的业务逻辑方法上需添加此注解,让项目识别到要执行的逻辑是哪个方法;cron表达式定义的是定时任务开始执行的时间和频率等信息,可用网上的生成工具一键生成,如https://cron.qqe2.com/
2.4、运行定时任务不需要编码调用API,直接运行启动类即可;
示例:
@SpringBootApplication
@EnableScheduling // 开启定时任务功能
public class MultiFileApp {
public static void main(String[] args) {
SpringApplication.run(MultiFileApp.class, args);
}
}
// 定时任务类
@Component
public class MyTask {
@Scheduled(cron = "0/2 * * * * ? ")
public void task1() {
// 定时任务要执行的功能
System.out.println("hello Timer1!");
}
@Scheduled(cron = "* 0/5 * * * ? *")
public void task2() {
// 定时任务要执行的功能
System.out.println("hello Timer2!");
}
}
Spring Task默认是以单线程的方式执行所有的定时任务,可以通过配置改成以多线程的方式执行定时任务,提高效率
// 多线程配置
spring:
task:
scheduling:
thread-name-prefix: mytask_ //配置线程名前缀
pool:
size: 5 //线程池里的线程数
shutdown:
await-termination: false //线程关闭前是否等待所有任务执行完毕,默认就是false
await-termination-period: 10s //线程关闭前的最大等待时间(防止执行某个任务时线程一直不结束)
Spring Task实现定时任务的缺点:
- 不支持持久化,也就意味着需要手动持久化数据
- 默认也是单线程执行所有任务
这些缺点都可以手动通过配置或编码弥补,所以这种方式还是比较常用的,Spring Task可以当作是轻量级的Quartz。
3. SpringBoot整合Quartz实现定时任务(常用)
3.1、quartz支持持久化,默认以多线程的方式执行;
3.2、概念:
Job:任务。定时执行的具体任务内容;
JobDetail:任务详情。即与任务相关的其他配置信息;
Trigger:触发器。定义定时任务执行的时间规则;
Scheduler:调度器。将Job和Trigger绑定;
3.3、具体实现:
- 引入依赖:spring-boot-starter-quartz
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
- 创建Job:
2.1. 定义定时任务类,实现Job接口;
2.2. @PersistJobDataAfterExecution,加了此注解的job称为有状态的job:多次执行的定时任务共享一个jobDataMap,真正的实现数据共享。
2.3. @DisallowConcurrentExecution,禁止并发访问同一个job:当任务处理时间超过任务间隔时间时,任务的间隔执行时间将变为任务处理时间,保证数据的准确性。
示例:
@PersistJobDataAfterExecution
@DisallowConcurrentExecution
public class MyJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
// 定时任务执行的功能
// context-定时任务执行的环境(上下文,可以理解为调度器:执行定时任务)
// 业务举例
// 获取jobDetail对象
JobDetail jobDetail = context.getJobDetail();
System.out.println("任务名字" + jobDetail.getKey().getName());
System.out.println("任务分组名字" + jobDetail.getKey().getGroup());
System.out.println("任务类名字" + jobDetail.getJobClass().getName());
System.out.println("本次执行时间" + context.getFireTime());
System.out.println("下次执行时间" + context.getNextFireTime());
// 记录任务执行次数
JobDataMap jobDataMap = jobDetail.getJobDataMap();
Integer count = (Integer)jobDataMap.get("count");
System.out.println("第" + count + "次执行");
jobDataMap.put("count", ++count); // 更新共享数据
}
}
- 创建JobDetail:
3.1. 定义配置类QuartzConfig;
3.2. @Bean public JobDetail jobDetail() {}
3.3. JobBuilder.newJob(绑定具体Job的class)
.storeDurably()
.withIdentity(“任务名”,“任务组名”)
.usingJobData(“count”,1)
.build();
示例:
@Configuration
public class QuartzConfig {
@Bean
public JobDetail jobDetail() {
return JobBuilder.newJob(MyJob.class)
.storeDurably() // 持久化(此JobDetail没有与Trigger绑定也不会被清理掉)
.withIdentity("job1", "group1") // 唯一标识
.usingJobData("count",1) // (多次执行的任务之间可共享的数据)数据初始化
.build();
}
}
- 创建Trigger
4.1. 配置类QuartzConfig;
4.2. @Bean public Trigger trigger() {}
4.3 TriggerBuilder.newTrigger()
.forJob(绑定具体JobDetail的实例名)
.withSchedule(CronScheduleBuilder.cronSchedule(cron表达式))
.withIdentity(“触发器名”,“任务组名”)
.build();
示例:
@Configuration
public class QuartzConfig {
@Bean
public JobDetail jobDetail() {
return JobBuilder.newJob(MyJob.class)
.storeDurably() // 持久化(此JobDetail没有与Trigger绑定也不会被清理掉)
.withIdentity("job1", "group1") // 唯一标识
.usingJobData("count",1) // (共享数据)数据初始化
.build();
}
@Bean
public Trigger trigger() {
String cron = "0/2 * * * * ? *"; // 每隔2秒执行一次
return TriggerBuilder.newTrigger()
.forJob(jobDetail()) // 关联jobDetail
.withIdentity("trigger1", "group1") // 唯一标识
.withSchedule(CronScheduleBuilder.cronSchedule(cron)) // 定时规则
.build();
}
}
- 运行启动类即可
Quartz实现定时任务的动态调度:
上面的例子虽已使用Quartz实现了定时任务功能,但定时任务是有系统调度的,也就是说定时任务会不断执行,直到项目结束运行;而在实际业务场景中,往往是需要根据需求自由的进行定时任务的生成、暂停、恢复、删除和更新操作的,所以,就需要动态调度定时任务。
Quartz本身没有提供动态调度的功能,需要自己根据相关的API开发。
动态定时任务的使用场景:
- 订单成功后自动发送短信通知
- 每日推送的功能引发用户不满,不再定时推送
- 每日下班前30分钟发送工作报表,遇节假日暂停发送
… …
实现动态调度定时任务,前面配置的QuartzConfig类其实就不需要了,然后自己去编写调度器。
Scheduler调度器常用的API:
scheduler.scheduleJob(jobDetail, trigger); 生成一个新的定时任务;
scheduler.pauseJob(jobKey); 暂停一个定时任务,参数jobKey代表该任务的唯一标识;
scheduler.resumeJob(jobKey); 恢复一个定时任务,参数jobKey代表该任务的唯一标识;
scheduler.deleteJob(jobKey); 删除一个定时任务,参数jobKey代表该任务的唯一标识;
scheduler.rescheduleJob(triggerKey, trigger); 修改一个定时任务;
scheduler.triggerJob(jobKey); 执行一次定时任务;
动态调度定时任务的具体实现:
- 定义任务类,实现Job接口,重写execute方法
@PersistJobDataAfterExecution
@DisallowConcurrentExecution
public class MyJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
// 定时任务执行的功能
// context-定时任务执行的环境(上下文,可以理解为调度器:执行定时任务)
}
}
- 任务Bean(创建的调度器通过JobBean来识别操作哪个Job)
JobBean
String jobName
String jobClass
String cronExpression
@Data
@AllArgsConstructor
@NoArgsConstructor
public class JobBean {
/**
* 任务名字
*/
private String jobName;
/**
* 任务类名
*/
private String jobClass;
/**
* cron表达式
*/
private String cronExpression;
}
- 操作任务的工具栏(重点)
JobUtils
static void createJob(Scheduler scheduler, JobBean jobBean)
static void pauseJob(Scheduler scheduler, String jobName)
static void resumeJob(Scheduler scheduler, String jobName)
static void deleteJob(Scheduler scheduler, String jobName)
static void runJobOnce(Scheduler scheduler, String jobName)
static void modifyJob(Scheduler scheduler, JobBean jobBean)
public class JobUtils {
/**
* 生成/创建一个定时任务
* @param scheduler 调度器
* @param jobBean 任务bean
*/
public static void createJob(Scheduler scheduler, JobBean jobBean) {
Class<? extends Job> jobClass = null;
JobDetail jobDetail = null;
Trigger trigger = null;
String cron = jobBean.getCronExpression();
try {
jobClass = (Class<? extends Job>) Class.forName(jobBean.getJobClass());
jobDetail = JobBuilder.newJob(jobClass)
.storeDurably()
.withIdentity(jobBean.getJobName())
.usingJobData("count",1)
.build();
trigger = TriggerBuilder.newTrigger()
.forJob(jobDetial)
.withSchedule(CronScheduleBuilder.cronSchedule(cron))
.withIdentity(jobBean.getJobName() + "_trigger")
.build();
scheduler.scheduleJob(jobDetail, trigger);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (SchedulerException e) {
throw new RuntimeException(e);
}
}
/**
* 暂停定时任务
* @param scheduler 调度器
* @param jobName 任务名字
*/
public static void pauseJob(Scheduler scheduler, String jobName) {
JobKey jobKey = JobKey.jobKey(jobName);
try {
scheduler.pauseJob(jobKey);
} catch (SchedulerException e) {
throw new RuntimeException(e);
}
}
/**
* 恢复定时任务
* @param scheduler 调度器
* @param jobName 任务名字
*/
public static void resumeJob(Scheduler scheduler, String jobName) {
JobKey jobKey = JobKey.jobKey(jobName);
try {
scheduler.resumeJob(jobKey);
} catch (SchedulerException e) {
throw new RuntimeException(e);
}
}
/**
* 删除定时任务
* @param scheduler 调度器
* @param jobName 任务名字
*/
public static void deleteJob(Scheduler scheduler, String jobName) {
JobKey jobKey = JobKey.jobKey(jobName);
try {
scheduler.deleteJob(jobKey);
} catch (SchedulerException e) {
throw new RuntimeException(e);
}
}
/**
* 执行一次定时任务(立即执行一次)
* @param scheduler 调度器
* @param jobName 任务名字
*/
public static void runJobOnce(Scheduler scheduler, String jobName) {
JobKey jobKey = JobKey.jobKey(jobName);
try {
scheduler.triggerJob(jobKey);
} catch (SchedulerException e) {
throw new RuntimeException(e);
}
}
/**
* 修改定时任务
* @param scheduler 调度器
* @param jobBean 任务bean
*/
public static void modifyJob(Scheduler scheduler, JobBean jobBean) {
// 获取任务触发器的唯一标识
TriggerKey triggerKey = TriggerKey.triggerKey(jobBean.getJobName() + "_trigger");
try {
// 通过触发器的唯一标识获取触发器对象
CronTrigger oldTrigger = (CronTrigger) scheduler.getTrigger(triggerKey);
// 使用新的cron表达式创建新的触发器
String newCron = jobBean.getCronExpression();
CronTrigger newTrigger = oldTrigger.getTriggerBuilder()
.withSchedule(CronScheduleBuilder.cronSchedule(newCron).withMisfireHandlingInstructionDoNothing()) //后面的.withMisfireHandlingInstructionDoNothing()是为了让调度器忽略所有错过的任务,按正常调度正常执行
.bulid();
// 调度器更新任务的触发器
scheduler.rescheduleJob(triggerKey, newTrigger);
} catch (SchedulerException e) {
throw new RuntimeException(e);
}
}
}
- 编写控制器操作任务
QuartzController
String createJob()
String pauseJob()
String resumeJob()
String deleteJob()
String modifyJob()
@RestController
@RequestMapping("/quartz")
public class QuartzController {
@Autowired
Scheduler scheduler;
private String jobName = "myjob";
@GetMapping("/create")
public String createJob() {
JobBean jobBean = new JobBean(jobName, MyJob.Class.getName, "0/2 * * * * ?");
JobUtils.createJob(scheduler, jobBean);
return "定时任务创建成功";
}
@GetMapping("/pause")
public String pauseJob() {
JobUtils.pauseJob(scheduler, jobName);
return "定时任务暂停成功";
}
@GetMapping("/resume")
public String resumeJob() {
JobUtils.resumeJob(scheduler, jobName);
return "定时任务恢复成功";
}
@GetMapping("/delete")
public String deleteJob() {
JobUtils.deleteJob(scheduler, jobName);
return "定时任务删除成功";
}
@GetMapping("/once")
public String runOnceJob() {
JobUtils.runJobOnce(scheduler, jobName);
return "定时任务执行一次成功";
}
@GetMapping("/modify")
public String modifyJob() {
JobBean jobBean = new JobBean(jobName, MyJob.Class.getName, "0/5 * * * * ?");
JobUtils.modifyJob(scheduler, jobBean);
return "定时任务修改成功";
}
}