在SpringBoot项目中使用定时任务时可以使用@Scheduled标注在需要定时执行的方法上,@Scheduled注解要生效还需要在系统启动类或配置类上添加@EnableScheduling注解。
@Scheduled属性:
1、启用Scheduling
@SpringBootApplication
@EnableScheduling
public class SpringbootApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootApplication.class, args);
}
}
2、创建任务调度类加入Spring容器中以及具体的任务
创建定时任务调度类TestSchedule,并定义taskSchedule1方法使用@Scheduled(cron = "0/10 * * * * ?")标注,表示该方法从0秒开始,每隔10秒执行一次,方法内部获取了执行当前任务的线程,打印了任务开始和结束的时间、线程ID,sleep 5秒表示任务执行完成需要花费5秒时间。
@Component
public class TestSchedule {
@Scheduled(cron = "0/10 * * * * ?")
public void taskSchdule1() throws InterruptedException {
Thread t = Thread.currentThread();
System.out.println("taskSchule1 "+ DateTimeUtils.dateToTimeStr(new Date()) +" ThreadID:"+ t.getId());
Thread.sleep(5000);
System.out.println("taskSchule1 end "+ DateTimeUtils.dateToTimeStr(new Date()) +" ThreadID:"+ t.getId());
}
}
该任务执行结果:
taskSchdule1每隔10秒执行一次,并且每次执行5秒。
在TestSchedule类中添加第二个需要任务调度方法,每隔3秒执行一次。
@Scheduled(cron = "0/3 * * * * ?")
public void taskSchdule2(){
Thread t = Thread.currentThread();
System.out.println("taskSchule2 "+ DateTimeUtils.dateToTimeStr(new Date()) +" ThreadID:"+ t.getId());
}
当前执行结果如下:
发现问题:taskSchule2,在a、b两处时间相差7秒已超过3秒,则taskSchule2是在taskSchule1执行结束才立即执行的,推测多个任务是串行的。
Spring中@EnableScheduling和@Scheduled标注的定时任务默认是单线程执行的,这里taskSchule1执行任务需要花费较长时间,所有阻塞了taskSchule2的执行。
解决方法:使用@Async和@EnableAsync异步执行任务,@Async 注解用于标记一个方法是异步方法,即该方法可以在独立的线程中执行,而不会阻塞当前线程。被 @Async 注解标记的方法通常需要在类上添加 @EnableAsync 注解来启用异步方法执行的支持。
修改启动类:
@SpringBootApplication
@EnableScheduling
@EnableAsync
public class SpringbootApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootApplication.class, args);
}
}
修改任务调度类:
@Component
@Async
public class TestSchedule {
@Scheduled(cron = "0/10 * * * * ?")
public void taskSchdule1() throws InterruptedException {
Thread t = Thread.currentThread();
System.out.println("taskSchule1 "+ DateTimeUtils.dateToTimeStr(new Date()) +" ThreadID:"+ t.getId());
Thread.sleep(5000);
System.out.println("taskSchule1 end "+ DateTimeUtils.dateToTimeStr(new Date()) +" ThreadID:"+ t.getId());
}
@Scheduled(cron = "0/3 * * * * ?")
public void taskSchdule2(){
Thread t = Thread.currentThread();
System.out.println("taskSchule2 "+ DateTimeUtils.dateToTimeStr(new Date()) +" ThreadID:"+ t.getId());
}
}
在TestSchedule类上标注@Async注解,表示该类中所有标注了@Scheduled的方法都使用异步处理方式。
修改后执行结果:taskSchdule1每次间隔10秒执行,taskSchdule2每次间隔3秒执行
再次稍作修改:将任务的睡眠时间改成11秒 Thread.sleep(11000);,此时任务1的执行时间已经超过了它的调度时间。再次运行程序结果如下:
发现taskSchdule1交叉执行,两组是不同线程执行的,因为taskSchdule1的执行时间超过了调度时间,所以,a处taskSchdule1.task-3开始执行,在未执行完成的情况下,任务的调度时间到了,其他线程有立马调度了任务从c处开始执行。
综上解决两种问题方法:
去掉启动类中的@EnableAsync和@EnableScheduling注解,去掉调度类中的@Async注解
//启动类
@SpringBootApplication
public class SpringbootApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootApplication.class, args);
}
}
//调度任务类
@Component
public class TestSchedule {
@Scheduled(cron = "0/10 * * * * ?")
public void taskSchdule1() throws InterruptedException {
Thread t = Thread.currentThread();
System.out.println("taskSchule1 "+ DateTimeUtils.dateToTimeStr(new Date()) +" ThreadID:"+ t.getId());
Thread.sleep(11000);
System.out.println("taskSchule1 end "+ DateTimeUtils.dateToTimeStr(new Date()) +" ThreadID:"+ t.getId());
}
@Scheduled(cron = "0/3 * * * * ?")
public void taskSchdule2(){
Thread t = Thread.currentThread();
System.out.println("taskSchule2 "+ DateTimeUtils.dateToTimeStr(new Date()) +" ThreadID:"+ t.getId());
}
}
创建一个任务配置类ScheduleConfig 实现SchedulingConfigurer接口的configureTasks方法,使用参数taskRegistrar为任务调度创建线程池。
@Configuration
@EnableScheduling
public class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(taskExecutor());
}
@Bean(destroyMethod="shutdown")
public Executor taskExecutor() {
return Executors.newScheduledThreadPool(10);
}
}
此时的执行结果:
现在taskSchdule1和taskSchdule2均运行正常,并且taskSchdule1不会出现交叉现象, taskSchdule1第二次调度会等到第一次调度执行完毕后的下一个调度时间点才会执行。
3、优化
如果我们修改了cron表达式,需要重启整个应用才能生效,不是很方便。想要实现修改cron表达式就生效就需要用到接口的方式来实现定时任务,并将cron表达式定义在数据库中,修改数据库中对应的cron表达式就可以实时生效。
1、首先定义一张表用来记录cron表达式
CREATE TABLE `scheduled` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(255) NULL,
`cron` varchar(255) NULL,
PRIMARY KEY (`id`)
)
INSERT INTO `scheduled` (`id`, `name`, `cron`) VALUES (1, '定时任务1', '0/6 * * * * ?')
2、实现定时任务
@Component
@EnableScheduling //开启定时任务
public class ScheduleTask implements SchedulingConfigurer {
@Autowired
private CronMapper cronMapper;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addTriggerTask(
//添加任务内容
() -> process(),
//设置执行的周期
triggerContext -> {
//查询cron表达式
String cron = cronMapper.getCron(1L);
if (cron.isEmpty()) {
System.out.println("cron is null");
}
return new CronTrigger(cron).nextExecutionTime(triggerContext);
});
}
private void process() {
System.out.println("基于接口的定时任务-具体业务逻辑");
}
}