From 54613c77d45f4f0a951923a6a6f048dc1f8f91ef Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 19 Apr 2021 14:03:31 +0200 Subject: [PATCH] Exclude beans with scheduled methods from global lazy init 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 --- ...edBeanLazyInitializationExcludeFilter.java | 78 +++++++++++++++++++ .../task/TaskSchedulingAutoConfiguration.java | 6 ++ ...nLazyInitializationExcludeFilterTests.java | 71 +++++++++++++++++ .../TaskSchedulingAutoConfigurationTests.java | 36 +++++++++ 4 files changed, 191 insertions(+) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/ScheduledBeanLazyInitializationExcludeFilter.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/ScheduledBeanLazyInitializationExcludeFilterTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/ScheduledBeanLazyInitializationExcludeFilter.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/ScheduledBeanLazyInitializationExcludeFilter.java new file mode 100644 index 0000000000..644a4bcb2b --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/ScheduledBeanLazyInitializationExcludeFilter.java @@ -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> 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> annotatedMethods = MethodIntrospector.selectMethods(targetType, + (MethodIntrospector.MetadataLookup>) (method) -> { + Set 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; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfiguration.java index 4282aac7c5..38ce98ce6b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfiguration.java @@ -19,6 +19,7 @@ package org.springframework.boot.autoconfigure.task; 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; @@ -54,6 +55,11 @@ public class TaskSchedulingAutoConfiguration { return builder.build(); } + @Bean + public static LazyInitializationExcludeFilter scheduledBeanLazyInitializationExcludeFilter() { + return new ScheduledBeanLazyInitializationExcludeFilter(); + } + @Bean @ConditionalOnMissingBean public TaskSchedulerBuilder taskSchedulerBuilder(TaskSchedulingProperties properties, diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/ScheduledBeanLazyInitializationExcludeFilterTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/ScheduledBeanLazyInitializationExcludeFilterTests.java new file mode 100644 index 0000000000..f53b864971 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/ScheduledBeanLazyInitializationExcludeFilterTests.java @@ -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() { + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java index 73f27a1f7a..5b2e79b727 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java @@ -16,6 +16,9 @@ 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; @@ -23,8 +26,10 @@ 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; @@ -121,6 +126,22 @@ class TaskSchedulingAutoConfigurationTests { }); } + @Test + void enableSchedulingWithLazyInitializationInvokeScheduledMethods() { + List 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 { @@ -193,6 +214,21 @@ class TaskSchedulingAutoConfigurationTests { } + static class LazyTestBean { + + private final List threadNames; + + LazyTestBean(List threadNames) { + this.threadNames = threadNames; + } + + @Scheduled(fixedRate = 2000) + void accumulate() { + this.threadNames.add(Thread.currentThread().getName()); + } + + } + static class TestTaskScheduler extends ThreadPoolTaskScheduler { TestTaskScheduler() {