SpringBoot中定时任务@Scheduled注解的使用解读
作者:Sunshine_Dongyang 发布时间:2022-11-24 17:20:11
项目开发中,经常会遇到定时任务的场景,Spring提供了@Scheduled注解
,方便进行定时任务的开发
概述
要使用@Scheduled
注解,首先需要在启动类添加@EnableScheduling
,启用Spring的计划任务执行功能,这样可以在容器中的任何Spring管理的bean上检测@Scheduled
注解,执行计划任务
注解定义
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {
String cron() default "";
String zone() default "";
long fixedDelay() default -1;
String fixedDelayString() default "";
long fixedRate() default -1;
String fixedRateString() default "";
long initialDelay() default -1;
String initialDelayString() default "";
}
参数说明
参数 | 参数说明 | 示例 |
---|---|---|
cron | 任务执行的cron表达式 | 0/1 * * * * ? |
zone | cron表达时解析使用的时区,默认为服务器的本地时区,使用java.util.TimeZone#getTimeZone(String)方法解析 | GMT-8:00 |
fixedDelay | 上一次任务执行结束到下一次执行开始的间隔时间,单位为ms | 1000 |
fixedDelayString | 上一次任务执行结束到下一次执行开始的间隔时间,使用java.time.Duration#parse解析 | PT15M |
fixedRate | 以固定间隔执行任务,即上一次任务执行开始到下一次执行开始的间隔时间,单位为ms,若在调度任务执行时,上一次任务还未执行完毕,会加入worker队列,等待上一次执行完成后立即执行下一次任务 | 2000 |
fixedRateString | 与fixedRate逻辑一致,只是使用java.time.Duration#parse解析 | PT15M |
initialDelay | 首次任务执行的延迟时间 | 1000 |
initialDelayString | 首次任务执行的延迟时间,使用java.time.Duration#parse解析 | PT15M |
源码解析
配置了@Scheduled
注解的方法,Spring的处理是通过注册ScheduledAnnotationBeanPostProcessor来执行,将不同配置参数的任务分配给不同的handler处理,核心代码如下
org.springframework.scheduling.annotation.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;
}
org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor#processScheduled
/**
* Process the given {@code @Scheduled} method declaration on the given bean.
* @param scheduled the @Scheduled annotation
* @param method the method that the annotation has been declared on
* @param bean the target bean instance
* @see #createRunnable(Object, Method)
*/
protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
try {
Runnable runnable = createRunnable(bean, method);
boolean processedSchedule = false;
String errorMessage =
"Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required";
Set<ScheduledTask> tasks = new LinkedHashSet<>(4);
// Determine initial delay
long initialDelay = scheduled.initialDelay();
String initialDelayString = scheduled.initialDelayString();
if (StringUtils.hasText(initialDelayString)) {
Assert.isTrue(initialDelay < 0, "Specify 'initialDelay' or 'initialDelayString', not both");
if (this.embeddedValueResolver != null) {
initialDelayString = this.embeddedValueResolver.resolveStringValue(initialDelayString);
}
if (StringUtils.hasLength(initialDelayString)) {
try {
initialDelay = parseDelayAsLong(initialDelayString);
}
catch (RuntimeException ex) {
throw new IllegalArgumentException(
"Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into long");
}
}
}
// Check cron expression
String cron = scheduled.cron();
if (StringUtils.hasText(cron)) {
String zone = scheduled.zone();
if (this.embeddedValueResolver != null) {
cron = this.embeddedValueResolver.resolveStringValue(cron);
zone = this.embeddedValueResolver.resolveStringValue(zone);
}
if (StringUtils.hasLength(cron)) {
Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");
processedSchedule = true;
if (!Scheduled.CRON_DISABLED.equals(cron)) {
TimeZone timeZone;
if (StringUtils.hasText(zone)) {
timeZone = StringUtils.parseTimeZoneString(zone);
}
else {
timeZone = TimeZone.getDefault();
}
tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));
}
}
}
// At this point we don't need to differentiate between initial delay set or not anymore
if (initialDelay < 0) {
initialDelay = 0;
}
// Check fixed delay
long fixedDelay = scheduled.fixedDelay();
if (fixedDelay >= 0) {
Assert.isTrue(!processedSchedule, errorMessage);
processedSchedule = true;
tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
}
String fixedDelayString = scheduled.fixedDelayString();
if (StringUtils.hasText(fixedDelayString)) {
if (this.embeddedValueResolver != null) {
fixedDelayString = this.embeddedValueResolver.resolveStringValue(fixedDelayString);
}
if (StringUtils.hasLength(fixedDelayString)) {
Assert.isTrue(!processedSchedule, errorMessage);
processedSchedule = true;
try {
fixedDelay = parseDelayAsLong(fixedDelayString);
}
catch (RuntimeException ex) {
throw new IllegalArgumentException(
"Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into long");
}
tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
}
}
// Check fixed rate
long fixedRate = scheduled.fixedRate();
if (fixedRate >= 0) {
Assert.isTrue(!processedSchedule, errorMessage);
processedSchedule = true;
tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));
}
String fixedRateString = scheduled.fixedRateString();
if (StringUtils.hasText(fixedRateString)) {
if (this.embeddedValueResolver != null) {
fixedRateString = this.embeddedValueResolver.resolveStringValue(fixedRateString);
}
if (StringUtils.hasLength(fixedRateString)) {
Assert.isTrue(!processedSchedule, errorMessage);
processedSchedule = true;
try {
fixedRate = parseDelayAsLong(fixedRateString);
}
catch (RuntimeException ex) {
throw new IllegalArgumentException(
"Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into long");
}
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);
}
}
catch (IllegalArgumentException ex) {
throw new IllegalStateException(
"Encountered invalid @Scheduled method '" + method.getName() + "': " + ex.getMessage());
}
}
org.springframework.scheduling.config.ScheduledTaskRegistrar#scheduleTasks
/**
* Schedule all registered tasks against the underlying
* {@linkplain #setTaskScheduler(TaskScheduler) task scheduler}.
*/
proected 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));
}
}
}
使用详解
定时任务同步/异步执行
定时任务执行默认是单线程模式,会创建一个本地线程池,线程池大小为1。当项目中有多个定时任务时,任务之间会相互等待,同步执行
源码:
// org.springframework.scheduling.config.ScheduledTaskRegistrar#scheduleTasks
if (this.taskScheduler == null) {
this.localExecutor = Executors.newSingleThreadScheduledExecutor();
this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
}
// java.util.concurrent.Executors#newSingleThreadScheduledExecutor()
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}
代码示例:
@Slf4j
@Component
public class RunIntervalTestScheduler {
@Scheduled(cron = "0/1 * * * * ?")
public void singleThreadTest1() {
log.info("singleThreadTest1");
LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
}
@Scheduled(cron = "0/1 * * * * ?")
public void singleThreadTest2() {
log.info("singleThreadTest2");
LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
}
@Scheduled(cron = "0/1 * * * * ?")
public void singleThreadTest3() {
log.info("singleThreadTest3");
LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
}
}
执行结果:
可以看到,默认情况下,三个任务串行执行,都使用pool-1-thread-1
同一个线程池,并且线程只有一个
可以通过实现SchedulingConfigurer
接口,手动创建线程池,配置期望的线程数量
示例代码:
@Configuration
public class ScheduledConfig implements SchedulingConfigurer {
/**
* 任务执行线程池大小
*/
private static final int TASK_POOL_SIZE = 50;
/**
* 线程名
*/
private static final String TASK_THREAD_PREFIX = "test-task-";
@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
ThreadPoolTaskScheduler taskPool = new ThreadPoolTaskScheduler();
taskPool.setPoolSize(TASK_POOL_SIZE);
taskPool.setThreadNamePrefix(TASK_THREAD_PREFIX);
taskPool.initialize();
scheduledTaskRegistrar.setTaskScheduler(taskPool);
}
}
任务执行结果:
此时任务的执行已经异步化,从自定义线程池中分配线程执行任务,在实际应用中需要考虑实际任务数量,创建相应大小的线程池
fixedRate/fixedDelay区别
fixedRate是配置上一次任务执行开始到下一次执行开始的间隔时间,不会等待上一次任务执行完成就会调度下一次任务,将其放入等待队列中
代码示例:
@Slf4j
@Component
public class RunIntervalTestScheduler {
@Scheduled(initialDelay = 1000, fixedRate = 1000)
public void fixedRate() throws Exception {
log.info("fixedRate run");
TimeUnit.SECONDS.sleep(3);
}
}
执行结果:
任务配置的fixedRate为1s,执行日志打印的时间间隔都是3s左右,也就是上一次执行完成后,紧接着就执行下一次任务
fixedDelay是配置的上一次任务执行结束到下一次执行开始的间隔时间,也就是说会等待上一次任务执行结束后,延迟间隔时间,再执行下一次任务
代码示例:
@Slf4j
@Component
public class RunIntervalTestScheduler {
@Scheduled(initialDelay = 1000, fixedDelay = 1000)
public void fixedDelay() throws Exception {
log.info("fixedDelay run");
TimeUnit.SECONDS.sleep(3);
}
}
执行结果:
任务配置的fixedDelay为1s,执行日志打印的时间间隔都是4s左右,也就是上一次执行完成后,延迟1s后执行下一次任务
cron表达式如果配置为类似每秒执行、每分钟执行(例:0/1 * * * * ?, 每秒执行),调度跟fixedDelay是一致的,也是在上一次任务执行结束后,等待间隔时间
代码示例:
@Slf4j
@Component
public class RunIntervalTestScheduler {
@Scheduled(cron = "0/1 * * * * ?")
public void cronRun() throws Exception{
log.info("cron run");
TimeUnit.SECONDS.sleep(3);
}
}
执行结果:
执行日志打印的时间间隔都是4s左右,也就是上一次执行完成后,延迟1s后执行下一次任务
cron表达式如果配置为固定时间执行(例:1 * * * * ?, 秒数为1时执行),若上一次任务没有执行完,则不会调度本次任务,跳过本次执行,等待下一次执行周期
代码示例:
@Slf4j
@Component
public class RunIntervalTestScheduler {
@Scheduled(cron = "1 * * * * ?")
public void cronRun() throws Exception{
log.info("cron run");
TimeUnit.SECONDS.sleep(70);
}
}
执行结果:
上一次任务未执行完毕,则跳过了本次执行
来源:https://blog.csdn.net/hu_dongyang/article/details/114270232


