/*
* Copyright 2002-2015 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.aspectj;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import org.junit.Before;
import org.junit.Test;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.tests.Assume;
import org.springframework.tests.TestGroup;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.concurrent.ListenableFuture;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.*;
/**
* Unit tests for {@link AnnotationAsyncExecutionAspect}.
*
* @author Ramnivas Laddad
* @author Stephane Nicoll
*/
public class AnnotationAsyncExecutionAspectTests {
private static final long WAIT_TIME = 1000; //milliseconds
private final AsyncUncaughtExceptionHandler defaultExceptionHandler = new SimpleAsyncUncaughtExceptionHandler();
private CountingExecutor executor;
@Before
public void setUp() {
Assume.group(TestGroup.PERFORMANCE);
executor = new CountingExecutor();
AnnotationAsyncExecutionAspect.aspectOf().setExecutor(executor);
}
@Test
public void asyncMethodGetsRoutedAsynchronously() {
ClassWithoutAsyncAnnotation obj = new ClassWithoutAsyncAnnotation();
obj.incrementAsync();
executor.waitForCompletion();
assertEquals(1, obj.counter);
assertEquals(1, executor.submitStartCounter);
assertEquals(1, executor.submitCompleteCounter);
}
@Test
public void asyncMethodReturningFutureGetsRoutedAsynchronouslyAndReturnsAFuture() throws InterruptedException, ExecutionException {
ClassWithoutAsyncAnnotation obj = new ClassWithoutAsyncAnnotation();
Future<Integer> future = obj.incrementReturningAFuture();
// No need to executor.waitForCompletion() as future.get() will have the same effect
assertEquals(5, future.get().intValue());
assertEquals(1, obj.counter);
assertEquals(1, executor.submitStartCounter);
assertEquals(1, executor.submitCompleteCounter);
}
@Test
public void syncMethodGetsRoutedSynchronously() {
ClassWithoutAsyncAnnotation obj = new ClassWithoutAsyncAnnotation();
obj.increment();
assertEquals(1, obj.counter);
assertEquals(0, executor.submitStartCounter);
assertEquals(0, executor.submitCompleteCounter);
}
@Test
public void voidMethodInAsyncClassGetsRoutedAsynchronously() {
Assume.group(TestGroup.PERFORMANCE);
ClassWithAsyncAnnotation obj = new ClassWithAsyncAnnotation();
obj.increment();
executor.waitForCompletion();
assertEquals(1, obj.counter);
assertEquals(1, executor.submitStartCounter);
assertEquals(1, executor.submitCompleteCounter);
}
@Test
public void methodReturningFutureInAsyncClassGetsRoutedAsynchronouslyAndReturnsAFuture() throws InterruptedException, ExecutionException {
ClassWithAsyncAnnotation obj = new ClassWithAsyncAnnotation();
Future<Integer> future = obj.incrementReturningAFuture();
assertEquals(5, future.get().intValue());
assertEquals(1, obj.counter);
assertEquals(1, executor.submitStartCounter);
assertEquals(1, executor.submitCompleteCounter);
}
/*
@Test
public void methodReturningNonVoidNonFutureInAsyncClassGetsRoutedSynchronously() {
ClassWithAsyncAnnotation obj = new ClassWithAsyncAnnotation();
int returnValue = obj.return5();
assertEquals(5, returnValue);
assertEquals(0, executor.submitStartCounter);
assertEquals(0, executor.submitCompleteCounter);
}
*/
@Test
public void qualifiedAsyncMethodsAreRoutedToCorrectExecutor() throws InterruptedException, ExecutionException {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerBeanDefinition("e1", new RootBeanDefinition(ThreadPoolTaskExecutor.class));
AnnotationAsyncExecutionAspect.aspectOf().setBeanFactory(beanFactory);
ClassWithQualifiedAsyncMethods obj = new ClassWithQualifiedAsyncMethods();
Future<Thread> defaultThread = obj.defaultWork();
assertThat(defaultThread.get(), not(Thread.currentThread()));
assertThat(defaultThread.get().getName(), not(startsWith("e1-")));
ListenableFuture<Thread> e1Thread = obj.e1Work();
assertThat(e1Thread.get().getName(), startsWith("e1-"));
CompletableFuture<Thread> e1OtherThread = obj.e1OtherWork();
assertThat(e1OtherThread.get().getName(), startsWith("e1-"));
}
@Test
public void exceptionHandlerCalled() {
Method m = ReflectionUtils.findMethod(ClassWithException.class, "failWithVoid");
TestableAsyncUncaughtExceptionHandler exceptionHandler = new TestableAsyncUncaughtExceptionHandler();
AnnotationAsyncExecutionAspect.aspectOf().setExceptionHandler(exceptionHandler);
try {
assertFalse("Handler should not have been called", exceptionHandler.isCalled());
ClassWithException obj = new ClassWithException();
obj.failWithVoid();
exceptionHandler.await(3000);
exceptionHandler.assertCalledWith(m, UnsupportedOperationException.class);
}
finally {
AnnotationAsyncExecutionAspect.aspectOf().setExceptionHandler(defaultExceptionHandler);
}
}
@Test
public void exceptionHandlerNeverThrowsUnexpectedException() {
Method m = ReflectionUtils.findMethod(ClassWithException.class, "failWithVoid");
TestableAsyncUncaughtExceptionHandler exceptionHandler = new TestableAsyncUncaughtExceptionHandler(true);
AnnotationAsyncExecutionAspect.aspectOf().setExceptionHandler(exceptionHandler);
try {
assertFalse("Handler should not have been called", exceptionHandler.isCalled());
ClassWithException obj = new ClassWithException();
try {
obj.failWithVoid();
exceptionHandler.await(3000);
exceptionHandler.assertCalledWith(m, UnsupportedOperationException.class);
}
catch (Exception ex) {
fail("No unexpected exception should have been received but got " + ex.getMessage());
}
}
finally {
AnnotationAsyncExecutionAspect.aspectOf().setExceptionHandler(defaultExceptionHandler);
}
}
@SuppressWarnings("serial")
private static class CountingExecutor extends SimpleAsyncTaskExecutor {
int submitStartCounter;
int submitCompleteCounter;
@Override
public <T> Future<T> submit(Callable<T> task) {
submitStartCounter++;
Future<T> future = super.submit(task);
submitCompleteCounter++;
synchronized (this) {
notifyAll();
}
return future;
}
public synchronized void waitForCompletion() {
try {
wait(WAIT_TIME);
}
catch (InterruptedException ex) {
fail("Didn't finish the async job in " + WAIT_TIME + " milliseconds");
}
}
}
static class ClassWithoutAsyncAnnotation {
int counter;
@Async public void incrementAsync() {
counter++;
}
public void increment() {
counter++;
}
@Async public Future<Integer> incrementReturningAFuture() {
counter++;
return new AsyncResult<Integer>(5);
}
/**
* It should raise an error to attach @Async to a method that returns a non-void
* or non-Future. This method must remain commented-out, otherwise there will be a
* compile-time error. Uncomment to manually verify that the compiler produces an
* error message due to the 'declare error' statement in
* {@link AnnotationAsyncExecutionAspect}.
*/
// @Async public int getInt() {
// return 0;
// }
}
@Async
static class ClassWithAsyncAnnotation {
int counter;
public void increment() {
counter++;
}
// Manually check that there is a warning from the 'declare warning' statement in
// AnnotationAsyncExecutionAspect
/*
public int return5() {
return 5;
}
*/
public Future<Integer> incrementReturningAFuture() {
counter++;
return new AsyncResult<Integer>(5);
}
}
static class ClassWithQualifiedAsyncMethods {
@Async
public Future<Thread> defaultWork() {
return new AsyncResult<Thread>(Thread.currentThread());
}
@Async("e1")
public ListenableFuture<Thread> e1Work() {
return new AsyncResult<Thread>(Thread.currentThread());
}
@Async("e1")
public CompletableFuture<Thread> e1OtherWork() {
return CompletableFuture.completedFuture(Thread.currentThread());
}
}
static class ClassWithException {
@Async
public void failWithVoid() {
throw new UnsupportedOperationException("failWithVoid");
}
}
}