Spring Scheduler 定时任务常用方法详解

本贴最后更新于 420 天前,其中的信息可能已经沧海桑田

1. 普通定时任务

新建一个 springboot 项目,在启动类上添加注解 @EnableScheduling
image.png

然后新建定时任务类

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Component
@Slf4j
public class SimpleSchedule {
	public static final String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss";

	@Scheduled(cron = "0/10 * * * * ? ")
	public void testScheduler() {
		log.info("定时任务开始时间: {}", LocalDateTime.now().format(DateTimeFormatter.ofPattern(YYYY_MM_DD_HH_MM_SS)));
		try {
			log.info("定时任务具体业务逻辑,模拟业务逻辑处理......");
			System.out.println("静态定时任务执行啦!!!!!!!");
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			log.error("定时任务执行失败", e);
			Thread.currentThread().interrupt();
		}
		log.info("定时任务结束时间: {}", LocalDateTime.now().format(DateTimeFormatter.ofPattern(YYYY_MM_DD_HH_MM_SS)));
	}
}

启动 spring, 可以看到定时任务运行情况
image.png

2.动态定时任务

首先我们将 SimpleSchedule 类中的定时任务时间改一下 @Scheduled(cron = "0 0 1 * * ?")//每天晚上一点执行 防止影响后面的日志查看。

在正常的使用中,难免会遇到需要修改定时设置的时候,这时候就需要动态定时任务,为了方便测试,我们先将 cron 表达式写在配置文件 application.yml 中

server:
  port: 8080

scheduler:
  # 定时任务的时间
  testCron: 0/10 * * * * ?

然后编写动态定时任务类,需要实现接口:org.springframework.scheduling.annotation.SchedulingConfigurer
从接口代码上看,其包含注解 @FunctionalInterface 是函数式接口,是可以使用 lambda 表达式来实现,我们实现他的代码
image.png

import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * @author windxiao
 */
@Component
@Slf4j
@Getter
@Setter
public class ConfigSchedule implements SchedulingConfigurer {

    public static final String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss";

    @Value("${scheduler.testCron}")
    private String testCron;

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        // 使用cron表达式可以动态地设置循环间隔
        taskRegistrar.addTriggerTask(() -> {
            log.info(">>>>>>动态定时任务开始时间: {}", LocalDateTime.now().format(DateTimeFormatter.ofPattern(YYYY_MM_DD_HH_MM_SS)));
            try {
                // 定时任务具体业务逻辑,模拟业务逻辑处理
                log.info("动态定时任务具体业务逻辑,模拟业务逻辑处理......");
                System.out.println("动态定时任务执行啦!!!!!!!");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                log.error("动态定时任务处理失败", e);
                Thread.currentThread().interrupt();
            }
            log.info(">>>>>>动态定时任务结束时间: {}", LocalDateTime.now().format(DateTimeFormatter.ofPattern(YYYY_MM_DD_HH_MM_SS)));
        }, triggerContext -> {
            // 使用CronTrigger触发器,可动态修改cron表达式来操作循环规则
            CronTrigger cronTrigger = new CronTrigger(testCron);
            return cronTrigger.nextExecutionTime(triggerContext);
        });
    }
}

因为要动态设置,我们给他写一个 controller 来实现动态设置

import com.example.demo01.scheduler.ConfigSchedule;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;


/**
 * @author windxiao
 */
@Slf4j
@RestController
@RequestMapping("scheduler")
public class ScheduleController {

    @Autowired
    ConfigSchedule configSchedule;



    @GetMapping("/set")
    public void setCron(@RequestParam("cron") String cron) {
        configSchedule.setTestCron(cron);
    }
}

用 controller 记得导入 spring-boot-starter-web 模块

然后我们启动项目
image.png

可以看到,启动后每 10s 执行一次,我们通过 postman 修改时间
image.png

再看后台日志
image.png

发现定时任务已经被修改为 5s 执行一次

3.同步的定时任务

首先我们还是将上一个定时任务时间换一下,换成 0 0 1 * * ?

我们在一个类中添加两个定时任务,一个每 15 秒执行一次,假设他要执行 10 秒;
第二个每 3 秒执行一次,代码如下:

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author windxiao
 */
