Java定时任务的实现
1. @Scheduled注解
引入依赖
<!-- Spring Boot Schedule依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
使用方法
-
首先,在定时任务方法所在的类加上 @Component 注解
-
接着,在实现定时任务的方法上加上 @Scheduled 注解
DemoScheduler.java:
@Component // 标记为Spring组件,使其被扫描并管理 public class DemoScheduler { // 日期格式化工具 private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); /** * 示例1:固定延迟执行 * 上一次任务执行完毕后,延迟固定时间再执行下一次 */ @Scheduled(fixedDelay = 5000) // 单位:毫秒,即5秒 public void scheduleTaskWithFixedDelay() { System.out.println("固定延迟任务执行时间: " + dateFormat.format(new Date())); System.out.println("这是一个固定延迟执行的任务(上一次执行完毕后5秒再次执行)"); } } -
最后,在启动类加上 @EnableScheduling 表示启用定时任务
TimerApplication.java:
// @SpringBootApplication 注解标记这是一个Spring Boot应用 // @EnableScheduling 注解启用定时任务功能 @SpringBootApplication @EnableScheduling public class TimerApplication { // 启动Spring Boot应用 // main方法是Java程序的入口点,args是命令行参数 public static void main(String[] args) { // SpringApplication.run()方法用于启动Spring Boot应用 // 第一个参数是当前类的Class对象,第二个参数是命令行参数 SpringApplication.run(TimerApplication.class, args); } }程序启动之后,执行流程如下:
TimerApplication(@SpringBootApplication)
↓
包含@EnableScheduling → 开启定时任务功能
↓
包含@ComponentScan → 扫描到DemoScheduler(@Component)
↓
发现DemoScheduler中带@Scheduled的方法
↓
Spring自动按照调度规则执行这些方法
总结:
Spring 通过 “组件扫描 + 注解驱动” 的方式,实现了 “约定优于配置” 的设计理念。只要类和方法上标注了正确的注解,即使没有显式的代码调用,Spring 也能自动识别并执行相应的逻辑,这也是 Spring 框架的核心特性之一。
注解参数
@Scheduled注解主要有三个如下的常用参数(参数的值都需要引号包裹):
cron
-
用法:可以接收一个cron表达式。表达式总共有六个位置,从左到右分别表示:秒、分、时、日、月、周和年。基本用法如:在秒的位置上,如果为10,即表示每分钟的第10秒执行一次定时任务;如果为5/10,则表示从每分钟的第5秒开始,每10秒执行一次。如果太麻烦,可通过网站Cron - 在线Cron表达式生成器 进行转换和反解析
-
例子:
/** * 示例:Cron表达式执行 * 使用Cron表达式定义更复杂的执行计划 * 这里的表达式表示每分钟的第10秒执行一次 */ @Scheduled(cron = "10 * * * * *") public void scheduleTaskWithCronExpression() { System.out.println("\nCron任务执行时间: " + dateFormat.format(new Date())); System.out.println("这是一个使用Cron表达式的任务(每分钟的第10秒执行)"); }
fixedDelay
-
用法:上一次任务执行完毕后,延迟固定时间再执行下一次,主要涉及两种参数:fixedDelay/fixedDelayString和initialDelay,分别表示每几秒执行一次和程序启动后延迟几秒执行(initialDelay可省略)。其中fixedDelayString与fixedDelay的效果一致,只是一个只能接收String类型,一个只能接收long类型
-
例子:
/** * 示例:初始延迟后执行 * 应用启动后,延迟指定时间开始第一次执行,之后按照固定速率执行 */ @Scheduled(initialDelay = 10000, fixedRate = 6000) // 初始延迟10秒,之后每6秒执行一次 public void scheduleTaskWithInitialDelay() { System.out.println("\n初始延迟任务执行时间: " + dateFormat.format(new Date())); System.out.println("这是一个带初始延迟的任务(启动后10秒首次执行,之后每6秒执行一次)"); }
fixedRate
-
用法:以固定的时间间隔执行,不管上一次任务是否完成,只有一中参数fixedRate / fixedRateString。
-
例子:
/** * 示例:固定速率执行 * 以固定的时间间隔执行,不管上一次任务是否完成 */ @Scheduled(fixedRate = 3000) // 单位:毫秒,即3秒 public void scheduleTaskWithFixedRate() { System.out.println("\n固定速率任务执行时间: " + dateFormat.format(new Date())); System.out.println("这是一个固定速率执行的任务(每3秒执行一次)"); }
通过配置指定参数值
在实际应用场景中,如果程序已经启动了,但是想控制定时任务的执行速率,或者想开关定时任务,可以通过application配置文件中的值来指定为注解参数的值。接着可通过修改nacos中的配置文件的值来控制定时任务的速率已经开关,这样就不需要修改代码,也不需要修改代码后重启应用程序来修改定时任务了。
例如:
-
application.yml:
# 应用名称 spring: application: name: spring-schedule-demo # 定时任务配置 task: scheduling: enable: useDemo = false cron: cornParam: 0/5 * * * * * fixeDelaydParm: 8000 # 服务器端口 server: port: 8080 -
DemoScheduler.java:
@Component // 标记为Spring组件,使其被扫描并管理 //使用@ConditionalOnProperty注解来控制是否启用定时任务 @ConditionalOnProperty(prefix = "spring.task.enable",name = "userDemo",havingValue = "${spring.task.enable.userDemo") public class DemoScheduler { // 日期格式化工具 private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); /** * 示例1:Cron表达式(application参数)执行 * 使用Cron表达式定义更复杂的执行计划 * application的表达式表示从每分钟0秒开始,每5秒执行一次 */ @Scheduled(cron = "${spring.task.cron.cornParam}") public void scheduleTaskWithCornApplicationParam() { System.out.println("\nCron任务(application参数)执行时间: " + dateFormat.format(new Date())); System.out.println("这是一个使用Cron表达式(application参数的任务(从每分钟0秒开始,每5秒执行一次)"); } /** * 示例2:固定延迟执行(application参数)执行 * 上一次任务执行完毕后,延迟固定时间再执行下一次 * fixedDelayString与fixedDelay的效果一致,只是一个只能接收String类型,一个只能接收long类型 */ @Scheduled(fixedDelayString = "${spring.task.cron.fixeDelaydParm}") public void scheduleTaskWithFixedDelayApplicationParam() { System.out.println("\n初始延迟任务执行时间: " + dateFormat.format(new Date())); System.out.println("这是一个带初始延迟的任务(启动后4秒首次执行,之后每8秒执行一次)"); } }
其中@ConditionalOnProperty注解、cron参数和fixedDelayString参数的值都是使用配置的值。
2. Quartz框架
核心类
Quartz的核心类有以下三部分:
-
任务
Job:需要实现的任务类,实现execute()方法,执行后完成任务。(可自定义执行的任务内容) -
触发器
Trigger:包括SimpleTrigger和CronTrigger。(可自定义执行任务的频率) -
调度器
Scheduler:任务调度器,负责基于Trigger触发器,来执行Job任务。
在 Quartz 框架中,JobDetail 和 Trigger 的 name是必须提供的,而 group是可选的。如果你不显式指定 group,Quartz 会为它们分配一个默认的组名,即 DEFAULT
引入依赖
<!--Quartz依赖-->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
2.1 简单使用方法(利用SimpleTrigger)
首先实现一个任务类TestJob ,自定义任务内容:
package org.example.quartz;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
public class TestJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
//通过继承Job类、重写方法自定义任务内容
System.out.println("Quartz定时任务执行~");
}
}
接着在main方法中,创建任务、创建触发器、创建调度器:
package org.example.quartz;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Date;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.SimpleScheduleBuilder.simpleSchedule;
import static org.quartz.TriggerBuilder.newTrigger;
@Component
public class TestQuartz {
public static void main(String[] args) throws SchedulerException {
//创建任务调度器
Scheduler scheduler = new StdSchedulerFactory().getScheduler();
//创建任务
JobDetail job = newJob(TestJob.class)//自定义任务内容
.withIdentity("myJob","group1")//定义任务名称与组织名称
.build();//创建
//创建触发器
Trigger trigger = newTrigger()
.withIdentity("myTigger","group1")//自定义任务内容
.startNow()//设置开始时间
.withSchedule(simpleSchedule()//利用静态方法指定任务执行频率
.withIntervalInSeconds(5)//间隔五秒执行一次
.repeatForever())//永久循环
.build();//创建
//将任务和触发器注册到调度器中
scheduler.scheduleJob(job,trigger);
//启动调度器
scheduler.start();
}
}
2.2 三者之间的关系
2.2.1 多个Trigger能够调度同一个JobDetail,一个Scheduler可以调度多个触发器Trigger
测试代码:
public static void main(String[] args) throws SchedulerException {
//创建任务调度器
Scheduler scheduler = new StdSchedulerFactory().getScheduler();
//创建任务
JobDetail job = newJob(TestJob.class)//自定义任务内容
.withIdentity("myJob","group2")//定义任务名称与组织名称
.build();//创建
//创建多个触发器
Trigger trigger = newTrigger()
.withIdentity("myTigger","group2")//自定义任务内容
.startNow()//设置开始时间
.withSchedule(simpleSchedule()//利用静态方法指定任务执行频率
.withIntervalInSeconds(5)//间隔五秒执行一次
.repeatForever())//永久循环
.build();//创建
Trigger trigger2 = newTrigger()
.withIdentity("myTigger2","group2")//自定义任务内容
.startNow()//设置开始时间
.forJob("myJob","group2")//无需重复注册时,用该方法绑定
.withSchedule(simpleSchedule()//利用静态方法指定任务执行频率
.withIntervalInSeconds(3)//间隔五秒执行一次
.repeatForever())//永久循环
.build();//创建
//将任务和触发器注册到调度器中
//同时也证明了一个调度器可以调度多个触发器Trigger
scheduler.scheduleJob(job,trigger);
scheduler.scheduleJob(trigger2);//主要:Job已经注册过一次了,无需重复利用scheduleJob方法注册
//启动调度器
scheduler.start();
}
成功运行:
Job相同的任务内容成功按照trigger1和trigger2的频率运行。
2.2.2 多个JobDetail不能使用同一个Trigger,多个JobDetail能够使用同一个Job
测试代码:
public static void main(String[] args) throws SchedulerException {
//创建任务调度器
Scheduler scheduler = new StdSchedulerFactory().getScheduler();
//创建多个任务
//同时也证明了:多个JobDetail能够使用同一个Job
JobDetail job1 = newJob(TestJob.class)//自定义任务内容
.withIdentity("myJob1","group3")//定义任务名称与组织名称
.build();//创建
JobDetail job2 = newJob(TestJob.class)//自定义任务内容
.withIdentity("myJob2","group3")//定义任务名称与组织名称
.build();//创建
//创建触发器
Trigger trigger = newTrigger()
.withIdentity("myTigger","group3")//自定义任务内容
.startNow()//设置开始时间
.withSchedule(simpleSchedule()//利用静态方法指定任务执行频率
.withIntervalInSeconds(5)//间隔五秒执行一次
.repeatForever())//永久循环
.build();//创建
//将任务和触发器注册到调度器中
scheduler.scheduleJob(job1,trigger);
scheduler.scheduleJob(job2,trigger);
//启动调度器
scheduler.start();
}
报错:
Exception in thread "main" org.quartz.SchedulerException: Trigger does not reference given job!
at org.quartz.core.QuartzScheduler.scheduleJob(QuartzScheduler.java:838)
at org.quartz.impl.StdScheduler.scheduleJob(StdScheduler.java:249)
at org.example.quartz._03_TestMultiJob.main(_03_TestMultiJob.java:41)
原因:
-
scheduler.scheduleJob(job1, trigger)已经将trigger绑定到job1。 -
再次调用
scheduler.scheduleJob(job2, trigger)时,Quartz 发现trigger已经关联了job1,所以抛出SchedulerException: Trigger does not reference given job!。
总结
-
一个Scheduler能够调度多个Trigger
-
一个Trigger只能绑定一个JobDetail(否则多个任务将在同一时间运行)
-
一个JobDetail可以绑定多个Trigger(相同的任务内容可以同时按照不同的频率运行)
-
一个Job可以使用供多个JobDeatil使用
2.3 使用Cron表达式(利用CronTrigger)
2.3.1 示例程序
public static void main(String[] args) throws SchedulerException {
//创建任务调度器
Scheduler scheduler = new StdSchedulerFactory().getScheduler();
//创建任务
JobDetail job = newJob(TestJob.class)//自定义任务内容
.withIdentity("myJob","group1")//定义任务名称与组织名称
.build();//创建
//创建触发器
Trigger trigger = newTrigger()
.withIdentity("myTigger","group1")//自定义任务内容
.startNow()//设置开始时间
//利用Cron表达式来创建Trigger
.withSchedule(CronScheduleBuilder
.cronSchedule("0/2 * * * * ? *"))
.build();//创建
//将任务和触发器注册到调度器中
scheduler.scheduleJob(job,trigger);
//启动调度器
scheduler.start();
}
2.3.2 Cron表达式的语法
概览
Cron共有七个位置,每个位置分布表示:
| 单位 | 允许符号 | 范围 |
|---|---|---|
| 秒 | , - * / | 0-59 |
| 分 | , - * / | 0-59 |
| 时 | , - * / | 0-23 |
| 日 | , - * / ? L w | 1-31 |
| 月 | , - * / | 0-11 |
| 星期 | , - * / ? L # | 1-7(1表示星期日) |
| 年(可选)) | , - * / | 1970-2199 |
若年份字段不选,则默认每年都执行
以下是每个符号的解释:
1. “,”
表示同时选择多个时间点,如秒的位置的值为"0,2"时,表示每次的第0秒和第2秒都会执行一次
2. “-”
表示选择某个时间段,如秒的位置的值为"0-10"时,表示每次的0到10秒的区间会执行一次
3. “*”
表示选择范围内容的所有时间,如秒的位置的值为"*“时,表示0-59秒都会执行
4. “/”
表示从某个时间段开始,间隔固定的时间执行一次。如秒的位置的值为"0/4"时,表示从第0秒开始,每隔4秒执行一次
5. “?”
表示 日 和 星期无特定值,因为日 和 星期 本质上都指天数,因此二者同时使用容易产生冲突造成表达式非法,因此二者一般不一起使用。当只在两个字段中指定一项时,”?" 很有用。
6. “L”
-
是”Last“的简写,如”月“字段使用”L“表示月的"最后一天",如非闰年的1月31日、2月28日,”星期“的字段使用则表示”星期六“或者”SAT“(周六)
