前言
对于一些周期性的工作,使用定时任务来执行是非常合适的操作。
Spring 3.0 时对定时任务提供了支持,其提供了一个接口TaskScheduler
作为定时任务的抽象,并且提供了一个默认的实现ThreadPoolTaskScheduler
,该实现是对 JDK ScheduledExecutorService
的包装并增加了一些扩展触发功能。
本文主要介绍下在 Spring Boot 中使用定时任务。
基本使用
在 Spring Boot 中,要使用定时任务,只需进行如下两步操作:
-
使用注解
@EnableScheduling
开启定时任务:@SpringBootApplication @ComponentScan("com.yn.scheduled") @EnableScheduling // 开启定时任务 public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
EnableScheduling
注解会注册一个ScheduledAnnotationBeanPostProcessor
,从而使能扫描 Spring IOC 容器中对象的@Scheduled
注解方法。 -
为需要定期执行的方法添加
@Scheduled
注解:@Component // 添加到 IOC 容器中 public class ScheduledTask { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Scheduled(fixedRate = 1000) public void scheduledTask() { logger.info("定时任务(每秒一次):{}",new Date()) ; } }
上述
@Scheduled
注解定义了一个每 1 秒执行一次的定时任务scheduledTask
。
以上,我们就完成了一个定时任务scheduledTask
,此时运行程序,就可以在控制台看到定时任务scheduledTask
每 1 秒都会得到执行。
注:被@Scheduled
注解的定时任务有如下限制:
- 定时方法必须是无参函数
- 定时方法通常返回
void
,如果设置了返回其他类型的数据,则返回值会被忽略。
@Scheduled 简解
通过上面内容,我们可以知道,在 Spring 中,定时任务的设置主要通过注解@Scheduled
来定义,其具体内容如下图所示:
以下我们只对@Scheduled
注解常用的属性进行介绍:
-
cron
:表示以 Unix 的 cron 方式定义时间。
注:cron
的定时设置功能十分灵活强大,具体的设置方式可参考 附录 - cron 表达式。 -
fixedRate
:表示以固定间隔执行定时任务。这里的间隔指的是:每次调用的时候就开始计时,到指定间隔时间再调用下一个定时任务。 -
fixedDelay
:表示以固定间隔执行定时任务。这里的间隔指的是:上一次定时任务完成后,才开始计时,到指定间隔时间再调用下一个定时任务。 -
initialDelay
:表示首次运行定时任务前的延时时间。可用在于fixedRate
和fixedDelay
的定时任务。
注:initialDelay
只有在第一次运行定时任务前有效,不会对后续定时任务有影响。
注:以上属性对应的字符串属性(比如,fixedRate
对应的字符串属性为fixedRateString
),其作用是一样的,只是字符串属性可以从外部文件中进行配置,比如可以把定时任务写到配置文件中,然后在代码中使用:
- 配置文件
Application.yml
:
scheduler:
fixedRate:
timeInMilliseconds: 3000
- 代码中引入配置文件配置:
@Scheduled(fixedRateString = "${scheduler.fixedRate.timeInMilliseconds}")
public void taskFromExternal(){
logger.info("Thread[{}] - taskFromExternal:{}", Thread.currentThread().getName(), new Date());
}
并发调度定时任务
需要注意的一点是,默认情况下,定时任务是串行运行的(具体原因可参考后文内容:源码分析),因此,哪怕即使使用的是fixedRate
,也有可能因为定时任务内部存在耗时操作等行为导致调度出现误差,比如当该耗时操作时间超过定时任务间隔时,就会导致系统中的定时任务调度间隔不准确。比如如下例子:
@Scheduled(fixedRate = 2000)
public void task01() throws InterruptedException {
// 模拟耗时操作
Thread.sleep(3000);
logger.info("Thread[{}] - task01:{}", Thread.currentThread().getName(),new Date());
}
@Scheduled(fixedRate = 5000)
public void task02() {
logger.info("Thread[{}] - task02:{}", Thread.currentThread().getName(),new Date());
}
运行程序,可以看到如下结果:
可以看到,对于task01
,我们设置的调度间隔是 2 秒,时间的运行间隔为 3 秒,原因就是定时任务task01
内部耗时操作超过了本身的调度时间间隔,而又由于 Spring 定时任务默认是串行运行,从而也影响了系统中其他定时任务,比如定时任务task02
设置的调度时间为 5 秒,但实际运行间隔却为 6 秒。
为了减少系统中的定时任务互相影响,最好让定时任务都运行在独立的线程中,也即并发运行定时任务,这样,即使其中一个或多个定时任务出问题,也不会影响到系统其他定时任务。
一个很方便的事是,刚好 Spring 也提供了异步功能支持,我们之前的文章也进行了介绍:Spring Boot - 执行异步任务。
简单来讲,对于定时任务,Spring 提供了TaskScheduler
接口进行抽象,同时借助注解@EnableScheduling
和@Scheduled
就可以开启并设置定时任务。
而对于异步任务,Spring 同样提供了一个接口TaskExecutor
进行抽象,同时借助注解@EnableAsync
和@Async
就可以开启并设置异步任务。
所以要将定时任务设置为并发调度,只需开启异步任务并为其增添异步执行即可,如下所示:
- 开启异步任务支持:
@Configuration
@EnableAsync // 开启异步任务
public class AsyncConfigure implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
return Executors.newCachedThreadPool();
}
}
- 为定时任务增加异步执行功能:
@Scheduled(fixedRate = 2000)
@Async // 异步定时任务
public void task01() throws InterruptedException {
// 模拟耗时操作
Thread.sleep(3000);
logger.info("Thread[{}] - task01:{}", Thread.currentThread().getName(), new Date());
}
@Scheduled(fixedRate = 5000)
@Async // 异步定时任务
public void task02() {
logger.info("Thread[{}] - task02:{}", Thread.currentThread().getName(), new Date());
}
此时运行程序,结果如下图所示:
可以看到,异步任务的调度间隔符合我们的设置,即使对于自身内部运行超过定时间隔的任务时,也会新开一条线程执行新的定时任务,不会由于上一个任务的超时而导致系统中其他定时任务受到影响。
注:Spring 对异步任务和定时任务的抽象和实现十分类似,比如对于异步任务,使用TaskExecutor
进行抽象,且只需提供@EnableAsync
和@Async
就可以开启并设置异步任务,而对于定时任务,也是同样的套路,使用TaskScheduler
进行抽象,且只需提供@EnableScheduling
和@Scheduled
就可以开启并设置定时任务...
在我们之前的文章(Spring Boot - 执行异步任务)中有提及到,Spring 提供了一个接口AsyncConfigurer
可以让我们对异步任务进行更加细粒度的设置,同样的,对于定时任务,Spring 也提供了一个类似的接口SchedulingConfigurer
,可以让我们对定时任务进行更加细粒度的设置,有时候使用@Scheduled
注解无法解决的问题,比如动态改变定时时间等,就可以通过SchedulingConfigurer
进行配置。这里,对于定时任务并发调度,我们也可以通过实现该接口进行实现,如下代码所示:
@Configuration
@EnableScheduling
public class ScheduledConfigure implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(taskExecutor());
}
@Bean
public Executor taskExecutor() {
return Executors.newScheduledThreadPool(100);
}
}
这样配置后,系统中所有的定时任务无需添加@Async
就会自动运行在线程池中。
注:这里需要注意的一个点是,ScheduledTaskRegistrar.setScheduler
方法只支持TaskScheduler
和ScheduledExecutorService
,因此这里不是采用前文的Executors.newCachedThreadPool
,而是使用Executors.newScheduledThreadPool
,这两者的效果有一点不同,具体运行结果如下所示:
可以看到,定时任务task01
设置的调度间隔是 2 秒,实际时间却是 3 秒,出现这种状况的原因是ScheduledExecutorService.scheduleAtFixedRate
方法的时间间隔是上一次运行成功后,才开始计时,也就是,ScheduledExecutorService
的执行方式是fixedDelay
模式,因此,只有在前一个任务结束后,才会开启下一个任务,所以就导致了上述现象,即不同任务可以并发调度,但是同一个任务只能串行调度,所以如果这种效果不是你所期望的,则应当采用上文介绍的结合异步任务来完成定时任务并发调度。
源码分析
这里我们简单对定时任务的整个过程做一个简要分析,如下所示:
首先,Spring 中的定时任务是通过注解@EnableScheduling
开启的,所以查看下该注解源码:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class)
@Documented
public @interface EnableScheduling {
}
很明显看到,@EnableScheduling
注解的主要操作就是导入了一个配置类SchedulingConfiguration.class
,查看下该配置类源码:
@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class SchedulingConfiguration {
@Bean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {
return new ScheduledAnnotationBeanPostProcessor();
}
}
SchedulingConfiguration
配置类主要作用就是创建了一个ScheduledAnnotationBeanPostProcessor
的 Bean 实例,其源码如下所示:
public class ScheduledAnnotationBeanPostProcessor
implements MergedBeanDefinitionPostProcessor, DestructionAwareBeanPostProcessor,ApplicationListener<ContextRefreshedEvent>,... {
// 构造函数
public ScheduledAnnotationBeanPostProcessor() {
this.registrar = new ScheduledTaskRegistrar();
}
// 扫描 @Scheduled 注解并创建相应的方法实例
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
...
AnnotationUtils.isCandidateClass(targetClass, Arrays.asList(Scheduled.class, Schedules.class))) {
Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
(MethodIntrospector.MetadataLookup<Set<Scheduled>>) method -> {
Set<Scheduled> scheduledMethods = AnnotatedElementUtils.getMergedRepeatableAnnotations(
method, Scheduled.class, Schedules.class);
return (!scheduledMethods.isEmpty() ? scheduledMethods : null);
});
...
// Non-empty set of methods
annotatedMethods.forEach((method, scheduledMethods) ->
scheduledMethods.forEach(scheduled -> processScheduled(scheduled, method, bean)));
...
}
// 解析 @Scheduled 并封装定时任务到 ScheduledTaskRegistrar
protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
...
Set<ScheduledTask> tasks = new LinkedHashSet<>(4);
// Determine initial delay
long initialDelay = scheduled.initialDelay();
String initialDelayString = scheduled.initialDelayString();
...
// Check cron expression
String cron = scheduled.cron();
if (StringUtils.hasText(cron)) {
...
tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));
}
}
...
// Check fixed delay
long fixedDelay = scheduled.fixedDelay();
if (fixedDelay >= 0) {
...
tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
}
String fixedDelayString = scheduled.fixedDelayString();
if (StringUtils.hasText(fixedDelayString)) {
...
tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
}
}
// Check fixed rate
long fixedRate = scheduled.fixedRate();
if (fixedRate >= 0) {
...
tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));
}
String fixedRateString = scheduled.fixedRateString();
if (StringUtils.hasText(fixedRateString)) {
...
tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));
}
}
// Check whether we had any attribute set
Assert.isTrue(processedSchedule, errorMessage);
// Finally register the scheduled tasks
synchronized (this.scheduledTasks) {
Set<ScheduledTask> regTasks = this.scheduledTasks.computeIfAbsent(bean, key -> new LinkedHashSet<>(4));
regTasks.addAll(tasks);
}
...
}
}
ScheduledAnnotationBeanPostProcessor
创建的时候首先会构造一个ScheduledTaskRegistrar
实例,由其内部成员registrar
持有。
然后,ScheduledAnnotationBeanPostProcessor
类实现了BeanPostProcessor
接口,其中,postProcessAfterInitialization
方法会扫描@Scheduled
注解并创建相应的方法实例,该方法内部是通过调用processScheduled
方法对注解@Scheduled
的内容进行解析,processScheduled
解析完@Scheduled
后,会将其封装到相应的定时任务实例中,并将这些定时任务添加到ScheduledTaskRegistrar
实例中。
一个完整的流程是:当 Spring 启动时,AbstractApplicationContext
中的finishRefresh
会触发所有监视者方法回调,其中,publishEvent
会触发ScheduledAnnotationBeanPostProcessor
的onApplicationEvent
方法(由于ScheduledAnnotationBeanPostProcessor
实现了ApplicationListener
,因此有该接口方法),查看下该方法源码:
public class ScheduledAnnotationBeanPostProcessor implements ApplicationListener<ContextRefreshedEvent>,...{
...
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if (event.getApplicationContext() == this.applicationContext) {
// Running in an ApplicationContext -> register tasks this late...
// giving other ContextRefreshedEvent listeners a chance to perform
// their work at the same time (e.g. Spring Batch's job registration).
finishRegistration();
}
}
...
}
可以看到,onApplicationEvent
方法内部调用了finishRegistration
方法,finishRegistration
主要用于查找并设置TaskScheduler
(注册调度器TaskScheduler
),也就是 Spring 对异步任务的抽象封装类。其源码如下所示:
private void finishRegistration() {
if (this.scheduler != null) {
this.registrar.setScheduler(this.scheduler);
}
if (this.beanFactory instanceof ListableBeanFactory) {
// 如果配置了定时任务 Bean: SchedulingConfigurer
Map<String, SchedulingConfigurer> beans =
((ListableBeanFactory) this.beanFactory).getBeansOfType(SchedulingConfigurer.class);
List<SchedulingConfigurer> configurers = new ArrayList<>(beans.values());
AnnotationAwareOrderComparator.sort(configurers);
for (SchedulingConfigurer configurer : configurers) {
// 回调
configurer.configureTasks(this.registrar);
}
}
if (this.registrar.hasTasks() && this.registrar.getScheduler() == null) {
Assert.state(this.beanFactory != null, "BeanFactory must be set to find scheduler by type");
try {
// Search for TaskScheduler bean...
this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, false));
}
catch (NoUniqueBeanDefinitionException ex) {
logger.trace("Could not find unique TaskScheduler bean", ex);
try {
this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, true));
}
catch (NoSuchBeanDefinitionException ex2) {
...
}
catch (NoSuchBeanDefinitionException ex) {
logger.trace("Could not find default TaskScheduler bean", ex);
// Search for ScheduledExecutorService bean next...
try {
this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, false));
}
catch (NoUniqueBeanDefinitionException ex2) {
logger.trace("Could not find unique ScheduledExecutorService bean", ex2);
...
}
catch (NoSuchBeanDefinitionException ex2) {
logger.trace("Could not find default ScheduledExecutorService bean", ex2);
// Giving up -> falling back to default scheduler within the registrar...
logger.info("No TaskScheduler/ScheduledExecutorService bean found for scheduled processing");
}
}
}
this.registrar.afterPropertiesSet();
}
finishRegistration
方法的逻辑很清晰,它对定时任务调度器TaskScheduler
的查找过程主要有 3 大步骤:
- 如果
ScheduledAnnotationBeanPostProcessor
本身设置了调度器,则将该调度器设置给ScheduledTaskRegistrar
,具体代码如下所示:
if (this.scheduler != null) {
this.registrar.setScheduler(this.scheduler);
}
- 如果用户配置了定时任务配置类
SchedulingConfigurer
,则回调配置类的configureTasks
方法:
if (this.beanFactory instanceof ListableBeanFactory) {
Map<String, SchedulingConfigurer> beans =
((ListableBeanFactory) this.beanFactory).getBeansOfType(SchedulingConfigurer.class);
List<SchedulingConfigurer> configurers = new ArrayList<>(beans.values());
AnnotationAwareOrderComparator.sort(configurers);
for (SchedulingConfigurer configurer : configurers) {
configurer.configureTasks(this.registrar);
}
}
-
这是 Spring 默认的搜索行为,其具体搜索逻辑如下:
1). 首先全局搜索唯一的TaskScheduler
类型 Bean 实例:// Search for TaskScheduler bean... this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, false));
2). 如果存在多个
TaskScheduler
类型 Bean,则搜索名称为taskScheduler
的 Bean 实例:private void finishRegistration() { ... this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, true)); ... } public static final String DEFAULT_TASK_SCHEDULER_BEAN_NAME = "taskScheduler"; private <T> T resolveSchedulerBean(BeanFactory beanFactory, Class<T> schedulerType, boolean byName) { if (byName) { T scheduler = beanFactory.getBean(DEFAULT_TASK_SCHEDULER_BEAN_NAME, schedulerType); if (this.beanName != null && this.beanFactory instanceof ConfigurableBeanFactory) { ((ConfigurableBeanFactory) this.beanFactory).registerDependentBean( DEFAULT_TASK_SCHEDULER_BEAN_NAME, this.beanName); } return scheduler; } ... }
也就是说,如果存在多个
TaskScheduler
Bean 实例,则选择名称为taskScheduler
的实例。3). 如果不存在
TaskScheduler
类型 Bean 实例,就降级转而查找ScheduledExecutorService
类型 Bean 实例:this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, false));
4). 如果存在多个
ScheduledExecutorService
类型的 Bean 实例,则查找名称为taskScheduler
的 Bean 实例:this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, true));
5). 如果
TaskScheduler
和ScheduledExecutorService
Bean 实例都不存在,则结束查找。综上,
finishRegistration
查找逻辑为:优先查找全局唯一TaskScheduler
Bean 实例,存在多个则选择名称为taskScheduler
的实例,否则降级查找ScheduledExecutorService
类型 Bean 实例,存在多个则选择名称为taskScheduler
的那个 Bean 实例。对定时任务调度器
TaskScheduler
的查找都是通过方法resolveSchedulerBean
进行的,事实上,默认情况下,Spring 在启动过程中,会自动帮我们注入一个TaskScheduler
Bean 实例,对应的代码如下所示:private void finishRegistration() { ... this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, false)); ... } private <T> T resolveSchedulerBean(BeanFactory beanFactory, Class<T> schedulerType, boolean byName) { ... else if (beanFactory instanceof AutowireCapableBeanFactory) { NamedBeanHolder<T> holder = ((AutowireCapableBeanFactory) beanFactory).resolveNamedBean(schedulerType); if (this.beanName != null && beanFactory instanceof ConfigurableBeanFactory) { ((ConfigurableBeanFactory) beanFactory).registerDependentBean(holder.getBeanName(), this.beanName); } return holder.getBeanInstance(); } ... }
所以默认情况下,Spring 提供了一个默认的
TaskScheduler
,其实我们前文也有提及,这个默认的TaskScheduler
就是ThreadPoolTaskScheduler
,所以默认情况下,会将ThreadPoolTaskScheduler
注册给ScheduledTaskRegistrar
,注册的方法如下所示:// ScheduledTaskRegistrar.java public void setTaskScheduler(TaskScheduler taskScheduler) { Assert.notNull(taskScheduler, "TaskScheduler must not be null"); this.taskScheduler = taskScheduler; }
我们对该方法进行单步调式,就可以看到
taskSchedulerThreadPoolTaskScheduler
默认的配置情况,如下图所示:可以看到,默认的定时任务调度器是一个名称为
taskScheduler
的ScheduledThreadPoolExecutor
(ScheduledThreadPoolExecutor
实现了ScheduledExecutorService
)线程池实例,且其线程数为1
,线程前缀为scheduling-
,所以默认情况下,定时任务都是串行运行的,且其线程名称都为scheduling-1
,另一个需要注意的点是,默认的调度器其deamon = false
,说明其是一个后台任务,即使应用主线程退出,定时任务仍然处于运行之中。
最后,finishRegistration
方法末尾还调用了afterPropertiesSet
方法,如下所示:
// ScheduledAnnotationBeanPostProcessor.java
private void finishRegistration() {
...
this.registrar.afterPropertiesSet();
}
// ScheduledTaskRegistrar.java
@Override
public void afterPropertiesSet() {
scheduleTasks();
}
所以afterPropertiesSet
最终是调用的是scheduleTasks
,见名知意,该方法用于调度已注册的定时任务,其源码如下所示:
// ScheduledTaskRegistrar.java
@SuppressWarnings("deprecation")
protected void scheduleTasks() {
if (this.taskScheduler == null) {
this.localExecutor = Executors.newSingleThreadScheduledExecutor();
this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
}
if (this.triggerTasks != null) {
for (TriggerTask task : this.triggerTasks) {
addScheduledTask(scheduleTriggerTask(task));
}
}
if (this.cronTasks != null) {
for (CronTask task : this.cronTasks) {
addScheduledTask(scheduleCronTask(task));
}
}
if (this.fixedRateTasks != null) {
for (IntervalTask task : this.fixedRateTasks) {
addScheduledTask(scheduleFixedRateTask(task));
}
}
if (this.fixedDelayTasks != null) {
for (IntervalTask task : this.fixedDelayTasks) {
addScheduledTask(scheduleFixedDelayTask(task));
}
}
}
private final Set<ScheduledTask> scheduledTasks = new LinkedHashSet<>(16);
private void addScheduledTask(@Nullable ScheduledTask task) {
if (task != null) {
this.scheduledTasks.add(task);
}
}
可以看到,scheduledTasks
方法对注册的不同的定时任务分别进行调度,调度的方法为scheduleXXXTask
,比如,对于fixedRate
的定时任务,其对应的调用方法为scheduleFixedRateTask
,每次调度完成一个方法后,都会将调度结果通过方法addScheduledTask
添加到一个集合中scheduledTasks
。
这里我们主要对调度方法进行分析,就分析一下scheduleFixedRateTask
,其余的调度方法与之类似:
@Deprecated
@Nullable
public ScheduledTask scheduleFixedRateTask(IntervalTask task) {
FixedRateTask taskToUse = (task instanceof FixedRateTask ? (FixedRateTask) task :
new FixedRateTask(task.getRunnable(), task.getInterval(), task.getInitialDelay()));
return scheduleFixedRateTask(taskToUse);
}
scheduleFixedRateTask(IntervalTask)
方法内部最终是通过调用重载函数scheduleFixedRateTask(FixedRateTask)
来完成调度,其源码如下:
@Nullable
public ScheduledTask scheduleFixedRateTask(FixedRateTask task) {
ScheduledTask scheduledTask = this.unresolvedTasks.remove(task);
...
scheduledTask.future =
this.taskScheduler.scheduleAtFixedRate(task.getRunnable(), task.getInterval());
...
return (newTask ? scheduledTask : null);
}
这里就可以看到了,最终是通过定时任务调度器taskScheduler
的scheduleAtFixedRate
来完成定时任务调度。
到此,源码分析基本已完成。
附录
-
cron 表达式:cron 表达式的语法如下所示:
<秒> <分> <小时> <日> <月> <周> [年]
其中,各字段允许的值和特殊符号如下表所示:
字段 允许值 允许的特殊字符 秒 0~59 , - * /
分 0~59 , - * /
小时 0~23 , - * /
日期 0~31 , - * ? / L W C
月份 1~12 或者 JAN~DEC , - * /
星期 1~7 或者 SUN~SAT , - * ? / L C #
年(可选) 留空,1970~2099 , - * /
上表中的特殊符号释义如下:
-
,
:表示枚举值。比如:假设小时为10,14,16
,则表示上午 10 时,下午 2 时 以及 下午 4 时各触发一次。 -
-
:表示间隔时间。比如:假设小时为8-12
,则表示上午 8 时到中午 12 时每个小时时间段都进行触发。 -
*
:表示匹配所有可能值。 -
/
:表示增量。比如:假设分钟为3/20
,则表示从第 3 分钟开始,以后每隔 20 分钟触发一次。 -
?
:仅被用于月和周两个子表达式,表示不指定值。 -
L
:仅被用于月和周两个子表达式,它是单词“last”的缩写。如果在“L”前有具体的内容,它就具有其他的含义了,比如:假设星期字段为6L
,则表示每个月的倒数第 6 天。 -
W
:表示工作日(Mon-Fri),并且仅能用于日域中。 -
C
:表示日期(Calendar)意思。比如:5C
表示日历 5 日以后的第一天,1C
在星期字段相当于星期日后的第一天。
以下是一些常用的 cron 表达式:
- 每隔5秒执行一次:
*/5 * * * * ?
- 每隔1分钟执行一次:
0 */1 * * * ?
- 每天上午10点,下午2点,4点:
0 0 10,14,16 * * ?
- 朝九晚五工作时间内每半小时:
0 0/30 9-17 * * ?
- 表示每个星期三中午12点:
0 0 12 ? * WED
- 每天中午12点触发:
0 0 12 * * ?
- 每天上午10:15触发:
0 15 10 ? * *
- 每天上午10:15触发:
0 15 10 * * ?
- 每天上午10:15触发:
0 15 10 * * ?
- 2005 2005年的每天上午10:15触发:
0 15 10 * * ?
- 在每天下午2点到下午2:59期间的每1分钟触发:
0 * 14 * * ?
- 在每天下午2点到下午2:55期间的每5分钟触发:
0 0/5 14 * * ?
- 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发:
0 0/5 14,18 * * ?
- 在每天下午2点到下午2:05期间的每1分钟触发:
0 0-5 14 * * ?
- 每年三月的星期三的下午2:10和2:44触发:
0 10,44 14 ? 3 WED
- 周一至周五的上午10:15触发:
0 15 10 ? * MON-FRI
- 每月15日上午10:15触发:
0 15 10 15 * ?
- 每月最后一日的上午10:15触发:
0 15 10 L * ?
- 每月的最后一个星期五上午10:15触发:
0 15 10 ? * 6L
- 2002年至2005年的每月的最后一个星期五上午10:15触发:
0 15 10 ? * 6L 2002-2005
- 每月的第三个星期五上午10:15触发:
0 15 10 ? * 6#3
比如在代码中配置如下:
// 每隔 5s 执行一次 @Scheduled(cron = "*/5 * * * * ?") public void cronTask(){ logger.info("Thread[{}] - cronTask:{}", Thread.currentThread().getName(), new Date()); }
以上就通过 cron 表达式就设置了一个每隔 5 秒运行的定时任务。
-
网友评论