猜你喜欢
- 由于经常用到文件处理,便自己封装了下 分享给大家。 包含写入文本 批量删除文件 下载文件 。--可直接使用/// <summary&g
- java中找不到符号问题 java找不到符号如果你的代码里没有报错,明明是存在的。但是java报错找不到符号。像下面这样子。解决步
- Spring bean配置单例或多例模式单例spring bean 默认是单例默认,在对应.xml文件中的配置是:<bean id=&
- 1、static是什么意思?static 关键字表明一个成员变量或者是成员方法可以在没有所属的类的实例变量的情况下被访问。例如Main类pa
- 前言上一篇博客内容对 RecyclerView 回收复用机制相关源码进行了分析,本博客从自定义 View 三大流程 measure、layo
- Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程。该框架使
- 异常是一个事件,它发生在程序运行期间,干扰了正常的指令流程。Java通过API中Throwable类的众多子类描述各种不同的异常。因而,Ja
- 在平时的工作中,估计大多数都做过轮询调度的任务,比如定时轮询数据库同步,定时邮件通知等等。大家通过windows计划任务,windows服务
- 本文实例为大家分享了Java实现点击按钮弹出新窗体的功能,旧窗体不进行操作分析:对于自定义窗体来说,最简单直接的做法就是让新窗体继承java
- 本文实例讲述了Java实现的简单网页截屏功能。分享给大家供大家参考,具体如下:package awtDemo;import java.awt
- 一、Druid简介Druid是阿里开源的数据库连接池,作为后起之秀,性能比dbcp、c3p0更高,使用也越来越广泛。当然Druid不仅仅是一
- 在 Servlet/Jsp 项目中,如果涉及到系统任务,例如在项目启动阶段要做一些数据初始化操作,这些操作有一个共同的特点,只在项目启动时进
- 说明:在填写表数据时当输入完一个文本框后,输入下一个文本框时需要用Tab键切换,但是有的人喜欢用Enter键切换下一个,此方法是Enter取
- 前言二进制文件读写两个重要的函数 , fread 和 fwrite , fread 用于读取文件 , fwrite 用于写出文件 ;frea
- 一、理解 “ 服务器 / 浏览器 ”沟通流程(3步)第1步:浏览器使用<img src=&qu
- 命名空间的特性首先熟悉一下命名空间的两个概念。声明区域:可以在其中进行声明的区域,如全局文件的声明区域是文件,函数内声明的变量声明区域为代码
- 前言我们从以下几个方面研究:SpringBoot的启动依赖启动器starter有什么作用启动引导类是怎么运行的内置的tomcat服务器原理p
- 一、前言系统执行业务逻辑之前,会对输入数据进行校验,检测数据是否有效合法的。所以我们可能会写大量的if else等判断逻辑,特别是在不同方法
- Spring Boot应用内存飙升一个简单的Spring Boot应用, 几乎只有一个用户在用,内存竟然达到1.2G, 可怕服务现状由于之前
- 1.系统架构包括哪些形式?C/S架构B/S架构2.什么是C/S架构?说白了就是客户端/服务端,我们需要安装特定的客户端软卷,例如:QQ。C/