侧边栏壁纸
博主头像
孔子说JAVA博主等级

成功只是一只沦落在鸡窝里的鹰,成功永远属于自信且有毅力的人!

  • 累计撰写 285 篇文章
  • 累计创建 125 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

SpringBoot定时任务方式@schedule详解

孔子说JAVA
2022-08-23 / 0 评论 / 0 点赞 / 137 阅读 / 15,805 字 / 正在检测是否收录...

SpringBoot内置了Sping Schedule定时框架,通过@schedule注解驱动方式添加所注解方法到定时任务,再根据配置的定时信息定时执行。@schedule注解内部默认采用一个线程的线程池来串行执行任务的。一般情况下没有什么问题,但是如果有多个定时任务,每个定时任务执行时间可能不短的情况下,那么有的定时任务可能一直没有机会执行。

Java 定时任务的几种实现方式:

  1. 基于 java.util.Timer 定时器,实现类似闹钟的定时任务。
  2. 使用 Quartz、elastic-job、xxl-job 等开源第三方定时任务框架,适合分布式项目应用。
  3. 使用 Spring 提供的一个注解: @Schedule,开发简单,使用比较方便,也是本文介绍的一种方式。

1、@schedule入门示例

1.1 创建定时任务

首先,需要在项目启动类上添加 @EnableScheduling 注解,以开启对定时任务的支持。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

其中 @EnableScheduling 注解的作用是发现注解 @Scheduled 的任务并后台执行。

1.2 编写定时任务类和方法

编写定时任务类和方法,定时任务类通过 Spring IOC 加载,使用 @Component 注解,定时方法使用 @Scheduled 注解。

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

@Component
public class ScheduledTask {
    @Scheduled(fixedRate = 3000)
    public void scheduledTask() {
        System.out.println("任务执行时间:" + LocalDateTime.now());
    }
}

fixedRate 是 long 类型,表示任务执行的间隔毫秒数,以上代码中的定时任务每 3 秒执行一次。运行该springboot项目,项目的启动和运行日志如下,可以看到每 3 秒打印一次日志执行记录。

2022-08-22 14:46:27.416  INFO 8856 --- [           main] com.kz.example.DemoApplication           : No active profile set, falling back to default profiles: default
2022-08-22 14:46:27.864  INFO 8856 --- [           main] o.s.s.c.ThreadPoolTaskScheduler          : Initializing ExecutorService 'taskScheduler'
2022-08-22 14:46:27.880  INFO 8856 --- [           main] com.kz.example.DemoApplication           : Started DemoApplication in 0.7 seconds (JVM running for 1.562)
任务执行时间:2022-08-22T14:46:27.880
任务执行时间:2022-08-22T14:46:30.893
任务执行时间:2022-08-22T14:46:33.895
任务执行时间:2022-08-22T14:46:36.899
任务执行时间:2022-08-22T14:46:39.889

2、@Scheduled详解

2.1 @Scheduled 使用方式

在@schedule入门示例中,使用了@Scheduled(fixedRate = 3000) 注解来定义每 3 秒执行的任务,对于 @Scheduled 的使用可以总结如下几种方式:

  1. @Scheduled(fixedRate = 3000) :上一次开始执行时间点之后 3 秒再执行(fixedRate 属性:定时任务开始后再次执行定时任务的延时(需等待上次定时任务完成),单位毫秒)
  2. @Scheduled(fixedDelay = 3000) :上一次执行完毕时间点之后 3 秒再执行(fixedDelay 属性:定时任务执行完成后再次执行定时任务的延时(需等待上次定时任务完成),单位毫秒)
  3. @Scheduled(initialDelay = 1000, fixedRate = 3000) :第一次延迟1秒后执行,之后按fixedRate的规则每 3 秒执行一次(initialDelay 属性:第一次执行定时任务的延迟时间,需配合fixedDelay或者fixedRate来使用)
  4. @Scheduled(cron="0 0 2 1 * ? *") :通过cron表达式定义规则

默认情况下 @Schedule 使用线程池的大小为 1。一般情况下没有什么问题,但是如果有多个定时任务,每个定时任务执行时间可能不短的情况下,那么有的定时任务可能一直没有机会执行。看下面的例子:

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;

