Spring Boot 任务调度

image

前言

  在产品的色彩斑斓的黑的需求中,有存在一类需求,是需要去定时执行的,此时就需要使用到定时任务。例如说,每分钟扫描超时支付的订单,每小时清理一次日志文件,每天统计前一天的数据并生成报表,每个月月初的工资单的推送,每年一次的生日提醒等等。定时任务实现的几种方式:

  • java.util.Timer
  • java.util.concurrent.ScheduledExecutorService
  • Spring Task
  • Quartz

定时任务

基于JDK内置类

Timer

  可以通过创建 java.util.TimerTask 调度任务,在同一个线程中串行执行,相互影响。也就是说,对于同一个Timer里的多个TimerTask任务,如果一个TimerTask任务在执行中,其它TimerTask即使到达执行的时间,也只能排队等待。目前在项目中用的较少,直接贴demo代码。

package org.dllwh.task;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Timer;
import java.util.TimerTask;

public class TimerDemo {

	public static void main(String[] args) {
		DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
		TimerTask timerTask = new TimerTask() {
			@Override
			public void run() {
				System.out.println("task  run: " + LocalDateTime.now().format(formatter));
			}
		};
		Timer timer = new Timer();
		// 安排指定的任务在指定的时间开始进行重复的固定延迟执行。这里是每3秒执行一次
		timer.schedule(timerTask, 10, 3000);
	}
}

ScheduledExecutorService

  ScheduledExecutorServicejdk1.5 之后自带的一个类,是基于线程池设计的定时任务类,每个调度任务都会分配到线程池中的一个线程去执行(任务是并发执行,互不影响)。

package org.dllwh.task;

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

public class ScheduledExecutorServiceDemo {
	public static void main(String[] args) {
		DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
		ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
		// 参数:1、任务体 2、首次执行的延时时间 3、任务执行间隔 4、间隔时间单位
		service.scheduleAtFixedRate(
				() -> System.out.println("task ScheduledExecutorService : " + LocalDateTime.now().format(formatter)), 0,
				3, TimeUnit.SECONDS);

	}
}

总结

在日常开发中,我们很少直接使用TimerScheduledExecutorService来实现定时任务的需求。主要有几点原因:

  1. 它们仅支持按照指定频率,不直接支持指定时间的定时调度,需要我们结合Calendar自行计算,才能实现复杂时间的调度。例如说,每天、每周五、2019-11-11 等等。
  2. 它们是进程级别,而我们为了实现定时任务的高可用,需要部署多个进程。此时需要等多考虑,多个进程下,同一个任务在相同时刻,不能重复执行。
  3. 项目可能存在定时任务较多,需要统一的管理,此时不得不进行二次封装。

使用Spring Task

Spring TaskSpringFramework 的模块,提供了轻量级的定时任务的实现。在引入spring-boot-starter-web 依赖后,无需特别引入它。

基于SchedulingConfigurer

  1. 在SpringBoot项目中,我们可以很优雅的使用注解来实现定时任务,首先创建任务类:
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

/**
 * 把今天最好的表现当作明天最新的起点..~
 *
 * Today the best performance as tomorrow newest starter!
 *
 * @类描述: 实现SchedulingConfigurer并重写configureTasks方法
 * @author: <a href="mailto:duleilewuhen@sina.com">独泪了无痕</a>
 * @since: JDK 1.8
 */
@Component
@Slf4j
public class TaskConfig implements SchedulingConfigurer {
	/** 注入线程池 */
	@Autowired
	private TaskScheduler taskScheduler;
	DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

	@Override
	public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
		// 添加定时任务到线程池
		taskRegistrar.setTaskScheduler(taskScheduler);

		taskRegistrar.getScheduler().schedule(new Runnable() {

			@Override
			public void run() {
				log.info("SchedulingConfigurer定时任务:" + LocalDateTime.now().format(formatter));
			}
		}, new CronTrigger("0/5 * * * * * "));// 每5秒执行一次
	}
}ßßß
  1. 在传统的Spring项目中,我们可以在xml配置文件添加task的配置,而在SpringBoot项目中一般使用config配置类的方式添加配置,所以新建一个TaskSchedulerConfig类。
import java.util.concurrent.ThreadPoolExecutor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;

/**
 * 把今天最好的表现当作明天最新的起点..~
 *
 * Today the best performance as tomorrow newest starter!
 *
 * @类描述: 定时任务配置,配置线程池,使用不同线程执行任务,提升效率
 * @since: JDK 1.8
 */
