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

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.aop.interceptor;
import java.lang.reflect.Method;
/**
* A strategy for handling uncaught exception thrown by asynchronous methods.
*
* <p>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);
}

View File

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