@Component
public class ScheduledTask {

    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    @Scheduled(cron = "*/5 * * * * ?")
    private void cron() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "-cron:" + LocalDateTime.now().format(FORMATTER));
        TimeUnit.SECONDS.sleep(6);
    }

    @Scheduled(fixedDelay = 5000)
    private void fixedDelay() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "-fixedDelay:" + LocalDateTime.now().format(FORMATTER));
        TimeUnit.SECONDS.sleep(6);
    }

    @Scheduled(fixedRate = 5000)
    private void fixedRate() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "-fixedRate:" + LocalDateTime.now().format(FORMATTER));
        TimeUnit.SECONDS.sleep(6);
    }
}

运行springboot项目后,看到的日志情况如下:

scheduling-1-fixedRate:2022-08-22 14:49:17
scheduling-1-fixedDelay:2022-08-22 14:49:23
scheduling-1-cron:2022-08-22 14:49:29
scheduling-1-fixedRate:2022-08-22 14:49:35
scheduling-1-fixedRate:2022-08-22 14:49:41
scheduling-1-fixedRate:2022-08-22 14:49:47
scheduling-1-fixedDelay:2022-08-22 14:49:53
scheduling-1-fixedRate:2022-08-22 14:49:59
scheduling-1-cron:2022-08-22 14:50:05
scheduling-1-fixedRate:2022-08-22 14:50:11
scheduling-1-fixedRate:2022-08-22 14:50:17
scheduling-1-fixedRate:2022-08-22 14:50:23
scheduling-1-fixedRate:2022-08-22 14:50:29
scheduling-1-fixedRate:2022-08-22 14:50:35
scheduling-1-fixedDelay:2022-08-22 14:50:41
scheduling-1-fixedRate:2022-08-22 14:50:47
scheduling-1-fixedRate:2022-08-22 14:50:53
scheduling-1-cron:2022-08-22 14:50:59
scheduling-1-fixedRate:2022-08-22 14:51:05
scheduling-1-fixedRate:2022-08-22 14:51:11
scheduling-1-fixedRate:2022-08-22 14:51:17
scheduling-1-fixedRate:2022-08-22 14:51:23
scheduling-1-fixedRate:2022-08-22 14:51:29
scheduling-1-fixedRate:2022-08-22 14:51:35
scheduling-1-fixedRate:2022-08-22 14:51:41
scheduling-1-fixedRate:2022-08-22 14:51:47
scheduling-1-fixedDelay:2022-08-22 14:51:53
scheduling-1-fixedRate:2022-08-22 14:51:59
scheduling-1-fixedRate:2022-08-22 14:52:05
scheduling-1-fixedRate:2022-08-22 14:52:11
scheduling-1-cron:2022-08-22 14:52:17
scheduling-1-fixedRate:2022-08-22 14:52:23
scheduling-1-fixedRate:2022-08-22 14:52:29
scheduling-1-fixedRate:2022-08-22 14:52:35
scheduling-1-fixedRate:2022-08-22 14:52:41
scheduling-1-fixedRate:2022-08-22 14:52:47
scheduling-1-fixedRate:2022-08-22 14:52:53
scheduling-1-fixedRate:2022-08-22 14:52:59
scheduling-1-fixedRate:2022-08-22 14:53:05
scheduling-1-fixedRate:2022-08-22 14:53:11
scheduling-1-fixedRate:2022-08-22 14:53:17
scheduling-1-fixedRate:2022-08-22 14:53:23
scheduling-1-fixedDelay:2022-08-22 14:53:29
scheduling-1-fixedRate:2022-08-22 14:53:35
scheduling-1-fixedRate:2022-08-22 14:53:41
scheduling-1-fixedRate:2022-08-22 14:53:47
scheduling-1-fixedRate:2022-08-22 14:53:53
scheduling-1-cron:2022-08-22 14:53:59
scheduling-1-fixedRate:2022-08-22 14:54:05
scheduling-1-fixedRate:2022-08-22 14:54:11
scheduling-1-fixedRate:2022-08-22 14:54:17
scheduling-1-fixedRate:2022-08-22 14:54:23
scheduling-1-fixedRate:2022-08-22 14:54:29
scheduling-1-fixedRate:2022-08-22 14:54:35
scheduling-1-fixedRate:2022-08-22 14:54:41
scheduling-1-fixedRate:2022-08-22 14:54:47
scheduling-1-fixedRate:2022-08-22 14:54:53
scheduling-1-fixedRate:2022-08-22 14:54:59
scheduling-1-fixedRate:2022-08-22 14:55:05
scheduling-1-fixedRate:2022-08-22 14:55:11
scheduling-1-fixedRate:2022-08-22 14:55:17
scheduling-1-fixedRate:2022-08-22 14:55:23
scheduling-1-fixedRate:2022-08-22 14:55:29
scheduling-1-fixedDelay:2022-08-22 14:55:35
scheduling-1-fixedRate:2022-08-22 14:55:41

