任务调度框架剖析系列 · Spring

Schedule调度能力源码剖析

一.使用

仅关注较为常见的一种使用方式,Spring框架下对能力启用的方式可以有很多中不同的形式,但是本质的原理是相同的。

1

2

3

4

1.@Scheduled

通过注解方法的方式将此方法抽取为可调度任务实体。

2.@EnableScheduling

Spring风格的能力引入,启动调度处理的内部逻辑。

二.原理解析

1.@Scheduled注解

两个作用:标注、携带调度信息

  • 标注  Spring收集调度任务(bean实例及其特定方法绑定在一起被封装为可调度任务)的标识
  • 调度信息  调度类型及其调度数据

调度类型

  • cron表达式  unix风格的cron表达式
  • 固定时延  单位(ms),仅生效一次 ??
  • 固定频率  单位(ms)

2.@EnableScheduling注解

标准的spring风格的逻辑使能编码风格,封装了@Import,引入真正的生效逻辑处理类。

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

    }

}

Configuration注解在Spring中的作用,引入bean实例。

public class ScheduledAnnotationBeanPostProcessor

        implements ScheduledTaskHolder, MergedBeanDefinitionPostProcessor, DestructionAwareBeanPostProcessor,

        Ordered, EmbeddedValueResolverAware, BeanNameAware, BeanFactoryAware, ApplicationContextAware,

        SmartInitializingSingleton, ApplicationListener<ContextRefreshedEvent>, DisposableBean {

    /**

     * The default name of the {@link TaskScheduler} bean to pick up: {@value}.

     * <p>Note that the initial lookup happens by type; this is just the fallback

     * in case of multiple scheduler beans found in the context.

     * @since 4.2

     */

    public static final String DEFAULT_TASK_SCHEDULER_BEAN_NAME = "taskScheduler";

    private final ScheduledTaskRegistrar registrar;

    @Nullable

    private Object scheduler;

    @Nullable

    private StringValueResolver embeddedValueResolver;

    @Nullable

    private String beanName;

    @Nullable

    private BeanFactory beanFactory;

    @Nullable

    private ApplicationContext applicationContext;

    private final Set<Class<?>> nonAnnotatedClasses = Collections.newSetFromMap(new ConcurrentHashMap<>(64));

    private final Map<Object, Set<ScheduledTask>> scheduledTasks = new IdentityHashMap<>(16);

}

ScheduledAnnotationBeanPostProcessor关键类,实现了MergedBeanDefinitionPostProcessor(BeanPostProcessor子类), SmartInitializingSingleton, ApplicationListener三个spring框架的hook,将类自身的处理逻辑嵌入到Spring框架的生命周期中生效。

  • SmartInitializingSingleton, ApplicationListener的具体逻辑均指向了finishRegistration(),适配应用上下文存在与不存在两种场景,作用是完成ScheduledTaskRegistrar的启动。
  • MergedBeanDefinitionPostProcessor.postProcessAfterInitialization()逻辑完成了自身的核心调度逻辑处理。
2.1 MergedBeanDefinitionPostProcessor.postProcessAfterInitialization()

// 1.采集“调度任务”,注意方法外层隐含所有bean实例的一层循环

// 另外此处的容器定义也表明了调度注解在方法上的可重复性,注解类定义时存在同样的信息

Map<Method, Set<Scheduled>> annotatedMethods

// 2.循环调用

annotatedMethods.forEach((method, scheduledMethods) -> scheduledMethods.forEach(scheduled -> processScheduled(scheduled, method, bean)));

trace至此,可以定位processScheduled就是核心逻辑。

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

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

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

        }

    }

方法可以拆解为如下几步:

  1. 解析初始时延数据;
  2. 解析cron表达式,如果存在有效的cron表达式,则创建CronTask加入临时容器;
  3. 解析fixedDelay,如果存在有效的fixedDelay信息,则创建FixedDelayTask加入临时容器;
  4. 解析fixedRate,如果存在有效的fixedRate信息,则创建FixedRateTask加入临时容器;
  5. 将临时容器的任务加入固定容器(spring全生命周期存在)。

此处需要注意的是Schedule注解与调度任务的对应关系并非是1:1,可能是1:多,应用的过程中需要注意。

2.2 ScheduledTaskRegistrar

截止2.1已经完成了调度任务的采集、构造和存储(容器内存存储),后续的逻辑应该是任务的调度和执行。

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 class ScheduledTaskRegistrar implements ScheduledTaskHolder, InitializingBean, DisposableBean {

    @Nullable

    private TaskScheduler taskScheduler;

    @Nullable

    private ScheduledExecutorService localExecutor;

    @Nullable

    private List<TriggerTask> triggerTasks;

    @Nullable

    private List<CronTask> cronTasks;

    @Nullable

    private List<IntervalTask> fixedRateTasks;

    @Nullable

    private List<IntervalTask> fixedDelayTasks;

    private final Map<Task, ScheduledTask> unresolvedTasks = new HashMap<>(16);

    private final Set<ScheduledTask> scheduledTasks = new LinkedHashSet<>(16);

}

