Skip to content

Commit a861453

Browse files
committed
Support for determining a target scheduler for a specific task
Introduces "scheduler" attribute on @scheduled annotation. TaskSchedulerRouter delegates to qualified/default scheduler. ScheduledMethodRunnable exposes qualifier through SchedulingAwareRunnable. Closes gh-20818
1 parent f0fe58f commit a861453

File tree

6 files changed

+460
-109
lines changed

6 files changed

+460
-109
lines changed

spring-context/src/main/java/org/springframework/scheduling/SchedulingAwareRunnable.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.scheduling;
1818

19+
import org.springframework.lang.Nullable;
20+
1921
/**
2022
* Extension of the {@link Runnable} interface, adding special callbacks
2123
* for long-running operations.
@@ -38,7 +40,27 @@ public interface SchedulingAwareRunnable extends Runnable {
3840
* pool (if any) but rather be considered as long-running background thread.
3941
* <p>This should be considered a hint. Of course TaskExecutor implementations
4042
* are free to ignore this flag and the SchedulingAwareRunnable interface overall.
43+
* <p>The default implementation returns {@code false}, as of 6.1.
44+
*/
45+
default boolean isLongLived() {
46+
return false;
47+
}
48+
49+
/**
50+
* Return a qualifier associated with this Runnable.
51+
* <p>The default implementation returns {@code null}.
52+
* <p>May be used for custom purposes depending on the scheduler implementation.
53+
* {@link org.springframework.scheduling.config.TaskSchedulerRouter} introspects
54+
* this qualifier in order to determine the target scheduler to be used
55+
* for a given Runnable, matching the qualifier value (or the bean name)
56+
* of a specific {@link org.springframework.scheduling.TaskScheduler} or
57+
* {@link java.util.concurrent.ScheduledExecutorService} bean definition.
58+
* @since 6.1
59+
* @see org.springframework.scheduling.annotation.Scheduled#scheduler()
4160
*/
42-
boolean isLongLived();
61+
@Nullable
62+
default String getQualifier() {
63+
return null;
64+
}
4365

4466
}

spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,4 +203,16 @@
203203
*/
204204
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
205205

206+
/**
207+
* A qualifier for determining a scheduler to run this scheduled method on.
208+
* <p>Defaults to an empty String, suggesting the default scheduler.
209+
* <p>May be used to determine the target scheduler to be used,
210+
* matching the qualifier value (or the bean name) of a specific
211+
* {@link org.springframework.scheduling.TaskScheduler} or
212+
* {@link java.util.concurrent.ScheduledExecutorService} bean definition.
213+
* @since 6.1
214+
* @see org.springframework.scheduling.SchedulingAwareRunnable#getQualifier()
215+
*/
216+
String scheduler() default "";
217+
206218
}

spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java

Lines changed: 31 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,8 @@
4343
import org.springframework.beans.factory.BeanNameAware;
4444
import org.springframework.beans.factory.DisposableBean;
4545
import org.springframework.beans.factory.ListableBeanFactory;
46-
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
47-
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
4846
import org.springframework.beans.factory.SmartInitializingSingleton;
49-
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
50-
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
5147
import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor;
52-
import org.springframework.beans.factory.config.NamedBeanHolder;
5348
import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor;
5449
import org.springframework.beans.factory.support.RootBeanDefinition;
5550
import org.springframework.context.ApplicationContext;
@@ -71,6 +66,7 @@
7166
import org.springframework.scheduling.config.ScheduledTask;
7267
import org.springframework.scheduling.config.ScheduledTaskHolder;
7368
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
69+
import org.springframework.scheduling.config.TaskSchedulerRouter;
7470
import org.springframework.scheduling.support.CronTrigger;
7571
import org.springframework.scheduling.support.ScheduledMethodRunnable;
7672
import org.springframework.util.Assert;
@@ -120,7 +116,7 @@ public class ScheduledAnnotationBeanPostProcessor
120116
* in case of multiple scheduler beans found in the context.
121117
* @since 4.2
122118
*/
123-
public static final String DEFAULT_TASK_SCHEDULER_BEAN_NAME = "taskScheduler";
119+
public static final String DEFAULT_TASK_SCHEDULER_BEAN_NAME = TaskSchedulerRouter.DEFAULT_TASK_SCHEDULER_BEAN_NAME;
124120

125121

