Spring项目中,@Scheduled注解配置的计划任务(Scheduled Tasks)可能会出现执行多次的情况,尤其是在以下场景中:

  • 一个父类定义了@Scheduled注解的方法,且被多个子类继承。
  • 父类或子类被Spring容器错误地实例化为多个Bean实例。

本文将针对该特定场景,剖析导致计划任务重复执行的原因,并针对性地提出解决措施。

一、现象描述

Spring项目中,我们定义了一个计划任务类ScheduledTaskParent,以及两个继承该类的子类FirstChildSecondChild

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Component
public class ScheduledTaskParent {

@Scheduled(fixedRate = 5000)
public void performTask() {
System.out.println("ScheduledTaskParent 执行计划任务");
}
}

@Component
public class FirstChild extends ScheduledTaskParent {

@Scheduled(fixedRate = 5000)
public void firstTask() {
System.out.println("FirstChild 执行特定的操作");
}
}

@Component
public class SecondChild extends ScheduledTaskParent {

@Scheduled(fixedRate = 5000)
public void secondTak() {
System.out.println("SecondChild 执行计划任务");
}
}

当应用启动后,发现ScheduledTaskParent中的计划任务被执行了多次,具体表现为每个子类实例都执行了父类的计划任务,导致执行次数为子类数目加1。

1
2
3
4
5
FirstChild 执行特定的操作
ScheduledTaskParent 执行计划任务
ScheduledTaskParent 执行计划任务
SecondChild 执行计划任务
ScheduledTaskParent 执行计划任务

二、原因分析

As of Spring Framework 4.3, @Scheduled methods are supported on beans of any scope.

Make sure that you are not initializing multiple instances of the same @Scheduled annotation class at runtime, unless you do want to schedule callbacks to each such instance. Related to this, make sure that you do not use @Configurable on bean classes that are annotated with @Scheduled and registered as regular Spring beans with the container. Otherwise, you would get double initialization (once through the container and once through the @Configurable aspect), with the consequence of each @Scheduled method being invoked twice.

Spring 官方文档 提到,从Spring Framework 4.3开始,@Scheduled注解支持任何作用域的bean。但是,文档也警告,不应该在运行时初始化同一@Scheduled注解类的多个实例,除非希望每个实例都调度回调。

在例子中,ScheduledTaskParent被标记为@Component,因此Spring容器会为其创建一个bean。由于FirstChildSecondChild都继承了ScheduledTaskParent,并且它们也被标记为@ComponentSpring容器为每个子类也创建了bean。每个bean都包含performTask方法上的@Scheduled注解,因此每个bean都会触发该任务的调度。

这就是为什么performTask被执行了三次:
一次来自ScheduledTaskParentbean,一次来自FirstChildbean,还有一次来自SecondChildbean

三、解决方案

为了防止计划任务在子类中被重复执行,我们可以在父类中定义一个抽象方法,并在子类中实现具体的计划任务。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public abstract class ScheduledTaskParent {

public abstract void performTask();
}

@Component
public class FirstChild extends ScheduledTaskParent {

@Scheduled(fixedRate = 5000)
@Override
public void performTask() {
System.out.println("FirstChild 执行特定的操作");
}
}

@Component
public class SecondChild extends ScheduledTaskParent {

@Scheduled(fixedRate = 5000)
@Override
public void performTask() {
System.out.println("SecondChild 执行计划任务");
}
}

通过这种方式,确保每个计划任务只被调度一次,即使有多个子类继承了父类。