See {@link EnableAsync} for usage examples. + * + * @author Chris Beams + * @since 3.1 + * @see AbstractAsyncConfiguration + * @see EnableAsync + */ +public interface AsyncConfigurer { + + /** + * The {@link Executor} instance to be used when processing async + * method invocations. + */ + Executor getExecutor(); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scheduling/annotation/EnableAsync.java b/org.springframework.context/src/main/java/org/springframework/scheduling/annotation/EnableAsync.java new file mode 100644 index 00000000000..3749bc03838 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scheduling/annotation/EnableAsync.java @@ -0,0 +1,121 @@ +/* + * Copyright 2002-2011 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.annotation.Annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Import; +import org.springframework.context.config.AdviceMode; +import org.springframework.core.Ordered; + +/** + * Enables Spring's asynchronous method execution capability. To be used + * on @{@link Configuration} classes as follows: + * + *
+ * @Configuration
+ * @EnableAsync
+ * public class AppConfig {
+ * @Bean
+ * public MyAsyncBean asyncBean() {
+ * return new MyAsyncBean();
+ * }
+ * }
+ *
+ * The various attributes of the annotation control how advice + * is applied ({@link #mode()}), and if the mode is {@link AdviceMode#PROXY} + * (the default), the other attributes control the behavior of the proxying. + * + *
Note that if the {@linkplain #mode} is set to {@link AdviceMode#ASPECTJ} + * the {@code org.springframework.aspects} module must be present on the classpath. + * + *
By default, a {@link org.springframework.core.task.SimpleAsyncTaskExecutor + * SimpleAsyncTaskExecutor} will be used to process async method invocations. To + * customize this behavior, implement {@link AsyncConfigurer} and + * provide your own {@link java.util.concurrent.Executor Executor} through the + * {@link AsyncConfigurer#getExecutor() getExecutor()} method. + * + *
+ * @Configuration
+ * @EnableAsync
+ * public class AppConfig implements AsyncConfigurer {
+ *
+ * @Bean
+ * public MyAsyncBean asyncBean() {
+ * return new MyAsyncBean();
+ * }
+ *
+ * public Executor getExecutor() {
+ * ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+ * executor.setThreadNamePrefix("Custom-");
+ * executor.initialize();
+ * return executor;
+ * }
+ * }
+ *
+ * @author Chris Beams
+ * @since 3.1
+ * @see Async
+ * @see AsyncConfigurer
+ * @see AsyncConfigurationSelector
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Import(AsyncConfigurationSelector.class)
+@Documented
+public @interface EnableAsync {
+
+ /**
+ * Indicate the 'async' annotation type to be detected at either class
+ * or method level. By default, both the {@link Async} annotation and
+ * the EJB 3.1 javax.ejb.Asynchronous annotation will be
+ * detected. This setter property exists so that developers can provide + * their own (non-Spring-specific) annotation type to indicate that a method + * (or all methods of a given class) should be invoked asynchronously. + */ + Class extends Annotation> annotation() default Annotation.class; + + /** + * Indicate whether class-based (CGLIB) proxies are to be created as opposed + * to standard Java interface-based proxies. The default is {@code false} + * + *
Note: Class-based proxies require the async {@link #annotation()} + * to be defined on the concrete class. Annotations in interfaces will + * not work in that case (they will rather only work with interface-based proxies)! + */ + boolean proxyTargetClass() default false; + + /** + * Indicate how async advice should be applied. + * The default is {@link AdviceMode#PROXY}. + */ + AdviceMode mode() default AdviceMode.PROXY; + + /** + * Indicate the order in which the + * {@link org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor} + * should be applied. Defaults to Order.LOWEST_PRIORITY in order to run + * after all other post-processors, so that it can add an advisor to + * existing proxies rather than double-proxy. + */ + int order() default Ordered.LOWEST_PRECEDENCE; +} diff --git a/org.springframework.context/src/main/java/org/springframework/scheduling/annotation/ProxyAsyncConfiguration.java b/org.springframework.context/src/main/java/org/springframework/scheduling/annotation/ProxyAsyncConfiguration.java new file mode 100644 index 00000000000..cc0136795b0 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scheduling/annotation/ProxyAsyncConfiguration.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2011 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.annotation.Annotation; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.AnnotationConfigUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.Assert; + +/** + * Enables proxy-based asynchronous method execution. + * + * @author Chris Beams + * @since 3.1 + * @see EnableAsync + * @see EnableAsync#mode + */ +@Configuration +public class ProxyAsyncConfiguration extends AbstractAsyncConfiguration { + + @Override + @Bean(name=AnnotationConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME) + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public AsyncAnnotationBeanPostProcessor asyncAdvisor() { + Assert.notNull(enableAsync, "@EnableAsync annotation metadata was not injected"); + + AsyncAnnotationBeanPostProcessor bpp = new AsyncAnnotationBeanPostProcessor(); + + @SuppressWarnings("unchecked") + Class extends Annotation> customAsyncAnnotation = + (Class extends Annotation>) enableAsync.get("annotation"); + if (customAsyncAnnotation != AnnotationUtils.getDefaultValue(EnableAsync.class, "annotation")) { + bpp.setAsyncAnnotationType(customAsyncAnnotation); + } + + if (this.executor != null) { + bpp.setExecutor(this.executor); + } + + bpp.setProxyTargetClass((Boolean) enableAsync.get("proxyTargetClass")); + + bpp.setOrder(((Integer) enableAsync.get("order"))); + + return bpp; + } + +} diff --git a/org.springframework.context/src/test/java/org/springframework/scheduling/annotation/EnableAsyncTests.java b/org.springframework.context/src/test/java/org/springframework/scheduling/annotation/EnableAsyncTests.java new file mode 100644 index 00000000000..8a4ca9ce863 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/scheduling/annotation/EnableAsyncTests.java @@ -0,0 +1,211 @@ +/* + * Copyright 2002-2011 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 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.Executor; + +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.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.config.AdviceMode; +import org.springframework.core.Ordered; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +/** + * Tests use of @EnableAsync on @Configuration classes. + * + * @author Chris Beams + * @since 3.1 + */ +public class EnableAsyncTests { + + @Test + public void proxyingOccurs() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(AsyncConfig.class); + ctx.refresh(); + + AsyncBean asyncBean = ctx.getBean(AsyncBean.class); + assertThat(AopUtils.isAopProxy(asyncBean), is(true)); + asyncBean.work(); + } + + + @Configuration + @EnableAsync + static class AsyncConfig { + @Bean + public AsyncBean asyncBean() { + return new AsyncBean(); + } + } + + + static class AsyncBean { + private Thread threadOfExecution; + + @Async + public void work() { + this.threadOfExecution = Thread.currentThread(); + } + + public Thread getThreadOfExecution() { + return threadOfExecution; + } + } + + + @Test + public void asyncProcessorIsOrderedLowestPrecedenceByDefault() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(AsyncConfig.class); + ctx.refresh(); + + AsyncAnnotationBeanPostProcessor bpp = ctx.getBean(AsyncAnnotationBeanPostProcessor.class); + assertThat(bpp.getOrder(), is(Ordered.LOWEST_PRECEDENCE)); + } + + + @Test + public void orderAttributeIsPropagated() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(OrderedAsyncConfig.class); + ctx.refresh(); + + AsyncAnnotationBeanPostProcessor bpp = ctx.getBean(AsyncAnnotationBeanPostProcessor.class); + assertThat(bpp.getOrder(), is(Ordered.HIGHEST_PRECEDENCE)); + } + + + @Configuration + @EnableAsync(order=Ordered.HIGHEST_PRECEDENCE) + static class OrderedAsyncConfig { + @Bean + public AsyncBean asyncBean() { + return new AsyncBean(); + } + } + + + @Test + public void customAsyncAnnotationIsPropagated() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(CustomAsyncAnnotationConfig.class); + ctx.refresh(); + + Object bean = ctx.getBean(CustomAsyncBean.class); + assertTrue(AopUtils.isAopProxy(bean)); + boolean isAsyncAdvised = false; + for (Advisor advisor : ((Advised)bean).getAdvisors()) { + if (advisor instanceof AsyncAnnotationAdvisor) { + isAsyncAdvised = true; + break; + } + } + assertTrue("bean was not async advised as expected", isAsyncAdvised); + } + + + @Configuration + @EnableAsync(annotation=CustomAsync.class) + static class CustomAsyncAnnotationConfig { + @Bean + public CustomAsyncBean asyncBean() { + return new CustomAsyncBean(); + } + } + + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @interface CustomAsync { + } + + + static class CustomAsyncBean { + @CustomAsync + public void work() { + } + } + + + /** + * Fails with classpath errors on trying to classload AnnotationAsyncExecutionAspect + */ + @Test(expected=BeanDefinitionStoreException.class) + public void aspectModeAspectJAttemptsToRegisterAsyncAspect() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(AspectJAsyncAnnotationConfig.class); + ctx.refresh(); + } + + + @Configuration + @EnableAsync(mode=AdviceMode.ASPECTJ) + static class AspectJAsyncAnnotationConfig { + @Bean + public AsyncBean asyncBean() { + return new AsyncBean(); + } + } + + + @Test + public void customExecutorIsPropagated() throws InterruptedException { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(CustomExecutorAsyncConfig.class); + ctx.refresh(); + + AsyncBean asyncBean = ctx.getBean(AsyncBean.class); + asyncBean.work(); + Thread.sleep(500); + ctx.close(); + assertThat(asyncBean.getThreadOfExecution().getName(), startsWith("Custom-")); + } + + + @Configuration + @EnableAsync + static class CustomExecutorAsyncConfig implements AsyncConfigurer { + @Bean + public AsyncBean asyncBean() { + return new AsyncBean(); + } + + public Executor getExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setThreadNamePrefix("Custom-"); + executor.initialize(); + return executor; + } + + } +}