126122
/**
@@ -254,6 +250,12 @@ private void finishRegistration() {
254250
if (this.scheduler != null) {
255251
this.registrar.setScheduler(this.scheduler);
256252
}
253+
else {
254+
TaskSchedulerRouter router = new TaskSchedulerRouter();
255+
router.setBeanName(this.beanName);
256+
router.setBeanFactory(this.beanFactory);
257+
this.registrar.setTaskScheduler(router);
258+
}
257259

258260
if (this.beanFactory instanceof ListableBeanFactory lbf) {
259261
Map<String, SchedulingConfigurer> beans = lbf.getBeansOfType(SchedulingConfigurer.class);
@@ -264,91 +266,9 @@ private void finishRegistration() {
264266
}
265267
}
266268

267-
if (this.registrar.hasTasks() && this.registrar.getScheduler() == null) {
268-
Assert.state(this.beanFactory != null, "BeanFactory must be set to find scheduler by type");
269-
try {
270-
// Search for TaskScheduler bean...
271-
this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, false));
272-
}
273-
catch (NoUniqueBeanDefinitionException ex) {
274-
if (logger.isTraceEnabled()) {
275-
logger.trace("Could not find unique TaskScheduler bean - attempting to resolve by name: " +
276-
ex.getMessage());
277-
}
278-
try {
279-
this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, true));
280-
}
281-
catch (NoSuchBeanDefinitionException ex2) {
282-
if (logger.isInfoEnabled()) {
283-
logger.info("More than one TaskScheduler bean exists within the context, and " +
284-
"none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' " +
285-
"(possibly as an alias); or implement the SchedulingConfigurer interface and call " +
286-
"ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback: " +
287-
ex.getBeanNamesFound());
288-
}
289-
}
290-
}
291-
catch (NoSuchBeanDefinitionException ex) {
292-
if (logger.isTraceEnabled()) {
293-
logger.trace("Could not find default TaskScheduler bean - attempting to find ScheduledExecutorService: " +
294-
ex.getMessage());
295-
}
296-
// Search for ScheduledExecutorService bean next...
297-
try {
298-
this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, false));
299-
}
300-
catch (NoUniqueBeanDefinitionException ex2) {
301-
if (logger.isTraceEnabled()) {
302-
logger.trace("Could not find unique ScheduledExecutorService bean - attempting to resolve by name: " +
303-
ex2.getMessage());
304-
}
305-
try {
306-
this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, true));
307-
}
308-
catch (NoSuchBeanDefinitionException ex3) {
309-
if (logger.isInfoEnabled()) {
310-
logger.info("More than one ScheduledExecutorService bean exists within the context, and " +
311-
"none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' " +
312-
"(possibly as an alias); or implement the SchedulingConfigurer interface and call " +
313-
"ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback: " +
314-
ex2.getBeanNamesFound());
315-
}
316-
}
317-
}
318-
catch (NoSuchBeanDefinitionException ex2) {
319-
if (logger.isTraceEnabled()) {
320-
logger.trace("Could not find default ScheduledExecutorService bean - falling back to default: " +
321-
ex2.getMessage());
322-
}
323-
// Giving up -> falling back to default scheduler within the registrar...
324-
logger.info("No TaskScheduler/ScheduledExecutorService bean found for scheduled processing");
325-
}
326-
}
327-
}
328-
329269
this.registrar.afterPropertiesSet();
330270
}
331271

332-
private <T> T resolveSchedulerBean(BeanFactory beanFactory, Class<T> schedulerType, boolean byName) {
333-
if (byName) {
334-
T scheduler = beanFactory.getBean(DEFAULT_TASK_SCHEDULER_BEAN_NAME, schedulerType);
335-
if (this.beanName != null && this.beanFactory instanceof ConfigurableBeanFactory cbf) {
336-
cbf.registerDependentBean(DEFAULT_TASK_SCHEDULER_BEAN_NAME, this.beanName);
337-
}
338-
return scheduler;
339-
}
340-
else if (beanFactory instanceof AutowireCapableBeanFactory acbf) {
341-
NamedBeanHolder<T> holder = acbf.resolveNamedBean(schedulerType);
342-
if (this.beanName != null && beanFactory instanceof ConfigurableBeanFactory cbf) {
343-
cbf.registerDependentBean(holder.getBeanName(), this.beanName);
344-
}
345-
return holder.getBeanInstance();
346-
}
347-
else {
348-
return beanFactory.getBean(schedulerType);
349-
}
350-
}
351-
352272

353273
@Override
354274
public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
@@ -424,12 +344,11 @@ protected void processScheduled(Scheduled scheduled, Method method, Object bean)
424344
* @param scheduled the {@code @Scheduled} annotation
425345
* @param method the method that the annotation has been declared on
426346
* @param bean the target bean instance
427-
* @see #createRunnable(Object, Method)
428347
*/
429348
private void processScheduledSync(Scheduled scheduled, Method method, Object bean) {
430349
Runnable task;
431350
try {
432-
task = createRunnable(bean, method);
351+
task = createRunnable(bean, method, scheduled.scheduler());
433352
}
434353
catch (IllegalArgumentException ex) {
435354
throw new IllegalStateException("Could not create recurring task for @Scheduled method '" +
@@ -606,13 +525,31 @@ private void processScheduledTask(Scheduled scheduled, Runnable runnable, Method
606525
* <p>The default implementation creates a {@link ScheduledMethodRunnable}.
607526
* @param target the target bean instance
608527
* @param method the scheduled method to call
609-
* @since 5.1
610-
* @see ScheduledMethodRunnable#ScheduledMethodRunnable(Object, Method)
528+
* @since 6.1
611529
*/
612-
protected Runnable createRunnable(Object target, Method method) {
530+
@SuppressWarnings("deprecation")
531+
protected Runnable createRunnable(Object target, Method method, @Nullable String qualifier) {
532+
Runnable runnable = createRunnable(target, method);
533+
if (runnable != null) {
534+
return runnable;
535+
}
613536
Assert.isTrue(method.getParameterCount() == 0, "Only no-arg methods may be annotated with @Scheduled");
614537
Method invocableMethod = AopUtils.selectInvocableMethod(method, target.getClass());
615-
return new ScheduledMethodRunnable(target, invocableMethod, this.registrar::getObservationRegistry);
538+
return new ScheduledMethodRunnable(target, invocableMethod, qualifier, this.registrar::getObservationRegistry);
539+
}
540+
541+
/**
542+
* Create a {@link Runnable} for the given bean instance,
543+
* calling the specified scheduled method.
544+
* @param target the target bean instance
545+
* @param method the scheduled method to call
546+
* @since 5.1
547+
* @deprecated in favor of {@link #createRunnable(Object, Method, String)}
548+
*/
549+
@Deprecated(since = "6.1")
550+
@Nullable
551+
protected Runnable createRunnable(Object target, Method method) {
552+
return null;
616553
}
617554

618555
private static Duration toDuration(long value, TimeUnit timeUnit) {

0 commit comments

Comments
 (0)