@Slf4j
@Component
public class BlockSchedule {

    /**
     * 定时任务1,每15秒执行一次,会执行10秒(造成10秒阻塞)
     * @throws InterruptedException InterruptedException
     */
    @Scheduled(cron = "0/15 * * * * ?")
    public void taskCron1() throws InterruptedException {
        SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
        log.info("taskCron1 - 执行定时任务【0/15 * * * * ?】,时间: " + dateFormat.format(new Date()));
        //模拟延时
        Thread.sleep(10*1000);
    }



    /**
     * 定时任务2,每3秒执行一次
     */
    @Scheduled(cron = "0/3 * * * * ?")
    public void taskCron2(){
        SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
        log.info("taskCron2 - 执行定时任务【0/3 * * * * ?】,时间: " + dateFormat.format(new Date()));
    }
}

先想一想,这段代码的执行结果。

然后我们在看他的执行结果:
image.png

在日志里能看到,先执行了 taskCron1,然后等 taskCron1 的 10s 执行完成后,才执行的 taskCron2,taskCron2 在 taskCron1 执行时被阻塞了,没能执行,那在这种情况下,我们怎么才能让 taskCron2 正常执行呢?
解决方案就是并发处理定时任务,每个线程执行一个定时任务,那自然不会阻塞,所以我们创建一个线程池,并且把他注册到 spring 容器中,代码如下:

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * @author windxiao
 */
@Slf4j
@EnableAsync
@Configuration
public class ScheduleThreadPool {

    @Bean(name = "asyncScheduleServiceExecutor")
    public Executor asyncScheduleServiceExecutor() {

        // SpringBoot项目,可使用Spring提供的对 ThreadPoolExecutor 封装的线程池 ThreadPoolTaskExecutor:
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

        // 配置核心线程数
        executor.setCorePoolSize(5);

        // 配置最大线程数
        executor.setMaxPoolSize(10);

        // 配置队列大小
        executor.setQueueCapacity(20);

        // 配置线程池中的线程的名称前缀(方便排查问题)
        executor.setThreadNamePrefix("async-scheduler-service-");

        // 配置线程拒绝策略
        // rejection-policy:当pool已经达到max size的时候,如何处理新任务
        // 1.CallerRunsPolicy:不在新线程中执行任务,而是由调用者所在的线程来执行。
        //   "该策略既不会抛弃任务,也不会抛出异常,而是将任务回推到调用者。"顾名思义,在饱和的情况下,调用者会执行该任务(而不是由多线程执行)
        // 2.AbortPolicy:拒绝策略,直接拒绝抛出异常
        // 3.DiscardPolicy:丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式
        // 4.DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

        //执行初始化
        executor.initialize();

        return executor;
    }

}

上面的代码使用了 @EnableAsync 注解,这个注解是 Spring 提供的,当线程池被这个注解标记后,这个线程池就开始了异步支持,再给其他需要异步支持的方法上添加 @Async 即可。
线程池创建之后,我们就需要修改 BlockSchedule 中的方法,给 taskCron1()和taskCron2() 添加 @Async(value = "asyncScheduleServiceExecutor")

@Async(value = "asyncScheduleServiceExecutor")
@Scheduled(cron = "0/15 * * * * ?")
public void taskCron1() throws InterruptedException {
    SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
    log.info("taskCron1 - 执行定时任务【0/15 * * * * ?】,时间: " + dateFormat.format(new Date()));
    //模拟延时
    Thread.sleep(10*1000);
}

@Async(value = "asyncScheduleServiceExecutor")
@Scheduled(cron = "0/3 * * * * ?")
public void taskCron2(){
    SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
    log.info("taskCron2 - 执行定时任务【0/3 * * * * ? 】,时间: " + dateFormat.format(new Date()));
}

然后再执行,看日志的结果:

image.png
如图可见,两个定时任务都在正常运行,没有阻塞的情况。

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3169 引用 • 8208 回帖
  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    942 引用 • 1458 回帖 • 118 关注
  • Scheduler
    2 引用 • 1 回帖
  • cron
    11 引用 • 3 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...