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 00000000000..f1bc5cf856f --- /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 b8b6f257d9e..20ed49adfbf 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 @@ -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,39 +44,39 @@ 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 {@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; + public AsyncExecutionInterceptor(Executor executor) { + super(executor); } /** - * Create a new AsyncExecutionInterceptor. - * @param asyncExecutor the java.util.concurrent Executor - * to delegate to (typically a {@link java.util.concurrent.ExecutorService} + * 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 AsyncExecutionInterceptor(Executor asyncExecutor) { - this.asyncExecutor = new TaskExecutorAdapter(asyncExecutor); - } - - public Object invoke(final MethodInvocation invocation) throws Throwable { - Future result = this.asyncExecutor.submit( + Future result = this.determineAsyncExecutor(invocation.getMethod()).submit( new Callable() { public Object call() throws Exception { try { @@ -99,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-aspects/src/main/java/org/springframework/scheduling/aspectj/AbstractAsyncExecutionAspect.aj b/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AbstractAsyncExecutionAspect.aj index aaa13647f6a..c8abf4a7297 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 @@ -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,19 +34,18 @@ import org.springframework.core.task.support.TaskExecutorAdapter; * * @author Ramnivas Laddad * @author Juergen Hoeller + * @author Chris Beams * @since 3.0.5 */ -public abstract aspect AbstractAsyncExecutionAspect { - - private AsyncTaskExecutor asyncExecutor; +public abstract aspect AbstractAsyncExecutionAspect extends AsyncExecutionAspectSupport { - 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); } /** @@ -57,7 +56,9 @@ public abstract aspect AbstractAsyncExecutionAspect { * 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() { @@ -68,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 { 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 d7144ddedb8..157215037a1 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 @@ -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; /** @@ -31,6 +35,7 @@ import org.springframework.scheduling.annotation.Async; * constraint, it produces only a warning. * * @author Ramnivas Laddad + * @author Chris Beams * @since 3.0.5 */ public aspect AnnotationAsyncExecutionAspect extends AbstractAsyncExecutionAspect { @@ -43,6 +48,28 @@ public aspect AnnotationAsyncExecutionAspect extends AbstractAsyncExecutionAspec public pointcut asyncMethod() : asyncMarkedMethod() || asyncTypeMarkedMethod(); + /** + * {@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"; 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 43fde1fcaad..6bfe60205ea 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 @@ -22,9 +22,16 @@ import java.util.concurrent.Future; 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.*; @@ -104,6 +111,22 @@ public class AnnotationAsyncExecutionAspectTests { 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 { @@ -180,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-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 00000000000..6ae62fe6e3a --- /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 7236747d080..be600d325c7 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 @@ -37,8 +37,9 @@ import java.lang.annotation.Target; * 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 2e20b2cf600..571ac6a30eb 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 @@ -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; @@ -51,12 +52,14 @@ import org.springframework.util.Assert; * @see org.springframework.dao.support.PersistenceExceptionTranslator */ @SuppressWarnings("serial") -public class AsyncAnnotationAdvisor extends AbstractPointcutAdvisor { +public class AsyncAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware { private Advice advice; private Pointcut pointcut; + private BeanFactory beanFactory; + /** * Create a new {@code AsyncAnnotationAdvisor} for bean-style configuration. @@ -81,14 +84,30 @@ public class AsyncAnnotationAdvisor extends AbstractPointcutAdvisor { // 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); } /** @@ -118,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 8f2adc277ba..77ad6786a58 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 86ebacfd3e2..3091b0ba071 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 @@ -36,6 +36,9 @@ Specifies the java.util.Executor instance to use when invoking asynchronous methods. If not provided, an instance of org.springframework.core.task.SimpleAsyncTaskExecutor will be used by default. + Note that as of Spring 3.2, individual @Async methods may qualify which executor to + use, meaning that the executor specified here acts as a default for all non-qualified + @Async methods. ]]> @@ -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 00000000000..b0ddea9e169 --- /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 209ba73117b..48317d6edae 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 @@ -25,11 +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.concurrent.ThreadPoolTaskExecutor; import static org.junit.Assert.*; /** * @author Juergen Hoeller + * @author Chris Beams */ public class AsyncExecutionTests { @@ -55,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(); @@ -165,6 +187,34 @@ 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 { 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 eb403b9a42f..e96c31f7395 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 @@ -21,7 +21,9 @@ 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; @@ -29,6 +31,7 @@ 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; @@ -71,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; @@ -208,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/src/dist/changelog.txt b/src/dist/changelog.txt index 9ced58bd939..5a9f5059eaf 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 e6a360d1e48..75b31ba03e6 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. +