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:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user