Through the previous article, we have basically sorted out the scheduled task system: Timer and ScheduledExecutorService are JDK's built-in scheduled task solutions, as well as the HashedWheelTimer implemented in Netty based on the time wheel, and then Quartz and distributed tasks (ElasticJob, xxl-job, etc.). For the simple application of Springboot, you can also use Spring's own task method. This article mainly introduces the case of Spring's own Task and its implementation method. @pdai

Implementation case

Spring Task is well packaged and very simple to use.

@EnableScheduling + @Scheduled

  • Enable scheduled tasks through @EnableScheduling, @Scheduled defines tasks
 @EnableScheduling
@Configuration
public class ScheduleDemo {

    /**
     * 每隔1分钟执行一次。
     */
    @Scheduled(fixedRate = 1000 * 60 * 1)
    public void runScheduleFixedRate() {
        log.info("runScheduleFixedRate: current DateTime, {}", LocalDateTime.now());
    }

    /**
     * 每个整点小时执行一次。
     */
    @Scheduled(cron = "0 0 */1 * * ?")
    public void runScheduleCron() {
        log.info("runScheduleCron: current DateTime, {}", LocalDateTime.now());
    }

}
  • Parameters supported by @Scheduled :
  1. cron : cron expression, specifying the task to be executed at a specific time;
  2. fixedDelay : Indicates how long after the last task is executed again, the parameter type is long, and the unit is ms;
  3. fixedDelayString : Same meaning as fixedDelay, but the parameter type is changed to String;
  4. fixedRate : Indicates that the task is executed at a certain frequency, the parameter type is long, and the unit is ms;
  5. fixedRateString : the same as fixedRate, but change the parameter type to String;
  6. initialDelay : Indicates how long to delay before executing the task for the first time, the parameter type is long, and the unit is ms;
  7. initialDelayString : the same as initialDelay, but change the parameter type to String;
  8. zone : Time zone, the default is the current time zone, generally not used.

further understanding

Let's go through some questions to help you better understand how Spring Task is implemented. @pdai

What should I pay attention to when using Spring Task?

  • About exception handling

It is recommended to handle exceptions by yourself

  • About timeout handling

In actual development, this problem often occurs. For example, after a period of time, the scheduled task will no longer be executed. This situation will occur when, for example, you call a third-party interface without setting the call timeout, and then an exception is thrown. At this time The current task is blocked.

The principle of Spring Task?

The source code of Spring Task is here:

@EnableScheduling annotation

Adding @EnableScheduling annotation will automatically inject SchedulingConfiguration

 * @author Chris Beams
 * @author Juergen Hoeller
 * @since 3.1
 * @see Scheduled
 * @see SchedulingConfiguration
 * @see SchedulingConfigurer
 * @see ScheduledTaskRegistrar
 * @see Trigger
 * @see ScheduledAnnotationBeanPostProcessor
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class)
@Documented
public @interface EnableScheduling {

}

Initialize ScheduledAnnotationBeanPostProcessor in SchedulingConfiguration

Automatically initialize ScheduledAnnotationBeanPostProcessor in SchedulingConfiguration configuration

 @Configuration(proxyBeanMethods = false)
@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();
    }

}

What is BeanPostProcessor? We have a detailed explanation in the previous article, specifically look at the Spring Core Inversion of Control (IOC) source code analysis

Lifecycle flow of beans in a Spring container

ScheduledTaskRegistrar register task

Initialized ScheduledTaskRegistrar in ScheduledAnnotationBeanPostProcessor constructor

 /**
    * Create a default {@code ScheduledAnnotationBeanPostProcessor}.
    */
public ScheduledAnnotationBeanPostProcessor() {
    this.registrar = new ScheduledTaskRegistrar();
}

The main thing about ScheduledTaskRegistrar is to register various types of tasks (this method has been deprecated in the new version)

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

Which Tasks are registered, and how are the classes designed?

ScheduledAnnotationBeanPostProcessor loads Scheduled annotations

Load Scheduled annotation in postProcessAfterInitialization phase of BeanPostProcessor

 @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> scheduledAnnotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(
                            method, Scheduled.class, Schedules.class);
                    return (!scheduledAnnotations.isEmpty() ? scheduledAnnotations : 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, scheduledAnnotations) ->
                    scheduledAnnotations.forEach(scheduled -> processScheduled(scheduled, method, bean)));
            if (logger.isTraceEnabled()) {
                logger.trace(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName +
                        "': " + annotatedMethods);
            }
        }
    }
    return bean;
}

