@Scheduled
注解是Spring提供的一种用于简化定时任务实现的机制。它允许开发者以一种声明式的方式处理周期性任务,无需自己编写底层的定时逻辑。通过@Scheduled
注解,可以方便地配置和执行周期性任务。
属性 | 描述 |
---|---|
cron | Cron的全称是“chronograph”,意为“计时器”。它允许用户安排作业(通常是脚本或命令)在指定的时间或日期自动运行,可以使用${} 方式定义。Cron - 在线Cron表达式生成器 |
zone | 解析cron表达式的时区,默认为本地时区 |
fixedDelay | 上个任务完成之后的间隔指定时间执行下一个任务 |
fixedDelayString | fixedDelay 的字符串类型,可以使用${} 方式读取属性 |
fixedRate | 固定频率执行任务,无论上个任务是否完成 |
fixedRateString | fixedDelay 的字符串类型,可以使用${} 方式读取属性 |
initialDelay | 在首次执行fixedRate 或fixedDelay 任务之前延迟的时间单位数。 |
initialDelayString | initialDelay 的字符串类型,可以使用${} 读取属性方式 |
timeUnit | 时间单位.默认为毫秒,影响fixedDelay ,fixedDelay ,initialDelay 以及其字符串属性 |
scheduler | Spring 6.1之后的属性,指定任务线程池。不指定返回默认线程池 |
如果不指定fixedRate
、fixedDelay
、cron
属性,那么initialDelay
属性是必须的
通过@EnableScheduling
注解启动定时任务模块, 示例代码如下:
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.annotation.Schedules;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* @author DawnStar
* @since : 2024/11/2
*/
@EnableScheduling
@SpringBootApplication
public class ScheduleApplication {
public static void main(String[] args) {
new SpringApplicationBuilder()
.sources(ScheduleApplication.class)
.web(WebApplicationType.NONE)
.run(args);
}
@Component
static class ScheduleTaskClass {
protected final Log logger = LogFactory.getLog(getClass());
@Schedules({@Scheduled(fixedDelay = 5000), @Scheduled(fixedDelay = 7000)})
public void delayTwoSeconds() {
logger.info("上个任务完成之后的等待指定间隔执行下一个任务...");
}
@Scheduled(fixedRate = 2000)
public void rateTwoSeconds() {
logger.info("固定频率执行任务....");
}
@Scheduled(initialDelay = 1, timeUnit = TimeUnit.MINUTES)
public void initialDelay() {
logger.info("一分钟之后执行一次任务...");
}
@Scheduled(cron = "0/9 * * * * *")
public void cron() {
logger.info("每九秒执行一次任务...");
} }
}
ScheduledAnnotationBeanPostProcessor
@EnableScheduling
注解注入ScheduledAnnotationBeanPostProcessor
后置处理器,Bean
名称为org.springframework.context.annotation.internalScheduledAnnotationProcessor
接下来点击这个常量,可以看到org.springframework.boot.autoconfigure.task.TaskSchedulingConfigurations
中,如果存在这个名称的ScheduledAnnotationBeanPostProcessor
的Bean,那么就会创建一个Bean名为taskScheduler
的ThreadPoolTaskScheduler
的对象,如图:
存在指定的ScheduledAnnotationBeanPostProcessor
并且不存在TaskScheduler
和ScheduledExecutorService
的双重条件下
创建默认的线程池,接下来看ScheduledTaskRegistrar
类,这是实现定时任务功能的重要助手类,是ScheduledAnnotationBeanPostProcessor
一个属性,如@Scheduled
中的属性可以找到,如cron
对应的cronTasks
回到ScheduledAnnotationBeanPostProcessor
类,解析@Scheduled
注解并创建定时任务的方法在processScheduledTask
这里只是创建任务并添加到对应的任务集合中,还没有对应的线程池执行这些任务。执行任务的方法在org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor#finishRegistration
方法中
在这里这里配置了ScheduledTaskRegistrar
的线程池,调用了ScheduledTaskRegistrar
的afterPropertiesSet
方法,类似scheduleCronTask
方法
调用了TaskScheduler
的schedule
方法得到一个ScheduledFuture
,非CronTask
有其对应的方法,如IntervalTask
对应的scheduleAtFixedRate
方法
可能会有疑惑这个方法不是实现了InitializingBean
接口,为什么要手动调用,因为默认的是不受容器管理,默认使用是ScheduledAnnotationBeanPostProcessor
的无参构造器。
继续看回ScheduledAnnotationBeanPostProcessor
的finishRegistration
的方法,如果scheduled
的属性为空时候,就会创建一个TaskSchedulerRouter
对象,并注入beanFactory
,这个类的determineDefaultScheduler
会获取对应的TaskScheduler
默认对象,这也是taskScheduler
由来
上述默认即为 defaultScheduler
属性的get方法执行,如果名称就先根据名称获取,否则就执行determineDefaultScheduler
方法
那么,如果注入了TaskScheduler
Bean会怎样,在上述的创建默认的taskScheduler
是有双重条件的,如:
@Bean
public TaskScheduler aaa() {
return new ThreadPoolTaskScheduler();
}
@Bean
public TaskScheduler bbb() {
return new ThreadPoolTaskScheduler();
}
这里创建了两个TaskScheduler
的bean,默认的就不会被创建,同时@Scheduled
没有指定scheduler
,这样就没有找到对应TaskScheduler
,那么determineDefaultScheduler
方法就会执行到最后,创建并返回ConcurrentTaskScheduler
对象,@Scheduled
中的scheduler
可以指定用到的TaskScheduler
了解了上面的代码之后,可以自定义ScheduledAnnotationBeanPostProcessor
,默认ScheduledAnnotationBeanPostProcessor
默认是不能被覆盖的
/**
* 注意:这个没有被使用,要使用如下
* @param taskScheduler 指定的线程池
* @return ScheduledTaskRegistrar
*/
@Bean
public ScheduledTaskRegistrar scheduledTaskRegistrar(@Qualifier("bbb") TaskScheduler taskScheduler) {
ScheduledTaskRegistrar taskRegistrar = new ScheduledTaskRegistrar();
taskRegistrar.setScheduler(taskScheduler);
return taskRegistrar;
}
/**
* 这个不会覆盖原始的:ScheduledAnnotationBeanPostProcessor
* @param scheduledTaskRegistrar taskScheduler
* @return ScheduledAnnotationBeanPostProcessor
*/@Bean
public ScheduledAnnotationBeanPostProcessor scheduledAnnotationBeanPostProcessor(ScheduledTaskRegistrar scheduledTaskRegistrar) {
return new ScheduledAnnotationBeanPostProcessor(scheduledTaskRegistrar);
}
了解了上面的代码之后,可以自定义ScheduledAnnotationBeanPostProcessor
,默认ScheduledAnnotationBeanPostProcessor
默认是不能被覆盖的
/**
* 注意:这个没有被使用
* @param taskScheduler 指定的线程池
* @return ScheduledTaskRegistrar
*/
@Bean
public ScheduledTaskRegistrar scheduledTaskRegistrar(@Qualifier("bbb") TaskScheduler taskScheduler) {
ScheduledTaskRegistrar taskRegistrar = new ScheduledTaskRegistrar();
taskRegistrar.setScheduler(taskScheduler);
return taskRegistrar;
}
/**
* 这个不会覆盖原始的:ScheduledAnnotationBeanPostProcessor
* @param scheduledTaskRegistrar taskScheduler
* @return ScheduledAnnotationBeanPostProcessor
*/
// @Bean(TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
@Bean
public ScheduledAnnotationBeanPostProcessor scheduledAnnotationBeanPostProcessor(ScheduledTaskRegistrar scheduledTaskRegistrar) {
// 使用上面
return new ScheduledAnnotationBeanPostProcessor(scheduledTaskRegistrar);
}
如果使用@Bean(TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
手动注入这个Bean,那么会出现以下错误:
不过这样重复任务,默认用官方的即可,我们只需要配置TaskScheduler
就好,如果有多个,建议配置一个名称为taskScheduler
的Bean,减少获取的条件。
SchedulingConfigurer 接口,实现动态刷新定时任务
通过@Scheduled
注解可以很方便的使用定时任务,然而,在开发业务的时候,可能会多次变更定时任务的频率,如修改cron
的表达式,因为这个而频繁重启服务,这是不推荐的,所以,我们可以实现SchedulingConfigurer
接口,从而实现动态刷新定时任务。
@FunctionalInterface
public interface SchedulingConfigurer {
/**
* Callback allowing a {@link org.springframework.scheduling.TaskScheduler}
* and specific {@link org.springframework.scheduling.config.Task} instances
* to be registered against the given the {@link ScheduledTaskRegistrar}.
* @param taskRegistrar the registrar to be configured
*/
void configureTasks(ScheduledTaskRegistrar taskRegistrar);
}
依旧是看ScheduledAnnotationBeanPostProcessor
的finishRegistration
方法,在这里会调用SchedulingConfigurer
的configureTasks
方法
这个registrar
已经注入了线程池,默认是TaskSchedulerRouter
,我们只需要实现SchedulingConfigurer
接口即可。以下是cron动态刷新任务的示例代码,不包含持久化:
package com.silvergravel;
import com.silvergravel.service.SyncServiceImpl;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.scheduling.annotation.EnableScheduling;
import java.util.concurrent.TimeUnit;
@SpringBootApplication
@EnableScheduling
public class ScheduleApplication {
public static void main(String[] args) throws InterruptedException {
ConfigurableApplicationContext context = new SpringApplicationBuilder(ScheduleApplication.class)
.web(WebApplicationType.NONE)
.run(args);
// 延迟15秒之后指定
TimeUnit.SECONDS.sleep(15);
SyncServiceImpl bean = context.getBean(SyncServiceImpl.class);
// 改成每10秒执行一次
bean.updateCron("0/10 * * * * *");
}}
package com.silvergravel.service;
import org.springframework.scheduling.support.CronTrigger;
/**
* @author DawnStar
* @since : 2024/11/3
*/public interface SyncService {
/**
* 执行同步
*/
void executeSync();
/**
* 返回指定触发器
* @return 返回指定的触发器
*/
CronTrigger getCronTrigger();
}
package com.silvergravel.service;
import com.silvergravel.event.CronEvent;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronExpression;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
/**
* @author DawnStar
* @since : 2024/10/27
*/@Service
public class SyncServiceImpl implements SyncService {
private final Logger log = LoggerFactory.getLogger(SyncServiceImpl.class);
private CronTrigger cronTrigger;
@Resource
private ApplicationEventPublisher applicationEventPublisher;
@Autowired
public SyncServiceImpl(@Value("${default-cron: 0/5 * * * * *}") String defaultCronExpression) {
this.cronTrigger = new CronTrigger(defaultCronExpression);
}
@Override
public CronTrigger getCronTrigger() {
return cronTrigger;
}
@Override
public void executeSync() {
log.info("执行同步");
}
public void updateCron(String cronExpression) {
Assert.notNull(cronExpression, "表达式不能空");
boolean equals = ScheduledTaskRegistrar.CRON_DISABLED.equals(cronExpression);
if (equals) {
cronTrigger = null;
applicationEventPublisher.publishEvent(new CronEvent(""));
return;
}
boolean validExpression = CronExpression.isValidExpression(cronExpression);
Assert.isTrue(validExpression, "表达式不正确");
this.cronTrigger = new CronTrigger(cronExpression);
applicationEventPublisher.publishEvent(new CronEvent(this.cronTrigger.getExpression()));
}
}
这里使用了ScheduledTaskRegistrar.CRON_DISABLED
,如果表达式为-
则不创建定时任务,源码在
org.springframework.scheduling.config.ScheduledTaskRegistrar#addCronTask(java.lang.Runnable, java.lang.String)
方法中使用到。
同时这里使用了CronExpression.isValidExpression(cronExpression)
方法校验表达式,这其实是对CronExpression.parse(String expression)
的一层封装。这里可以使用parse
,相对的需要try catch进行业务逻辑开发。
package com.silvergravel.event;
import com.silvergravel.service.SyncService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.lang.Nullable;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.CronTask;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ScheduledFuture;
/**
* @author DawnStar
* @since : 2024/11/3
*/@Component
public class ScheduleTaskConfigurer implements SchedulingConfigurer, ApplicationContextAware {
private final Logger log = LoggerFactory.getLogger(ScheduleTaskConfigurer.class);
/**
* 容器上下文,用于获取指定的服务
*/
private ApplicationContext appCxt;
private ScheduledTaskRegistrar taskRegistrar;
private final HashMap<String, String> beanNameExpressionMap = new HashMap<>(16);
private final HashMap<String, ScheduledFuture<?>> beanNameScheduledTaskMap = new HashMap<>(16);
@Override
public void setApplicationContext(@Nullable ApplicationContext applicationContext) throws BeansException {
this.appCxt = applicationContext;
}
@Override
public void configureTasks(@Nullable ScheduledTaskRegistrar taskRegistrar) {
this.taskRegistrar = taskRegistrar;
refresh();
}
synchronized void refresh() {
Map<String, SyncService> syncServiceMap = appCxt.getBeansOfType(SyncService.class);
syncServiceMap.forEach(this::refreshTaskCron);
}
private void refreshTaskCron(String beanName, SyncService syncService) {
CronTrigger cronTrigger = syncService.getCronTrigger();
if (cronTrigger == null) {
log.warn("{} 没有 cron ", beanName);
cancelTask(beanName);
return;
} String currentExpression = beanNameExpressionMap.get(beanName);
if (cronTrigger.getExpression().equals(currentExpression)) {
log.info("{} 当前cron表达式未变更", beanName);
return;
} cancelTask(beanName);
scheduleNewTask(beanName, cronTrigger, syncService::executeSync);
}
private void scheduleNewTask(String taskName, CronTrigger cronTrigger, Runnable executeSync) {
CronTask cronTask = new CronTask(executeSync, cronTrigger);
ScheduledFuture<?> schedule = Objects.requireNonNull(taskRegistrar.getScheduler()).schedule(cronTask.getRunnable(), cronTask.getTrigger());
beanNameExpressionMap.put(taskName, cronTrigger.getExpression());
beanNameScheduledTaskMap.put(taskName, schedule);
log.info("创建新定时任务 {}", taskName);
}
private void cancelTask(String beanName) {
ScheduledFuture<?> scheduledFuture = beanNameScheduledTaskMap.get(beanName);
if (Objects.isNull(scheduledFuture)) {
return;
} // false 表示允许任务完成后取消
scheduledFuture.cancel(false);
log.info("取消定时任务 {}", beanName);
}
}
package com.silvergravel.event;
import org.springframework.context.ApplicationEvent;
/**
* @author DawnStar
* @since : 2024/11/3
*/public class CronEvent extends ApplicationEvent {
public CronEvent(Object source) {
super(source);
}}
package com.silvergravel.event;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
/**
* @author DawnStar
* @since : 2024/11/3
*/@Component
public class ScheduleTaskApplicationListener implements ApplicationListener<CronEvent> {
private final Logger log = LoggerFactory.getLogger(ScheduleTaskApplicationListener.class);
@Resource
private ScheduleTaskConfigurer taskConfigurer;
@Override
public void onApplicationEvent(@NonNull CronEvent event) {
log.info("任务事件:" + event);
taskConfigurer.refresh();
}}
版本
- JDK 17
- Spring Boot 3.3.2
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
</parent>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>