仔细观察可以发现,上面的任务中,fixedDelay 与 cron 很久都不会被执行。

@Schedule 的三种方式 cron、fixedDelay、fixedRate 不管线程够不够都会阻塞到上一次执行完成,才会执行下一次。如果任务方法执行时间非常短,上面三种方式其实基本没有太多的区别。如果任务方法执行时间比较长,大于了设置的执行周期,那么就有很大的区别。例如,假设执行任务的线程足够,执行周期是 5s,任务方法会执行 6s。

  1. cron 的执行方式是,任务方法执行完,遇到下一次匹配的时间再次执行,基本就会 10s 执行一次,因为执行任务方法的时间区间会错过一次匹配。
  2. fixedDelay 的执行方式是,方法执行了 6s,然后会再等 5s 再执行下一次,在上面的条件下,基本就是每 11s 执行一次。
  3. fixedRate 的执行方式就变成了每隔 6s 执行一次,因为按固定区间执行它没 5s 就应该执行一次,但是任务方法执行了 6s,没办法,只好 6s 执行一次。

2.2 @Scheduled 多线程

要解决上面的问题,可以把执行任务的线程池设置大一点。

2.2.1 实现 SchedulingConfigurer 接口

新建一个MyScheduleConfig类,实现 SchedulingConfigurer 接口,在 configureTasks 方法中配置线程数。

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

import java.util.concurrent.Executors;

@Configuration
public class MyScheduleConfig implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10));
    }
}

再次运行项目,日志如下:

pool-1-thread-1-fixedRate:2022-08-22 15:06:07
pool-1-thread-3-fixedDelay:2022-08-22 15:06:07
pool-1-thread-2-cron:2022-08-22 15:06:10
pool-1-thread-1-fixedRate:2022-08-22 15:06:13
pool-1-thread-3-fixedDelay:2022-08-22 15:06:18
pool-1-thread-5-fixedRate:2022-08-22 15:06:19
pool-1-thread-1-cron:2022-08-22 15:06:20
pool-1-thread-2-fixedRate:2022-08-22 15:06:25
pool-1-thread-6-fixedDelay:2022-08-22 15:06:29
pool-1-thread-5-cron:2022-08-22 15:06:30
pool-1-thread-2-fixedRate:2022-08-22 15:06:31
pool-1-thread-2-fixedRate:2022-08-22 15:06:37
pool-1-thread-8-cron:2022-08-22 15:06:40
pool-1-thread-6-fixedDelay:2022-08-22 15:06:40
pool-1-thread-2-fixedRate:2022-08-22 15:06:43
pool-1-thread-2-fixedRate:2022-08-22 15:06:49
pool-1-thread-8-cron:2022-08-22 15:06:50
pool-1-thread-4-fixedDelay:2022-08-22 15:06:51
pool-1-thread-2-fixedRate:2022-08-22 15:06:55
pool-1-thread-8-cron:2022-08-22 15:07:00
pool-1-thread-2-fixedRate:2022-08-22 15:07:01
pool-1-thread-1-fixedDelay:2022-08-22 15:07:02
pool-1-thread-2-fixedRate:2022-08-22 15:07:07
pool-1-thread-3-cron:2022-08-22 15:07:10