The Scheduled annotation is added to the method level as follows

 @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {

    /**
     * A special cron expression value that indicates a disabled trigger: {@value}.
     * <p>This is primarily meant for use with <code>${...}</code> placeholders,
     * allowing for external disabling of corresponding scheduled methods.
     * @since 5.1
     * @see ScheduledTaskRegistrar#CRON_DISABLED
     */
    String CRON_DISABLED = ScheduledTaskRegistrar.CRON_DISABLED;


    /**
     * A cron-like expression, extending the usual UN*X definition to include triggers
     * on the second, minute, hour, day of month, month, and day of week.
     * <p>For example, {@code "0 * * * * MON-FRI"} means once per minute on weekdays
     * (at the top of the minute - the 0th second).
     * <p>The fields read from left to right are interpreted as follows.
     * <ul>
     * <li>second</li>
     * <li>minute</li>
     * <li>hour</li>
     * <li>day of month</li>
     * <li>month</li>
     * <li>day of week</li>
     * </ul>
     * <p>The special value {@link #CRON_DISABLED "-"} indicates a disabled cron
     * trigger, primarily meant for externally specified values resolved by a
     * <code>${...}</code> placeholder.
     * @return an expression that can be parsed to a cron schedule
     * @see org.springframework.scheduling.support.CronExpression#parse(String)
     */
    String cron() default "";

    /**
     * A time zone for which the cron expression will be resolved. By default, this
     * attribute is the empty String (i.e. the server's local time zone will be used).
     * @return a zone id accepted by {@link java.util.TimeZone#getTimeZone(String)},
     * or an empty String to indicate the server's default time zone
     * @since 4.0
     * @see org.springframework.scheduling.support.CronTrigger#CronTrigger(String, java.util.TimeZone)
     * @see java.util.TimeZone
     */
    String zone() default "";

    /**
     * Execute the annotated method with a fixed period between the end of the
     * last invocation and the start of the next.
     * <p>The time unit is milliseconds by default but can be overridden via
     * {@link #timeUnit}.
     * @return the delay
     */
    long fixedDelay() default -1;

    /**
     * Execute the annotated method with a fixed period between the end of the
     * last invocation and the start of the next.
     * <p>The time unit is milliseconds by default but can be overridden via
     * {@link #timeUnit}.
     * @return the delay as a String value &mdash; for example, a placeholder
     * or a {@link java.time.Duration#parse java.time.Duration} compliant value
     * @since 3.2.2
     */
    String fixedDelayString() default "";

    /**
     * Execute the annotated method with a fixed period between invocations.
     * <p>The time unit is milliseconds by default but can be overridden via
     * {@link #timeUnit}.
     * @return the period
     */
    long fixedRate() default -1;

    /**
     * Execute the annotated method with a fixed period between invocations.
     * <p>The time unit is milliseconds by default but can be overridden via
     * {@link #timeUnit}.
     * @return the period as a String value &mdash; for example, a placeholder
     * or a {@link java.time.Duration#parse java.time.Duration} compliant value
     * @since 3.2.2
     */
    String fixedRateString() default "";

    /**
     * Number of units of time to delay before the first execution of a
     * {@link #fixedRate} or {@link #fixedDelay} task.
     * <p>The time unit is milliseconds by default but can be overridden via
     * {@link #timeUnit}.
     * @return the initial
     * @since 3.2
     */
    long initialDelay() default -1;

    /**
     * Number of units of time to delay before the first execution of a
     * {@link #fixedRate} or {@link #fixedDelay} task.
     * <p>The time unit is milliseconds by default but can be overridden via
     * {@link #timeUnit}.
     * @return the initial delay as a String value &mdash; for example, a placeholder
     * or a {@link java.time.Duration#parse java.time.Duration} compliant value
     * @since 3.2.2
     */
    String initialDelayString() default "";

    /**
     * The {@link TimeUnit} to use for {@link #fixedDelay}, {@link #fixedDelayString},
     * {@link #fixedRate}, {@link #fixedRateString}, {@link #initialDelay}, and
     * {@link #initialDelayString}.
     * <p>Defaults to {@link TimeUnit#MICROSECONDS}.
     * <p>This attribute is ignored for {@linkplain #cron() cron expressions}
     * and for {@link java.time.Duration} values supplied via {@link #fixedDelayString},
     * {@link #fixedRateString}, or {@link #initialDelayString}.
     * @return the {@code TimeUnit} to use
     * @since 5.3.10
     */
    TimeUnit timeUnit() default TimeUnit.MILLISECONDS;

}

Parameters supported by @Scheduled:

  1. cron : a cron expression that specifies the task to be executed at a specific time;
  2. fixedDelay : Indicates how long after the last task is executed again, the parameter type is long, and the unit is ms;
  3. fixedDelayString : The meaning is the same as fixedDelay, but the parameter type is changed to String;
  4. fixedRate : Indicates that the task is executed at a certain frequency, the parameter type is long, and the unit is ms;
  5. fixedRateString : Same meaning as fixedRate, but change the parameter type to String;
  6. initialDelay : Indicates how long to delay before executing the task for the first time, the parameter type is long, and the unit is ms;
  7. initialDelayString : the same as initialDelay, but change the parameter type to String;
  8. zone : Time zone, the default is the current time zone, generally not used.