@Configuration
public class TaskSchedulerConfig {
    /**
	 * @方法描述: ThreadPoolTaskScheduler 任务调度器
	 */
	@Bean(name = "taskScheduler")
	public TaskScheduler getThreadPoolTaskExecutor() {
		ThreadPoolTaskScheduler taskExecutor = new ThreadPoolTaskScheduler();
		// 线程池对拒绝任务(无线程可用)的处理策略,目前只支持AbortPolicy、CallerRunsPolicy
		taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
		taskExecutor.initialize();
		return taskExecutor;
	}
}
  1. application.yml 中,添加 Spring Task 定时任务的配置,如下:
server:
  port: 8215
  tomcat:
    uri-encoding: utf-8
  servlet:
    context-path: /task
    session:
      timeout: 60   # session最大超时时间(分钟),默认为30
---
spring:
  task:
    scheduling: # Spring Task 调度任务的配置,对应 TaskSchedulingProperties 配置类
      thread-name-prefix: dllwh-taskExecutor- # 线程池的线程名的前缀。默认为 scheduling- ,建议根据自己应用来设置
      pool:
        size: 20 # 线程池大小。默认为 1 ,根据自己应用来设置
      shutdown: # 为了实现 Spring Task 定时任务的优雅关闭
        await-termination: true # 应用关闭时,是否等待定时任务执行完成。默认为 false ,建议设置为 true
        await-termination-period: 60 # 等待任务完成的最大时长,单位为秒。默认为 0 ,根据自己应用来设置

  1. 在类上使用 @EnableScheduling 开启对定时任务的支持,然后启动项目
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling // 启动 Spring Task 的定时任务调度的功能
public class TaskApplication {
	public static void main(String[] args) {
		SpringApplication.run(TaskApplication.class, args);
	}
}

任务调度@Scheduled

  1. 创建任务类:
package org.dllwh.task;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;

@Component
@Slf4j
public class ScheduledTask {
	private DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

	/**
	 * 从启动时间开始,间隔 5s 执行
	 * 固定间隔时间
	 */
	@Scheduled(fixedRate = 5000)
	public void getCurrentDate() {
		log.info("Scheduled定时任务执行:" + LocalDateTime.now().format(formatter));
	}
    
    /**
     * 按照标准时间来算,每隔 5s 执行一次
     */
	@Scheduled(cron = "0/5 * * * * *")
	public void scheduled() {
		log.info("=====>>>>>使用cron  {}", System.currentTimeMillis());
	}

    /**
     * 从启动时间开始,延迟 5s 后间隔 5s 执行
     * 固定等待时间
     */
	@Scheduled(fixedDelay = 5000, initialDelay = 5000)
	public void scheduled2() {
		log.info("=====>>>>>fixedDelay{}", System.currentTimeMillis());
	}
}
  1. 在主类上使用 @EnableScheduling 开启对定时任务的支持,然后启动项目
package org.dllwh;

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

@SpringBootApplication
@EnableScheduling // 开启定时任务的配置
public class TaskApplication {
	public static void main(String[] args) {
		SpringApplication.run(TaskApplication.class, args);
	}
}

在上面的定时任务中,我们在方法上使用 @Scheduled 来设置任务的执行时间,并且使用三种属性配置方式:

  • @Scheduled(fixedRate = 5000)

    • 定义一个按一定频率执行的定时任务,即上一次开始执行时间点之后5秒再执行
  • Scheduled(fixedDelay = 5000)

    • 定义一个按一定频率执行的定时任务,与上面不同的是,改属性可以配合initialDelay, 定义该任务延迟执行时间。
  • @Scheduled(cron="*/5 * * * * *")

    • 通过表达式来配置任务执行时间

任务调用之@Async

  什么是"异步调用"?"异步调用"对应的是"同步调用",同步调用指程序按照定义顺序依次执行,每一行程序都必须等待上一行程序执行完成之后才能执行;异步调用指程序在顺序执行时,不等待异步调用的语句返回结果就执行后面的程序。

  通常我们在某网站发验证码时,首先会提示验证码已发送,请检查邮箱或者短信,这就是图中的1和3。然而此时查看邮箱或短信可能并没有收到验证码,往往要过几秒种才真正收到,这就是图中的2和4。2和4所消耗的时间比1和3要多,如果是同步,先执行4后执行3,那么这个请求将一直占用后台服务器。如果是异步,可以在第一时间通知用户已发送,并释放请求,而完全不用去管2和4的执行过程。如果2和4执行不成功怎么办?在验证码提示消息结尾,我们经常可以看到:"如果你在XXX秒钟之内没有收到验证码,请重发"!

  在开始学习"异步调用"之前,先了解下什么是"同步调用"。

  1. 首先定义个Task类,创建三个处理函数分别模拟三个执行任务的操作,操作消耗时间随机取:
