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
This commit is contained in:
Stephane Nicoll
2014-01-21 16:25:19 +01:00
parent 59703981c4
commit db23ec733b
22 changed files with 2182 additions and 59 deletions

View File

@@ -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();
}
}

View File

@@ -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());
}
/**

View File

@@ -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<Class<? extends Annotation>> asyncAnnotationTypes = new LinkedHashSet<Class<? extends Annotation>>(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);
}
/**

View File

@@ -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.
*
* <p>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.
*
* <p>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<? extends Annotation> 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);
}

View File

@@ -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.
*
* <p>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.
*
* <p>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();
}

View File

@@ -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;
}
}

View File

@@ -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.
*
* <p>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.
*
* <p>To customize all this, implement {@link AsyncConfigurer} and
* provide:
* <ul>
* <li>your own {@link java.util.concurrent.Executor Executor} through the
* {@link AsyncConfigurer#getAsyncExecutor() getAsyncExecutor()} method, and</li>
* <li>your own {@link org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler
* AsyncUncaughtExceptionHandler} through the {@link AsyncConfigurer#getAsyncUncaughtExceptionHandler()
* getAsyncUncaughtExceptionHandler()}
* method.</li>
* </ul>
*
* <pre class="code">
* &#064;Configuration
@@ -81,19 +91,29 @@ import org.springframework.core.Ordered;
* executor.initialize();
* return executor;
* }
*
* &#064;Override
* public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
* return MyAsyncUncaughtExceptionHandler();
* }
* }</pre>
*
* <p>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.
*
* <p>For reference, the example above can be compared to the following Spring XML
* configuration:
* <pre class="code">
* {@code
* <beans>
* <task:annotation-driven executor="myExecutor"/>
* <task:annotation-driven executor="myExecutor" exception-handler="exceptionHandler"/>
* <task:executor id="myExecutor" pool-size="7-42" queue-capacity="11"/>
* <bean id="asyncBean" class="com.foo.MyAsyncBean"/>
* <bean id="exceptionHandler" class="com.foo.MyAsyncUncaughtExceptionHandler"/>
* </beans>
* }</pre>
* the examples are equivalent save the setting of the <em>thread name prefix</em> of the
* the examples are equivalent except the setting of the <em>thread name prefix</em> 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

View File

@@ -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.<Integer>getNumber("order"));
return bpp;

View File

@@ -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);
}