Get the Scheduled annotation on the method (the definition of the task), and process the specific type of task through processScheduled

 /**
    * Process the given {@code @Scheduled} method declaration on the given bean.
    * @param scheduled the {@code @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 = convertToMillis(scheduled.initialDelay(), scheduled.timeUnit());
        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 = convertToMillis(initialDelayString, scheduled.timeUnit());
                }
                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 = convertToMillis(scheduled.fixedDelay(), scheduled.timeUnit());
        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 = convertToMillis(fixedDelayString, scheduled.timeUnit());
                }
                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 = convertToMillis(scheduled.fixedRate(), scheduled.timeUnit());
        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 = convertToMillis(fixedRateString, scheduled.timeUnit());
                }
                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());
    }
}

Parse tasks in ScheduledTaskRegistrar

Taking CronTask as an example, if taskScheduler is defined, it will be executed by taskScheduler, if not put in unresolvedTasks.

 /**
    * Schedule the specified cron task, either right away if possible
    * or on initialization of the scheduler.
    * @return a handle to the scheduled task, allowing to cancel it
    * (or {@code null} if processing a previously registered task)
    * @since 4.3
    */
@Nullable
public ScheduledTask scheduleCronTask(CronTask task) {
    ScheduledTask scheduledTask = this.unresolvedTasks.remove(task);
    boolean newTask = false;
    if (scheduledTask == null) {
        scheduledTask = new ScheduledTask(task);
        newTask = true;
    }
    if (this.taskScheduler != null) {
        scheduledTask.future = this.taskScheduler.schedule(task.getRunnable(), task.getTrigger());
    }
    else {
        addCronTask(task);
        this.unresolvedTasks.put(task, scheduledTask);
    }
    return (newTask ? scheduledTask : null);
}

TaskScheduler handles Task

The default is ConcurrentTaskScheduler, the processing method is as follows

 @Override
@Nullable
public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) {
    try {
        if (this.enterpriseConcurrentScheduler) {
            return new EnterpriseConcurrentTriggerScheduler().schedule(decorateTask(task, true), trigger);
        }
        else {
            ErrorHandler errorHandler =
                    (this.errorHandler != null ? this.errorHandler : TaskUtils.getDefaultErrorHandler(true));
            return new ReschedulingRunnable(task, trigger, this.clock, this.scheduledExecutor, errorHandler).schedule();
        }
    }
    catch (RejectedExecutionException ex) {
        throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex);
    }
}

EnterpriseConcurrentTriggerScheduler is the JSR-236 Trigger standard, and its processing method is as follows

 /**
    * Delegate that adapts a Spring Trigger to a JSR-236 Trigger.
    * Separated into an inner class in order to avoid a hard dependency on the JSR-236 API.
    */
private class EnterpriseConcurrentTriggerScheduler {

    public ScheduledFuture<?> schedule(Runnable task, final Trigger trigger) {
        ManagedScheduledExecutorService executor = (ManagedScheduledExecutorService) scheduledExecutor;
        return executor.schedule(task, new javax.enterprise.concurrent.Trigger() {
            @Override
            @Nullable
            public Date getNextRunTime(@Nullable LastExecution le, Date taskScheduledTime) {
                return (trigger.nextExecutionTime(le != null ?
                        new SimpleTriggerContext(le.getScheduledStart(), le.getRunStart(), le.getRunEnd()) :
                        new SimpleTriggerContext()));
            }
            @Override
            public boolean skipRun(LastExecution lastExecution, Date scheduledRunTime) {
                return false;
            }
        });
    }
}

If EnterpriseConcurrentTriggerScheduler is not used, ReschedulingRunnable is used, which is essentially handled by ScheduledExecutorService

 public ReschedulingRunnable(Runnable delegate, Trigger trigger, Clock clock,
            ScheduledExecutorService executor, ErrorHandler errorHandler) {

    super(delegate, errorHandler);
    this.trigger = trigger;
    this.triggerContext = new SimpleTriggerContext(clock);
    this.executor = 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() - this.triggerContext.getClock().millis();
        this.currentFuture = this.executor.schedule(this, initialDelay, TimeUnit.MILLISECONDS);
        return this;
    }
}

Sample source code

https://github.com/realpdai/tech-pdai-spring-demos

more content

Say goodbye to fragmented learning, one-stop systematic learning without routines Back-end development: Java full stack knowledge system https://pdai.tech


pdai
67 声望158 粉丝