package org.dllwh.core.task;

import java.util.Random;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class SyncTask {
	public static Random random = new Random();

	public void doTaskOne() throws Exception {
		log.info("开始做任务一");
		long start = System.currentTimeMillis();
		Thread.sleep(random.nextInt(10000));
		long end = System.currentTimeMillis();
		log.info("完成任务一,耗时:" + (end - start) + "毫秒");
	}

	public void doTaskTwo() throws Exception {
		log.info("开始做任务二");
		long start = System.currentTimeMillis();
		Thread.sleep(random.nextInt(10000));
		long end = System.currentTimeMillis();
		log.info("完成任务二,耗时:" + (end - start) + "毫秒");
	}

	public void doTaskThree() throws Exception {
		log.info("开始做任务三");
		long start = System.currentTimeMillis();
		Thread.sleep(random.nextInt(10000));
		long end = System.currentTimeMillis();
		log.info("完成任务三,耗时:" + (end - start) + "毫秒");
	}
}
  1. 接下来在单元测试用例中,注入Task 对象,并在测试用例中执行单个函数。
package org.dllwh;

import org.dllwh.core.task.SyncTask;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class SyncTaskApplictionTest {
	/** ----------------------------------------------------- Fields start */
	@Autowired
	private SyncTask syncTask;

	/** ----------------------------------------------------- Fields end */
	@Test
	public void test() throws Exception {
		syncTask.doTaskOne();
		syncTask.doTaskTwo();
		syncTask.doTaskThree();
	}
}
  1. 执行单元测试,可以看到任务一、二、三顺序的执行完成。

  上述的同步调用虽然顺利的执行完三个任务,但是可以看到执行时间比较长。如果这三个任务中间不存在依赖关系可以并发执行的话,同步调用在执行效率方面比较差,那就可以考虑采用异步调用的方式并并发执行。

  1. 在SpringBoot中,我们只需要通过使用 @Async 就能简单的将原来的同步函数变为异步函数,为了让 @Async 能够生效,还需要在 SpringBoot 的主程序中配置 @EnableAsync

  @Async 其实是Spring内的一个组件,可以完成对类内单个或者多个方法实现异步调用,这样可以大大的节省等待耗时。其内部实现机制是线程池任务 ThreadPoolTaskExecutor,通过线程池来对配置 @Async 的方法或者类做出执行行动。

注:

  1. @Async所修饰的函数不要定义为static类型,这样异步调用不会生效
  2. 使用 @Asyn 声明这是一个异步任务的方法,如果在类上使用该注解,则该类下所有方法都是异步任务的方法
package org.dllwh.core.task;

import java.util.Random;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class AsyncTask {
	public static Random random = new Random();

	@Async
	public void doTaskOne() throws Exception {
		log.info("开始做任务一");
		long start = System.currentTimeMillis();
		Thread.sleep(random.nextInt(10000));
		long end = System.currentTimeMillis();
		log.info("完成任务一,耗时:" + (end - start) + "毫秒");
	}

	@Async
	public void doTaskTwo() throws Exception {
		log.info("开始做任务二");
		long start = System.currentTimeMillis();
		Thread.sleep(random.nextInt(10000));
		long end = System.currentTimeMillis();
		log.info("完成任务二,耗时:" + (end - start) + "毫秒");
	}

	@Async
	public void doTaskThree() throws Exception {
		log.info("开始做任务三");
		long start = System.currentTimeMillis();
		Thread.sleep(random.nextInt(10000));
		long end = System.currentTimeMillis();
		log.info("完成任务三,耗时:" + (end - start) + "毫秒");
	}
}

执行上面的单元测试,我们可以在控制台中看到所有输出的线程名前都是之前我们定义的线程池前缀名开始的,说明我们使用线程池来执行异步任务的试验成功了!

 

  1. 从控制台可以看出:多个任务时用的是同一个线程,可以自定义线程池不同的任务用不同线程来执行
