Skip to content

Commit edc62be

Browse files
committed
@scheduled reliably applies after other post-processors and shuts down before TaskScheduler
Issue: SPR-14692 Issue: SPR-15067
1 parent a30ceaf commit edc62be

File tree

5 files changed

+121
-25
lines changed

5 files changed

+121
-25
lines changed

spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -983,7 +983,7 @@ public <T> NamedBeanHolder<T> resolveNamedBean(Class<T> requiredType) throws Bea
983983
if (parent instanceof AutowireCapableBeanFactory) {
984984
return ((AutowireCapableBeanFactory) parent).resolveNamedBean(requiredType);
985985
}
986-
return null;
986+
throw new NoSuchBeanDefinitionException(requiredType);
987987
}
988988

989989
@SuppressWarnings("unchecked")

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

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,18 @@
3333
import org.springframework.aop.support.AopUtils;
3434
import org.springframework.beans.factory.BeanFactory;
3535
import org.springframework.beans.factory.BeanFactoryAware;
36+
import org.springframework.beans.factory.BeanNameAware;
3637
import org.springframework.beans.factory.DisposableBean;
3738
import org.springframework.beans.factory.ListableBeanFactory;
3839
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
3940
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
4041
import org.springframework.beans.factory.SmartInitializingSingleton;
42+
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
43+
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
4144
import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor;
45+
import org.springframework.beans.factory.config.NamedBeanHolder;
46+
import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor;
47+
import org.springframework.beans.factory.support.RootBeanDefinition;
4248
import org.springframework.context.ApplicationContext;
4349
import org.springframework.context.ApplicationContextAware;
4450
import org.springframework.context.ApplicationListener;
@@ -85,8 +91,9 @@
8591
* @see org.springframework.scheduling.config.ScheduledTaskRegistrar
8692
* @see AsyncAnnotationBeanPostProcessor
8793
*/
88-
public class ScheduledAnnotationBeanPostProcessor implements DestructionAwareBeanPostProcessor,
89-
Ordered, EmbeddedValueResolverAware, BeanFactoryAware, ApplicationContextAware,
94+
public class ScheduledAnnotationBeanPostProcessor
95+
implements MergedBeanDefinitionPostProcessor, DestructionAwareBeanPostProcessor,
96+
Ordered, EmbeddedValueResolverAware, BeanNameAware, BeanFactoryAware, ApplicationContextAware,
9097
SmartInitializingSingleton, ApplicationListener<ContextRefreshedEvent>, DisposableBean {
9198

9299
/**
@@ -104,6 +111,8 @@ public class ScheduledAnnotationBeanPostProcessor implements DestructionAwareBea
104111

105112
private StringValueResolver embeddedValueResolver;
106113

114+
private String beanName;
115+
107116
private BeanFactory beanFactory;
108117

109118
private ApplicationContext applicationContext;
@@ -140,6 +149,11 @@ public void setEmbeddedValueResolver(StringValueResolver resolver) {
140149
this.embeddedValueResolver = resolver;
141150
}
142151

152+
@Override
153+
public void setBeanName(String beanName) {
154+
this.beanName = beanName;
155+
}
156+
143157
/**
144158
* Making a {@link BeanFactory} available is optional; if not set,
145159
* {@link SchedulingConfigurer} beans won't get autodetected and
@@ -199,12 +213,11 @@ private void finishRegistration() {
199213
Assert.state(this.beanFactory != null, "BeanFactory must be set to find scheduler by type");
200214
try {
201215
// Search for TaskScheduler bean...
202-
this.registrar.setTaskScheduler(this.beanFactory.getBean(TaskScheduler.class));
216+
this.registrar.setTaskScheduler(resolveSchedulerBean(TaskScheduler.class, false));
203217
}
204218
catch (NoUniqueBeanDefinitionException ex) {
205219
try {
206-
this.registrar.setTaskScheduler(
207-
this.beanFactory.getBean(DEFAULT_TASK_SCHEDULER_BEAN_NAME, TaskScheduler.class));
220+
this.registrar.setTaskScheduler(resolveSchedulerBean(TaskScheduler.class, true));
208221
}
209222
catch (NoSuchBeanDefinitionException ex2) {
210223
if (logger.isInfoEnabled()) {
@@ -220,12 +233,11 @@ private void finishRegistration() {
220233
logger.debug("Could not find default TaskScheduler bean", ex);
221234
// Search for ScheduledExecutorService bean next...
222235
try {
223-
this.registrar.setScheduler(this.beanFactory.getBean(ScheduledExecutorService.class));
236+
this.registrar.setScheduler(resolveSchedulerBean(ScheduledExecutorService.class, false));
224237
}
225238
catch (NoUniqueBeanDefinitionException ex2) {
226239
try {
227-
this.registrar.setScheduler(
228-
this.beanFactory.getBean(DEFAULT_TASK_SCHEDULER_BEAN_NAME, ScheduledExecutorService.class));
240+
this.registrar.setScheduler(resolveSchedulerBean(ScheduledExecutorService.class, true));
229241
}
230242
catch (NoSuchBeanDefinitionException ex3) {
231243
if (logger.isInfoEnabled()) {
@@ -248,6 +260,32 @@ private void finishRegistration() {
248260
this.registrar.afterPropertiesSet();
249261
}
250262

263+
private <T> T resolveSchedulerBean(Class<T> schedulerType, boolean byName) {
264+
if (byName) {
265+
T scheduler = this.beanFactory.getBean(DEFAULT_TASK_SCHEDULER_BEAN_NAME, schedulerType);
266+
if (this.beanFactory instanceof ConfigurableBeanFactory) {
267+
((ConfigurableBeanFactory) this.beanFactory).registerDependentBean(
268+
DEFAULT_TASK_SCHEDULER_BEAN_NAME, this.beanName);
269+
}
270+
return scheduler;
271+
}
272+
else if (this.beanFactory instanceof AutowireCapableBeanFactory) {
273+
NamedBeanHolder<T> holder = ((AutowireCapableBeanFactory) this.beanFactory).resolveNamedBean(schedulerType);
274+
if (this.beanFactory instanceof ConfigurableBeanFactory) {
275+
((ConfigurableBeanFactory) this.beanFactory).registerDependentBean(
276+
holder.getBeanName(), this.beanName);
277+
}
278+
return holder.getBeanInstance();
279+
}
280+
else {
281+
return this.beanFactory.getBean(schedulerType);
282+
}
283+
}
284+
285+
286+
@Override
287+
public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
288+
}
251289

252290
@Override
253291
public Object postProcessBeforeInitialization(Object bean, String beanName) {

spring-context/src/test/java/org/springframework/scheduling/annotation/EnableSchedulingTests.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.scheduling.annotation;
1818

19+
import java.util.Arrays;
1920
import java.util.Date;
2021
import java.util.concurrent.atomic.AtomicInteger;
2122

@@ -31,6 +32,7 @@
3132
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
3233
import org.springframework.scheduling.config.IntervalTask;
3334
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
35+
import org.springframework.scheduling.config.TaskManagementConfigUtils;
3436
import org.springframework.tests.Assume;
3537
import org.springframework.tests.TestGroup;
3638

@@ -86,6 +88,8 @@ public void withExplicitScheduler() throws InterruptedException {
8688
Thread.sleep(100);
8789
assertThat(ctx.getBean(AtomicInteger.class).get(), greaterThanOrEqualTo(10));
8890
assertThat(ctx.getBean(ExplicitSchedulerConfig.class).threadName, startsWith("explicitScheduler-"));
91+
assertTrue(Arrays.asList(ctx.getDefaultListableBeanFactory().getDependentBeans("myTaskScheduler")).contains(
92+
TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME));
8993
}
9094

9195
@Test
@@ -201,7 +205,7 @@ static class ExplicitSchedulerConfig {
201205
String threadName;
202206

203207
@Bean
204-
public TaskScheduler taskScheduler() {
208+
public TaskScheduler myTaskScheduler() {
205209
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
206210
scheduler.setThreadNamePrefix("explicitScheduler-");
207211
return scheduler;
@@ -275,10 +279,6 @@ public void task() {
275279
counter().incrementAndGet();
276280
}
277281

278-
public Object getScheduler() {
279-
return null;
280-
}
281-
282282
@Override
283283
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
284284
taskRegistrar.setScheduler(taskScheduler1());

spring-jms/src/main/java/org/springframework/jms/annotation/JmsListenerAnnotationBeanPostProcessor.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,10 @@
3434
import org.springframework.beans.factory.ListableBeanFactory;
3535
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
3636
import org.springframework.beans.factory.SmartInitializingSingleton;
37-
import org.springframework.beans.factory.config.BeanPostProcessor;
3837
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
3938
import org.springframework.beans.factory.config.EmbeddedValueResolver;
39+
import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor;
40+
import org.springframework.beans.factory.support.RootBeanDefinition;
4041
import org.springframework.core.MethodIntrospector;
4142
import org.springframework.core.Ordered;
4243
import org.springframework.core.annotation.AnnotatedElementUtils;
@@ -81,7 +82,7 @@
8182
* @see MethodJmsListenerEndpoint
8283
*/
8384
public class JmsListenerAnnotationBeanPostProcessor
84-
implements BeanPostProcessor, Ordered, BeanFactoryAware, SmartInitializingSingleton {
85+
implements MergedBeanDefinitionPostProcessor, Ordered, BeanFactoryAware, SmartInitializingSingleton {
8586

8687
/**
8788
* The bean name of the default {@link JmsListenerContainerFactory}.
@@ -192,6 +193,10 @@ public void afterSingletonsInstantiated() {
192193
}
193194

194195

196+
@Override
197+
public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
198+
}
199+
195200
@Override
196201
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
197202
return bean;

src/test/java/org/springframework/scheduling/annotation/ScheduledAndTransactionalAnnotationIntegrationTests.java

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@
1818

1919
import java.util.concurrent.atomic.AtomicInteger;
2020

21+
import org.aspectj.lang.annotation.Aspect;
2122
import org.junit.Before;
2223
import org.junit.Test;
2324

25+
import org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator;
2426
import org.springframework.aop.support.AopUtils;
2527
import org.springframework.beans.factory.BeanCreationException;
28+
import org.springframework.beans.factory.annotation.Autowired;
2629
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
2730
import org.springframework.context.annotation.Bean;
2831
import org.springframework.context.annotation.Configuration;
@@ -46,6 +49,7 @@
4649
* as @Transactional or @Async processing.
4750
*
4851
* @author Chris Beams
52+
* @author Juergen Hoeller
4953
* @since 3.1
5054
*/
5155
@SuppressWarnings("resource")
@@ -76,11 +80,11 @@ public void succeedsWhenSubclassProxyAndScheduledMethodNotPresentOnInterface() t
7680
ctx.register(Config.class, SubclassProxyTxConfig.class, RepoConfigA.class);
7781
ctx.refresh();
7882

79-
Thread.sleep(100); // allow @Scheduled method to be called several times
83+
Thread.sleep(100); // allow @Scheduled method to be called several times
8084

8185
MyRepository repository = ctx.getBean(MyRepository.class);
8286
CallCountingTransactionManager txManager = ctx.getBean(CallCountingTransactionManager.class);
83-
assertThat("repository is not a proxy", AopUtils.isAopProxy(repository), equalTo(true));
87+
assertThat("repository is not a proxy", AopUtils.isCglibProxy(repository), equalTo(true));
8488
assertThat("@Scheduled method never called", repository.getInvocationCount(), greaterThan(0));
8589
assertThat("no transactions were committed", txManager.commits, greaterThan(0));
8690
}
@@ -91,15 +95,28 @@ public void succeedsWhenJdkProxyAndScheduledMethodIsPresentOnInterface() throws
9195
ctx.register(Config.class, JdkProxyTxConfig.class, RepoConfigB.class);
9296
ctx.refresh();
9397

94-
Thread.sleep(50); // allow @Scheduled method to be called several times
98+
Thread.sleep(100); // allow @Scheduled method to be called several times
9599

96100
MyRepositoryWithScheduledMethod repository = ctx.getBean(MyRepositoryWithScheduledMethod.class);
97101
CallCountingTransactionManager txManager = ctx.getBean(CallCountingTransactionManager.class);
98-
assertThat("repository is not a proxy", AopUtils.isAopProxy(repository), is(true));
102+
assertThat("repository is not a proxy", AopUtils.isJdkDynamicProxy(repository), is(true));
99103
assertThat("@Scheduled method never called", repository.getInvocationCount(), greaterThan(0));
100104
assertThat("no transactions were committed", txManager.commits, greaterThan(0));
101105
}
102106

107+
@Test
108+
public void withAspectConfig() throws InterruptedException {
109+
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
110+
ctx.register(AspectConfig.class, MyRepositoryWithScheduledMethodImpl.class);
111+
ctx.refresh();
112+
113+
Thread.sleep(100); // allow @Scheduled method to be called several times
114+
115+
MyRepositoryWithScheduledMethod repository = ctx.getBean(MyRepositoryWithScheduledMethod.class);
116+
assertThat("repository is not a proxy", AopUtils.isCglibProxy(repository), is(true));
117+
assertThat("@Scheduled method never called", repository.getInvocationCount(), greaterThan(0));
118+
}
119+
103120

104121
@Configuration
105122
@EnableTransactionManagement
@@ -108,7 +125,7 @@ static class JdkProxyTxConfig {
108125

109126

110127
@Configuration
111-
@EnableTransactionManagement(proxyTargetClass=true)
128+
@EnableTransactionManagement(proxyTargetClass = true)
112129
static class SubclassProxyTxConfig {
113130
}
114131

@@ -137,19 +154,49 @@ public MyRepositoryWithScheduledMethod repository() {
137154
@EnableScheduling
138155
static class Config {
139156

157+
@Bean
158+
public PlatformTransactionManager txManager() {
159+
return new CallCountingTransactionManager();
160+
}
161+
162+
@Bean
163+
public PersistenceExceptionTranslator peTranslator() {
164+
return mock(PersistenceExceptionTranslator.class);
165+
}
166+
140167
@Bean
141168
public PersistenceExceptionTranslationPostProcessor peTranslationPostProcessor() {
142169
return new PersistenceExceptionTranslationPostProcessor();
143170
}
171+
}
172+
173+
174+
@Configuration
175+
@EnableScheduling
176+
static class AspectConfig {
144177

145178
@Bean
146-
public PlatformTransactionManager txManager() {
147-
return new CallCountingTransactionManager();
179+
public static AnnotationAwareAspectJAutoProxyCreator autoProxyCreator() {
180+
AnnotationAwareAspectJAutoProxyCreator apc = new AnnotationAwareAspectJAutoProxyCreator();
181+
apc.setProxyTargetClass(true);
182+
return apc;
148183
}
149184

150185
@Bean
151-
public PersistenceExceptionTranslator peTranslator() {
152-
return mock(PersistenceExceptionTranslator.class);
186+
public static MyAspect myAspect() {
187+
return new MyAspect();
188+
}
189+
}
190+
191+
192+
@Aspect
193+
public static class MyAspect {
194+
195+
private final AtomicInteger count = new AtomicInteger(0);
196+
197+
@org.aspectj.lang.annotation.Before("execution(* scheduled())")
198+
public void checkTransaction() {
199+
this.count.incrementAndGet();
153200
}
154201
}
155202

@@ -191,6 +238,9 @@ static class MyRepositoryWithScheduledMethodImpl implements MyRepositoryWithSche
191238

192239
private final AtomicInteger count = new AtomicInteger(0);
193240

241+
@Autowired(required = false)
242+
private MyAspect myAspect;
243+
194244
@Override
195245
@Transactional
196246
@Scheduled(fixedDelay = 5)
@@ -200,6 +250,9 @@ public void scheduled() {
200250

201251
@Override
202252
public int getInvocationCount() {
253+
if (this.myAspect != null) {
254+
assertEquals(this.count.get(), this.myAspect.count.get());
255+
}
203256
return this.count.get();
204257
}
205258
}

0 commit comments

Comments
 (0)