ScheduledTaskRegistrar实现了InitializingBean,Spring生命周期的hook之一。

public void afterPropertiesSet() {

    scheduleTasks();

}

/**

 * Schedule all registered tasks against the underlying

 * {@linkplain #setTaskScheduler(TaskScheduler) task scheduler}.

 */

@SuppressWarnings("deprecation")

protected 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));

        }

    }

}

核心逻辑如上,其中taskScheduler的具体bean对象的创建通常是spring boot框架auto-configure注入的ThreadPoolTaskScheduler(视具体情况而定)。

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

public class ThreadPoolTaskScheduler extends ExecutorConfigurationSupport

        implements AsyncListenableTaskExecutor, SchedulingTaskExecutor, TaskScheduler {

    private volatile int poolSize = 1;

    @Nullable

    private ScheduledExecutorService scheduledExecutor;

     

    @Override

    @Nullable

    public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) {

        ScheduledExecutorService executor = getScheduledExecutor();

        try {

            ErrorHandler errorHandler = this.errorHandler;

            if (errorHandler == null) {

                errorHandler = TaskUtils.getDefaultErrorHandler(true);

            }

            return new ReschedulingRunnable(task, trigger, executor, errorHandler).schedule();

        }

        catch (RejectedExecutionException ex) {

            throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex);

        }

    }

}

ReschedulingRunnable作为一个重要的设计点,实现了任务的可重复调度(扩展性的支持Trigger)。

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

27

28

29

30

31

32

33

34

35

class ReschedulingRunnable extends DelegatingErrorHandlingRunnable implements ScheduledFuture<Object> {

    private final Trigger trigger;

    private final SimpleTriggerContext triggerContext = new SimpleTriggerContext();

    private final ScheduledExecutorService executor;

    @Nullable

    public ScheduledFuture<?> schedule() {

        synchronized (this.triggerContextMonitor) {

            this.scheduledExecutionTime = this.trigger.nextExecutionTime(this.triggerContext);

            if (this.scheduledExecutionTime == null) {

                return null;

            }

            long initialDelay = this.scheduledExecutionTime.getTime() - System.currentTimeMillis();

            this.currentFuture = this.executor.schedule(this, initialDelay, TimeUnit.MILLISECONDS);

            return this;

        }

    }

    @Override

    public void run() {

        Date actualExecutionTime = new Date();

        super.run();

        Date completionTime = new Date();

        synchronized (this.triggerContextMonitor) {

            Assert.state(this.scheduledExecutionTime != null"No scheduled execution");

            this.triggerContext.update(this.scheduledExecutionTime, actualExecutionTime, completionTime);

            if (!obtainCurrentFuture().isCancelled()) {

                schedule();

            }

        }

    }

}

ScheduledExecutorService属性最终选择java.util.concurrent.ScheduledThreadPoolExecutor作为最终的执行工具类,而其schedule方法本身是一个one-shot调度方法,如何支持cron的周期性调度?关键在于command对象传递为this对象(ReschedulingRunnable),该类的run方法就是cron周期性执行的核心。

基于xxl-job改造,支持1.6jdk。改分布式任务调度特性如下: 1、简单:支持通过Web页面对任务进行CRUD操作,操作简单,一分钟上手; 2、动态:支持动态修改任务状态、暂停/恢复任务,以及终止运行中任务,即时生效; 3、调度中心HA(中心式):调度采用中心式设计,“调度中心”基于集群Quartz实现,可保证调度中心HA; 4、执行器HA(分布式):任务分布式执行,任务"执行器"支持集群部署,可保证任务执行HA; 5、任务Failover:执行器集群部署时,任务路由策略选择"故障转移"情况下调度失败时将会平滑切换执行器进行Failover; 6、一致性:“调度中心”通过DB锁保证集群分布式调度的一致性, 一次任务调度只会触发一次执行; 7、自定义任务参数:支持在线配置调度任务入参,即时生效; 8、调度线程池:调度系统多线程触发调度运行,确保调度精确执行,不被堵塞; 9、弹性扩容缩容:一旦有新执行器机器上线或者下线,下次调度时将会重新分配任务; 10、邮件报警:任务失败时支持邮件报警,支持配置多邮件地址群发报警邮件; 11、状态监控:支持实时监控任务进度; 12、Rolling执行日志:支持在线查看调度结果,并且支持以Rolling方式实时查看执行器输出的完整的执行日志; 13、GLUE:提供Web IDE,支持在线开发任务逻辑代码,动态发布,实时编译生效,省略部署上线的过程。支持30个版本的历史版本回溯。 14、数据加密:调度中心和执行器之间的通讯进行数据加密,提升调度信息安全性; 15、任务依赖:支持配置子任务依赖,当父任务执行结束且执行成功后将会主动触发一次子任务的执行, 多个子任务用逗号分隔;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值