2.2.2 实现 SchedulingConfigurer 接口

也可以创建一个配置类(@Configuration),注入一个TaskScheduler bean。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;

@Configuration
public class MyConfig {

    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setPoolSize(5);
        return taskScheduler;
    }
}

当然也可以使用 ScheduledExecutorService,和上面的方式是一样的效果。

package com.kz.example.task;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;

@Configuration
public class MyConfig {

    @Bean
    public ScheduledExecutorService scheduledExecutorService() {
        return Executors.newScheduledThreadPool(10);
    }
}

2.2.3 基于注解设定多线程定时任务

基于 @EnableAsync 注解设定多线程定时任务,在定时任务所属的类上添加 @EnableAsync 注解开启多线程(@EnableAsync也可以添加在springboot的启动类上)。在定时任务方法上添加 @Async 注解开启该方法的多线程,且开启多线程的方法需为public。

import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;

@Component
@EnableAsync //开启多线程
public class ScheduledTask {

    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    @Scheduled(cron = "*/5 * * * * ?")
    @Async
    public void cron() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "-cron:" + LocalDateTime.now().format(FORMATTER));
        TimeUnit.SECONDS.sleep(6);
    }

    @Scheduled(fixedDelay = 5000)
    @Async
    public void fixedDelay() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "-fixedDelay:" + LocalDateTime.now().format(FORMATTER));
        TimeUnit.SECONDS.sleep(6);
    }

    @Scheduled(fixedRate = 5000)
    @Async
    public void fixedRate() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "-fixedRate:" + LocalDateTime.now().format(FORMATTER));
        TimeUnit.SECONDS.sleep(6);
    }
}

2.3 cron表达式

cronExpression定义时间规则,Cron表达式由6或7个空格分隔的时间字段组成:秒 分钟 小时 日期 月份 星期 年(可选)

字段 允许值 允许的特殊字符
0-59 , - * /
0-59 , - * /
小时 0-23 , - * /
日期 1-31 , - * ? / L W C
月份 1-12 , - * /
星期 1-7 , - * ? / L C #
1970-2099 , - * /

,:表示列出枚举值,例如分钟使用5,20,则表示5和20分钟各执行一次
-:表示范围,例如分钟使用5-20,表示5-20分钟每分钟触发一次
*:表示匹配该域任意值,例如分钟使用 * ,表示每分钟都会执行一次
/ :表示起始时间开始触发,以后每隔多长时间触发一次,例如秒使用0/3,表示从0开始触发,后每三分钟触发一次
?:只能在日和星期使用,表示匹配任意值,但实际不会;因为日和星期可能会存在冲突,如果想表示每月20号0点执行,则需要写为 0 0 0 20 * ?,星期位必须写为?,虽然概念上 * 也表示通配
L:表示最后,只出现在日和星期;例如在星期的5L,表示最后一个星期五触发
W:表示有效工作日(周一-周五),只出现在日,如果指定的当天在某月刚好为周末,则就近取周五或周一执行
LW:连用表示每个月最后一个星期五,只在日使用
#:用于确定第几个星期的星期几,只在星期使用;例如2#3,表示在每月的第三个星期一

常用的cron表达式有:

0/3 * * * * ? :表示每三秒钟执行一次
0 0 2 1 * ? * :表示在每月 1 日的凌晨 2 点执行
0 15 10 ? * MON-FRI :表示周一到周五每天上午 10:15 执行
0 15 10 ? * 6#3 :每月的第三个星期五上午10:15触发 
0 15 10 ? 6L 2019-2020 :表示 2019-2020 年的每个月的最后一个星期五上午 10:15 执行
0 0 10,14,16 * * ? :每天上午 10 点,下午 2 点,4 点执行
0 0/30 9-17 * * ? :朝九晚五工作时间内每半小时执行
0 0 12 ? * WED :表示每个星期三中午 12 点执行
0 0 12 * * ? :每天中午 12点执行
0 15 10 ? * * :每天上午 10:15 执行
0 15 10 * * ? :每天上午 10:15 执行
0 15 10 * * ? * :每天上午 10:15 执行
0 15 10 * * ? 2019 :2019 年的每天上午 10:15 执行

