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-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<Object> 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<Object> failWithFuture();
void failWithVoid();
void await(long timeout);
}
@@ -128,6 +205,16 @@ public class AsyncAnnotationBeanPostProcessorTests {
this.latch.countDown();
}
@Async
public Future<Object> 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();
}
}
}

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

View File

@@ -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<? extends Throwable> 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;
}
}
}

View File

@@ -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">
<!--
<context:load-time-weaver aspectj-weaving="on"/>
-->
<task:annotation-driven executor="executor" scheduler="scheduler"/>
<task:annotation-driven executor="executor" exception-handler="exceptionHandler" scheduler="scheduler"/>
<task:scheduled-tasks scheduler="scheduler">
<task:scheduled ref="target" method="test" fixed-rate="1000"/>
@@ -21,6 +21,9 @@
<property name="threadNamePrefix" value="testExecutor"/>
</bean>
<bean id="exceptionHandler"
class="org.springframework.scheduling.annotation.TestableAsyncUncaughtExceptionHandler"/>
<bean id="scheduler" class="org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler">
<property name="threadNamePrefix" value="testScheduler"/>
</bean>