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) 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 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. +