3、@Schedule 原理

在 SpringBoot 中,我们使用 @EnableScheduling 来启用 @Schedule。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class)
@Documented
public @interface EnableScheduling {

}

EnableScheduling 注解需要注意 import 了 SchedulingConfiguration。SchedulingConfiguration 一看名字就知道是一个配置类,是为了添加相应的依赖类。

@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。看样子 SpringBoot 定时任务的核心就是 ScheduledAnnotationBeanPostProcessor 类了,下面我们来看一看 ScheduledAnnotationBeanPostProcessor 类。

ScheduledAnnotationBeanPostProcessor

public class ScheduledAnnotationBeanPostProcessor
		implements ScheduledTaskHolder, MergedBeanDefinitionPostProcessor, DestructionAwareBeanPostProcessor,
		Ordered, EmbeddedValueResolverAware, BeanNameAware, BeanFactoryAware, ApplicationContextAware,
		SmartInitializingSingleton, ApplicationListener<ContextRefreshedEvent>, DisposableBean {
}

可以看到 ScheduledAnnotationBeanPostProcessor 实现了很多接口,这里重点关注 2 个,ApplicationListener 和 DestructionAwareBeanPostProcessor。

DestructionAwareBeanPostProcessor 封装任务

DestructionAwareBeanPostProcessor 继承了 BeanPostProcessor。BeanPostProcessor的作用就是在 Bean 创建时执行 setter 之后,在自定义的 afterPropertiesSet 和 init-method 前后提供拦截点,大致执行的先后顺序是:

Bean 实例化 -> setter -> BeanPostProcessor#postProcessBeforeInitialization ->
-> InitializingBean#afterPropertiesSet -> init-method -> BeanPostProcessor#postProcessAfterInitialization

ScheduledAnnotationBeanPostProcessor 的 postProcessAfterInitialization 方法如下:

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
    if (bean instanceof AopInfrastructureBean || bean instanceof TaskScheduler ||
            bean instanceof ScheduledExecutorService) {
        // Ignore AOP infrastructure such as scoped proxies.
        return bean;
    }

    Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
    if (!this.nonAnnotatedClasses.contains(targetClass) &&
            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);
                });
        if (annotatedMethods.isEmpty()) {
            this.nonAnnotatedClasses.add(targetClass);
            if (logger.isTraceEnabled()) {
                logger.trace("No @Scheduled annotations found on bean class: " + targetClass);
            }
        }
        else {
            // Non-empty set of methods
            annotatedMethods.forEach((method, scheduledMethods) ->
                    scheduledMethods.forEach(scheduled -> processScheduled(scheduled, method, bean)));
            if (logger.isTraceEnabled()) {
                logger.trace(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName +
                        "': " + annotatedMethods);
            }
        }
    }
    return bean;
}

流程简单描述:找到所有的 Schedule 方法,把它封装为 ScheduledMethodRunnable 类 (ScheduledMethodRunnable 类实现了 Runnable 接口),并把其做为一个任务注册到 ScheduledTaskRegistrar 中。

ApplicationListener 执行任务

通过 BeanPostProcessor 解析出了所有的任务,接下来要做的事情就是提交任务了。

@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();
    }
}

ScheduledAnnotationBeanPostProcessor 监听的事件是 ContextRefreshedEvent,就是在容器初始化,或者刷新的时候被调用。监听到 ContextRefreshedEvent 事件之后,值调用了 finishRegistration 方法,这个方法的基本流程如下:

  1. 找到容器中的 SchedulingConfigurer,并调用它的 configureTasks,SchedulingConfigurer 的作用主要就是配置 ScheduledTaskRegistrar 类,例如线程池等参数,例如:
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

import java.util.concurrent.Executors;

@Configuration
public class MyScheduleConfig implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10));
    }
}
  1. 调用 ScheduledTaskRegistrar 的 afterPropertiesSet 方法执行任务,如果对具体的逻辑感兴趣,可以阅读 ScheduledTaskRegistrar 的 scheduleTasks 方法。
0

评论区