From db23ec733be832b0d86e7218aee9180f4263b16e Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 21 Jan 2014 16:25:19 +0100 Subject: [PATCH] Add exception handling of asynchronous method Prior to this commit, an exception thrown by an @Async void method was not further processed as there is no way to transmit that exception to the caller. The AsyncUncaughtExceptionHandler is a new strategy interface that can be implemented to handle unexpected exception thrown during the invocation of such asynchronous method. The handler can be specified using either the XML namespace or by implementing the AsyncConfigurer interface with the EnableAsync annotation. Issue: SPR-8995 --- .../AsyncExecutionInterceptor.java | 70 +- .../AsyncUncaughtExceptionHandler.java | 42 + .../SimpleAsyncUncaughtExceptionHandler.java | 41 + .../main/resources/META-INF/spring.schemas | 6 +- .../beans/factory/xml/spring-beans-4.1.xsd | 1185 +++++++++++++++++ .../beans/factory/xml/spring-tool-4.1.xsd | 115 ++ .../AbstractAsyncConfiguration.java | 6 +- .../AnnotationAsyncExecutionInterceptor.java | 21 +- .../annotation/AsyncAnnotationAdvisor.java | 28 +- .../AsyncAnnotationBeanPostProcessor.java | 21 +- .../annotation/AsyncConfigurer.java | 22 +- .../annotation/AsyncConfigurerSupport.java | 42 + .../scheduling/annotation/EnableAsync.java | 35 +- .../annotation/ProxyAsyncConfiguration.java | 6 +- .../AnnotationDrivenBeanDefinitionParser.java | 7 +- .../main/resources/META-INF/spring.schemas | 3 +- .../scheduling/config/spring-task-4.1.xsd | 308 +++++ ...AsyncAnnotationBeanPostProcessorTests.java | 131 +- .../annotation/EnableAsyncTests.java | 33 +- ...TestableAsyncUncaughtExceptionHandler.java | 86 ++ .../annotation/taskNamespaceTests.xml | 11 +- src/asciidoc/index.adoc | 22 + 22 files changed, 2182 insertions(+), 59 deletions(-) create mode 100644 spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncUncaughtExceptionHandler.java create mode 100644 spring-aop/src/main/java/org/springframework/aop/interceptor/SimpleAsyncUncaughtExceptionHandler.java create mode 100644 spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-beans-4.1.xsd create mode 100644 spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-tool-4.1.xsd create mode 100644 spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurerSupport.java create mode 100644 spring-context/src/main/resources/org/springframework/scheduling/config/spring-task-4.1.xsd create mode 100644 spring-context/src/test/java/org/springframework/scheduling/annotation/TestableAsyncUncaughtExceptionHandler.java 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