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 3da0d2e3d0..45df368430 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-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -23,6 +23,8 @@ import java.util.concurrent.Future; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.aop.support.AopUtils; import org.springframework.core.BridgeMethodResolver; @@ -46,12 +48,18 @@ import org.springframework.util.ReflectionUtils; * (like Spring's {@link org.springframework.scheduling.annotation.AsyncResult} * or EJB 3.1's {@code javax.ejb.AsyncResult}). * + *

When the return type is {@code java.util.concurrent.Future}, any exception thrown + * during the execution can be accessed and managed by the caller. With {@code void} + * return type however, such exceptions cannot be transmitted back. In that case an + * {@link AsyncUncaughtExceptionHandler} can be registered to process such exceptions. + * *

As of Spring 3.1.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 + * @author Stephane Nicoll * @since 3.0 * @see org.springframework.scheduling.annotation.Async * @see org.springframework.scheduling.annotation.AsyncAnnotationAdvisor @@ -60,15 +68,35 @@ import org.springframework.util.ReflectionUtils; public class AsyncExecutionInterceptor extends AsyncExecutionAspectSupport implements MethodInterceptor, Ordered { + private final Log logger = LogFactory.getLog(getClass()); + + private AsyncUncaughtExceptionHandler exceptionHandler; + /** * Create a new {@code AsyncExecutionInterceptor}. - * @param executor the {@link Executor} (typically a Spring {@link AsyncTaskExecutor} + * @param defaultExecutor the {@link Executor} (typically a Spring {@link AsyncTaskExecutor} * or {@link java.util.concurrent.ExecutorService}) to delegate to. + * @param exceptionHandler the {@link AsyncUncaughtExceptionHandler} to use */ - public AsyncExecutionInterceptor(Executor executor) { - super(executor); + public AsyncExecutionInterceptor(Executor defaultExecutor, AsyncUncaughtExceptionHandler exceptionHandler) { + super(defaultExecutor); + this.exceptionHandler = exceptionHandler; } + /** + * Create a new instance with a default {@link AsyncUncaughtExceptionHandler}. + */ + public AsyncExecutionInterceptor(Executor defaultExecutor) { + this(defaultExecutor, new SimpleAsyncUncaughtExceptionHandler()); + } + + /** + * Supply the {@link AsyncUncaughtExceptionHandler} to use to handle exceptions + * thrown by invoking asynchronous methods with a {@code void} return type. + */ + public void setExceptionHandler(AsyncUncaughtExceptionHandler exceptionHandler) { + this.exceptionHandler = exceptionHandler; + } /** * Intercept the given method invocation, submit the actual calling of the method to @@ -80,8 +108,8 @@ public class AsyncExecutionInterceptor extends AsyncExecutionAspectSupport @Override public Object invoke(final MethodInvocation invocation) throws Throwable { Class targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null); - Method specificMethod = ClassUtils.getMostSpecificMethod(invocation.getMethod(), targetClass); - specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); + Method tmp = ClassUtils.getMostSpecificMethod(invocation.getMethod(), targetClass); + final Method specificMethod = BridgeMethodResolver.findBridgedMethod(tmp); AsyncTaskExecutor executor = determineAsyncExecutor(specificMethod); if (executor == null) { @@ -100,7 +128,7 @@ public class AsyncExecutionInterceptor extends AsyncExecutionAspectSupport } } catch (Throwable ex) { - ReflectionUtils.rethrowException(ex); + handleError(ex, specificMethod, invocation.getArguments()); } return null; } @@ -114,6 +142,34 @@ public class AsyncExecutionInterceptor extends AsyncExecutionAspectSupport } } + /** + * Handles a fatal error thrown while asynchronously invoking the specified + * {@link Method}. + *

If the return type of the method is a {@link Future} object, the original + * exception can be propagated by just throwing it at the higher level. However, + * for all other cases, the exception will not be transmitted back to the client. + * In that later case, the current {@link AsyncUncaughtExceptionHandler} will be + * used to manage such exception. + * + * @param ex the exception to handle + * @param method the method that was invoked + * @param params the parameters used to invoke the method + */ + protected void handleError(Throwable ex, Method method, Object... params) throws Exception { + if (method.getReturnType().isAssignableFrom(Future.class)) { + ReflectionUtils.rethrowException(ex); + } + else { // Could not transmit the exception to the caller with default executor + try { + exceptionHandler.handleUncaughtException(ex, method, params); + } + catch (Exception e) { + logger.error("exception handler has thrown an unexpected " + + "exception while invoking '" + method.toGenericString() + "'", e); + } + } + } + /** * This implementation is a no-op for compatibility in Spring 3.1.2. * Subclasses may override to provide support for extracting qualifier information, diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncUncaughtExceptionHandler.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncUncaughtExceptionHandler.java new file mode 100644 index 0000000000..1d54b0da0b --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncUncaughtExceptionHandler.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2014 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; + +/** + * A strategy for handling uncaught exception thrown by asynchronous methods. + * + *

An asynchronous method usually returns a {@link java.util.concurrent.Future} + * instance that gives access to the underlying exception. When the method + * does not provide that return type, this handler can be used to managed such + * uncaught exceptions. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public interface AsyncUncaughtExceptionHandler { + + /** + * Handle the given uncaught error thrown while processing + * an asynchronous method. + * @param ex the exception thrown by invoking the async operation + * @param method the async operation + * @param params the parameters used to invoked the method + */ + void handleUncaughtException(Throwable ex, Method method, Object... params); +} diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/SimpleAsyncUncaughtExceptionHandler.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/SimpleAsyncUncaughtExceptionHandler.java new file mode 100644 index 0000000000..984a7ec245 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/SimpleAsyncUncaughtExceptionHandler.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2014 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 org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * A default {@link AsyncUncaughtExceptionHandler} that simply logs the exception. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public class SimpleAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler { + + private final Log logger = LogFactory.getLog(SimpleAsyncUncaughtExceptionHandler.class); + + @Override + public void handleUncaughtException(Throwable ex, Method method, Object... params) { + if (logger.isErrorEnabled()) { + logger.error(String.format("Unexpected error occurred invoking async " + + "method '%s'.", method), ex); + } + } +} diff --git a/spring-beans/src/main/resources/META-INF/spring.schemas b/spring-beans/src/main/resources/META-INF/spring.schemas index 7fe3564dc4..e09ad7e0d5 100644 --- a/spring-beans/src/main/resources/META-INF/spring.schemas +++ b/spring-beans/src/main/resources/META-INF/spring.schemas @@ -4,14 +4,16 @@ http\://www.springframework.org/schema/beans/spring-beans-3.0.xsd=org/springfram http\://www.springframework.org/schema/beans/spring-beans-3.1.xsd=org/springframework/beans/factory/xml/spring-beans-3.1.xsd http\://www.springframework.org/schema/beans/spring-beans-3.2.xsd=org/springframework/beans/factory/xml/spring-beans-3.2.xsd http\://www.springframework.org/schema/beans/spring-beans-4.0.xsd=org/springframework/beans/factory/xml/spring-beans-4.0.xsd -http\://www.springframework.org/schema/beans/spring-beans.xsd=org/springframework/beans/factory/xml/spring-beans-4.0.xsd +http\://www.springframework.org/schema/beans/spring-beans-4.1.xsd=org/springframework/beans/factory/xml/spring-beans-4.1.xsd +http\://www.springframework.org/schema/beans/spring-beans.xsd=org/springframework/beans/factory/xml/spring-beans-4.1.xsd http\://www.springframework.org/schema/tool/spring-tool-2.0.xsd=org/springframework/beans/factory/xml/spring-tool-2.0.xsd http\://www.springframework.org/schema/tool/spring-tool-2.5.xsd=org/springframework/beans/factory/xml/spring-tool-2.5.xsd http\://www.springframework.org/schema/tool/spring-tool-3.0.xsd=org/springframework/beans/factory/xml/spring-tool-3.0.xsd http\://www.springframework.org/schema/tool/spring-tool-3.1.xsd=org/springframework/beans/factory/xml/spring-tool-3.1.xsd http\://www.springframework.org/schema/tool/spring-tool-3.2.xsd=org/springframework/beans/factory/xml/spring-tool-3.2.xsd http\://www.springframework.org/schema/tool/spring-tool-4.0.xsd=org/springframework/beans/factory/xml/spring-tool-4.0.xsd -http\://www.springframework.org/schema/tool/spring-tool.xsd=org/springframework/beans/factory/xml/spring-tool-4.0.xsd +http\://www.springframework.org/schema/tool/spring-tool-4.1.xsd=org/springframework/beans/factory/xml/spring-tool-4.1.xsd +http\://www.springframework.org/schema/tool/spring-tool.xsd=org/springframework/beans/factory/xml/spring-tool-4.1.xsd http\://www.springframework.org/schema/util/spring-util-2.0.xsd=org/springframework/beans/factory/xml/spring-util-2.0.xsd http\://www.springframework.org/schema/util/spring-util-2.5.xsd=org/springframework/beans/factory/xml/spring-util-2.5.xsd http\://www.springframework.org/schema/util/spring-util-3.0.xsd=org/springframework/beans/factory/xml/spring-util-3.0.xsd diff --git a/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-beans-4.1.xsd b/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-beans-4.1.xsd new file mode 100644 index 0000000000..78ef2dcc10 --- /dev/null +++ b/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-beans-4.1.xsd @@ -0,0 +1,1185 @@ + + + + + + + + + + + + + + + + + + element. + ]]> + + + + + + + + and other elements, typically the root element in the document. + Allows the definition of default values for all nested bean definitions. May itself + be nested for the purpose of defining a subset of beans with certain default values or + to be registered only when certain profile(s) are active. Any such nested element + must be declared as the last element in the document. + ]]> + + + + + + + + + + + + + + + element should be parsed. Multiple profiles + can be separated by spaces, commas, or semi-colons. + + If one or more of the specified profiles are active at time of parsing, the + element will be parsed, and all of its elements registered, <import> + elements followed, etc. If none of the specified profiles are active at time of + parsing, then the entire element and its contents will be ignored. + + If a profile is prefixed with the NOT operator '!', e.g. + + + + indicates that the element should be parsed if profile "p1" is active or + if profile "p2" is not active. + + Profiles are activated in one of two ways: + Programmatic: + ConfigurableEnvironment#setActiveProfiles(String...) + ConfigurableEnvironment#setDefaultProfiles(String...) + + Properties (typically through -D system properties, environment variables, or + servlet context init params): + spring.profiles.active=p1,p2 + spring.profiles.default=p1,p2 + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + element (or "ref" + attribute). We recommend this in most cases as it makes documentation + more explicit. + + Note that this default mode also allows for annotation-driven autowiring, + if activated. "no" refers to externally driven autowiring only, not + affecting any autowiring demands that the bean class itself expresses. + + 2. "byName" + Autowiring by property name. If a bean of class Cat exposes a "dog" + property, Spring will try to set this to the value of the bean "dog" + in the current container. If there is no matching bean by name, nothing + special happens. + + 3. "byType" + Autowiring if there is exactly one bean of the property type in the + container. If there is more than one, a fatal error is raised, and + you cannot use byType autowiring for that bean. If there is none, + nothing special happens. + + 4. "constructor" + Analogous to "byType" for constructor arguments. If there is not exactly + one bean of the constructor argument type in the bean factory, a fatal + error is raised. + + Note that explicit dependencies, i.e. "property" and "constructor-arg" + elements, always override autowiring. + + Note: This attribute will not be inherited by child bean definitions. + Hence, it needs to be specified per concrete bean definition. + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + " element. + ]]> + + + + + ..." element. + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ". + ]]> + + + + + ..." element. + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ". + ]]> + + + + + ..." + element. + ]]> + + + + + ". + ]]> + + + + + ..." element. + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-tool-4.1.xsd b/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-tool-4.1.xsd new file mode 100644 index 0000000000..9d84906ade --- /dev/null +++ b/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-tool-4.1.xsd @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AbstractAsyncConfiguration.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AbstractAsyncConfiguration.java index 95ba8a99ef..78cf5691fa 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/AbstractAsyncConfiguration.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AbstractAsyncConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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.scheduling.annotation; import java.util.Collection; import java.util.concurrent.Executor; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportAware; @@ -32,6 +33,7 @@ import org.springframework.util.CollectionUtils; * Spring's asynchronous method execution capability. * * @author Chris Beams + * @author Stephane Nicoll * @since 3.1 * @see EnableAsync */ @@ -41,6 +43,7 @@ public abstract class AbstractAsyncConfiguration implements ImportAware { protected AnnotationAttributes enableAsync; protected Executor executor; + protected AsyncUncaughtExceptionHandler exceptionHandler; @Override @@ -64,6 +67,7 @@ public abstract class AbstractAsyncConfiguration implements ImportAware { } AsyncConfigurer configurer = configurers.iterator().next(); this.executor = configurer.getAsyncExecutor(); + this.exceptionHandler = configurer.getAsyncUncaughtExceptionHandler(); } } 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 index 542aa15e32..61ea21a0d8 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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,6 +20,8 @@ import java.lang.reflect.Method; import java.util.concurrent.Executor; import org.springframework.aop.interceptor.AsyncExecutionInterceptor; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler; import org.springframework.core.annotation.AnnotationUtils; /** @@ -30,6 +32,7 @@ import org.springframework.core.annotation.AnnotationUtils; * declaring class level. See {@link #getExecutorQualifier(Method)} for details. * * @author Chris Beams + * @author Stephane Nicoll * @since 3.1.2 * @see org.springframework.scheduling.annotation.Async * @see org.springframework.scheduling.annotation.AsyncAnnotationAdvisor @@ -40,9 +43,23 @@ public class AnnotationAsyncExecutionInterceptor extends AsyncExecutionIntercept * 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()} + * @param exceptionHandler the {@link AsyncUncaughtExceptionHandler} to use to + * handle exceptions thrown by asynchronous method executions with {@code void} + * return type + */ + public AnnotationAsyncExecutionInterceptor(Executor defaultExecutor, + AsyncUncaughtExceptionHandler exceptionHandler) { + super(defaultExecutor, exceptionHandler); + } + + /** + * Create a new {@code AnnotationAsyncExecutionInterceptor} with the given executor + * and a simple {@link AsyncUncaughtExceptionHandler}. + * @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); + this(defaultExecutor, new SimpleAsyncUncaughtExceptionHandler()); } /** 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 11cffe2542..ba66479ba2 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-2013 the original author or authors. + * Copyright 2002-2014 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,6 +25,8 @@ import java.util.concurrent.Executor; import org.aopalliance.aop.Advice; import org.springframework.aop.Pointcut; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler; import org.springframework.aop.support.AbstractPointcutAdvisor; import org.springframework.aop.support.ComposablePointcut; import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; @@ -53,6 +55,8 @@ import org.springframework.util.Assert; @SuppressWarnings("serial") public class AsyncAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware { + private AsyncUncaughtExceptionHandler exceptionHandler; + private Advice advice; private Pointcut pointcut; @@ -62,15 +66,17 @@ public class AsyncAnnotationAdvisor extends AbstractPointcutAdvisor implements B * Create a new {@code AsyncAnnotationAdvisor} for bean-style configuration. */ public AsyncAnnotationAdvisor() { - this(new SimpleAsyncTaskExecutor()); + this(null, null); } /** * Create a new {@code AsyncAnnotationAdvisor} for the given task executor. * @param executor the task executor to use for asynchronous methods + * @param exceptionHandler the {@link org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler} to use to + * handle unexpected exception thrown by asynchronous method executions */ @SuppressWarnings("unchecked") - public AsyncAnnotationAdvisor(Executor executor) { + public AsyncAnnotationAdvisor(Executor executor, AsyncUncaughtExceptionHandler exceptionHandler) { Set> asyncAnnotationTypes = new LinkedHashSet>(2); asyncAnnotationTypes.add(Async.class); ClassLoader cl = AsyncAnnotationAdvisor.class.getClassLoader(); @@ -80,7 +86,15 @@ public class AsyncAnnotationAdvisor extends AbstractPointcutAdvisor implements B catch (ClassNotFoundException ex) { // If EJB 3.1 API not present, simply ignore. } - this.advice = buildAdvice(executor); + if (executor == null) { + executor = new SimpleAsyncTaskExecutor(); + } + if (exceptionHandler != null) { + this.exceptionHandler = exceptionHandler; + } else { + this.exceptionHandler = new SimpleAsyncUncaughtExceptionHandler(); + } + this.advice = buildAdvice(executor, this.exceptionHandler); this.pointcut = buildPointcut(asyncAnnotationTypes); } @@ -89,7 +103,7 @@ public class AsyncAnnotationAdvisor extends AbstractPointcutAdvisor implements B * Specify the default task executor to use for asynchronous methods. */ public void setTaskExecutor(Executor executor) { - this.advice = buildAdvice(executor); + this.advice = buildAdvice(executor, exceptionHandler); } /** @@ -130,8 +144,8 @@ public class AsyncAnnotationAdvisor extends AbstractPointcutAdvisor implements B } - protected Advice buildAdvice(Executor executor) { - return new AnnotationAsyncExecutionInterceptor(executor); + protected Advice buildAdvice(Executor executor, AsyncUncaughtExceptionHandler exceptionHandler) { + return new AnnotationAsyncExecutionInterceptor(executor, exceptionHandler); } /** 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 202f2ae47d..f0a743951a 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-2013 the original author or authors. + * Copyright 2002-2014 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,6 +20,7 @@ import java.lang.annotation.Annotation; import java.util.concurrent.Executor; import org.springframework.aop.framework.AbstractAdvisingBeanPostProcessor; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.core.task.TaskExecutor; @@ -38,11 +39,17 @@ import org.springframework.util.Assert; * processor will detect both Spring's {@link Async @Async} annotation as well * as the EJB 3.1 {@code javax.ejb.Asynchronous} annotation. * + *

For methods having a {@code void} return type, any exception thrown + * during the asynchronous method invocation cannot be accessed by the + * caller. An {@link AsyncUncaughtExceptionHandler} can be specified to handle + * these cases. + * *

Note: The underlying async advisor applies before existing advisors by default, * in order to switch to async execution as early as possible in the invocation chain. * * @author Mark Fisher * @author Juergen Hoeller + * @author Stephane Nicoll * @since 3.0 * @see Async * @see AsyncAnnotationAdvisor @@ -55,6 +62,7 @@ public class AsyncAnnotationBeanPostProcessor extends AbstractAdvisingBeanPostPr private Class asyncAnnotationType; private Executor executor; + private AsyncUncaughtExceptionHandler exceptionHandler; public AsyncAnnotationBeanPostProcessor() { @@ -82,10 +90,17 @@ public class AsyncAnnotationBeanPostProcessor extends AbstractAdvisingBeanPostPr this.executor = executor; } + /** + * Set the {@link AsyncUncaughtExceptionHandler} to use to handle uncaught + * exceptions thrown by asynchronous method executions. + */ + public void setExceptionHandler(AsyncUncaughtExceptionHandler exceptionHandler) { + this.exceptionHandler = exceptionHandler; + } + @Override public void setBeanFactory(BeanFactory beanFactory) { - AsyncAnnotationAdvisor advisor = (this.executor != null ? - new AsyncAnnotationAdvisor(this.executor) : new AsyncAnnotationAdvisor()); + AsyncAnnotationAdvisor advisor = new AsyncAnnotationAdvisor(this.executor, this.exceptionHandler); if (this.asyncAnnotationType != null) { advisor.setAsyncAnnotationType(this.asyncAnnotationType); } diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurer.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurer.java index c57ba71e40..81d1e0afb4 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurer.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2014 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,17 +18,28 @@ package org.springframework.scheduling.annotation; import java.util.concurrent.Executor; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; + /** * Interface to be implemented by @{@link org.springframework.context.annotation.Configuration * Configuration} classes annotated with @{@link EnableAsync} that wish to customize the - * {@link Executor} instance used when processing async method invocations. + * {@link Executor} instance used when processing async method invocations or the + * {@link AsyncUncaughtExceptionHandler} instance used to process exception thrown from + * async method with {@code void} return type. + * + *

Consider using {@link AsyncConfigurerSupport} providing default implementations for + * both methods if only one element needs to be customized. Furthermore, backward compatibility + * of this interface will be insured in case new customization options are introduced + * in the future. * *

See @{@link EnableAsync} for usage examples. * * @author Chris Beams + * @author Stephane Nicoll * @since 3.1 * @see AbstractAsyncConfiguration * @see EnableAsync + * @see AsyncConfigurerSupport */ public interface AsyncConfigurer { @@ -38,4 +49,11 @@ public interface AsyncConfigurer { */ Executor getAsyncExecutor(); + /** + * The {@link AsyncUncaughtExceptionHandler} instance to be used + * when an exception is thrown during an asynchronous method execution + * with {@code void} return type. + */ + AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler(); + } diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurerSupport.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurerSupport.java new file mode 100644 index 0000000000..27a953379f --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurerSupport.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2014 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.util.concurrent.Executor; + +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; + +/** + * A convenience {@link AsyncConfigurer} that implements all methods + * so that the defaults are used. Provides a backward compatible alternative + * of implementing {@link AsyncConfigurer} directly. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public class AsyncConfigurerSupport implements AsyncConfigurer { + + @Override + public Executor getAsyncExecutor() { + return null; + } + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return null; + } +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/EnableAsync.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/EnableAsync.java index 8d88bf85dd..e02ce9dd5c 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/EnableAsync.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/EnableAsync.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2014 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. @@ -56,10 +56,20 @@ import org.springframework.core.Ordered; * {@code spring-aspects} module JAR 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#getAsyncExecutor() getExecutor()} method. + * SimpleAsyncTaskExecutor} will be used to process async method invocations. Besides, + * annotated methods having a {@code void} return type cannot transmit any exception + * back to the caller. By default, such uncaught exceptions are only logged. + * + *

To customize all this, implement {@link AsyncConfigurer} and + * provide: + *

* *
  * @Configuration
@@ -81,19 +91,29 @@ import org.springframework.core.Ordered;
  *         executor.initialize();
  *         return executor;
  *     }
+ *
+ *     @Override
+ *     public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
+ *         return MyAsyncUncaughtExceptionHandler();
+ *     }
  * }
* + *

If only one item needs to be customized, {@code null} can be returned to + * keep the default settings. Consider also extending from {@link AsyncConfigurerSupport} + * when possible. + * *

For reference, the example above can be compared to the following Spring XML * configuration: *

  * {@code
  * 
- *     
+ *     
  *     
  *     
+ *     
  * 
  * }
- * the examples are equivalent save the setting of the thread name prefix of the + * the examples are equivalent except the setting of the thread name prefix of the * Executor; this is because the the {@code task:} namespace {@code executor} element does * not expose such an attribute. This demonstrates how the code-based approach allows for * maximum configurability through direct access to actual componentry. @@ -105,6 +125,7 @@ import org.springframework.core.Ordered; * automatically when the bean is initialized. * * @author Chris Beams + * @author Stephane Nicoll * @since 3.1 * @see Async * @see AsyncConfigurer diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/ProxyAsyncConfiguration.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/ProxyAsyncConfiguration.java index 19595dfb48..c6f7443717 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/ProxyAsyncConfiguration.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/ProxyAsyncConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -31,6 +31,7 @@ import org.springframework.util.Assert; * to enable proxy-based asynchronous method execution. * * @author Chris Beams + * @author Stephane Nicoll * @since 3.1 * @see EnableAsync * @see AsyncConfigurationSelector @@ -50,6 +51,9 @@ public class ProxyAsyncConfiguration extends AbstractAsyncConfiguration { if (this.executor != null) { bpp.setExecutor(this.executor); } + if (this.exceptionHandler != null) { + bpp.setExceptionHandler(this.exceptionHandler); + } bpp.setProxyTargetClass(this.enableAsync.getBoolean("proxyTargetClass")); bpp.setOrder(this.enableAsync.getNumber("order")); return bpp; diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/AnnotationDrivenBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/scheduling/config/AnnotationDrivenBeanDefinitionParser.java index 7900b9988e..ac4a16f59a 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/config/AnnotationDrivenBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/scheduling/config/AnnotationDrivenBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2014 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. @@ -37,6 +37,7 @@ import org.springframework.util.StringUtils; * @author Juergen Hoeller * @author Ramnivas Laddad * @author Chris Beams + * @author Stephane Nicoll * @since 3.0 */ public class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { @@ -99,6 +100,10 @@ public class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParse if (StringUtils.hasText(executor)) { builder.addPropertyReference("executor", executor); } + String exceptionHandler = element.getAttribute("exception-handler"); + if (StringUtils.hasText(exceptionHandler)) { + builder.addPropertyReference("exceptionHandler", exceptionHandler); + } if (Boolean.valueOf(element.getAttribute(AopNamespaceUtils.PROXY_TARGET_CLASS_ATTRIBUTE))) { builder.addPropertyValue("proxyTargetClass", true); } diff --git a/spring-context/src/main/resources/META-INF/spring.schemas b/spring-context/src/main/resources/META-INF/spring.schemas index 6559107273..b246784089 100644 --- a/spring-context/src/main/resources/META-INF/spring.schemas +++ b/spring-context/src/main/resources/META-INF/spring.schemas @@ -22,7 +22,8 @@ http\://www.springframework.org/schema/task/spring-task-3.0.xsd=org/springframew http\://www.springframework.org/schema/task/spring-task-3.1.xsd=org/springframework/scheduling/config/spring-task-3.1.xsd http\://www.springframework.org/schema/task/spring-task-3.2.xsd=org/springframework/scheduling/config/spring-task-3.2.xsd http\://www.springframework.org/schema/task/spring-task-4.0.xsd=org/springframework/scheduling/config/spring-task-4.0.xsd -http\://www.springframework.org/schema/task/spring-task.xsd=org/springframework/scheduling/config/spring-task-4.0.xsd +http\://www.springframework.org/schema/task/spring-task-4.1.xsd=org/springframework/scheduling/config/spring-task-4.1.xsd +http\://www.springframework.org/schema/task/spring-task.xsd=org/springframework/scheduling/config/spring-task-4.1.xsd http\://www.springframework.org/schema/cache/spring-cache-3.1.xsd=org/springframework/cache/config/spring-cache-3.1.xsd http\://www.springframework.org/schema/cache/spring-cache-3.2.xsd=org/springframework/cache/config/spring-cache-3.2.xsd http\://www.springframework.org/schema/cache/spring-cache-4.0.xsd=org/springframework/cache/config/spring-cache-4.0.xsd diff --git a/spring-context/src/main/resources/org/springframework/scheduling/config/spring-task-4.1.xsd b/spring-context/src/main/resources/org/springframework/scheduling/config/spring-task-4.1.xsd new file mode 100644 index 0000000000..57f6296c02 --- /dev/null +++ b/spring-context/src/main/resources/org/springframework/scheduling/config/spring-task-4.1.xsd @@ -0,0 +1,308 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessorTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessorTests.java index 8611615193..faed2eb6df 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2014 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,34 +16,38 @@ package org.springframework.scheduling.annotation; +import static org.junit.Assert.*; + +import java.lang.reflect.Method; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import static org.junit.Assert.*; import org.junit.Test; import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.GenericXmlApplicationContext; import org.springframework.context.support.StaticApplicationContext; import org.springframework.core.io.ClassPathResource; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.util.ReflectionUtils; /** * @author Mark Fisher * @author Juergen Hoeller + * @author Stephane Nicoll */ public class AsyncAnnotationBeanPostProcessorTests { @Test public void proxyCreated() { - StaticApplicationContext context = new StaticApplicationContext(); - BeanDefinition processorDefinition = new RootBeanDefinition(AsyncAnnotationBeanPostProcessor.class); - BeanDefinition targetDefinition = new RootBeanDefinition(AsyncAnnotationBeanPostProcessorTests.TestBean.class); - context.registerBeanDefinition("postProcessor", processorDefinition); - context.registerBeanDefinition("target", targetDefinition); - context.refresh(); + ConfigurableApplicationContext context = initContext( + new RootBeanDefinition(AsyncAnnotationBeanPostProcessor.class)); Object target = context.getBean("target"); assertTrue(AopUtils.isAopProxy(target)); context.close(); @@ -51,12 +55,8 @@ public class AsyncAnnotationBeanPostProcessorTests { @Test public void invokedAsynchronously() { - StaticApplicationContext context = new StaticApplicationContext(); - BeanDefinition processorDefinition = new RootBeanDefinition(AsyncAnnotationBeanPostProcessor.class); - BeanDefinition targetDefinition = new RootBeanDefinition(AsyncAnnotationBeanPostProcessorTests.TestBean.class); - context.registerBeanDefinition("postProcessor", processorDefinition); - context.registerBeanDefinition("target", targetDefinition); - context.refresh(); + ConfigurableApplicationContext context = initContext( + new RootBeanDefinition(AsyncAnnotationBeanPostProcessor.class)); ITestBean testBean = (ITestBean) context.getBean("target"); testBean.test(); Thread mainThread = Thread.currentThread(); @@ -68,16 +68,12 @@ public class AsyncAnnotationBeanPostProcessorTests { @Test public void threadNamePrefix() { - StaticApplicationContext context = new StaticApplicationContext(); BeanDefinition processorDefinition = new RootBeanDefinition(AsyncAnnotationBeanPostProcessor.class); ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setThreadNamePrefix("testExecutor"); executor.afterPropertiesSet(); processorDefinition.getPropertyValues().add("executor", executor); - BeanDefinition targetDefinition = new RootBeanDefinition(AsyncAnnotationBeanPostProcessorTests.TestBean.class); - context.registerBeanDefinition("postProcessor", processorDefinition); - context.registerBeanDefinition("target", targetDefinition); - context.refresh(); + ConfigurableApplicationContext context = initContext(processorDefinition); ITestBean testBean = (ITestBean) context.getBean("target"); testBean.test(); testBean.await(3000); @@ -96,9 +92,86 @@ public class AsyncAnnotationBeanPostProcessorTests { testBean.await(3000); Thread asyncThread = testBean.getThread(); assertTrue(asyncThread.getName().startsWith("testExecutor")); + + TestableAsyncUncaughtExceptionHandler exceptionHandler = (TestableAsyncUncaughtExceptionHandler) + context.getBean("exceptionHandler"); + assertFalse("handler should not have been called yet", exceptionHandler.isCalled()); + + testBean.failWithVoid(); + exceptionHandler.await(3000); + Method m = ReflectionUtils.findMethod(TestBean.class, "failWithVoid"); + exceptionHandler.assertCalledWith(m, UnsupportedOperationException.class); context.close(); } + @Test + public void handleExceptionWithFuture() { + ConfigurableApplicationContext context = initContext( + new RootBeanDefinition(AsyncAnnotationBeanPostProcessor.class)); + ITestBean testBean = context.getBean("target", ITestBean.class); + final Future result = testBean.failWithFuture(); + + try { + result.get(); + } + catch (InterruptedException e) { + fail("Should not have failed with InterruptedException"); + } + catch (ExecutionException e) { + // expected + assertEquals("Wrong exception cause", UnsupportedOperationException.class, e.getCause().getClass()); + } + } + + @Test + public void handleExceptionWithCustomExceptionHandler() { + Method m = ReflectionUtils.findMethod(TestBean.class, "failWithVoid"); + TestableAsyncUncaughtExceptionHandler exceptionHandler = + new TestableAsyncUncaughtExceptionHandler(); + BeanDefinition processorDefinition = new RootBeanDefinition(AsyncAnnotationBeanPostProcessor.class); + processorDefinition.getPropertyValues().add("exceptionHandler", exceptionHandler); + + ConfigurableApplicationContext context = initContext(processorDefinition); + ITestBean testBean = context.getBean("target", ITestBean.class); + + assertFalse("Handler should not have been called", exceptionHandler.isCalled()); + testBean.failWithVoid(); + exceptionHandler.await(3000); + exceptionHandler.assertCalledWith(m, UnsupportedOperationException.class); + } + + @Test + public void exceptionHandlerThrowsUnexpectedException() { + Method m = ReflectionUtils.findMethod(TestBean.class, "failWithVoid"); + TestableAsyncUncaughtExceptionHandler exceptionHandler = + new TestableAsyncUncaughtExceptionHandler(true); + BeanDefinition processorDefinition = new RootBeanDefinition(AsyncAnnotationBeanPostProcessor.class); + processorDefinition.getPropertyValues().add("exceptionHandler", exceptionHandler); + processorDefinition.getPropertyValues().add("executor", new DirectExecutor()); + + ConfigurableApplicationContext context = initContext(processorDefinition); + ITestBean testBean = context.getBean("target", ITestBean.class); + + assertFalse("Handler should not have been called", exceptionHandler.isCalled()); + try { + testBean.failWithVoid(); + exceptionHandler.assertCalledWith(m, UnsupportedOperationException.class); + } + catch (Exception e) { + fail("No unexpected exception should have been received"); + } + } + + private ConfigurableApplicationContext initContext( + BeanDefinition asyncAnnotationBeanPostProcessorDefinition) { + StaticApplicationContext context = new StaticApplicationContext(); + BeanDefinition targetDefinition = + new RootBeanDefinition(AsyncAnnotationBeanPostProcessorTests.TestBean.class); + context.registerBeanDefinition("postProcessor", asyncAnnotationBeanPostProcessorDefinition); + context.registerBeanDefinition("target", targetDefinition); + context.refresh(); + return context; + } private static interface ITestBean { @@ -106,6 +179,10 @@ public class AsyncAnnotationBeanPostProcessorTests { void test(); + Future failWithFuture(); + + void failWithVoid(); + void await(long timeout); } @@ -128,6 +205,16 @@ public class AsyncAnnotationBeanPostProcessorTests { this.latch.countDown(); } + @Async + public Future failWithFuture() { + throw new UnsupportedOperationException("failWithFuture"); + } + + @Async + public void failWithVoid() { + throw new UnsupportedOperationException("failWithVoid"); + } + @Override public void await(long timeout) { try { @@ -139,4 +226,10 @@ public class AsyncAnnotationBeanPostProcessorTests { } } + private static class DirectExecutor implements Executor { + @Override + public void execute(Runnable r) { + r.run(); + } + } } 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 bc30910d5f..f9a4e41da8 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-2012 the original author or authors. + * Copyright 2002-2014 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,6 +21,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.lang.reflect.Method; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Future; @@ -29,6 +30,7 @@ import org.junit.Test; import org.springframework.aop.Advisor; import org.springframework.aop.framework.Advised; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.annotation.Qualifier; @@ -38,6 +40,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.util.ReflectionUtils; import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.Matchers.startsWith; @@ -48,6 +51,7 @@ import static org.junit.Assert.*; * Tests use of @EnableAsync on @Configuration classes. * * @author Chris Beams + * @author Stephane Nicoll * @since 3.1 */ public class EnableAsyncTests { @@ -123,6 +127,11 @@ public class EnableAsyncTests { this.threadOfExecution = Thread.currentThread(); } + @Async + public void fail() { + throw new UnsupportedOperationException(); + } + public Thread getThreadOfExecution() { return threadOfExecution; } @@ -233,8 +242,18 @@ public class EnableAsyncTests { AsyncBean asyncBean = ctx.getBean(AsyncBean.class); asyncBean.work(); Thread.sleep(500); - ctx.close(); assertThat(asyncBean.getThreadOfExecution().getName(), startsWith("Custom-")); + + TestableAsyncUncaughtExceptionHandler exceptionHandler = (TestableAsyncUncaughtExceptionHandler) + ctx.getBean("exceptionHandler"); + assertFalse("handler should not have been called yet", exceptionHandler.isCalled()); + + asyncBean.fail(); + Thread.sleep(500); + Method m = ReflectionUtils.findMethod(AsyncBean.class, "fail"); + exceptionHandler.assertCalledWith(m, UnsupportedOperationException.class); + + ctx.close(); } @@ -253,6 +272,16 @@ public class EnableAsyncTests { executor.initialize(); return executor; } + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return exceptionHandler(); + } + + @Bean + public AsyncUncaughtExceptionHandler exceptionHandler() { + return new TestableAsyncUncaughtExceptionHandler(); + } } diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/TestableAsyncUncaughtExceptionHandler.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/TestableAsyncUncaughtExceptionHandler.java new file mode 100644 index 0000000000..5a9bcccd1e --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/TestableAsyncUncaughtExceptionHandler.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2014 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.junit.Assert.*; + +import java.lang.reflect.Method; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; + +/** + * A {@link AsyncUncaughtExceptionHandler} implementation used for testing purposes. + * @author Stephane Nicoll + */ +class TestableAsyncUncaughtExceptionHandler + implements AsyncUncaughtExceptionHandler { + + private final CountDownLatch latch = new CountDownLatch(1); + + private UncaughtExceptionDescriptor descriptor; + + private final boolean throwUnexpectedException; + + TestableAsyncUncaughtExceptionHandler() { + this(false); + } + + TestableAsyncUncaughtExceptionHandler(boolean throwUnexpectedException) { + this.throwUnexpectedException = throwUnexpectedException; + } + + @Override + public void handleUncaughtException(Throwable ex, Method method, Object... params) { + descriptor = new UncaughtExceptionDescriptor(ex, method); + this.latch.countDown(); + if (throwUnexpectedException) { + throw new IllegalStateException("Test exception"); + } + } + + public boolean isCalled() { + return descriptor != null; + } + + public void assertCalledWith(Method expectedMethod, Class expectedExceptionType) { + assertNotNull("Handler not called", descriptor); + assertEquals("Wrong exception type", expectedExceptionType, descriptor.ex.getClass()); + assertEquals("Wrong method", expectedMethod, descriptor.method); + } + + public void await(long timeout) { + try { + this.latch.await(timeout, TimeUnit.MILLISECONDS); + } + catch (Exception e) { + Thread.currentThread().interrupt(); + } + } + + private static class UncaughtExceptionDescriptor { + private final Throwable ex; + + private final Method method; + + private UncaughtExceptionDescriptor(Throwable ex, Method method) { + this.ex = ex; + this.method = method; + } + } +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/taskNamespaceTests.xml b/spring-context/src/test/java/org/springframework/scheduling/annotation/taskNamespaceTests.xml index 2dc689973c..231c6dce4e 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/annotation/taskNamespaceTests.xml +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/taskNamespaceTests.xml @@ -3,15 +3,15 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:task="http://www.springframework.org/schema/task" - xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd - http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd - http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd"> + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd + http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd + http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.1.xsd"> - + @@ -21,6 +21,9 @@ + + diff --git a/src/asciidoc/index.adoc b/src/asciidoc/index.adoc index 80aa69074a..c4af101ea6 100644 --- a/src/asciidoc/index.adoc +++ b/src/asciidoc/index.adoc @@ -44652,6 +44652,28 @@ specified with the `` element or Spring's `@Qualifier` annotation. +[[scheduling-annotation-support-exception]] +==== Exception management with @Async +When an `@Async` method has a `Future` typed return value, it is easy to manage +an exception that was thrown during the method execution as this exception will +be thrown when calling `get` on the `Future` result. With a void return type +however, the exception is uncaught and cannot be transmitted. For those cases, an +`AsyncUncaughtExceptionHandler` can be provided to handle such exceptions. + +[source,java,indent=0] +[subs="verbatim,quotes"] +---- + public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler { + + @Override + public void handleUncaughtException(Throwable ex, Method method, Object... params) { + // handle exception + } + } +---- + +By default, the exception is simply logged. A custom `AsyncUncaughtExceptionHandler` can +be defined _via_ `AsyncConfigurer` or the `task:annotation-driven` XML element. [[scheduling-task-namespace]] === The Task Namespace