Skip to content

Commit

Permalink
Exclude beans with scheduled methods from global lazy init
Browse files Browse the repository at this point in the history
This commit updates TaskSchedulingAutoConfiguration to contribute a
LazyInitializationExcludeFilter that processes beans that have
@scheduled methods. This lets them be contributed to the context so
that scheduled methods are invoked as expected.

Closes gh-25315
  • Loading branch information
snicoll committed Apr 19, 2021
1 parent aa9d0bc commit 54613c7
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.boot.autoconfigure.task;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;

import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.LazyInitializationExcludeFilter;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.annotation.Schedules;
import org.springframework.util.ClassUtils;

/**
* A {@link LazyInitializationExcludeFilter} that detects bean methods annotated with
* {@link Scheduled} or {@link Schedules}.
*
* @author Stephane Nicoll
*/
class ScheduledBeanLazyInitializationExcludeFilter implements LazyInitializationExcludeFilter {

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

ScheduledBeanLazyInitializationExcludeFilter() {
// Ignore AOP infrastructure such as scoped proxies.
this.nonAnnotatedClasses.add(AopInfrastructureBean.class);
this.nonAnnotatedClasses.add(TaskScheduler.class);
this.nonAnnotatedClasses.add(ScheduledExecutorService.class);
}

@Override
public boolean isExcluded(String beanName, BeanDefinition beanDefinition, Class<?> beanType) {
return hasScheduledTask(beanType);
}

private boolean hasScheduledTask(Class<?> type) {
Class<?> targetType = ClassUtils.getUserClass(type);
if (!this.nonAnnotatedClasses.contains(targetType)
&& AnnotationUtils.isCandidateClass(targetType, Arrays.asList(Scheduled.class, Schedules.class))) {
Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetType,
(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(targetType);
}
return !annotatedMethods.isEmpty();
}
return false;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.util.concurrent.ScheduledExecutorService;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.LazyInitializationExcludeFilter;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
Expand Down Expand Up @@ -54,6 +55,11 @@ public ThreadPoolTaskScheduler taskScheduler(TaskSchedulerBuilder builder) {
return builder.build();
}

@Bean
public static LazyInitializationExcludeFilter scheduledBeanLazyInitializationExcludeFilter() {
return new ScheduledBeanLazyInitializationExcludeFilter();
}

@Bean
@ConditionalOnMissingBean
public TaskSchedulerBuilder taskSchedulerBuilder(TaskSchedulingProperties properties,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.boot.autoconfigure.task;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.annotation.Schedules;

import static org.assertj.core.api.Assertions.assertThat;

/**
* Tests for {@link ScheduledBeanLazyInitializationExcludeFilter}.
*
* @author Stephane Nicoll
*/
class ScheduledBeanLazyInitializationExcludeFilterTests {

private final ScheduledBeanLazyInitializationExcludeFilter filter = new ScheduledBeanLazyInitializationExcludeFilter();

@Test
void beanWithScheduledMethodIsDetected() {
assertThat(isExcluded(TestBean.class)).isTrue();
}

@Test
void beanWithSchedulesMethodIsDetected() {
assertThat(isExcluded(AnotherTestBean.class)).isTrue();
}

@Test
void beanWithoutScheduledMethodIsDetected() {
assertThat(isExcluded(ScheduledBeanLazyInitializationExcludeFilterTests.class)).isFalse();
}

private boolean isExcluded(Class<?> type) {
return this.filter.isExcluded("test", new RootBeanDefinition(type), type);
}

private static class TestBean {

@Scheduled
void doStuff() {
}

}

private static class AnotherTestBean {

@Schedules({ @Scheduled(fixedRate = 5000), @Scheduled(fixedRate = 2500) })
void doStuff() {
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,20 @@

package org.springframework.boot.autoconfigure.task;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.awaitility.Awaitility;
import org.junit.jupiter.api.Test;

import org.springframework.boot.LazyInitializationBeanFactoryPostProcessor;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.task.TaskSchedulerCustomizer;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
Expand Down Expand Up @@ -121,6 +126,22 @@ void enableSchedulingWithConfigurerBacksOff() {
});
}

@Test
void enableSchedulingWithLazyInitializationInvokeScheduledMethods() {
List<String> threadNames = new ArrayList<>();
new ApplicationContextRunner()
.withInitializer((context) -> context
.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor()))
.withPropertyValues("spring.task.scheduling.thread-name-prefix=scheduling-test-")
.withBean(LazyTestBean.class, () -> new LazyTestBean(threadNames))
.withUserConfiguration(SchedulingConfiguration.class)
.withConfiguration(AutoConfigurations.of(TaskSchedulingAutoConfiguration.class)).run((context) -> {
// No lazy lookup.
Awaitility.waitAtMost(Duration.ofSeconds(3)).until(() -> !threadNames.isEmpty());
assertThat(threadNames).allMatch((name) -> name.contains("scheduling-test-"));
});
}

@Configuration(proxyBeanMethods = false)
@EnableScheduling
static class SchedulingConfiguration {
Expand Down Expand Up @@ -193,6 +214,21 @@ void accumulate() {

}

static class LazyTestBean {

private final List<String> threadNames;

LazyTestBean(List<String> threadNames) {
this.threadNames = threadNames;
}

@Scheduled(fixedRate = 2000)
void accumulate() {
this.threadNames.add(Thread.currentThread().getName());
}

}

static class TestTaskScheduler extends ThreadPoolTaskScheduler {

TestTaskScheduler() {
Expand Down

0 comments on commit 54613c7

Please sign in to comment.