@Configuration
public class ThreadPoolConfig {
	/**
	 * 配置线程池
	 * 
	 * @return
	 */
	@Bean(name = "scheduledPoolTaskExecutor")
	public ThreadPoolTaskExecutor getAsyncThreadPoolTaskExecutor() {
		ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
		// 线程池创建时候初始化的线程数
		taskExecutor.setCorePoolSize(20);
		// 线程池最大的线程数
		taskExecutor.setMaxPoolSize(200);
		// 用来缓冲执行任务的队列
		taskExecutor.setQueueCapacity(25);
		// 允许线程的空闲时间
		taskExecutor.setKeepAliveSeconds(200);
		// 线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
		taskExecutor.setThreadNamePrefix("oKong-Scheduled-");
		// 线程池对拒绝任务(无线程可用)的处理策略,目前只支持AbortPolicy、CallerRunsPolicy
		taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
		// 调度器shutdown被调用时等待当前被调度的任务完成
		taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
		// 等待时长
		taskExecutor.setAwaitTerminationSeconds(60);
		taskExecutor.initialize();
		return taskExecutor;
	}
}

scheduled服务修改为

package org.dllwh.core.task;

import java.util.Random;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class AsyncTask {
	public static Random random = new Random();

	@Async("scheduledPoolTaskExecutor")
	public void doTaskOne() throws Exception {
		log.info("开始做任务一");
		long start = System.currentTimeMillis();
		Thread.sleep(random.nextInt(10000));
		long end = System.currentTimeMillis();
		log.info("完成任务一,耗时:" + (end - start) + "毫秒");
	}

	@Async("scheduledPoolTaskExecutor")
	public void doTaskTwo() throws Exception {
		log.info("开始做任务二");
		long start = System.currentTimeMillis();
		Thread.sleep(random.nextInt(10000));
		long end = System.currentTimeMillis();
		log.info("完成任务二,耗时:" + (end - start) + "毫秒");
	}

	@Async("scheduledPoolTaskExecutor")
	public void doTaskThree() throws Exception {
		log.info("开始做任务三");
		long start = System.currentTimeMillis();
		Thread.sleep(random.nextInt(10000));
		long end = System.currentTimeMillis();
		log.info("完成任务三,耗时:" + (end - start) + "毫秒");
	}
}

  Spring给我们提供了异步多线程任务的功能,我们只需通过 @EnableAsync 开启异步多线程任务的支持,然后使用 @Async 来注解需要执行的异步多线程任务。

整合Quartz

快速入门

  Quartz 是OpenSymphony开源组织在 Job scheduling 领域又一个开源项目,是一个完全由java编写的开源作业调度框架。在开发Quartz相关应用时,只要定义了 Job(任务),Trigger(触发器)和Scheduler(调度器),即可实现一个定时调度能力。其中SchedulerQuartz 中的核心,Scheduler负责管理Quartz应用运行时环境,Scheduler不是靠自己完成所有的工作,是根据Trigger的触发标准,调用Job中的任务执行逻辑,来完成完整的定时任务调度。

在Quartz中的主要概念:

  • Scheduler:调度任务的主要API
  • ScheduleBuilder:用于构建Scheduler,例如其简单实现类SimpleScheduleBuilder
  • Job:调度任务执行的接口,也即定时任务执行的方法
  • JobDetail:定时任务作业的实例
  • JobBuilder:关联具体的Job,用于构建JobDetail
  • Trigger:定义调度执行计划的组件,即定时执行
  • TriggerBuilder:构建Trigger

引入依赖

在SpringBoot中应用Quartz,需要依赖下述资源:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Quartz 定时调度依赖 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

示例Demo

  1. 创建任务类QuartzTask,该类主要是继承QuartzJobBean实现executeInternal,该方法即定时执行任务逻辑
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class QuartzTask extends QuartzJobBean {
	private DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

	/**
	 * 执行定时任务
	 * 
	 * @param jobExecutionContext
	 * @throws JobExecutionException
	 */
	@Override
	protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
		log.info("quartz task: " + LocalDateTime.now().format(formatter));
	}
}
  1. 创建配置类QuartzConfig
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QuartzConfig {
    /**
     * 定义**JobDetail**,JobDetail由JobBuilder构建,同时关联了任务
     */
	@Bean
	public JobDetail testQuartzDetail() {
	    // 关联了任务QuartzTask
		return JobBuilder.newJob(QuartzTask.class).storeDurably().build();
	}

    /**
     * 定义定时调度**Trigger**
     * 1. 简单实现类SimpleScheduleBuilder用于构建Scheduler
     * 2. TriggerBuilder则用于构建Trigger
     */
	@Bean
	public Trigger testQuartzTrigger() {
		SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule();
		// 设置时间周期:每10秒执行一次,单位秒
		scheduleBuilder.withIntervalInSeconds(10);
		// 永久重复,一直执行下去
		scheduleBuilder.repeatForever();
		return TriggerBuilder.newTrigger()
		    .forJob(testQuartzDetail())
			.withSchedule(scheduleBuilder)
			.build();
	}
}

