From 00984781af674438bab1759475836106771b7629 Mon Sep 17 00:00:00 2001 From: Ramnivas Laddad Date: Wed, 6 Oct 2010 20:13:22 +0000 Subject: [PATCH] SPR-7369: @Async support in spring-aspects with AspectJ --- .../AbstractAsynchronousExecutionAspect.aj | 58 +++++++ ...tationDrivenAsynchronousExecutionAspect.aj | 35 ++++ ...DrivenAsynchronousExecutionAspectTest.java | 161 ++++++++++++++++++ 3 files changed, 254 insertions(+) create mode 100644 org.springframework.aspects/src/main/java/org/springframework/scheduling/AbstractAsynchronousExecutionAspect.aj create mode 100644 org.springframework.aspects/src/main/java/org/springframework/scheduling/AnnotationDrivenAsynchronousExecutionAspect.aj create mode 100644 org.springframework.aspects/src/test/java/org/springframework/scheduling/AnnotationDrivenAsynchronousExecutionAspectTest.java diff --git a/org.springframework.aspects/src/main/java/org/springframework/scheduling/AbstractAsynchronousExecutionAspect.aj b/org.springframework.aspects/src/main/java/org/springframework/scheduling/AbstractAsynchronousExecutionAspect.aj new file mode 100644 index 0000000000..0701de71ca --- /dev/null +++ b/org.springframework.aspects/src/main/java/org/springframework/scheduling/AbstractAsynchronousExecutionAspect.aj @@ -0,0 +1,58 @@ +package org.springframework.scheduling; + +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.concurrent.Future; + +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.core.task.support.TaskExecutorAdapter; + +/** + * Abstract aspect that routes selected methods asynchronously. + *

+ * This aspect, by default, uses {@link SimpleAsyncTaskExecutor} to route method + * execution. However, you may inject it with any implementation of + * {@link Executor} to override the default. + * + * @author Ramnivas Laddad + */ +public abstract aspect AbstractAsynchronousExecutionAspect { + private AsyncTaskExecutor asyncExecutor; + + public AbstractAsynchronousExecutionAspect() { + // Set default executor, which may be replaced by calling setExecutor(Executor) + setExecutor(new SimpleAsyncTaskExecutor()); + } + + public abstract pointcut asyncMethod(); + + Object around() : asyncMethod() { + Callable callable = new Callable() { + public Object call() throws Exception { + Object result = proceed(); + if (result instanceof Future) { + return ((Future) result).get(); + } + return null; + }}; + + Future result = asyncExecutor.submit(callable); + + if (Future.class.isAssignableFrom(((MethodSignature)thisJoinPointStaticPart.getSignature()).getReturnType())) { + return result; + } else { + return null; + } + } + + public void setExecutor(Executor executor) { + if (executor instanceof AsyncTaskExecutor) { + this.asyncExecutor = (AsyncTaskExecutor) executor; + } else { + this.asyncExecutor = new TaskExecutorAdapter(asyncExecutor); + } + } + +} diff --git a/org.springframework.aspects/src/main/java/org/springframework/scheduling/AnnotationDrivenAsynchronousExecutionAspect.aj b/org.springframework.aspects/src/main/java/org/springframework/scheduling/AnnotationDrivenAsynchronousExecutionAspect.aj new file mode 100644 index 0000000000..4da2cd526e --- /dev/null +++ b/org.springframework.aspects/src/main/java/org/springframework/scheduling/AnnotationDrivenAsynchronousExecutionAspect.aj @@ -0,0 +1,35 @@ +package org.springframework.scheduling; + +import java.util.concurrent.Future; +import org.springframework.scheduling.annotation.Async; + +/** + * Aspect to route methods based on the {@link Async} annotation. + *

+ * This aspect routes methods marked with the {@link Async} annotation + * as well as methods in classes marked with the same. Any method expected + * to be routed asynchronously must return either void, {@link Future}, + * or a subtype of {@link Future}. This aspect, therefore, will produce + * a compile-time error for methods that violate this constraint on the return type. + * If, however, a class marked with @Async contains a method that + * violates this constraint, it produces only a warning. + * + * @author Ramnivas Laddad + * + */ +public aspect AnnotationDrivenAsynchronousExecutionAspect extends AbstractAsynchronousExecutionAspect { + private pointcut asyncMarkedMethod() + : execution(@Async (void || Future+) *(..)); + private pointcut asyncTypeMarkedMethod() + : execution((void || Future+) (@Async *).*(..)); + + public pointcut asyncMethod() : asyncMarkedMethod() || asyncTypeMarkedMethod(); + + declare error: + execution(@Async !(void||Future) *(..)): + "Only method that return void or Future may have @Async annotation"; + + declare warning: + execution(!(void||Future) (@Async *).*(..)): + "Method in class marked with @Async that do not return void or Future will be routed synchronously"; +} diff --git a/org.springframework.aspects/src/test/java/org/springframework/scheduling/AnnotationDrivenAsynchronousExecutionAspectTest.java b/org.springframework.aspects/src/test/java/org/springframework/scheduling/AnnotationDrivenAsynchronousExecutionAspectTest.java new file mode 100644 index 0000000000..9317687976 --- /dev/null +++ b/org.springframework.aspects/src/test/java/org/springframework/scheduling/AnnotationDrivenAsynchronousExecutionAspectTest.java @@ -0,0 +1,161 @@ +package org.springframework.scheduling; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import junit.framework.Assert; + +import static junit.framework.Assert.*; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.AsyncResult; + +/** + * Test for AnnotationDrivenAsynchronousExecutionAspect + * + * @author Ramnivas Laddad + */ +public class AnnotationDrivenAsynchronousExecutionAspectTest { + private static final long WAIT_TIME = 1000; //milli seconds + private CountingExecutor executor; + + @Before + public void setUp() { + executor = new CountingExecutor(); + AnnotationDrivenAsynchronousExecutionAspect.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 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() { + 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 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); + } + + @SuppressWarnings("serial") + private static class CountingExecutor extends SimpleAsyncTaskExecutor { + int submitStartCounter; + int submitCompleteCounter; + + @Override + public Future submit(Callable task) { + submitStartCounter++; + Future future = super.submit(task); + submitCompleteCounter++; + synchronized (this) { + notifyAll(); + } + return future; + } + + public synchronized void waitForCompletion() { + try { + wait(WAIT_TIME); + } catch (InterruptedException e) { + Assert.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 incrementReturningAFuture() { + counter++; + return new AsyncResult(5); + } + + // It should be an error to attach @Async to a method that returns a non-void + // or non-Future. + // We need to keep this commented out, otherwise there will be a compile-time error. + // Please uncomment and re-comment this periodically to check that the compiler + // produces an error message due to the 'declare error' statement + // in AnnotationDrivenAsynchronousExecutionAspect +// @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 AnnotationDrivenAsynchronousExecutionAspect + public int return5() { + return 5; + } + + public Future incrementReturningAFuture() { + counter++; + return new AsyncResult(5); + } + } +}