diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java
new file mode 100644
index 0000000000..f1bc5cf856
--- /dev/null
+++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2002-2012 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.aop.interceptor;
+
+import java.lang.reflect.Method;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.BeanFactoryAware;
+import org.springframework.beans.factory.BeanFactoryUtils;
+import org.springframework.core.task.AsyncTaskExecutor;
+import org.springframework.core.task.support.TaskExecutorAdapter;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Base class for asynchronous method execution aspects, such as
+ * {@link org.springframework.scheduling.annotation.AnnotationAsyncExecutionInterceptor}
+ * or {@link org.springframework.scheduling.aspectj.AnnotationAsyncExecutionAspect}.
+ *
+ *
Provides support for executor qualification on a method-by-method basis.
+ * {@code AsyncExecutionAspectSupport} objects must be constructed with a default {@code
+ * Executor}, but each individual method may further qualify a specific {@code Executor}
+ * bean to be used when executing it, e.g. through an annotation attribute.
+ *
+ * @author Chris Beams
+ * @since 3.2
+ */
+public abstract class AsyncExecutionAspectSupport implements BeanFactoryAware {
+
+ private final Map executors = new HashMap();
+
+ private Executor defaultExecutor;
+
+ private BeanFactory beanFactory;
+
+
+ /**
+ * Create a new {@link AsyncExecutionAspectSupport}, using the provided default
+ * executor unless individual async methods indicate via qualifier that a more
+ * specific executor should be used.
+ * @param defaultExecutor the executor to use when executing asynchronous methods
+ */
+ public AsyncExecutionAspectSupport(Executor defaultExecutor) {
+ this.setExecutor(defaultExecutor);
+ }
+
+
+ /**
+ * Supply the executor to be used when executing async methods.
+ * @param defaultExecutor the {@code Executor} (typically a Spring {@code
+ * AsyncTaskExecutor} or {@link java.util.concurrent.ExecutorService}) to delegate to
+ * unless a more specific executor has been requested via a qualifier on the async
+ * method, in which case the executor will be looked up at invocation time against the
+ * enclosing bean factory.
+ * @see #getExecutorQualifier
+ * @see #setBeanFactory(BeanFactory)
+ */
+ public void setExecutor(Executor defaultExecutor) {
+ this.defaultExecutor = defaultExecutor;
+ }
+
+ /**
+ * Set the {@link BeanFactory} to be used when looking up executors by qualifier.
+ */
+ public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
+ this.beanFactory = beanFactory;
+ }
+
+ /**
+ * Return the qualifier or bean name of the executor to be used when executing the
+ * given async method, typically specified in the form of an annotation attribute.
+ * Returning an empty string or {@code null} indicates that no specific executor has
+ * been specified and that the {@linkplain #setExecutor(Executor) default executor}
+ * should be used.
+ * @param method the method to inspect for executor qualifier metadata
+ * @return the qualifier if specified, otherwise empty string or {@code null}
+ * @see #determineAsyncExecutor(Method)
+ */
+ protected abstract String getExecutorQualifier(Method method);
+
+ /**
+ * Determine the specific executor to use when executing the given method.
+ * @returns the executor to use (never {@code null})
+ */
+ protected AsyncTaskExecutor determineAsyncExecutor(Method method) {
+ if (!this.executors.containsKey(method)) {
+ Executor executor = this.defaultExecutor;
+
+ String qualifier = getExecutorQualifier(method);
+ if (StringUtils.hasLength(qualifier)) {
+ Assert.notNull(this.beanFactory,
+ "BeanFactory must be set on " + this.getClass().getSimpleName() +
+ " to access qualified executor [" + qualifier + "]");
+ executor = BeanFactoryUtils.qualifiedBeanOfType(this.beanFactory, Executor.class, qualifier);
+ }
+
+ if (executor instanceof AsyncTaskExecutor) {
+ this.executors.put(method, (AsyncTaskExecutor) executor);
+ }
+ else if (executor instanceof Executor) {
+ this.executors.put(method, new TaskExecutorAdapter(executor));
+ }
+ }
+
+ return this.executors.get(method);
+ }
+
+}
diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java
index 1cc6f8e022..20ed49adfb 100644
--- a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java
+++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2009 the original author or authors.
+ * Copyright 2002-2012 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.
@@ -16,6 +16,8 @@
package org.springframework.aop.interceptor;
+import java.lang.reflect.Method;
+
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
@@ -25,8 +27,6 @@ import org.aopalliance.intercept.MethodInvocation;
import org.springframework.core.Ordered;
import org.springframework.core.task.AsyncTaskExecutor;
-import org.springframework.core.task.support.TaskExecutorAdapter;
-import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
@@ -44,50 +44,53 @@ import org.springframework.util.ReflectionUtils;
* (like Spring's {@link org.springframework.scheduling.annotation.AsyncResult}
* or EJB 3.1's javax.ejb.AsyncResult).
*
+ * As of Spring 3.2 the {@code AnnotationAsyncExecutionInterceptor} subclass is
+ * preferred for use due to its support for executor qualification in conjunction with
+ * Spring's {@code @Async} annotation.
+ *
* @author Juergen Hoeller
+ * @author Chris Beams
* @since 3.0
* @see org.springframework.scheduling.annotation.Async
* @see org.springframework.scheduling.annotation.AsyncAnnotationAdvisor
+ * @see org.springframework.scheduling.annotation.AnnotationAsyncExecutionInterceptor
*/
-public class AsyncExecutionInterceptor implements MethodInterceptor, Ordered {
-
- private final AsyncTaskExecutor asyncExecutor;
-
+public class AsyncExecutionInterceptor extends AsyncExecutionAspectSupport
+ implements MethodInterceptor, Ordered {
/**
- * Create a new AsyncExecutionInterceptor.
- * @param asyncExecutor the Spring AsyncTaskExecutor to delegate to
+ * Create a new {@code AsyncExecutionInterceptor}.
+ * @param executor the {@link Executor} (typically a Spring {@link AsyncTaskExecutor}
+ * or {@link java.util.concurrent.ExecutorService}) to delegate to.
*/
- public AsyncExecutionInterceptor(AsyncTaskExecutor asyncExecutor) {
- Assert.notNull(asyncExecutor, "TaskExecutor must not be null");
- this.asyncExecutor = asyncExecutor;
- }
-
- /**
- * Create a new AsyncExecutionInterceptor.
- * @param asyncExecutor the java.util.concurrent Executor
- * to delegate to (typically a {@link java.util.concurrent.ExecutorService}
- */
- public AsyncExecutionInterceptor(Executor asyncExecutor) {
- this.asyncExecutor = new TaskExecutorAdapter(asyncExecutor);
+ public AsyncExecutionInterceptor(Executor executor) {
+ super(executor);
}
+ /**
+ * Intercept the given method invocation, submit the actual calling of the method to
+ * the correct task executor and return immediately to the caller.
+ * @param invocation the method to intercept and make asynchronous
+ * @return {@link Future} if the original method returns {@code Future}; {@code null}
+ * otherwise.
+ */
public Object invoke(final MethodInvocation invocation) throws Throwable {
- Future result = this.asyncExecutor.submit(new Callable() {
- public Object call() throws Exception {
- try {
- Object result = invocation.proceed();
- if (result instanceof Future) {
- return ((Future) result).get();
+ Future> result = this.determineAsyncExecutor(invocation.getMethod()).submit(
+ new Callable() {
+ public Object call() throws Exception {
+ try {
+ Object result = invocation.proceed();
+ if (result instanceof Future) {
+ return ((Future>) result).get();
+ }
+ }
+ catch (Throwable ex) {
+ ReflectionUtils.rethrowException(ex);
+ }
+ return null;
}
- }
- catch (Throwable ex) {
- ReflectionUtils.rethrowException(ex);
- }
- return null;
- }
- });
+ });
if (Future.class.isAssignableFrom(invocation.getMethod().getReturnType())) {
return result;
}
@@ -96,6 +99,20 @@ public class AsyncExecutionInterceptor implements MethodInterceptor, Ordered {
}
}
+ /**
+ * {@inheritDoc}
+ * This implementation is a no-op for compatibility in Spring 3.2. Subclasses may
+ * override to provide support for extracting qualifier information, e.g. via an
+ * annotation on the given method.
+ * @return always {@code null}
+ * @see #determineAsyncExecutor(Method)
+ * @since 3.2
+ */
+ @Override
+ protected String getExecutorQualifier(Method method) {
+ return null;
+ }
+
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
diff --git a/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java b/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java
index f08481efba..d222d652fc 100644
--- a/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java
+++ b/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java
@@ -206,6 +206,7 @@ public abstract class AopUtils {
* @return whether the pointcut can apply on any method
*/
public static boolean canApply(Pointcut pc, Class> targetClass, boolean hasIntroductions) {
+ Assert.notNull(pc, "Pointcut must not be null");
if (!pc.getClassFilter().matches(targetClass)) {
return false;
}
diff --git a/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AbstractAsyncExecutionAspect.aj b/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AbstractAsyncExecutionAspect.aj
index c540227114..c8abf4a729 100644
--- a/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AbstractAsyncExecutionAspect.aj
+++ b/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AbstractAsyncExecutionAspect.aj
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2010 the original author or authors.
+ * Copyright 2002-2012 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.
@@ -21,9 +21,9 @@ import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import org.aspectj.lang.reflect.MethodSignature;
+
+import org.springframework.aop.interceptor.AsyncExecutionAspectSupport;
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.
@@ -34,25 +34,33 @@ import org.springframework.core.task.support.TaskExecutorAdapter;
*
* @author Ramnivas Laddad
* @author Juergen Hoeller
+ * @author Chris Beams
* @since 3.0.5
*/
-public abstract aspect AbstractAsyncExecutionAspect {
+public abstract aspect AbstractAsyncExecutionAspect extends AsyncExecutionAspectSupport {
- private AsyncTaskExecutor asyncExecutor;
-
- public void setExecutor(Executor executor) {
- if (executor instanceof AsyncTaskExecutor) {
- this.asyncExecutor = (AsyncTaskExecutor) executor;
- }
- else {
- this.asyncExecutor = new TaskExecutorAdapter(executor);
- }
+ /**
+ * Create an {@code AnnotationAsyncExecutionAspect} with a {@code null} default
+ * executor, which should instead be set via {@code #aspectOf} and
+ * {@link #setExecutor(Executor)}.
+ */
+ public AbstractAsyncExecutionAspect() {
+ super(null);
}
+ /**
+ * Apply around advice to methods matching the {@link #asyncMethod()} pointcut,
+ * submit the actual calling of the method to the correct task executor and return
+ * immediately to the caller.
+ * @return {@link Future} if the original method returns {@code Future}; {@code null}
+ * otherwise.
+ */
Object around() : asyncMethod() {
- if (this.asyncExecutor == null) {
+ MethodSignature methodSignature = (MethodSignature) thisJoinPointStaticPart.getSignature();
+ AsyncTaskExecutor executor = determineAsyncExecutor(methodSignature.getMethod());
+ if (executor == null) {
return proceed();
- }
+ }
Callable callable = new Callable() {
public Object call() throws Exception {
Object result = proceed();
@@ -61,8 +69,8 @@ public abstract aspect AbstractAsyncExecutionAspect {
}
return null;
}};
- Future> result = this.asyncExecutor.submit(callable);
- if (Future.class.isAssignableFrom(((MethodSignature) thisJoinPointStaticPart.getSignature()).getReturnType())) {
+ Future> result = executor.submit(callable);
+ if (Future.class.isAssignableFrom(methodSignature.getReturnType())) {
return result;
}
else {
@@ -70,6 +78,9 @@ public abstract aspect AbstractAsyncExecutionAspect {
}
}
+ /**
+ * Return the set of joinpoints at which async advice should be applied.
+ */
public abstract pointcut asyncMethod();
}
diff --git a/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AnnotationAsyncExecutionAspect.aj b/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AnnotationAsyncExecutionAspect.aj
index 328e742670..157215037a 100644
--- a/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AnnotationAsyncExecutionAspect.aj
+++ b/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AnnotationAsyncExecutionAspect.aj
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2010 the original author or authors.
+ * Copyright 2002-2012 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.
@@ -16,7 +16,11 @@
package org.springframework.scheduling.aspectj;
+import java.lang.reflect.Method;
+
import java.util.concurrent.Future;
+
+import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.scheduling.annotation.Async;
/**
@@ -24,31 +28,55 @@ import org.springframework.scheduling.annotation.Async;
*
* 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.
- *
+ * to be routed asynchronously must return either {@code 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 {@code @Async} contains a method that violates this
+ * constraint, it produces only a warning.
+ *
* @author Ramnivas Laddad
+ * @author Chris Beams
* @since 3.0.5
*/
public aspect AnnotationAsyncExecutionAspect extends AbstractAsyncExecutionAspect {
- private pointcut asyncMarkedMethod()
+ private pointcut asyncMarkedMethod()
: execution(@Async (void || Future+) *(..));
- private pointcut asyncTypeMarkedMethod()
+ private pointcut asyncTypeMarkedMethod()
: execution((void || Future+) (@Async *).*(..));
-
+
public pointcut asyncMethod() : asyncMarkedMethod() || asyncTypeMarkedMethod();
-
- declare error:
- execution(@Async !(void||Future) *(..)):
+
+ /**
+ * {@inheritDoc}
+ *
This implementation inspects the given method and its declaring class for the
+ * {@code @Async} annotation, returning the qualifier value expressed by
+ * {@link Async#value()}. If {@code @Async} is specified at both the method and class level, the
+ * method's {@code #value} takes precedence (even if empty string, indicating that
+ * the default executor should be used preferentially).
+ * @return the qualifier if specified, otherwise empty string indicating that the
+ * {@linkplain #setExecutor(Executor) default executor} should be used
+ * @see #determineAsyncExecutor(Method)
+ */
+ @Override
+ protected String getExecutorQualifier(Method method) {
+ // maintainer's note: changes made here should also be made in
+ // AnnotationAsyncExecutionInterceptor#getExecutorQualifier
+ Async async = AnnotationUtils.findAnnotation(method, Async.class);
+ if (async == null) {
+ async = AnnotationUtils.findAnnotation(method.getDeclaringClass(), Async.class);
+ }
+ return async == null ? null : async.value();
+ }
+
+ declare error:
+ execution(@Async !(void||Future) *(..)):
"Only methods that return void or Future may have an @Async annotation";
- declare warning:
- execution(!(void||Future) (@Async *).*(..)):
- "Methods in a class marked with @Async that do not return void or Future will be routed synchronously";
+ declare warning:
+ execution(!(void||Future) (@Async *).*(..)):
+ "Methods in a class marked with @Async that do not return void or Future will " +
+ "be routed synchronously";
}
diff --git a/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationAsyncExecutionAspectTests.java b/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationAsyncExecutionAspectTests.java
index e2194cfe5c..6bfe60205e 100644
--- a/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationAsyncExecutionAspectTests.java
+++ b/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationAsyncExecutionAspectTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2010 the original author or authors.
+ * Copyright 2002-2012 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.
@@ -20,31 +20,38 @@ 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.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 static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.Matchers.startsWith;
+
+import static org.junit.Assert.*;
/**
+ * Unit tests for {@link AnnotationAsyncExecutionAspect}.
+ *
* @author Ramnivas Laddad
*/
public class AnnotationAsyncExecutionAspectTests {
- private static final long WAIT_TIME = 1000; //milli seconds
+ private static final long WAIT_TIME = 1000; //milliseconds
private CountingExecutor executor;
-
+
@Before
public void setUp() {
executor = new CountingExecutor();
AnnotationAsyncExecutionAspect.aspectOf().setExecutor(executor);
}
-
+
@Test
public void asyncMethodGetsRoutedAsynchronously() {
ClassWithoutAsyncAnnotation obj = new ClassWithoutAsyncAnnotation();
@@ -54,7 +61,7 @@ public class AnnotationAsyncExecutionAspectTests {
assertEquals(1, executor.submitStartCounter);
assertEquals(1, executor.submitCompleteCounter);
}
-
+
@Test
public void asyncMethodReturningFutureGetsRoutedAsynchronouslyAndReturnsAFuture() throws InterruptedException, ExecutionException {
ClassWithoutAsyncAnnotation obj = new ClassWithoutAsyncAnnotation();
@@ -73,8 +80,8 @@ public class AnnotationAsyncExecutionAspectTests {
assertEquals(1, obj.counter);
assertEquals(0, executor.submitStartCounter);
assertEquals(0, executor.submitCompleteCounter);
- }
-
+ }
+
@Test
public void voidMethodInAsyncClassGetsRoutedAsynchronously() {
ClassWithAsyncAnnotation obj = new ClassWithAsyncAnnotation();
@@ -102,13 +109,30 @@ public class AnnotationAsyncExecutionAspectTests {
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 defaultThread = obj.defaultWork();
+ assertThat(defaultThread.get(), not(Thread.currentThread()));
+ assertThat(defaultThread.get().getName(), not(startsWith("e1-")));
+
+ Future e1Thread = obj.e1Work();
+ assertThat(e1Thread.get().getName(), startsWith("e1-"));
+ }
+
@SuppressWarnings("serial")
private static class CountingExecutor extends SimpleAsyncTaskExecutor {
int submitStartCounter;
int submitCompleteCounter;
-
+
@Override
public Future submit(Callable task) {
submitStartCounter++;
@@ -119,52 +143,56 @@ public class AnnotationAsyncExecutionAspectTests {
}
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");
+ 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 AnnotationAsyncExecutionAspect
+
+ /**
+ * 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 AnnotationAsynchExecutionAspect
+
+ // Manually check that there is a warning from the 'declare warning' statement in
+ // AnnotationAsyncExecutionAspect
public int return5() {
return 5;
}
@@ -175,4 +203,16 @@ public class AnnotationAsyncExecutionAspectTests {
}
}
+
+ static class ClassWithQualifiedAsyncMethods {
+ @Async
+ public Future defaultWork() {
+ return new AsyncResult(Thread.currentThread());
+ }
+
+ @Async("e1")
+ public Future e1Work() {
+ return new AsyncResult(Thread.currentThread());
+ }
+ }
}
diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java
index 5bd1787dde..4b0410040a 100644
--- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java
+++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2010 the original author or authors.
+ * Copyright 2002-2012 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.
@@ -16,6 +16,8 @@
package org.springframework.beans.factory;
+import java.lang.reflect.Method;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
@@ -23,7 +25,14 @@ import java.util.List;
import java.util.Map;
import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.beans.factory.support.AbstractBeanDefinition;
+import org.springframework.beans.factory.support.AutowireCandidateQualifier;
+import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
@@ -37,6 +46,7 @@ import org.springframework.util.StringUtils;
*
* @author Rod Johnson
* @author Juergen Hoeller
+ * @author Chris Beams
* @since 04.07.2003
*/
public abstract class BeanFactoryUtils {
@@ -431,4 +441,102 @@ public abstract class BeanFactoryUtils {
}
}
+ /**
+ * Obtain a bean of type {@code T} from the given {@code BeanFactory} declaring a
+ * qualifier (e.g. via {@code } or {@code @Qualifier}) matching the given
+ * qualifier, or having a bean name matching the given qualifier.
+ * @param bf the BeanFactory to get the target bean from
+ * @param beanType the type of bean to retrieve
+ * @param qualifier the qualifier for selecting between multiple bean matches
+ * @return the matching bean of type {@code T} (never {@code null})
+ * @throws IllegalStateException if no matching bean of type {@code T} found
+ * @since 3.2
+ */
+ public static T qualifiedBeanOfType(BeanFactory beanFactory, Class beanType, String qualifier) {
+ if (beanFactory instanceof ConfigurableListableBeanFactory) {
+ // Full qualifier matching supported.
+ return qualifiedBeanOfType((ConfigurableListableBeanFactory) beanFactory, beanType, qualifier);
+ }
+ else if (beanFactory.containsBean(qualifier)) {
+ // Fallback: target bean at least found by bean name.
+ return beanFactory.getBean(qualifier, beanType);
+ }
+ else {
+ throw new IllegalStateException("No matching " + beanType.getSimpleName() +
+ " bean found for bean name '" + qualifier +
+ "'! (Note: Qualifier matching not supported because given " +
+ "BeanFactory does not implement ConfigurableListableBeanFactory.)");
+ }
+ }
+
+ /**
+ * Obtain a bean of type {@code T} from the given {@code BeanFactory} declaring a
+ * qualifier (e.g. {@code } or {@code @Qualifier}) matching the given
+ * qualifier
+ * @param bf the BeanFactory to get the target bean from
+ * @param beanType the type of bean to retrieve
+ * @param qualifier the qualifier for selecting between multiple bean matches
+ * @return the matching bean of type {@code T} (never {@code null})
+ * @throws IllegalStateException if no matching bean of type {@code T} found
+ */
+ private static T qualifiedBeanOfType(ConfigurableListableBeanFactory bf, Class beanType, String qualifier) {
+ Map candidateBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(bf, beanType);
+ T matchingBean = null;
+ for (String beanName : candidateBeans.keySet()) {
+ if (isQualifierMatch(qualifier, beanName, bf)) {
+ if (matchingBean != null) {
+ throw new IllegalStateException("No unique " + beanType.getSimpleName() +
+ " bean found for qualifier '" + qualifier + "'");
+ }
+ matchingBean = candidateBeans.get(beanName);
+ }
+ }
+ if (matchingBean != null) {
+ return matchingBean;
+ }
+ else {
+ throw new IllegalStateException("No matching " + beanType.getSimpleName() +
+ " bean found for qualifier '" + qualifier + "' - neither qualifier " +
+ "match nor bean name match!");
+ }
+ }
+
+ /**
+ * Check whether the named bean declares a qualifier of the given name.
+ * @param qualifier the qualifier to match
+ * @param beanName the name of the candidate bean
+ * @param bf the {@code BeanFactory} from which to retrieve the named bean
+ * @return {@code true} if either the bean definition (in the XML case)
+ * or the bean's factory method (in the {@code @Bean} case) defines a matching
+ * qualifier value (through {@code } or {@code @Qualifier})
+ */
+ private static boolean isQualifierMatch(String qualifier, String beanName, ConfigurableListableBeanFactory bf) {
+ if (bf.containsBean(beanName)) {
+ try {
+ BeanDefinition bd = bf.getMergedBeanDefinition(beanName);
+ if (bd instanceof AbstractBeanDefinition) {
+ AbstractBeanDefinition abd = (AbstractBeanDefinition) bd;
+ AutowireCandidateQualifier candidate = abd.getQualifier(Qualifier.class.getName());
+ if ((candidate != null && qualifier.equals(candidate.getAttribute(AutowireCandidateQualifier.VALUE_KEY))) ||
+ qualifier.equals(beanName) || ObjectUtils.containsElement(bf.getAliases(beanName), qualifier)) {
+ return true;
+ }
+ }
+ if (bd instanceof RootBeanDefinition) {
+ Method factoryMethod = ((RootBeanDefinition) bd).getResolvedFactoryMethod();
+ if (factoryMethod != null) {
+ Qualifier targetAnnotation = factoryMethod.getAnnotation(Qualifier.class);
+ if (targetAnnotation != null && qualifier.equals(targetAnnotation.value())) {
+ return true;
+ }
+ }
+ }
+ }
+ catch (NoSuchBeanDefinitionException ex) {
+ // ignore - can't compare qualifiers for a manually registered singleton object
+ }
+ }
+ return false;
+ }
+
}
diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptor.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptor.java
new file mode 100644
index 0000000000..6ae62fe6e3
--- /dev/null
+++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptor.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2002-2012 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.lang.reflect.Method;
+import java.util.concurrent.Executor;
+
+import org.springframework.aop.interceptor.AsyncExecutionInterceptor;
+import org.springframework.core.annotation.AnnotationUtils;
+
+/**
+ * Specialization of {@link AsyncExecutionInterceptor} that delegates method execution to
+ * an {@code Executor} based on the {@link Async} annotation. Specifically designed to
+ * support use of {@link Async#value()} executor qualification mechanism introduced in
+ * Spring 3.2. Supports detecting qualifier metadata via {@code @Async} at the method or
+ * declaring class level. See {@link #getExecutorQualifier(Method)} for details.
+ *
+ * @author Chris Beams
+ * @since 3.2
+ * @see org.springframework.scheduling.annotation.Async
+ * @see org.springframework.scheduling.annotation.AsyncAnnotationAdvisor
+ */
+public class AnnotationAsyncExecutionInterceptor extends AsyncExecutionInterceptor {
+
+ /**
+ * Create a new {@code AnnotationAsyncExecutionInterceptor} with the given executor.
+ * @param defaultExecutor the executor to be used by default if no more specific
+ * executor has been qualified at the method level using {@link Async#value()}.
+ */
+ public AnnotationAsyncExecutionInterceptor(Executor defaultExecutor) {
+ super(defaultExecutor);
+ }
+
+ /**
+ * Return the qualifier or bean name of the executor to be used when executing the
+ * given method, specified via {@link Async#value} at the method or declaring
+ * class level. If {@code @Async} is specified at both the method and class level, the
+ * method's {@code #value} takes precedence (even if empty string, indicating that
+ * the default executor should be used preferentially).
+ * @param method the method to inspect for executor qualifier metadata
+ * @return the qualifier if specified, otherwise empty string indicating that the
+ * {@linkplain #setExecutor(Executor) default executor} should be used
+ * @see #determineAsyncExecutor(Method)
+ */
+ @Override
+ protected String getExecutorQualifier(Method method) {
+ // maintainer's note: changes made here should also be made in
+ // AnnotationAsyncExecutionAspect#getExecutorQualifier
+ Async async = AnnotationUtils.findAnnotation(method, Async.class);
+ if (async == null) {
+ async = AnnotationUtils.findAnnotation(method.getDeclaringClass(), Async.class);
+ }
+ return async == null ? null : async.value();
+ }
+
+}
diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/Async.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/Async.java
index c79f9ecd83..be600d325c 100644
--- a/spring-context/src/main/java/org/springframework/scheduling/annotation/Async.java
+++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/Async.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2009 the original author or authors.
+ * Copyright 2002-2012 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.
@@ -28,17 +28,18 @@ import java.lang.annotation.Target;
* considered as asynchronous.
*
* In terms of target method signatures, any parameter types are supported.
- * However, the return type is constrained to either void or
- * java.util.concurrent.Future. In the latter case, the Future handle
- * returned from the proxy will be an actual asynchronous Future that can be used
+ * However, the return type is constrained to either {@code void} or
+ * {@link java.util.concurrent.Future}. In the latter case, the {@code Future} handle
+ * returned from the proxy will be an actual asynchronous {@code Future} that can be used
* to track the result of the asynchronous method execution. However, since the
* target method needs to implement the same signature, it will have to return
- * a temporary Future handle that just passes the return value through: e.g.
- * Spring's {@link AsyncResult} or EJB 3.1's javax.ejb.AsyncResult.
+ * a temporary {@code Future} handle that just passes the return value through: e.g.
+ * Spring's {@link AsyncResult} or EJB 3.1's {@link javax.ejb.AsyncResult}.
*
* @author Juergen Hoeller
+ * @author Chris Beams
* @since 3.0
- * @see org.springframework.aop.interceptor.AsyncExecutionInterceptor
+ * @see AnnotationAsyncExecutionInterceptor
* @see AsyncAnnotationAdvisor
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@@ -46,4 +47,18 @@ import java.lang.annotation.Target;
@Documented
public @interface Async {
+ /**
+ * A qualifier value for the specified asynchronous operation(s).
+ *
May be used to determine the target executor to be used when executing this
+ * method, matching the qualifier value (or the bean name) of a specific
+ * {@link java.util.concurrent.Executor Executor} or
+ * {@link org.springframework.core.task.TaskExecutor TaskExecutor}
+ * bean definition.
+ *
When specified on a class level {@code @Async} annotation, indicates that the
+ * given executor should be used for all methods within the class. Method level use
+ * of {@link Async#value} always overrides any value set at the class level.
+ * @since 3.2
+ */
+ String value() default "";
+
}
diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationAdvisor.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationAdvisor.java
index 245ff645b9..571ac6a30e 100644
--- a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationAdvisor.java
+++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationAdvisor.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2009 the original author or authors.
+ * Copyright 2002-2012 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.
@@ -25,11 +25,12 @@ import java.util.concurrent.Executor;
import org.aopalliance.aop.Advice;
import org.springframework.aop.Pointcut;
-import org.springframework.aop.interceptor.AsyncExecutionInterceptor;
import org.springframework.aop.support.AbstractPointcutAdvisor;
import org.springframework.aop.support.ComposablePointcut;
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
-import org.springframework.core.task.AsyncTaskExecutor;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.util.Assert;
@@ -50,22 +51,25 @@ import org.springframework.util.Assert;
* @see org.springframework.dao.DataAccessException
* @see org.springframework.dao.support.PersistenceExceptionTranslator
*/
-public class AsyncAnnotationAdvisor extends AbstractPointcutAdvisor {
+@SuppressWarnings("serial")
+public class AsyncAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {
private Advice advice;
private Pointcut pointcut;
+ private BeanFactory beanFactory;
+
/**
- * Create a new ConcurrencyAnnotationBeanPostProcessor for bean-style configuration.
+ * Create a new {@code AsyncAnnotationAdvisor} for bean-style configuration.
*/
public AsyncAnnotationAdvisor() {
this(new SimpleAsyncTaskExecutor());
}
/**
- * Create a new ConcurrencyAnnotationBeanPostProcessor for the given task executor.
+ * Create a new {@code AsyncAnnotationAdvisor} for the given task executor.
* @param executor the task executor to use for asynchronous methods
*/
@SuppressWarnings("unchecked")
@@ -74,20 +78,36 @@ public class AsyncAnnotationAdvisor extends AbstractPointcutAdvisor {
asyncAnnotationTypes.add(Async.class);
ClassLoader cl = AsyncAnnotationAdvisor.class.getClassLoader();
try {
- asyncAnnotationTypes.add((Class) cl.loadClass("javax.ejb.Asynchronous"));
+ asyncAnnotationTypes.add((Class extends Annotation>) cl.loadClass("javax.ejb.Asynchronous"));
}
catch (ClassNotFoundException ex) {
// If EJB 3.1 API not present, simply ignore.
}
this.advice = buildAdvice(executor);
+ this.setTaskExecutor(executor);
this.pointcut = buildPointcut(asyncAnnotationTypes);
}
+ /**
+ * Set the {@code BeanFactory} to be used when looking up executors by qualifier.
+ */
+ public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
+ this.beanFactory = beanFactory;
+ delegateBeanFactory(beanFactory);
+ }
+
+ public void delegateBeanFactory(BeanFactory beanFactory) {
+ if (this.advice instanceof AnnotationAsyncExecutionInterceptor) {
+ ((AnnotationAsyncExecutionInterceptor)this.advice).setBeanFactory(beanFactory);
+ }
+ }
+
/**
* Specify the task executor to use for asynchronous methods.
*/
public void setTaskExecutor(Executor executor) {
this.advice = buildAdvice(executor);
+ delegateBeanFactory(this.beanFactory);
}
/**
@@ -117,12 +137,7 @@ public class AsyncAnnotationAdvisor extends AbstractPointcutAdvisor {
protected Advice buildAdvice(Executor executor) {
- if (executor instanceof AsyncTaskExecutor) {
- return new AsyncExecutionInterceptor((AsyncTaskExecutor) executor);
- }
- else {
- return new AsyncExecutionInterceptor(executor);
- }
+ return new AnnotationAsyncExecutionInterceptor(executor);
}
/**
diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessor.java
index 8f2adc277b..77ad6786a5 100644
--- a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessor.java
+++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessor.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2011 the original author or authors.
+ * Copyright 2002-2012 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.
@@ -24,7 +24,10 @@ import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.aop.framework.ProxyConfig;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.AopUtils;
+import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanClassLoaderAware;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.Ordered;
@@ -53,7 +56,8 @@ import org.springframework.util.ClassUtils;
*/
@SuppressWarnings("serial")
public class AsyncAnnotationBeanPostProcessor extends ProxyConfig
- implements BeanPostProcessor, BeanClassLoaderAware, InitializingBean, Ordered {
+ implements BeanPostProcessor, BeanClassLoaderAware, BeanFactoryAware,
+ InitializingBean, Ordered {
private Class extends Annotation> asyncAnnotationType;
@@ -69,6 +73,8 @@ public class AsyncAnnotationBeanPostProcessor extends ProxyConfig
*/
private int order = Ordered.LOWEST_PRECEDENCE;
+ private BeanFactory beanFactory;
+
/**
* Set the 'async' annotation type to be detected at either class or method
@@ -95,12 +101,17 @@ public class AsyncAnnotationBeanPostProcessor extends ProxyConfig
this.beanClassLoader = classLoader;
}
+ public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
+ this.beanFactory = beanFactory;
+ }
+
public void afterPropertiesSet() {
this.asyncAnnotationAdvisor = (this.executor != null ?
new AsyncAnnotationAdvisor(this.executor) : new AsyncAnnotationAdvisor());
if (this.asyncAnnotationType != null) {
this.asyncAnnotationAdvisor.setAsyncAnnotationType(this.asyncAnnotationType);
}
+ this.asyncAnnotationAdvisor.setBeanFactory(this.beanFactory);
}
public int getOrder() {
diff --git a/spring-context/src/main/resources/org/springframework/scheduling/config/spring-task-3.2.xsd b/spring-context/src/main/resources/org/springframework/scheduling/config/spring-task-3.2.xsd
index 8d17fffb84..3091b0ba07 100644
--- a/spring-context/src/main/resources/org/springframework/scheduling/config/spring-task-3.2.xsd
+++ b/spring-context/src/main/resources/org/springframework/scheduling/config/spring-task-3.2.xsd
@@ -35,7 +35,10 @@
@@ -98,6 +101,9 @@
required even when defining the executor as an inner bean: The executor
won't be directly accessible then but will nevertheless use the specified
id as the thread name prefix of the threads that it manages.
+ In the case of multiple task:executors, as of Spring 3.2 this value may be used to
+ qualify which executor should handle a given @Async method, e.g. @Async("executorId").
+ See the Javadoc for the #value attribute of Spring's @Async annotation for details.
]]>
diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptorTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptorTests.java
new file mode 100644
index 0000000000..b0ddea9e16
--- /dev/null
+++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptorTests.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2002-2012 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 org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests for {@link AnnotationAsyncExecutionInterceptor}.
+ *
+ * @author Chris Beams
+ * @since 3.2
+ */
+public class AnnotationAsyncExecutionInterceptorTests {
+
+ @Test
+ @SuppressWarnings("unused")
+ public void testGetExecutorQualifier() throws SecurityException, NoSuchMethodException {
+ AnnotationAsyncExecutionInterceptor i = new AnnotationAsyncExecutionInterceptor(null);
+ {
+ class C { @Async("qMethod") void m() { } }
+ assertThat(i.getExecutorQualifier(C.class.getDeclaredMethod("m")), is("qMethod"));
+ }
+ {
+ @Async("qClass") class C { void m() { } }
+ assertThat(i.getExecutorQualifier(C.class.getDeclaredMethod("m")), is("qClass"));
+ }
+ {
+ @Async("qClass") class C { @Async("qMethod") void m() { } }
+ assertThat(i.getExecutorQualifier(C.class.getDeclaredMethod("m")), is("qMethod"));
+ }
+ {
+ @Async("qClass") class C { @Async void m() { } }
+ assertThat(i.getExecutorQualifier(C.class.getDeclaredMethod("m")), is(""));
+ }
+ }
+}
diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncExecutionTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncExecutionTests.java
index da4436b35e..48317d6eda 100644
--- a/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncExecutionTests.java
+++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncExecutionTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2009 the original author or authors.
+ * Copyright 2002-2012 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.
@@ -18,8 +18,6 @@ package org.springframework.scheduling.annotation;
import java.util.concurrent.Future;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
import org.junit.Test;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
@@ -27,10 +25,13 @@ import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.support.GenericApplicationContext;
-import org.springframework.scheduling.annotation.AsyncResult;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import static org.junit.Assert.*;
/**
* @author Juergen Hoeller
+ * @author Chris Beams
*/
public class AsyncExecutionTests {
@@ -56,6 +57,26 @@ public class AsyncExecutionTests {
assertEquals("20", future.get());
}
+ @Test
+ public void asyncMethodsWithQualifier() throws Exception {
+ originalThreadName = Thread.currentThread().getName();
+ GenericApplicationContext context = new GenericApplicationContext();
+ context.registerBeanDefinition("asyncTest", new RootBeanDefinition(AsyncMethodWithQualifierBean.class));
+ context.registerBeanDefinition("autoProxyCreator", new RootBeanDefinition(DefaultAdvisorAutoProxyCreator.class));
+ context.registerBeanDefinition("asyncAdvisor", new RootBeanDefinition(AsyncAnnotationAdvisor.class));
+ context.registerBeanDefinition("e0", new RootBeanDefinition(ThreadPoolTaskExecutor.class));
+ context.registerBeanDefinition("e1", new RootBeanDefinition(ThreadPoolTaskExecutor.class));
+ context.registerBeanDefinition("e2", new RootBeanDefinition(ThreadPoolTaskExecutor.class));
+ context.refresh();
+ AsyncMethodWithQualifierBean asyncTest = context.getBean("asyncTest", AsyncMethodWithQualifierBean.class);
+ asyncTest.doNothing(5);
+ asyncTest.doSomething(10);
+ Future future = asyncTest.returnSomething(20);
+ assertEquals("20", future.get());
+ Future future2 = asyncTest.returnSomething2(30);
+ assertEquals("30", future2.get());
+ }
+
@Test
public void asyncClass() throws Exception {
originalThreadName = Thread.currentThread().getName();
@@ -155,7 +176,6 @@ public class AsyncExecutionTests {
@Async
public void doSomething(int i) {
- System.out.println(Thread.currentThread().getName() + ": " + i);
assertTrue(!Thread.currentThread().getName().equals(originalThreadName));
}
@@ -167,11 +187,38 @@ public class AsyncExecutionTests {
}
+ @Async("e0")
+ public static class AsyncMethodWithQualifierBean {
+
+ public void doNothing(int i) {
+ assertTrue(Thread.currentThread().getName().equals(originalThreadName));
+ }
+
+ @Async("e1")
+ public void doSomething(int i) {
+ assertTrue(!Thread.currentThread().getName().equals(originalThreadName));
+ assertTrue(Thread.currentThread().getName().startsWith("e1-"));
+ }
+
+ @Async("e2")
+ public Future returnSomething(int i) {
+ assertTrue(!Thread.currentThread().getName().equals(originalThreadName));
+ assertTrue(Thread.currentThread().getName().startsWith("e2-"));
+ return new AsyncResult(Integer.toString(i));
+ }
+
+ public Future returnSomething2(int i) {
+ assertTrue(!Thread.currentThread().getName().equals(originalThreadName));
+ assertTrue(Thread.currentThread().getName().startsWith("e0-"));
+ return new AsyncResult(Integer.toString(i));
+ }
+ }
+
+
@Async
public static class AsyncClassBean {
public void doSomething(int i) {
- System.out.println(Thread.currentThread().getName() + ": " + i);
assertTrue(!Thread.currentThread().getName().equals(originalThreadName));
}
@@ -194,7 +241,6 @@ public class AsyncExecutionTests {
public static class AsyncInterfaceBean implements AsyncInterface {
public void doSomething(int i) {
- System.out.println(Thread.currentThread().getName() + ": " + i);
assertTrue(!Thread.currentThread().getName().equals(originalThreadName));
}
@@ -224,7 +270,6 @@ public class AsyncExecutionTests {
}
public void doSomething(int i) {
- System.out.println(Thread.currentThread().getName() + ": " + i);
assertTrue(!Thread.currentThread().getName().equals(originalThreadName));
}
@@ -235,7 +280,7 @@ public class AsyncExecutionTests {
}
- public static class AsyncMethodListener implements ApplicationListener {
+ public static class AsyncMethodListener implements ApplicationListener {
@Async
public void onApplicationEvent(ApplicationEvent event) {
@@ -246,7 +291,7 @@ public class AsyncExecutionTests {
@Async
- public static class AsyncClassListener implements ApplicationListener {
+ public static class AsyncClassListener implements ApplicationListener {
public AsyncClassListener() {
listenerConstructed++;
diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableAsyncTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableAsyncTests.java
index 224da8f4fe..e96c31f739 100644
--- a/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableAsyncTests.java
+++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableAsyncTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2011 the original author or authors.
+ * Copyright 2002-2012 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.
@@ -16,22 +16,22 @@
package org.springframework.scheduling.annotation;
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.Matchers.startsWith;
-import static org.junit.Assert.assertThat;
-import static org.junit.Assert.assertTrue;
-
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
+
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
+import java.util.concurrent.Future;
import org.junit.Test;
+
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.BeanDefinitionStoreException;
+import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.AdviceMode;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
@@ -39,6 +39,11 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import static org.hamcrest.CoreMatchers.*;
+import static org.hamcrest.Matchers.startsWith;
+
+import static org.junit.Assert.*;
+
/**
* Tests use of @EnableAsync on @Configuration classes.
*
@@ -69,6 +74,48 @@ public class EnableAsyncTests {
}
+ @SuppressWarnings("unchecked")
+ @Test
+ public void withAsyncBeanWithExecutorQualifiedByName() throws ExecutionException, InterruptedException {
+ AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
+ ctx.register(AsyncWithExecutorQualifiedByNameConfig.class);
+ ctx.refresh();
+
+ AsyncBeanWithExecutorQualifiedByName asyncBean = ctx.getBean(AsyncBeanWithExecutorQualifiedByName.class);
+ Future workerThread0 = asyncBean.work0();
+ assertThat(workerThread0.get().getName(), not(anyOf(startsWith("e1-"), startsWith("otherExecutor-"))));
+ Future workerThread = asyncBean.work();
+ assertThat(workerThread.get().getName(), startsWith("e1-"));
+ Future workerThread2 = asyncBean.work2();
+ assertThat(workerThread2.get().getName(), startsWith("otherExecutor-"));
+ Future workerThread3 = asyncBean.work3();
+ assertThat(workerThread3.get().getName(), startsWith("otherExecutor-"));
+ }
+
+
+ static class AsyncBeanWithExecutorQualifiedByName {
+ @Async
+ public Future work0() {
+ return new AsyncResult(Thread.currentThread());
+ }
+
+ @Async("e1")
+ public Future work() {
+ return new AsyncResult(Thread.currentThread());
+ }
+
+ @Async("otherExecutor")
+ public Future work2() {
+ return new AsyncResult(Thread.currentThread());
+ }
+
+ @Async("e2")
+ public Future work3() {
+ return new AsyncResult(Thread.currentThread());
+ }
+ }
+
+
static class AsyncBean {
private Thread threadOfExecution;
@@ -206,6 +253,28 @@ public class EnableAsyncTests {
executor.initialize();
return executor;
}
+ }
+
+ @Configuration
+ @EnableAsync
+ static class AsyncWithExecutorQualifiedByNameConfig {
+ @Bean
+ public AsyncBeanWithExecutorQualifiedByName asyncBean() {
+ return new AsyncBeanWithExecutorQualifiedByName();
+ }
+
+ @Bean
+ public Executor e1() {
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+ return executor;
+ }
+
+ @Bean
+ @Qualifier("e2")
+ public Executor otherExecutor() {
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+ return executor;
+ }
}
}
diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java
index 88c0ba5fae..da32371e96 100644
--- a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java
+++ b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2011 the original author or authors.
+ * Copyright 2002-2012 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.
@@ -19,6 +19,7 @@ package org.springframework.test.context.transaction;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.IdentityHashMap;
@@ -29,6 +30,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.test.annotation.NotTransactional;
import org.springframework.test.annotation.Rollback;
@@ -40,7 +42,6 @@ import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource;
import org.springframework.transaction.interceptor.DelegatingTransactionAttribute;
-import org.springframework.transaction.interceptor.TransactionAspectUtils;
import org.springframework.transaction.interceptor.TransactionAttribute;
import org.springframework.transaction.interceptor.TransactionAttributeSource;
import org.springframework.util.Assert;
@@ -154,7 +155,7 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis
// qualifier matching (only exposed on the internal BeanFactory,
// not on the ApplicationContext).
BeanFactory bf = testContext.getApplicationContext().getAutowireCapableBeanFactory();
- tm = TransactionAspectUtils.getTransactionManager(bf, qualifier);
+ tm = BeanFactoryUtils.qualifiedBeanOfType(bf, PlatformTransactionManager.class, qualifier);
}
else {
tm = getTransactionManager(testContext);
diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java
index 5890e5bf20..ec4348e5c2 100644
--- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java
+++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2010 the original author or authors.
+ * Copyright 2002-2012 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.
@@ -242,7 +242,7 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
}
String qualifier = txAttr.getQualifier();
if (StringUtils.hasLength(qualifier)) {
- return TransactionAspectUtils.getTransactionManager(this.beanFactory, qualifier);
+ return BeanFactoryUtils.qualifiedBeanOfType(this.beanFactory, PlatformTransactionManager.class, qualifier);
}
else if (this.transactionManagerBeanName != null) {
return this.beanFactory.getBean(this.transactionManagerBeanName, PlatformTransactionManager.class);
diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectUtils.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectUtils.java
index 84b082a272..5b9bd14824 100644
--- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectUtils.java
+++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2011 the original author or authors.
+ * Copyright 2002-2012 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.
@@ -16,120 +16,47 @@
package org.springframework.transaction.interceptor;
-import java.lang.reflect.Method;
-import java.util.Map;
-
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryUtils;
-import org.springframework.beans.factory.NoSuchBeanDefinitionException;
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
-import org.springframework.beans.factory.support.AbstractBeanDefinition;
-import org.springframework.beans.factory.support.AutowireCandidateQualifier;
-import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.transaction.PlatformTransactionManager;
-import org.springframework.util.ObjectUtils;
/**
* Utility methods for obtaining a PlatformTransactionManager by
* {@link TransactionAttribute#getQualifier() qualifier value}.
*
* @author Juergen Hoeller
+ * @author Chris Beams
* @since 3.0.2
+ * @deprecated as of Spring 3.2 in favor of {@link BeanFactoryUtils}
*/
+@Deprecated
public abstract class TransactionAspectUtils {
/**
- * Obtain a PlatformTransactionManager from the given BeanFactory,
- * matching the given qualifier.
- * @param beanFactory the BeanFactory to get the PlatformTransactionManager bean from
- * @param qualifier the qualifier for selecting between multiple PlatformTransactionManager matches
- * @return the chosen PlatformTransactionManager (never null)
- * @throws IllegalStateException if no matching PlatformTransactionManager bean found
+ * Obtain a PlatformTransactionManager from the given BeanFactory, matching the given qualifier.
+ * @param beanFactory the BeanFactory to get the {@code PlatformTransactionManager} bean from
+ * @param qualifier the qualifier for selecting between multiple {@code PlatformTransactionManager} matches
+ * @return the chosen {@code PlatformTransactionManager} (never {@code null})
+ * @throws IllegalStateException if no matching {@code PlatformTransactionManager} bean found
+ * @deprecated as of Spring 3.2 in favor of
+ * {@link BeanFactoryUtils#qualifiedBeanOfType(BeanFactory, Class, String)}
*/
public static PlatformTransactionManager getTransactionManager(BeanFactory beanFactory, String qualifier) {
- if (beanFactory instanceof ConfigurableListableBeanFactory) {
- // Full qualifier matching supported.
- return getTransactionManager((ConfigurableListableBeanFactory) beanFactory, qualifier);
- }
- else if (beanFactory.containsBean(qualifier)) {
- // Fallback: PlatformTransactionManager at least found by bean name.
- return beanFactory.getBean(qualifier, PlatformTransactionManager.class);
- }
- else {
- throw new IllegalStateException("No matching PlatformTransactionManager bean found for bean name '" +
- qualifier + "'! (Note: Qualifier matching not supported because given BeanFactory does not " +
- "implement ConfigurableListableBeanFactory.)");
- }
+ return BeanFactoryUtils.qualifiedBeanOfType(beanFactory, PlatformTransactionManager.class, qualifier);
}
/**
- * Obtain a PlatformTransactionManager from the given BeanFactory,
- * matching the given qualifier.
- * @param bf the BeanFactory to get the PlatformTransactionManager bean from
- * @param qualifier the qualifier for selecting between multiple PlatformTransactionManager matches
- * @return the chosen PlatformTransactionManager (never null)
- * @throws IllegalStateException if no matching PlatformTransactionManager bean found
+ * Obtain a PlatformTransactionManager from the given BeanFactory, matching the given qualifier.
+ * @param bf the BeanFactory to get the {@code PlatformTransactionManager} bean from
+ * @param qualifier the qualifier for selecting between multiple {@code PlatformTransactionManager} matches
+ * @return the chosen {@code PlatformTransactionManager} (never {@code null})
+ * @throws IllegalStateException if no matching {@code PlatformTransactionManager} bean found
+ * @deprecated as of Spring 3.2 in favor of
+ * {@link BeanFactoryUtils#qualifiedBeanOfType(BeanFactory, Class, String)}
*/
public static PlatformTransactionManager getTransactionManager(ConfigurableListableBeanFactory bf, String qualifier) {
- Map tms =
- BeanFactoryUtils.beansOfTypeIncludingAncestors(bf, PlatformTransactionManager.class);
- PlatformTransactionManager chosen = null;
- for (String beanName : tms.keySet()) {
- if (isQualifierMatch(qualifier, beanName, bf)) {
- if (chosen != null) {
- throw new IllegalStateException("No unique PlatformTransactionManager bean found " +
- "for qualifier '" + qualifier + "'");
- }
- chosen = tms.get(beanName);
- }
- }
- if (chosen != null) {
- return chosen;
- }
- else {
- throw new IllegalStateException("No matching PlatformTransactionManager bean found for qualifier '" +
- qualifier + "' - neither qualifier match nor bean name match!");
- }
- }
-
- /**
- * Check whether we have a qualifier match for the given candidate bean.
- * @param qualifier the qualifier that we are looking for
- * @param beanName the name of the candidate bean
- * @param bf the BeanFactory to get the bean definition from
- * @return true if either the bean definition (in the XML case)
- * or the bean's factory method (in the @Bean case) defines a matching qualifier
- * value (through <qualifier<> or @Qualifier)
- */
- private static boolean isQualifierMatch(String qualifier, String beanName, ConfigurableListableBeanFactory bf) {
- if (bf.containsBean(beanName)) {
- try {
- BeanDefinition bd = bf.getMergedBeanDefinition(beanName);
- if (bd instanceof AbstractBeanDefinition) {
- AbstractBeanDefinition abd = (AbstractBeanDefinition) bd;
- AutowireCandidateQualifier candidate = abd.getQualifier(Qualifier.class.getName());
- if ((candidate != null && qualifier.equals(candidate.getAttribute(AutowireCandidateQualifier.VALUE_KEY))) ||
- qualifier.equals(beanName) || ObjectUtils.containsElement(bf.getAliases(beanName), qualifier)) {
- return true;
- }
- }
- if (bd instanceof RootBeanDefinition) {
- Method factoryMethod = ((RootBeanDefinition) bd).getResolvedFactoryMethod();
- if (factoryMethod != null) {
- Qualifier targetAnnotation = factoryMethod.getAnnotation(Qualifier.class);
- if (targetAnnotation != null && qualifier.equals(targetAnnotation.value())) {
- return true;
- }
- }
- }
- }
- catch (NoSuchBeanDefinitionException ex) {
- // ignore - can't compare qualifiers for a manually registered singleton object
- }
- }
- return false;
+ return BeanFactoryUtils.qualifiedBeanOfType(bf, PlatformTransactionManager.class, qualifier);
}
}
diff --git a/src/dist/changelog.txt b/src/dist/changelog.txt
index 9ced58bd93..5a9f5059ea 100644
--- a/src/dist/changelog.txt
+++ b/src/dist/changelog.txt
@@ -29,6 +29,7 @@ Changes in version 3.2 M1
* add option in MappingJacksonJsonView for setting the Content-Length header
* decode path variables when url decoding is turned off in AbstractHandlerMapping
* add required flag to @RequestBody annotation
+* support executor qualification with @Async#value (SPR-6847)
Changes in version 3.1.1 (2012-02-16)
-------------------------------------
diff --git a/src/reference/docbook/scheduling.xml b/src/reference/docbook/scheduling.xml
index e6a360d1e4..75b31ba03e 100644
--- a/src/reference/docbook/scheduling.xml
+++ b/src/reference/docbook/scheduling.xml
@@ -638,6 +638,29 @@ public class SampleBeanInititalizer {
scheduler reference is provided for managing those methods annotated
with @Scheduled.
+
+
+ Executor qualification with @Async
+
+ By default when specifying @Async on
+ a method, the executor that will be used is the one supplied to the
+ 'annotation-driven' element as described above. However, the
+ value attribute of the
+ @Async annotation can be used when needing
+ to indicate that an executor other than the default should be used when
+ executing a given method.
+ @Async("otherExecutor")
+void doSomething(String s) {
+ // this will be executed asynchronously by "otherExecutor"
+}
+
+ In this case, "otherExecutor" may be the name of any
+ Executor bean in the Spring container, or
+ may be the name of a qualifier associated with any
+ Executor , e.g. as specified with the
+ <qualifier> element or Spring's
+ @Qualifier annotation.
+