/*
* Copyright 2002-2016 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
*
* http://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.scheduling.annotation;
import java.util.concurrent.atomic.AtomicInteger;
import org.aspectj.lang.annotation.Aspect;
import org.junit.Before;
import org.junit.Test;
import org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.stereotype.Repository;
import org.springframework.tests.Assume;
import org.springframework.tests.TestGroup;
import org.springframework.tests.transaction.CallCountingTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.Transactional;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.mockito.BDDMockito.*;
/**
* Integration tests cornering bug SPR-8651, which revealed that @Scheduled methods may
* not work well with beans that have already been proxied for other reasons such
* as @Transactional or @Async processing.
*
* @author Chris Beams
* @author Juergen Hoeller
* @since 3.1
*/
@SuppressWarnings("resource")
public class ScheduledAndTransactionalAnnotationIntegrationTests {
@Before
public void setUp() {
Assume.group(TestGroup.PERFORMANCE);
}
@Test
public void failsWhenJdkProxyAndScheduledMethodNotPresentOnInterface() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(Config.class, JdkProxyTxConfig.class, RepoConfigA.class);
try {
ctx.refresh();
fail("Should have thrown BeanCreationException");
}
catch (BeanCreationException ex) {
assertTrue(ex.getRootCause() instanceof IllegalStateException);
}
}
@Test
public void succeedsWhenSubclassProxyAndScheduledMethodNotPresentOnInterface() throws InterruptedException {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(Config.class, SubclassProxyTxConfig.class, RepoConfigA.class);
ctx.refresh();
Thread.sleep(100); // allow @Scheduled method to be called several times
MyRepository repository = ctx.getBean(MyRepository.class);
CallCountingTransactionManager txManager = ctx.getBean(CallCountingTransactionManager.class);
assertThat("repository is not a proxy", AopUtils.isCglibProxy(repository), equalTo(true));
assertThat("@Scheduled method never called", repository.getInvocationCount(), greaterThan(0));
assertThat("no transactions were committed", txManager.commits, greaterThan(0));
}
@Test
public void succeedsWhenJdkProxyAndScheduledMethodIsPresentOnInterface() throws InterruptedException {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(Config.class, JdkProxyTxConfig.class, RepoConfigB.class);
ctx.refresh();
Thread.sleep(100); // allow @Scheduled method to be called several times
MyRepositoryWithScheduledMethod repository = ctx.getBean(MyRepositoryWithScheduledMethod.class);
CallCountingTransactionManager txManager = ctx.getBean(CallCountingTransactionManager.class);
assertThat("repository is not a proxy", AopUtils.isJdkDynamicProxy(repository), is(true));
assertThat("@Scheduled method never called", repository.getInvocationCount(), greaterThan(0));
assertThat("no transactions were committed", txManager.commits, greaterThan(0));
}
@Test
public void withAspectConfig() throws InterruptedException {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(AspectConfig.class, MyRepositoryWithScheduledMethodImpl.class);
ctx.refresh();
Thread.sleep(100); // allow @Scheduled method to be called several times
MyRepositoryWithScheduledMethod repository = ctx.getBean(MyRepositoryWithScheduledMethod.class);
assertThat("repository is not a proxy", AopUtils.isCglibProxy(repository), is(true));
assertThat("@Scheduled method never called", repository.getInvocationCount(), greaterThan(0));
}
@Configuration
@EnableTransactionManagement
static class JdkProxyTxConfig {
}
@Configuration
@EnableTransactionManagement(proxyTargetClass = true)
static class SubclassProxyTxConfig {
}
@Configuration
static class RepoConfigA {
@Bean
public MyRepository repository() {
return new MyRepositoryImpl();
}
}
@Configuration
static class RepoConfigB {
@Bean
public MyRepositoryWithScheduledMethod repository() {
return new MyRepositoryWithScheduledMethodImpl();
}
}
@Configuration
@EnableScheduling
static class Config {
@Bean
public PlatformTransactionManager txManager() {
return new CallCountingTransactionManager();
}
@Bean
public PersistenceExceptionTranslator peTranslator() {
return mock(PersistenceExceptionTranslator.class);
}
@Bean
public PersistenceExceptionTranslationPostProcessor peTranslationPostProcessor() {
return new PersistenceExceptionTranslationPostProcessor();
}
}
@Configuration
@EnableScheduling
static class AspectConfig {
@Bean
public static AnnotationAwareAspectJAutoProxyCreator autoProxyCreator() {
AnnotationAwareAspectJAutoProxyCreator apc = new AnnotationAwareAspectJAutoProxyCreator();
apc.setProxyTargetClass(true);
return apc;
}
@Bean
public static MyAspect myAspect() {
return new MyAspect();
}
}
@Aspect
public static class MyAspect {
private final AtomicInteger count = new AtomicInteger(0);
@org.aspectj.lang.annotation.Before("execution(* scheduled())")
public void checkTransaction() {
this.count.incrementAndGet();
}
}
public interface MyRepository {
int getInvocationCount();
}
@Repository
static class MyRepositoryImpl implements MyRepository {
private final AtomicInteger count = new AtomicInteger(0);
@Transactional
@Scheduled(fixedDelay = 5)
public void scheduled() {
this.count.incrementAndGet();
}
@Override
public int getInvocationCount() {
return this.count.get();
}
}
public interface MyRepositoryWithScheduledMethod {
int getInvocationCount();
void scheduled();
}
@Repository
static class MyRepositoryWithScheduledMethodImpl implements MyRepositoryWithScheduledMethod {
private final AtomicInteger count = new AtomicInteger(0);
@Autowired(required = false)
private MyAspect myAspect;
@Override
@Transactional
@Scheduled(fixedDelay = 5)
public void scheduled() {
this.count.incrementAndGet();
}
@Override
public int getInvocationCount() {
if (this.myAspect != null) {
assertEquals(this.count.get(), this.myAspect.count.get());
}
return this.count.get();
}
}
}