启动项目

package org.dllwh;

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

@SpringBootApplication
@EnableScheduling // 开启定时任务机制
public class TaskApplication {
	public static void main(String[] args) {
		SpringApplication.run(TaskApplication.class, args);
	}
}

题外记

cron表达式

Cron表达式介绍

  Cron表达式是一个具有时间含义的字符串,字符串以空格隔开,格式为 X X X X X X X 。其中X是一个域的占位符。单个域有多个取值时,使用半角逗号,隔开取值。每个域可以是确定的取值,也可以是具有逻辑意义的特殊字符。

域取值

下表为Cron表达式中六个域能够取的值以及支持的特殊字符

字段允许值允许的特殊字符
秒(Seconds)0~59的整数, - * / 四个字符
分(Minutes)0~59的整数, - * / 四个字符
小时(Hours)0~23的整数, - * / 四个字符
日期(DayofMonth)1~31的整数(但是需要考虑每月的天数),- * ? / L W C 八个字符
月份(Month)1~12的整数或者 JAN-DEC, - * / 四个字符
星期(DayofWeek)1~7的整数或者 SUN-SAT, - * ? / L C # 八个字符
年(可选,留空)(Year)1970~2099, - * / 四个字符

每一个域都使用数字,但还可以出现如下特殊字符,它们的含义是:

特殊字符含义示例
*所有可能的值在月域中,*表示每个月;在星期域中,*表示星期的每一天。
,列出枚举值在分钟域中,5,20表示分别在5分钟和20分钟触发一次
-范围在分钟域中,5-20表示从5分钟到20分钟之间每隔一分钟触发一次
/指定数值的增量在分钟域中,0/15表示从第0分钟开始,每15分钟。<br/>在分钟域中,3/20表示从第3分钟开始,每20分钟。
?不指定值,仅日期和星期域支持当日期或星期域其中之一被指定了值以后,为了避免冲突,需要将另一个域的值设为?。
L单词Last的首字母,表示最后一天,仅日期和星期域支持该字符在日期域中,L表示某个月的最后一天。<br/>在星期域中,L表示一个星期的最后一天,也就是星期六
W除周末以外的有效工作日,<br/>在离指定日期的最近的有效工作日触发事件在日期域中5W,<br/>如果5日是星期六,则将在最近的工作日星期五,即4日触发<br/>如果5日是星期天,则将在最近的工作日星期一,即6日触发<br/>如果5日在星期一到星期五中的一天,则就在5日触发
LW指定月份的最后一个工作日,即最后一个星期五 
#确定每个月第几个星期几,仅星期域支持在星期域中,4#2表示某月的第二个星期三

取值示例

示例说明
0 15 10 ? * *每天上午10:15执行任务
0 15 10 * * ?每天上午10:15执行任务
0 0 12 * * ?每天中午12:00执行任务
0 0 10,14,16 * * ?每天上午10:00点、下午14:00以及下午16:00执行任务
0 0/30 9-17 * * ?每天上午09:00到下午17:00时间段内每隔半小时执行任务
0 * 14 * * ?每天下午14:00到下午14:59时间段内每隔1分钟执行任务
0 0-5 14 * * ?每天下午14:00到下午14:05时间段内每隔1分钟执行任务
0 0/5 14 * * ?每天下午14:00到下午14:55时间段内每隔5分钟执行任务
0 0/5 14,18 * * ?每天下午14:00到下午14:55、下午18:00到下午18:55时间段内每隔5分钟执行任务
0 0 12 ? * WED每个星期三中午12:00执行任务
0 15 10 15 * ?每月15日上午10:15执行任务
0 0 2 1 * ? *表示在每月的1日的凌晨2点调整任务
0 15 10 L * ?每月最后一日上午10:15执行任务
0 15 10 ? * 6L每月最后一个星期五上午10:15执行任务
0 15 10 ? * 6#3每月第三个星期五上午10:15执行任务
0 10,44 14 ? 3 WED每年3月的每个星期三下午14:10到14:44时间段内执行任务
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

独泪了无痕

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值