From f036ed639f95b64842aa0abe742cad26f37cc50b Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 26 Oct 2012 17:55:28 -0400 Subject: [PATCH] Polish (major) MVC async processing interceptors New afterTimeout and afterCompletion callbacks afterTimeout can provide a concurrent result to be used instead of the one that could not be set or returned on time Interceptor exceptions cause async processing to resume treating the exception as the concurrent result Adapter classes for convenient implementation of the interfaces Issue: SPR-9914 --- .../support/OpenSessionInViewFilter.java | 16 +- .../support/OpenSessionInViewInterceptor.java | 16 +- .../support/OpenSessionInViewFilter.java | 16 +- .../support/OpenSessionInViewInterceptor.java | 16 +- .../OpenEntityManagerInViewFilter.java | 18 +- .../OpenEntityManagerInViewInterceptor.java | 18 +- .../support/OpenSessionInViewTests.java | 9 +- .../support/OpenEntityManagerInViewTests.java | 6 +- .../web/servlet/TestDispatcherServlet.java | 18 +- .../async/CallableInterceptorChain.java | 42 +++- .../async/CallableProcessingInterceptor.java | 75 ++++-- .../CallableProcessingInterceptorAdapter.java | 57 +++++ .../context/request/async/DeferredResult.java | 27 +- .../async/DeferredResultInterceptorChain.java | 39 ++- .../DeferredResultProcessingInterceptor.java | 83 ++++--- ...redResultProcessingInterceptorAdapter.java | 54 ++++ .../async/StandardServletAsyncWebRequest.java | 36 +-- .../request/async/WebAsyncManager.java | 139 ++++++----- .../request/async/DeferredResultTests.java | 22 -- .../StandardServletAsyncWebRequestTests.java | 3 +- .../request/async/WebAsyncManagerTests.java | 213 ++++++++++++---- .../async/WebAsyncManagerTimeoutTests.java | 230 ++++++++++++++++++ .../web/servlet/FrameworkServlet.java | 12 +- 23 files changed, 846 insertions(+), 319 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/web/context/request/async/CallableProcessingInterceptorAdapter.java create mode 100644 spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultProcessingInterceptorAdapter.java create mode 100644 spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTimeoutTests.java diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate3/support/OpenSessionInViewFilter.java b/spring-orm/src/main/java/org/springframework/orm/hibernate3/support/OpenSessionInViewFilter.java index 7cc99cf9d29a..ddc59cf61a91 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate3/support/OpenSessionInViewFilter.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate3/support/OpenSessionInViewFilter.java @@ -34,7 +34,7 @@ import org.springframework.util.Assert; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.context.request.async.CallableProcessingInterceptor; +import org.springframework.web.context.request.async.CallableProcessingInterceptorAdapter; import org.springframework.web.context.request.async.WebAsyncManager; import org.springframework.web.context.request.async.WebAsyncUtils; import org.springframework.web.context.support.WebApplicationContextUtils; @@ -318,7 +318,7 @@ private boolean applySessionBindingInterceptor(WebAsyncManager asyncManager, Str /** * Bind and unbind the Hibernate {@code Session} to the current thread. */ - private static class SessionBindingCallableInterceptor implements CallableProcessingInterceptor { + private static class SessionBindingCallableInterceptor extends CallableProcessingInterceptorAdapter { private final SessionFactory sessionFactory; @@ -329,16 +329,18 @@ public SessionBindingCallableInterceptor(SessionFactory sessionFactory, SessionH this.sessionHolder = sessionHolder; } - public void preProcess(NativeWebRequest request, Callable task) { + @Override + public void preProcess(NativeWebRequest request, Callable task) { initializeThread(); } - private void initializeThread() { - TransactionSynchronizationManager.bindResource(this.sessionFactory, this.sessionHolder); + @Override + public void postProcess(NativeWebRequest request, Callable task, Object concurrentResult) { + TransactionSynchronizationManager.unbindResource(this.sessionFactory); } - public void postProcess(NativeWebRequest request, Callable task, Object concurrentResult) { - TransactionSynchronizationManager.unbindResource(this.sessionFactory); + private void initializeThread() { + TransactionSynchronizationManager.bindResource(this.sessionFactory, this.sessionHolder); } } diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate3/support/OpenSessionInViewInterceptor.java b/spring-orm/src/main/java/org/springframework/orm/hibernate3/support/OpenSessionInViewInterceptor.java index d259a55bea09..068424acd7b8 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate3/support/OpenSessionInViewInterceptor.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate3/support/OpenSessionInViewInterceptor.java @@ -29,7 +29,7 @@ import org.springframework.web.context.request.AsyncWebRequestInterceptor; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.WebRequest; -import org.springframework.web.context.request.async.CallableProcessingInterceptor; +import org.springframework.web.context.request.async.CallableProcessingInterceptorAdapter; import org.springframework.web.context.request.async.WebAsyncManager; import org.springframework.web.context.request.async.WebAsyncUtils; @@ -276,7 +276,7 @@ private boolean applySessionBindingInterceptor(WebAsyncManager asyncManager, Str /** * Bind and unbind the Hibernate {@code Session} to the current thread. */ - private class SessionBindingCallableInterceptor implements CallableProcessingInterceptor { + private class SessionBindingCallableInterceptor extends CallableProcessingInterceptorAdapter { private final SessionHolder sessionHolder; @@ -284,16 +284,18 @@ public SessionBindingCallableInterceptor(SessionHolder sessionHolder) { this.sessionHolder = sessionHolder; } - public void preProcess(NativeWebRequest request, Callable task) { + @Override + public void preProcess(NativeWebRequest request, Callable task) { initializeThread(); } - private void initializeThread() { - TransactionSynchronizationManager.bindResource(getSessionFactory(), this.sessionHolder); + @Override + public void postProcess(NativeWebRequest request, Callable task, Object concurrentResult) { + TransactionSynchronizationManager.unbindResource(getSessionFactory()); } - public void postProcess(NativeWebRequest request, Callable task, Object concurrentResult) { - TransactionSynchronizationManager.unbindResource(getSessionFactory()); + private void initializeThread() { + TransactionSynchronizationManager.bindResource(getSessionFactory(), this.sessionHolder); } } } diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate4/support/OpenSessionInViewFilter.java b/spring-orm/src/main/java/org/springframework/orm/hibernate4/support/OpenSessionInViewFilter.java index 4393d31f6733..cbc5cd8e46d2 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate4/support/OpenSessionInViewFilter.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate4/support/OpenSessionInViewFilter.java @@ -34,7 +34,7 @@ import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.context.request.async.CallableProcessingInterceptor; +import org.springframework.web.context.request.async.CallableProcessingInterceptorAdapter; import org.springframework.web.context.request.async.WebAsyncManager; import org.springframework.web.context.request.async.WebAsyncUtils; import org.springframework.web.context.support.WebApplicationContextUtils; @@ -215,7 +215,7 @@ private boolean applySessionBindingInterceptor(WebAsyncManager asyncManager, Str /** * Bind and unbind the Hibernate {@code Session} to the current thread. */ - private static class SessionBindingCallableInterceptor implements CallableProcessingInterceptor { + private static class SessionBindingCallableInterceptor extends CallableProcessingInterceptorAdapter { private final SessionFactory sessionFactory; @@ -226,16 +226,18 @@ public SessionBindingCallableInterceptor(SessionFactory sessionFactory, SessionH this.sessionHolder = sessionHolder; } - public void preProcess(NativeWebRequest request, Callable task) { + @Override + public void preProcess(NativeWebRequest request, Callable task) { initializeThread(); } - private void initializeThread() { - TransactionSynchronizationManager.bindResource(this.sessionFactory, this.sessionHolder); + @Override + public void postProcess(NativeWebRequest request, Callable task, Object concurrentResult) { + TransactionSynchronizationManager.unbindResource(this.sessionFactory); } - public void postProcess(NativeWebRequest request, Callable task, Object concurrentResult) { - TransactionSynchronizationManager.unbindResource(this.sessionFactory); + private void initializeThread() { + TransactionSynchronizationManager.bindResource(this.sessionFactory, this.sessionHolder); } } } diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate4/support/OpenSessionInViewInterceptor.java b/spring-orm/src/main/java/org/springframework/orm/hibernate4/support/OpenSessionInViewInterceptor.java index 58faf09215df..3b5677f83d81 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate4/support/OpenSessionInViewInterceptor.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate4/support/OpenSessionInViewInterceptor.java @@ -33,7 +33,7 @@ import org.springframework.web.context.request.AsyncWebRequestInterceptor; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.WebRequest; -import org.springframework.web.context.request.async.CallableProcessingInterceptor; +import org.springframework.web.context.request.async.CallableProcessingInterceptorAdapter; import org.springframework.web.context.request.async.WebAsyncManager; import org.springframework.web.context.request.async.WebAsyncUtils; @@ -215,7 +215,7 @@ private boolean applySessionBindingInterceptor(WebAsyncManager asyncManager, Str /** * Bind and unbind the Hibernate {@code Session} to the current thread. */ - private class SessionBindingCallableInterceptor implements CallableProcessingInterceptor { + private class SessionBindingCallableInterceptor extends CallableProcessingInterceptorAdapter { private final SessionHolder sessionHolder; @@ -223,16 +223,18 @@ public SessionBindingCallableInterceptor(SessionHolder sessionHolder) { this.sessionHolder = sessionHolder; } - public void preProcess(NativeWebRequest request, Callable task) { + @Override + public void preProcess(NativeWebRequest request, Callable task) { initializeThread(); } - private void initializeThread() { - TransactionSynchronizationManager.bindResource(getSessionFactory(), this.sessionHolder); + @Override + public void postProcess(NativeWebRequest request, Callable task, Object concurrentResult) { + TransactionSynchronizationManager.unbindResource(getSessionFactory()); } - public void postProcess(NativeWebRequest request, Callable task, Object concurrentResult) { - TransactionSynchronizationManager.unbindResource(getSessionFactory()); + private void initializeThread() { + TransactionSynchronizationManager.bindResource(getSessionFactory(), this.sessionHolder); } } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewFilter.java b/spring-orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewFilter.java index e4fe37f8b3ba..694b0a42fae2 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewFilter.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 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. @@ -34,7 +34,7 @@ import org.springframework.util.StringUtils; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.context.request.async.CallableProcessingInterceptor; +import org.springframework.web.context.request.async.CallableProcessingInterceptorAdapter; import org.springframework.web.context.request.async.WebAsyncManager; import org.springframework.web.context.request.async.WebAsyncUtils; import org.springframework.web.context.support.WebApplicationContextUtils; @@ -242,7 +242,7 @@ private boolean applyEntityManagerBindingInterceptor(WebAsyncManager asyncManage /** * Bind and unbind the {@code EntityManager} to the current thread. */ - private static class EntityManagerBindingCallableInterceptor implements CallableProcessingInterceptor { + private static class EntityManagerBindingCallableInterceptor extends CallableProcessingInterceptorAdapter { private final EntityManagerFactory emFactory; @@ -254,16 +254,18 @@ public EntityManagerBindingCallableInterceptor(EntityManagerFactory emFactory, E this.emHolder = emHolder; } - public void preProcess(NativeWebRequest request, Callable task) { + @Override + public void preProcess(NativeWebRequest request, Callable task) { initializeThread(); } - private void initializeThread() { - TransactionSynchronizationManager.bindResource(this.emFactory, this.emHolder); + @Override + public void postProcess(NativeWebRequest request, Callable task, Object concurrentResult) { + TransactionSynchronizationManager.unbindResource(this.emFactory); } - public void postProcess(NativeWebRequest request, Callable task, Object concurrentResult) { - TransactionSynchronizationManager.unbindResource(this.emFactory); + private void initializeThread() { + TransactionSynchronizationManager.bindResource(this.emFactory, this.emHolder); } } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewInterceptor.java b/spring-orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewInterceptor.java index 3930a1ae8cf4..8c9cf595ca88 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewInterceptor.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2008 the original author or authors. + * Copyright 2002-2012 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,7 +31,7 @@ import org.springframework.web.context.request.AsyncWebRequestInterceptor; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.WebRequest; -import org.springframework.web.context.request.async.CallableProcessingInterceptor; +import org.springframework.web.context.request.async.CallableProcessingInterceptorAdapter; import org.springframework.web.context.request.async.WebAsyncManager; import org.springframework.web.context.request.async.WebAsyncUtils; @@ -163,7 +163,7 @@ private boolean applyCallableInterceptor(WebAsyncManager asyncManager, String ke /** * Bind and unbind the Hibernate {@code Session} to the current thread. */ - private class EntityManagerBindingCallableInterceptor implements CallableProcessingInterceptor { + private class EntityManagerBindingCallableInterceptor extends CallableProcessingInterceptorAdapter { private final EntityManagerHolder emHolder; @@ -172,16 +172,18 @@ public EntityManagerBindingCallableInterceptor(EntityManagerHolder emHolder) { this.emHolder = emHolder; } - public void preProcess(NativeWebRequest request, Callable task) { + @Override + public void preProcess(NativeWebRequest request, Callable task) { initializeThread(); } - private void initializeThread() { - TransactionSynchronizationManager.bindResource(getEntityManagerFactory(), this.emHolder); + @Override + public void postProcess(NativeWebRequest request, Callable task, Object concurrentResult) { + TransactionSynchronizationManager.unbindResource(getEntityManagerFactory()); } - public void postProcess(NativeWebRequest request, Callable task, Object concurrentResult) { - TransactionSynchronizationManager.unbindResource(getEntityManagerFactory()); + private void initializeThread() { + TransactionSynchronizationManager.bindResource(getEntityManagerFactory(), this.emHolder); } } diff --git a/spring-orm/src/test/java/org/springframework/orm/hibernate3/support/OpenSessionInViewTests.java b/spring-orm/src/test/java/org/springframework/orm/hibernate3/support/OpenSessionInViewTests.java index 6eb9b8ae34d8..5cc6d78bb450 100644 --- a/spring-orm/src/test/java/org/springframework/orm/hibernate3/support/OpenSessionInViewTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/hibernate3/support/OpenSessionInViewTests.java @@ -16,7 +16,7 @@ package org.springframework.orm.hibernate3.support; -import static org.easymock.EasyMock.anyObject; +import static org.easymock.EasyMock.*; import static org.easymock.EasyMock.createMock; import static org.easymock.EasyMock.createStrictMock; import static org.easymock.EasyMock.expect; @@ -39,6 +39,7 @@ import javax.servlet.ServletResponse; import javax.transaction.TransactionManager; +import org.easymock.EasyMock; import org.hibernate.FlushMode; import org.hibernate.HibernateException; import org.hibernate.SessionFactory; @@ -63,8 +64,8 @@ import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.context.request.async.AsyncWebRequest; -import org.springframework.web.context.request.async.WebAsyncUtils; import org.springframework.web.context.request.async.WebAsyncManager; +import org.springframework.web.context.request.async.WebAsyncUtils; import org.springframework.web.context.support.StaticWebApplicationContext; @@ -177,6 +178,8 @@ public void testOpenSessionInViewInterceptorAsyncScenario() throws Exception { AsyncWebRequest asyncWebRequest = createStrictMock(AsyncWebRequest.class); asyncWebRequest.addCompletionHandler((Runnable) anyObject()); + asyncWebRequest.setTimeoutHandler((Runnable) anyObject()); + asyncWebRequest.addCompletionHandler((Runnable) anyObject()); asyncWebRequest.startAsync(); replay(asyncWebRequest); @@ -491,6 +494,8 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo AsyncWebRequest asyncWebRequest = createMock(AsyncWebRequest.class); asyncWebRequest.addCompletionHandler((Runnable) anyObject()); + asyncWebRequest.setTimeoutHandler(EasyMock.anyObject()); + asyncWebRequest.addCompletionHandler((Runnable) anyObject()); asyncWebRequest.startAsync(); expect(asyncWebRequest.isAsyncStarted()).andReturn(true).anyTimes(); replay(asyncWebRequest); diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewTests.java index 7e3d1a9f9b2c..28c56ea2b723 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 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. @@ -155,6 +155,8 @@ public void testOpenEntityManagerInViewInterceptorAsyncScenario() throws Excepti AsyncWebRequest asyncWebRequest = createStrictMock(AsyncWebRequest.class); asyncWebRequest.addCompletionHandler((Runnable) anyObject()); + asyncWebRequest.setTimeoutHandler((Runnable) anyObject()); + asyncWebRequest.addCompletionHandler((Runnable) anyObject()); asyncWebRequest.startAsync(); replay(asyncWebRequest); @@ -344,6 +346,8 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo AsyncWebRequest asyncWebRequest = createMock(AsyncWebRequest.class); asyncWebRequest.addCompletionHandler((Runnable) anyObject()); + asyncWebRequest.setTimeoutHandler((Runnable) anyObject()); + asyncWebRequest.addCompletionHandler((Runnable) anyObject()); asyncWebRequest.startAsync(); expect(asyncWebRequest.isAsyncStarted()).andReturn(true).anyTimes(); replay(asyncWebRequest); diff --git a/spring-test-mvc/src/main/java/org/springframework/test/web/servlet/TestDispatcherServlet.java b/spring-test-mvc/src/main/java/org/springframework/test/web/servlet/TestDispatcherServlet.java index eccdead211a4..795f32b35b90 100644 --- a/spring-test-mvc/src/main/java/org/springframework/test/web/servlet/TestDispatcherServlet.java +++ b/spring-test-mvc/src/main/java/org/springframework/test/web/servlet/TestDispatcherServlet.java @@ -27,9 +27,9 @@ import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.context.request.async.CallableProcessingInterceptor; +import org.springframework.web.context.request.async.CallableProcessingInterceptorAdapter; import org.springframework.web.context.request.async.DeferredResult; -import org.springframework.web.context.request.async.DeferredResultProcessingInterceptor; +import org.springframework.web.context.request.async.DeferredResultProcessingInterceptorAdapter; import org.springframework.web.context.request.async.WebAsyncManager; import org.springframework.web.context.request.async.WebAsyncUtils; import org.springframework.web.servlet.DispatcherServlet; @@ -48,6 +48,8 @@ @SuppressWarnings("serial") final class TestDispatcherServlet extends DispatcherServlet { + private static final String KEY = TestDispatcherServlet.class.getName() + "-interceptor"; + /** * Create a new instance with the given web application context. */ @@ -70,19 +72,15 @@ private CountDownLatch registerAsyncInterceptors(HttpServletRequest request) { WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); - asyncManager.registerCallableInterceptor("mockmvc", new CallableProcessingInterceptor() { - public void preProcess(NativeWebRequest request, Callable task) throws Exception { } - public void postProcess(NativeWebRequest request, Callable task, Object value) throws Exception { + asyncManager.registerCallableInterceptor(KEY, new CallableProcessingInterceptorAdapter() { + public void postProcess(NativeWebRequest request, Callable task, Object value) throws Exception { asyncResultLatch.countDown(); } }); - - asyncManager.registerDeferredResultInterceptor("mockmvc", new DeferredResultProcessingInterceptor() { - public void preProcess(NativeWebRequest request, DeferredResult result) throws Exception { } - public void postProcess(NativeWebRequest request, DeferredResult result, Object value) throws Exception { + asyncManager.registerDeferredResultInterceptor(KEY, new DeferredResultProcessingInterceptorAdapter() { + public void postProcess(NativeWebRequest request, DeferredResult result, Object value) throws Exception { asyncResultLatch.countDown(); } - public void afterExpiration(NativeWebRequest request, DeferredResult result) throws Exception { } }); return asyncResultLatch; diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/CallableInterceptorChain.java b/spring-web/src/main/java/org/springframework/web/context/request/async/CallableInterceptorChain.java index 120d224040d5..72ac05a0ed99 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/CallableInterceptorChain.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/CallableInterceptorChain.java @@ -36,7 +36,7 @@ class CallableInterceptorChain { private final List interceptors; - private int interceptorIndex = -1; + private int preProcessIndex = -1; public CallableInterceptorChain(Collection interceptors) { @@ -44,21 +44,47 @@ public CallableInterceptorChain(Collection interc } public void applyPreProcess(NativeWebRequest request, Callable task) throws Exception { - for (int i = 0; i < this.interceptors.size(); i++) { - this.interceptors.get(i).preProcess(request, task); - this.interceptorIndex = i; + for (CallableProcessingInterceptor interceptor : this.interceptors) { + interceptor.preProcess(request, task); + this.preProcessIndex++; } } - public void applyPostProcess(NativeWebRequest request, Callable task, Object concurrentResult) { - for (int i = this.interceptorIndex; i >= 0; i--) { + public Object applyPostProcess(NativeWebRequest request, Callable task, Object concurrentResult) { + for (int i = this.preProcessIndex; i >= 0; i--) { try { this.interceptors.get(i).postProcess(request, task, concurrentResult); } - catch (Exception ex) { - logger.error("CallableProcessingInterceptor.postProcess threw exception", ex); + catch (Throwable t) { + return t; } } + return concurrentResult; } + public Object triggerAfterTimeout(NativeWebRequest request, Callable task) { + for (int i = this.interceptors.size()-1; i >= 0; i--) { + try { + Object result = this.interceptors.get(i).afterTimeout(request, task); + if (result != CallableProcessingInterceptor.RESULT_NONE) { + return result; + } + } + catch (Throwable t) { + return t; + } + } + return CallableProcessingInterceptor.RESULT_NONE; + } + + public void triggerAfterCompletion(NativeWebRequest request, Callable task) { + for (int i = this.interceptors.size()-1; i >= 0; i--) { + try { + this.interceptors.get(i).afterCompletion(request, task); + } + catch (Throwable t) { + logger.error("afterCompletion error", t); + } + } + } } diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/CallableProcessingInterceptor.java b/spring-web/src/main/java/org/springframework/web/context/request/async/CallableProcessingInterceptor.java index 0b0c9accd204..10a3099cd435 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/CallableProcessingInterceptor.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/CallableProcessingInterceptor.java @@ -19,49 +19,80 @@ import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.context.request.WebRequestInterceptor; /** * Intercepts concurrent request handling, where the concurrent result is - * obtained by executing a {@link Callable} on behalf of the application with an - * {@link AsyncTaskExecutor}. - *

- * A {@code CallableProcessingInterceptor} is invoked before and after the - * invocation of the {@code Callable} task in the asynchronous thread. + * obtained by executing a {@link Callable} on behalf of the application with + * an {@link AsyncTaskExecutor}. * - *

A {@code CallableProcessingInterceptor} may be registered as follows: - *

- * CallableProcessingInterceptor interceptor = ... ;
- * WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
- * asyncManager.registerCallableInterceptor("key", interceptor);
- * 
+ *

A {@code CallableProcessingInterceptor} is invoked before and after the + * invocation of the {@code Callable} task in the asynchronous thread, as well + * as on timeout from a container thread, or after completing for any reason + * including a timeout or network error. * - *

To register an interceptor for every request, the above can be done through - * a {@link WebRequestInterceptor} during pre-handling. + *

As a general rule exceptions raised by interceptor methods will cause + * async processing to resume by dispatching back to the container and using + * the Exception instance as the concurrent result. Such exceptions will then + * be processed through the {@code HandlerExceptionResolver} mechanism. + * + *

The {@link #afterTimeout(NativeWebRequest, Callable) afterTimeout} method + * can select a value to be used to resume processing. * * @author Rossen Stoyanchev * @since 3.2 */ public interface CallableProcessingInterceptor { + public static final Object RESULT_NONE = new Object(); + /** - * Invoked from the asynchronous thread in which the {@code Callable} is - * executed, before the {@code Callable} is invoked. + * Invoked after the start of concurrent handling in the async + * thread in which the {@code Callable} is executed and before the + * actual invocation of the {@code Callable}. * * @param request the current request - * @param task the task that will produce a result + * @param task the task for the current async request + * @throws Exception in case of errors */ - void preProcess(NativeWebRequest request, Callable task) throws Exception; + void preProcess(NativeWebRequest request, Callable task) throws Exception; /** - * Invoked from the asynchronous thread in which the {@code Callable} is - * executed, after the {@code Callable} returned a result. + * Invoked after the {@code Callable} has produced a result in the + * async thread in which the {@code Callable} is executed. This method may + * be invoked later than {@code afterTimeout} or {@code afterCompletion} + * depending on when the {@code Callable} finishes processing. * * @param request the current request - * @param task the task that produced the result + * @param task the task for the current async request * @param concurrentResult the result of concurrent processing, which could * be a {@link Throwable} if the {@code Callable} raised an exception + * @throws Exception in case of errors + */ + void postProcess(NativeWebRequest request, Callable task, Object concurrentResult) throws Exception; + + /** + * Invoked from a container thread when the async request times out before + * the {@code Callable} task completes. Implementations may return a value, + * including an {@link Exception}, to use instead of the value the + * {@link Callable} did not return in time. + * + * @param request the current request + * @param task the task for the current async request + * @return a concurrent result value; if the value is anything other than + * {@link #RESULT_NONE}, concurrent processing is resumed and subsequent + * interceptors are not invoked + * @throws Exception in case of errors + */ + Object afterTimeout(NativeWebRequest request, Callable task) throws Exception; + + /** + * Invoked from a container thread when async processing completes for any + * reason including timeout or network error. + * + * @param request the current request + * @param task the task for the current async request + * @throws Exception in case of errors */ - void postProcess(NativeWebRequest request, Callable task, Object concurrentResult) throws Exception; + void afterCompletion(NativeWebRequest request, Callable task) throws Exception; } diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/CallableProcessingInterceptorAdapter.java b/spring-web/src/main/java/org/springframework/web/context/request/async/CallableProcessingInterceptorAdapter.java new file mode 100644 index 000000000000..6f305edd7325 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/CallableProcessingInterceptorAdapter.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2012 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.web.context.request.async; + +import java.util.concurrent.Callable; + +import org.springframework.web.context.request.NativeWebRequest; + +/** + * Abstract adapter class for the {@link CallableProcessingInterceptor} interface, + * for simplified implementation of individual methods. + * + * @author Rossen Stoyanchev + * @since 3.2 + */ +public abstract class CallableProcessingInterceptorAdapter implements CallableProcessingInterceptor { + + /** + * This implementation is empty. + */ + public void preProcess(NativeWebRequest request, Callable task) throws Exception { + } + + /** + * This implementation is empty. + */ + public void postProcess(NativeWebRequest request, Callable task, Object concurrentResult) throws Exception { + } + + /** + * This implementation always returns + * {@link CallableProcessingInterceptor#RESULT_NONE RESULT_NONE}. + */ + public Object afterTimeout(NativeWebRequest request, Callable task) throws Exception { + return RESULT_NONE; + } + + /** + * This implementation is empty. + */ + public void afterCompletion(NativeWebRequest request, Callable task) throws Exception { + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResult.java b/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResult.java index 365a7531679d..9b4cc499bd3e 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResult.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResult.java @@ -66,7 +66,7 @@ public DeferredResult(long timeout) { /** * Create a DeferredResult with a timeout and a default result to use on timeout. * @param timeout timeout value in milliseconds; ignored if {@code null} - * @param timeoutResult the result to use, possibly {@code null} + * @param timeoutResult the result to use */ public DeferredResult(Long timeout, Object timeoutResult) { this.timeoutResult = timeoutResult; @@ -118,13 +118,7 @@ private boolean setResultInternal(Object result) { } this.result = result; if (this.resultHandler != null) { - try { - this.resultHandler.handleResult(this.result); - } - catch (Throwable t) { - logger.trace("DeferredResult not handled", t); - return false; - } + this.resultHandler.handleResult(this.result); } } return true; @@ -158,24 +152,19 @@ public boolean isSetOrExpired() { } /** - * Set the "expired" flag if and only if the result value was not already set. - * @return {@code true} if expiration succeeded, {@code false} otherwise + * Mark this instance expired so it may no longer be used. + * @return the previous value of the expiration flag */ boolean expire() { synchronized (this) { - if (!isSetOrExpired()) { - this.expired = true; - } + boolean previous = this.expired; + this.expired = true; + return previous; } - return this.expired; - } - - boolean hasTimeoutResult() { - return this.timeoutResult != RESULT_NONE; } boolean applyTimeoutResult() { - return hasTimeoutResult() ? setResultInternal(this.timeoutResult) : false; + return (this.timeoutResult != RESULT_NONE) ? setResultInternal(this.timeoutResult) : false; } diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultInterceptorChain.java b/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultInterceptorChain.java index 600fe0bbde12..f36659655e65 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultInterceptorChain.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultInterceptorChain.java @@ -35,35 +35,48 @@ class DeferredResultInterceptorChain { private final List interceptors; + private int preProcessingIndex = -1; + public DeferredResultInterceptorChain(Collection interceptors) { this.interceptors = new ArrayList(interceptors); } - public void applyPreProcess(NativeWebRequest request, DeferredResult task) throws Exception { + public void applyPreProcess(NativeWebRequest request, DeferredResult deferredResult) throws Exception { for (DeferredResultProcessingInterceptor interceptor : this.interceptors) { - interceptor.preProcess(request, task); + interceptor.preProcess(request, deferredResult); + this.preProcessingIndex++; } } - public void applyPostProcess(NativeWebRequest request, DeferredResult task, Object concurrentResult) { - for (int i = this.interceptors.size()-1; i >= 0; i--) { - try { - this.interceptors.get(i).postProcess(request, task, concurrentResult); + public Object applyPostProcess(NativeWebRequest request, DeferredResult deferredResult, Object concurrentResult) { + try { + for (int i = this.preProcessingIndex; i >= 0; i--) { + this.interceptors.get(i).postProcess(request, deferredResult, concurrentResult); } - catch (Exception ex) { - logger.error("DeferredResultProcessingInterceptor.postProcess threw exception", ex); + } + catch (Throwable t) { + return t; + } + return concurrentResult; + } + + public void triggerAfterTimeout(NativeWebRequest request, DeferredResult deferredResult) throws Exception { + for (int i = this.preProcessingIndex; i >= 0; i--) { + if (deferredResult.isSetOrExpired()) { + return; } + this.interceptors.get(i).afterTimeout(request, deferredResult); } } - public void triggerAfterExpiration(NativeWebRequest request, DeferredResult task) { - for (int i = this.interceptors.size()-1; i >= 0; i--) { + public void triggerAfterCompletion(NativeWebRequest request, DeferredResult deferredResult) { + for (int i = this.preProcessingIndex; i >= 0; i--) { try { - this.interceptors.get(i).afterExpiration(request, task); + this.interceptors.get(i).afterCompletion(request, deferredResult); } - catch (Exception ex) { - logger.error("DeferredResultProcessingInterceptor.afterExpiration threw exception", ex); + catch (Throwable t) { + logger.error("afterCompletion error", t); } } } diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultProcessingInterceptor.java b/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultProcessingInterceptor.java index 32750e2b8fe3..c50f2e2bfafe 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultProcessingInterceptor.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultProcessingInterceptor.java @@ -16,26 +16,24 @@ package org.springframework.web.context.request.async; import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.context.request.WebRequestInterceptor; /** * Intercepts concurrent request handling, where the concurrent result is * obtained by waiting for a {@link DeferredResult} to be set from a thread * chosen by the application (e.g. in response to some external event). * - *

A {@code DeferredResultProcessingInterceptor} is invoked before the start of - * asynchronous processing and either when the {@code DeferredResult} is set or - * when when the underlying request ends, whichever comes fist. + *

A {@code DeferredResultProcessingInterceptor} is invoked before the start + * of async processing, after the {@code DeferredResult} is set as well as on + * timeout, or or after completing for any reason including a timeout or network + * error. * - *

A {@code DeferredResultProcessingInterceptor} may be registered as follows: - *

- * DeferredResultProcessingInterceptor interceptor = ... ;
- * WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
- * asyncManager.registerDeferredResultInterceptor("key", interceptor);
- * 
+ *

As a general rule exceptions raised by interceptor methods will cause + * async processing to resume by dispatching back to the container and using + * the Exception instance as the concurrent result. Such exceptions will then + * be processed through the {@code HandlerExceptionResolver} mechanism. * - *

To register an interceptor for every request, the above can be done through - * a {@link WebRequestInterceptor} during pre-handling. + *

The {@link #afterTimeout(NativeWebRequest, DeferredResult) afterTimeout} + * method can set the {@code DeferredResult} in order to resume processing. * * @author Rossen Stoyanchev * @since 3.2 @@ -43,40 +41,59 @@ public interface DeferredResultProcessingInterceptor { /** - * Invoked before the start of concurrent handling using a - * {@link DeferredResult}. The invocation occurs in the thread that - * initiated concurrent handling. + * Invoked immediately after the start of concurrent handling, in the same + * thread that started it. This method may be used to detect the start of + * concurrent processing with the given {@code DeferredResult}. + * + *

The {@code DeferredResult} may have already been set, for example at + * the time of its creation or by another thread. * * @param request the current request - * @param deferredResult the DeferredResult instance + * @param deferredResult the DeferredResult for the current request + * @throws Exception in case of errors */ - void preProcess(NativeWebRequest request, DeferredResult deferredResult) throws Exception; + void preProcess(NativeWebRequest request, DeferredResult deferredResult) throws Exception; /** - * Invoked when a {@link DeferredResult} is set via - * {@link DeferredResult#setResult(Object) setResult}, or - * {@link DeferredResult#setErrorResult(Object) setErrorResult}, or after - * a timeout if a {@code DeferredResult} was created with a constructor - * accepting a default timeout result. - *

- * If the request ends before the {@code DeferredResult} is set, then - * {@link #afterExpiration(NativeWebRequest, DeferredResult)} is called. + * Invoked after a {@code DeferredResult} has been set, via + * {@link DeferredResult#setResult(Object)} or + * {@link DeferredResult#setErrorResult(Object)}, and is also ready to + * handle the concurrent result. + * + *

This method may also be invoked after a timeout when the + * {@code DeferredResult} was created with a constructor accepting a default + * timeout result. * * @param request the current request - * @param deferredResult the DeferredResult that has been set + * @param deferredResult the DeferredResult for the current request * @param concurrentResult the result to which the {@code DeferredResult} - * was set + * @throws Exception in case of errors + */ + void postProcess(NativeWebRequest request, DeferredResult deferredResult, Object concurrentResult) throws Exception; + + /** + * Invoked from a container thread when an async request times out before + * the {@code DeferredResult} has been set. Implementations may invoke + * {@link DeferredResult#setResult(Object) setResult} or + * {@link DeferredResult#setErrorResult(Object) to resume processing. + * + * @param request the current request + * @param deferredResult the DeferredResult for the current request; if the + * {@code DeferredResult} is set, then concurrent processing is resumed and + * subsequent interceptors are not invoked + * @throws Exception in case of errors */ - void postProcess(NativeWebRequest request, DeferredResult deferredResult, - Object concurrentResult) throws Exception; + void afterTimeout(NativeWebRequest request, DeferredResult deferredResult) throws Exception; /** - * Invoked when a {@link DeferredResult} was never set before the request - * completed due to a timeout or network error. + * Invoked from a container thread when an async request completed for any + * reason including timeout and network error. This method is useful for + * detecting that a {@code DeferredResult} instance is no longer usable. * * @param request the current request - * @param deferredResult the DeferredResult that has been set + * @param deferredResult the DeferredResult for the current request + * @throws Exception in case of errors */ - void afterExpiration(NativeWebRequest request, DeferredResult deferredResult) throws Exception; + void afterCompletion(NativeWebRequest request, DeferredResult deferredResult) throws Exception; } diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultProcessingInterceptorAdapter.java b/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultProcessingInterceptorAdapter.java new file mode 100644 index 000000000000..6120cc67afea --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultProcessingInterceptorAdapter.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2012 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.web.context.request.async; + +import org.springframework.web.context.request.NativeWebRequest; + +/** + * Abstract adapter class for the {@link DeferredResultProcessingInterceptor} + * interface for simplified implementation of individual methods. + * + * @author Rossen Stoyanchev + * @since 3.2 + */ +public abstract class DeferredResultProcessingInterceptorAdapter implements DeferredResultProcessingInterceptor { + + /** + * This implementation is empty. + */ + public void preProcess(NativeWebRequest request, DeferredResult deferredResult) throws Exception { + } + + /** + * This implementation is empty. + */ + public void postProcess(NativeWebRequest request, DeferredResult deferredResult, + Object concurrentResult) throws Exception { + } + + /** + * This implementation is empty. + */ + public void afterTimeout(NativeWebRequest request, DeferredResult deferredResult) throws Exception { + } + + /** + * This implementation is empty. + */ + public void afterCompletion(NativeWebRequest request, DeferredResult deferredResult) throws Exception { + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java index 4049c11a6630..7b90779314d1 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java @@ -27,7 +27,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.springframework.http.HttpStatus; import org.springframework.util.Assert; import org.springframework.web.context.request.ServletWebRequest; @@ -50,7 +49,7 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements private AtomicBoolean asyncCompleted = new AtomicBoolean(false); - private Runnable timeoutHandler = new DefaultTimeoutHandler(); + private Runnable timeoutHandler; private final List completionHandlers = new ArrayList(); @@ -74,15 +73,8 @@ public void setTimeout(Long timeout) { this.timeout = timeout; } - /** - * {@inheritDoc} - *

If not set, by default a timeout is handled by returning - * SERVICE_UNAVAILABLE (503). - */ public void setTimeoutHandler(Runnable timeoutHandler) { - if (timeoutHandler != null) { - this.timeoutHandler = timeoutHandler; - } + this.timeoutHandler = timeoutHandler; } public void addCompletionHandler(Runnable runnable) { @@ -135,31 +127,17 @@ public void onError(AsyncEvent event) throws IOException { } public void onTimeout(AsyncEvent event) throws IOException { - this.timeoutHandler.run(); + if (this.timeoutHandler != null) { + this.timeoutHandler.run(); + } } public void onComplete(AsyncEvent event) throws IOException { - for (Runnable runnable : this.completionHandlers) { - runnable.run(); + for (Runnable handler : this.completionHandlers) { + handler.run(); } this.asyncContext = null; this.asyncCompleted.set(true); } - - /** - * Sends a SERVICE_UNAVAILABLE (503). - */ - private class DefaultTimeoutHandler implements Runnable { - - public void run() { - try { - getResponse().sendError(HttpStatus.SERVICE_UNAVAILABLE.value()); - } - catch (IOException ex) { - // ignore - } - } - } - } diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java index efa6e960417f..53f269171aaf 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java @@ -44,8 +44,6 @@ * result can be accessed via {@link #getConcurrentResult()} or its presence * detected via {@link #hasConcurrentResult()}. * - *

TODO .. Servlet 3 config - * * @author Rossen Stoyanchev * @since 3.2 * @@ -61,13 +59,13 @@ public final class WebAsyncManager { private static final Log logger = LogFactory.getLog(WebAsyncManager.class); + private static final UrlPathHelper urlPathHelper = new UrlPathHelper(); + private AsyncWebRequest asyncWebRequest; private AsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(this.getClass().getSimpleName()); - private Runnable timeoutHandler; - private Object concurrentResult = RESULT_NONE; private Object[] concurrentResultContext; @@ -78,8 +76,6 @@ public final class WebAsyncManager { private final Map deferredResultInterceptors = new LinkedHashMap(); - private static final UrlPathHelper urlPathHelper = new UrlPathHelper(); - /** * Package private constructor. @@ -119,15 +115,6 @@ public void setTaskExecutor(AsyncTaskExecutor taskExecutor) { this.taskExecutor = taskExecutor; } - /** - * Set the handler to use when concurrent handling times out. If not set, by - * default a timeout is handled by returning SERVICE_UNAVAILABLE (503). - * @param timeoutHandler the handler - */ - public void setTimeoutHandler(Runnable timeoutHandler) { - this.timeoutHandler = timeoutHandler; - } - /** * Whether the selected handler for the current request chose to handle the * request asynchronously. A return value of "true" indicates concurrent @@ -226,40 +213,63 @@ public void clearConcurrentResult() { */ public void startCallableProcessing(final Callable callable, Object... processingContext) { Assert.notNull(callable, "Callable must not be null"); + Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null"); - startAsyncProcessing(processingContext); + final CallableInterceptorChain chain = new CallableInterceptorChain(this.callableInterceptors.values()); - this.taskExecutor.submit(new Runnable() { + this.asyncWebRequest.setTimeoutHandler(new Runnable() { + public void run() { + logger.debug("Processing timeout"); + Object result = chain.triggerAfterTimeout(asyncWebRequest, callable); + if (result != CallableProcessingInterceptor.RESULT_NONE) { + setConcurrentResultAndDispatch(result); + } + } + }); + this.asyncWebRequest.addCompletionHandler(new Runnable() { public void run() { + chain.triggerAfterCompletion(asyncWebRequest, callable); + } + }); - CallableInterceptorChain chain = - new CallableInterceptorChain(callableInterceptors.values()); + startAsyncProcessing(processingContext); + this.taskExecutor.submit(new Runnable() { + public void run() { + Object result = null; try { chain.applyPreProcess(asyncWebRequest, callable); - concurrentResult = callable.call(); + result = callable.call(); } catch (Throwable t) { - concurrentResult = t; + result = t; } finally { - chain.applyPostProcess(asyncWebRequest, callable, concurrentResult); + result = chain.applyPostProcess(asyncWebRequest, callable, result); } + setConcurrentResultAndDispatch(result); + } + }); + } - if (logger.isDebugEnabled()) { - logger.debug("Concurrent result value [" + concurrentResult + "]"); - } + private void setConcurrentResultAndDispatch(Object result) { + synchronized (WebAsyncManager.this) { + if (hasConcurrentResult()) { + return; + } + concurrentResult = result; + } - if (asyncWebRequest.isAsyncComplete()) { - logger.error("Could not complete processing due to a timeout or network error"); - return; - } + if (asyncWebRequest.isAsyncComplete()) { + logger.error("Could not complete async processing due to timeout or network error"); + return; + } - logger.debug("Dispatching request to continue processing"); - asyncWebRequest.dispatch(); - } - }); + logger.debug("Concurrent result value [" + concurrentResult + "]"); + logger.debug("Dispatching request to resume processing"); + + asyncWebRequest.dispatch(); } /** @@ -273,6 +283,7 @@ public void run() { */ public void startCallableProcessing(AsyncTask asyncTask, Object... processingContext) { Assert.notNull(asyncTask, "AsyncTask must not be null"); + Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null"); Long timeout = asyncTask.getTimeout(); if (timeout != null) { @@ -302,53 +313,54 @@ public void startCallableProcessing(AsyncTask asyncTask, Object... processing * @see #getConcurrentResult() * @see #getConcurrentResultContext() */ - public void startDeferredResultProcessing(final DeferredResult deferredResult, - Object... processingContext) throws Exception { + public void startDeferredResultProcessing( + final DeferredResult deferredResult, Object... processingContext) { Assert.notNull(deferredResult, "DeferredResult must not be null"); + Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null"); Long timeout = deferredResult.getTimeoutMilliseconds(); if (timeout != null) { this.asyncWebRequest.setTimeout(timeout); } - if (deferredResult.hasTimeoutResult()) { - this.asyncWebRequest.setTimeoutHandler(new Runnable() { - public void run() { - deferredResult.applyTimeoutResult(); - } - }); - } - final DeferredResultInterceptorChain chain = new DeferredResultInterceptorChain(this.deferredResultInterceptors.values()); - chain.applyPreProcess(this.asyncWebRequest, deferredResult); + this.asyncWebRequest.setTimeoutHandler(new Runnable() { + public void run() { + if (!deferredResult.applyTimeoutResult()) { + try { + chain.triggerAfterTimeout(asyncWebRequest, deferredResult); + } + catch (Throwable t) { + setConcurrentResultAndDispatch(t); + } + } + } + }); this.asyncWebRequest.addCompletionHandler(new Runnable() { public void run() { - if (deferredResult.expire()) { - chain.triggerAfterExpiration(asyncWebRequest, deferredResult); - } + deferredResult.expire(); + chain.triggerAfterCompletion(asyncWebRequest, deferredResult); } }); startAsyncProcessing(processingContext); - deferredResult.setResultHandler(new DeferredResultHandler() { - - public void handleResult(Object result) { - concurrentResult = result; - if (logger.isDebugEnabled()) { - logger.debug("Deferred result value [" + concurrentResult + "]"); + try { + chain.applyPreProcess(this.asyncWebRequest, deferredResult); + deferredResult.setResultHandler(new DeferredResultHandler() { + public void handleResult(Object result) { + result = chain.applyPostProcess(asyncWebRequest, deferredResult, result); + setConcurrentResultAndDispatch(result); } - - chain.applyPostProcess(asyncWebRequest, deferredResult, result); - - logger.debug("Dispatching request to complete processing"); - asyncWebRequest.dispatch(); - } - }); + }); + } + catch (Throwable t) { + setConcurrentResultAndDispatch(t); + } } private void startAsyncProcessing(Object[] processingContext) { @@ -356,15 +368,10 @@ private void startAsyncProcessing(Object[] processingContext) { clearConcurrentResult(); this.concurrentResultContext = processingContext; - Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null"); this.asyncWebRequest.startAsync(); - if (this.timeoutHandler != null) { - this.asyncWebRequest.setTimeoutHandler(this.timeoutHandler); - } - if (logger.isDebugEnabled()) { - HttpServletRequest request = asyncWebRequest.getNativeRequest(HttpServletRequest.class); + HttpServletRequest request = this.asyncWebRequest.getNativeRequest(HttpServletRequest.class); String requestUri = urlPathHelper.getRequestUri(request); logger.debug("Concurrent handling starting for " + request.getMethod() + " [" + requestUri + "]"); } diff --git a/spring-web/src/test/java/org/springframework/web/context/request/async/DeferredResultTests.java b/spring-web/src/test/java/org/springframework/web/context/request/async/DeferredResultTests.java index 2ad5aba381fa..c4e70096dfb3 100644 --- a/spring-web/src/test/java/org/springframework/web/context/request/async/DeferredResultTests.java +++ b/spring-web/src/test/java/org/springframework/web/context/request/async/DeferredResultTests.java @@ -17,7 +17,6 @@ package org.springframework.web.context.request.async; import static org.easymock.EasyMock.createMock; -import static org.easymock.EasyMock.expectLastCall; import static org.easymock.EasyMock.replay; import static org.easymock.EasyMock.verify; import static org.junit.Assert.assertFalse; @@ -62,21 +61,6 @@ public void setResultTwice() { verify(handler); } - @Test - public void setResultWithException() { - DeferredResultHandler handler = createMock(DeferredResultHandler.class); - handler.handleResult("hello"); - expectLastCall().andThrow(new IllegalStateException()); - replay(handler); - - DeferredResult result = new DeferredResult(); - result.setResultHandler(handler); - - assertFalse(result.setResult("hello")); - - verify(handler); - } - @Test public void isSetOrExpired() { DeferredResultHandler handler = createMock(DeferredResultHandler.class); @@ -105,12 +89,6 @@ public void setExpired() { assertFalse(result.setResult("hello")); } - @Test - public void hasTimeout() { - assertFalse(new DeferredResult().hasTimeoutResult()); - assertTrue(new DeferredResult(null, "timed out").hasTimeoutResult()); - } - @Test public void applyTimeoutResult() { DeferredResultHandler handler = createMock(DeferredResultHandler.class); diff --git a/spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java b/spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java index e8ce85bae338..2e91abf993cb 100644 --- a/spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java +++ b/spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java @@ -33,7 +33,6 @@ import org.easymock.EasyMock; import org.junit.Before; import org.junit.Test; -import org.springframework.http.HttpStatus; import org.springframework.mock.web.MockAsyncContext; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -120,7 +119,7 @@ public void startAsyncAfterCompleted() throws Exception { @Test public void onTimeoutDefaultBehavior() throws Exception { this.asyncRequest.onTimeout(new AsyncEvent(null)); - assertEquals(HttpStatus.SERVICE_UNAVAILABLE.value(), this.response.getStatus()); + assertEquals(200, this.response.getStatus()); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTests.java b/spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTests.java index fde9f63425b8..f4335766467c 100644 --- a/spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTests.java +++ b/spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTests.java @@ -19,6 +19,7 @@ import static org.easymock.EasyMock.createMock; import static org.easymock.EasyMock.createStrictMock; import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.expectLastCall; import static org.easymock.EasyMock.notNull; import static org.easymock.EasyMock.replay; import static org.easymock.EasyMock.reset; @@ -30,15 +31,15 @@ import java.util.concurrent.Callable; +import org.easymock.EasyMock; import org.junit.Before; import org.junit.Test; import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.mock.web.MockHttpServletRequest; - /** - * Test fixture with an {@link WebAsyncManager}. + * Test fixture with an {@link WebAsyncManager} with a mock AsyncWebRequest. * * @author Rossen Stoyanchev */ @@ -48,6 +49,7 @@ public class WebAsyncManagerTests { private AsyncWebRequest asyncWebRequest; + @Before public void setUp() { this.asyncManager = WebAsyncUtils.getAsyncManager(new MockHttpServletRequest()); @@ -63,6 +65,27 @@ public void setUp() { reset(this.asyncWebRequest); } + @Test + public void startAsyncProcessingWithoutAsyncWebRequest() { + WebAsyncManager manager = WebAsyncUtils.getAsyncManager(new MockHttpServletRequest()); + + try { + manager.startCallableProcessing(new StubCallable(1)); + fail("Expected exception"); + } + catch (IllegalStateException ex) { + assertEquals(ex.getMessage(), "AsyncWebRequest must not be null"); + } + + try { + manager.startDeferredResultProcessing(new DeferredResult()); + fail("Expected exception"); + } + catch (IllegalStateException ex) { + assertEquals(ex.getMessage(), "AsyncWebRequest must not be null"); + } + } + @Test public void isConcurrentHandlingStarted() { @@ -91,32 +114,102 @@ public void setAsyncWebRequestAfterAsyncStarted() { @Test public void startCallableProcessing() throws Exception { - Callable task = new StubCallable(); + int concurrentResult = 21; + Callable task = new StubCallable(concurrentResult); CallableProcessingInterceptor interceptor = createStrictMock(CallableProcessingInterceptor.class); interceptor.preProcess(this.asyncWebRequest, task); - interceptor.postProcess(this.asyncWebRequest, task, new Integer(1)); + interceptor.postProcess(this.asyncWebRequest, task, new Integer(concurrentResult)); replay(interceptor); - this.asyncWebRequest.startAsync(); - expect(this.asyncWebRequest.isAsyncComplete()).andReturn(false); - this.asyncWebRequest.dispatch(); - replay(this.asyncWebRequest); + setupDefaultAsyncScenario(); + + this.asyncManager.registerCallableInterceptor("interceptor", interceptor); + this.asyncManager.startCallableProcessing(task); + + assertTrue(this.asyncManager.hasConcurrentResult()); + assertEquals(concurrentResult, this.asyncManager.getConcurrentResult()); + + verify(interceptor, this.asyncWebRequest); + } + + @Test + public void startCallableProcessingCallableException() throws Exception { + + Exception concurrentResult = new Exception(); + Callable task = new StubCallable(concurrentResult); + + CallableProcessingInterceptor interceptor = createStrictMock(CallableProcessingInterceptor.class); + interceptor.preProcess(this.asyncWebRequest, task); + interceptor.postProcess(this.asyncWebRequest, task, concurrentResult); + replay(interceptor); + + setupDefaultAsyncScenario(); + + this.asyncManager.registerCallableInterceptor("interceptor", interceptor); + this.asyncManager.startCallableProcessing(task); + + assertTrue(this.asyncManager.hasConcurrentResult()); + assertEquals(concurrentResult, this.asyncManager.getConcurrentResult()); + + verify(interceptor, this.asyncWebRequest); + } + + @Test + public void startCallableProcessingPreProcessException() throws Exception { + + Callable task = new StubCallable(21); + Exception exception = new Exception(); + + CallableProcessingInterceptor interceptor = createStrictMock(CallableProcessingInterceptor.class); + interceptor.preProcess(this.asyncWebRequest, task); + expectLastCall().andThrow(exception); + replay(interceptor); + + setupDefaultAsyncScenario(); + + this.asyncManager.registerCallableInterceptor("interceptor", interceptor); + this.asyncManager.startCallableProcessing(task); + + assertTrue(this.asyncManager.hasConcurrentResult()); + assertEquals(exception, this.asyncManager.getConcurrentResult()); + + verify(interceptor, this.asyncWebRequest); + } + + @Test + public void startCallableProcessingPostProcessException() throws Exception { + + Callable task = new StubCallable(21); + Exception exception = new Exception(); + + CallableProcessingInterceptor interceptor = createStrictMock(CallableProcessingInterceptor.class); + interceptor.preProcess(this.asyncWebRequest, task); + interceptor.postProcess(this.asyncWebRequest, task, 21); + expectLastCall().andThrow(exception); + replay(interceptor); + + setupDefaultAsyncScenario(); this.asyncManager.registerCallableInterceptor("interceptor", interceptor); this.asyncManager.startCallableProcessing(task); + assertTrue(this.asyncManager.hasConcurrentResult()); + assertEquals(exception, this.asyncManager.getConcurrentResult()); + verify(interceptor, this.asyncWebRequest); } @Test - public void startCallableProcessingAsyncTask() { + public void startCallableProcessingWithAsyncTask() { AsyncTaskExecutor executor = createMock(AsyncTaskExecutor.class); expect(executor.submit((Runnable) notNull())).andReturn(null); replay(executor); this.asyncWebRequest.setTimeout(1000L); + this.asyncWebRequest.setTimeoutHandler(EasyMock.anyObject()); + this.asyncWebRequest.addCompletionHandler(EasyMock.anyObject()); this.asyncWebRequest.startAsync(); replay(this.asyncWebRequest); @@ -128,7 +221,7 @@ public void startCallableProcessingAsyncTask() { } @Test - public void startCallableProcessingNullCallable() { + public void startCallableProcessingNullInput() { try { this.asyncManager.startCallableProcessing((Callable) null); fail("Expected exception"); @@ -138,72 +231,108 @@ public void startCallableProcessingNullCallable() { } } - @Test - public void startCallableProcessingNullRequest() { - WebAsyncManager manager = WebAsyncUtils.getAsyncManager(new MockHttpServletRequest()); - try { - manager.startCallableProcessing(new StubCallable()); - fail("Expected exception"); - } - catch (IllegalStateException ex) { - assertEquals(ex.getMessage(), "AsyncWebRequest must not be null"); - } - } - @Test public void startDeferredResultProcessing() throws Exception { - DeferredResult deferredResult = new DeferredResult(1000L, 10); + DeferredResult deferredResult = new DeferredResult(1000L); + String concurrentResult = "abc"; + + DeferredResultProcessingInterceptor interceptor = createStrictMock(DeferredResultProcessingInterceptor.class); + interceptor.preProcess(this.asyncWebRequest, deferredResult); + interceptor.postProcess(asyncWebRequest, deferredResult, concurrentResult); + replay(interceptor); this.asyncWebRequest.setTimeout(1000L); - this.asyncWebRequest.setTimeoutHandler((Runnable) notNull()); - this.asyncWebRequest.addCompletionHandler((Runnable) notNull()); - this.asyncWebRequest.startAsync(); - replay(this.asyncWebRequest); + setupDefaultAsyncScenario(); + + this.asyncManager.registerDeferredResultInterceptor("interceptor", interceptor); + this.asyncManager.startDeferredResultProcessing(deferredResult); + + deferredResult.setResult(concurrentResult); + + assertEquals(concurrentResult, this.asyncManager.getConcurrentResult()); + verify(this.asyncWebRequest, interceptor); + } + + @Test + public void startDeferredResultProcessingPreProcessException() throws Exception { + + DeferredResult deferredResult = new DeferredResult(); + Exception exception = new Exception(); DeferredResultProcessingInterceptor interceptor = createStrictMock(DeferredResultProcessingInterceptor.class); interceptor.preProcess(this.asyncWebRequest, deferredResult); + expectLastCall().andThrow(exception); replay(interceptor); + setupDefaultAsyncScenario(); + this.asyncManager.registerDeferredResultInterceptor("interceptor", interceptor); this.asyncManager.startDeferredResultProcessing(deferredResult); + deferredResult.setResult(25); + + assertEquals(exception, this.asyncManager.getConcurrentResult()); verify(this.asyncWebRequest, interceptor); - reset(this.asyncWebRequest, interceptor); + } - this.asyncWebRequest.dispatch(); - replay(this.asyncWebRequest); + @Test + public void startDeferredResultProcessingPostProcessException() throws Exception { + + DeferredResult deferredResult = new DeferredResult(); + Exception exception = new Exception(); - interceptor.postProcess(asyncWebRequest, deferredResult, 25); + DeferredResultProcessingInterceptor interceptor = createStrictMock(DeferredResultProcessingInterceptor.class); + interceptor.preProcess(this.asyncWebRequest, deferredResult); + interceptor.postProcess(this.asyncWebRequest, deferredResult, 25); + expectLastCall().andThrow(exception); replay(interceptor); + setupDefaultAsyncScenario(); + + this.asyncManager.registerDeferredResultInterceptor("interceptor", interceptor); + this.asyncManager.startDeferredResultProcessing(deferredResult); + deferredResult.setResult(25); - assertEquals(25, this.asyncManager.getConcurrentResult()); + assertEquals(exception, this.asyncManager.getConcurrentResult()); verify(this.asyncWebRequest, interceptor); } @Test - public void setTimeoutHandler() throws Exception { - - Runnable timeoutHandler = new Runnable() { public void run() {} }; - this.asyncManager.setTimeoutHandler(timeoutHandler); + public void startDeferredResultProcessingNullInput() { + try { + this.asyncManager.startDeferredResultProcessing((DeferredResult) null); + fail("Expected exception"); + } + catch (IllegalArgumentException ex) { + assertEquals(ex.getMessage(), "DeferredResult must not be null"); + } + } + private void setupDefaultAsyncScenario() { + this.asyncWebRequest.setTimeoutHandler((Runnable) notNull()); + this.asyncWebRequest.addCompletionHandler((Runnable) notNull()); this.asyncWebRequest.startAsync(); - this.asyncWebRequest.setTimeoutHandler(timeoutHandler); expect(this.asyncWebRequest.isAsyncComplete()).andReturn(false); this.asyncWebRequest.dispatch(); replay(this.asyncWebRequest); - - this.asyncManager.startCallableProcessing(new StubCallable()); - - verify(this.asyncWebRequest); } private final class StubCallable implements Callable { + + private Object value; + + public StubCallable(Object value) { + this.value = value; + } + public Object call() throws Exception { - return 1; + if (this.value instanceof Exception) { + throw ((Exception) this.value); + } + return this.value; } } diff --git a/spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTimeoutTests.java b/spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTimeoutTests.java new file mode 100644 index 000000000000..cae90eac72e7 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTimeoutTests.java @@ -0,0 +1,230 @@ +/* + * Copyright 2002-2012 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.web.context.request.async; + +import static org.springframework.web.context.request.async.CallableProcessingInterceptor.RESULT_NONE; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.createStrictMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.notNull; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import java.util.concurrent.Callable; + +import javax.servlet.AsyncEvent; +import javax.servlet.DispatcherType; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.web.context.request.NativeWebRequest; + +/** + * {@link WebAsyncManager} tests where container-triggered timeout/completion + * events are simulated. + * + * @author Rossen Stoyanchev + */ +public class WebAsyncManagerTimeoutTests { + + private static final AsyncEvent ASYNC_EVENT = null; + + private WebAsyncManager asyncManager; + + private StandardServletAsyncWebRequest asyncWebRequest; + + private MockHttpServletRequest servletRequest; + + + @Before + public void setUp() { + this.servletRequest = new MockHttpServletRequest(); + this.servletRequest.setAsyncSupported(true); + this.asyncWebRequest = new StandardServletAsyncWebRequest(servletRequest, new MockHttpServletResponse()); + + AsyncTaskExecutor executor = createMock(AsyncTaskExecutor.class); + expect(executor.submit((Runnable) notNull())).andReturn(null); + replay(executor); + + this.asyncManager = WebAsyncUtils.getAsyncManager(servletRequest); + this.asyncManager.setTaskExecutor(executor); + this.asyncManager.setAsyncWebRequest(this.asyncWebRequest); + } + + @Test + public void startCallableProcessingTimeoutAndComplete() throws Exception { + + StubCallable callable = new StubCallable(); + + CallableProcessingInterceptor interceptor = createStrictMock(CallableProcessingInterceptor.class); + expect(interceptor.afterTimeout(this.asyncWebRequest, callable)).andReturn(RESULT_NONE); + interceptor.afterCompletion(this.asyncWebRequest, callable); + replay(interceptor); + + this.asyncManager.registerCallableInterceptor("interceptor", interceptor); + this.asyncManager.startCallableProcessing(callable); + + this.asyncWebRequest.onTimeout(ASYNC_EVENT); + this.asyncWebRequest.onComplete(ASYNC_EVENT); + + assertFalse(this.asyncManager.hasConcurrentResult()); + assertEquals(DispatcherType.REQUEST, this.servletRequest.getDispatcherType()); + + verify(interceptor); + } + + @Test + public void startCallableProcessingTimeoutAndResume() throws Exception { + + StubCallable callable = new StubCallable(); + + CallableProcessingInterceptor interceptor = createStrictMock(CallableProcessingInterceptor.class); + expect(interceptor.afterTimeout(this.asyncWebRequest, callable)).andReturn(22); + replay(interceptor); + + this.asyncManager.registerCallableInterceptor("timeoutInterceptor", interceptor); + this.asyncManager.startCallableProcessing(callable); + + this.asyncWebRequest.onTimeout(ASYNC_EVENT); + + assertEquals(22, this.asyncManager.getConcurrentResult()); + assertEquals(DispatcherType.ASYNC, this.servletRequest.getDispatcherType()); + + verify(interceptor); + } + + @Test + public void startCallableProcessingAfterTimeoutException() throws Exception { + + StubCallable callable = new StubCallable(); + Exception exception = new Exception(); + + CallableProcessingInterceptor interceptor = createStrictMock(CallableProcessingInterceptor.class); + expect(interceptor.afterTimeout(this.asyncWebRequest, callable)).andThrow(exception); + replay(interceptor); + + this.asyncManager.registerCallableInterceptor("timeoutInterceptor", interceptor); + this.asyncManager.startCallableProcessing(callable); + + this.asyncWebRequest.onTimeout(ASYNC_EVENT); + + assertEquals(exception, this.asyncManager.getConcurrentResult()); + assertEquals(DispatcherType.ASYNC, this.servletRequest.getDispatcherType()); + + verify(interceptor); + } + + @Test + public void startDeferredResultProcessingTimeoutAndComplete() throws Exception { + + DeferredResult deferredResult = new DeferredResult(); + + DeferredResultProcessingInterceptor interceptor = createStrictMock(DeferredResultProcessingInterceptor.class); + interceptor.preProcess(this.asyncWebRequest, deferredResult); + interceptor.afterTimeout(this.asyncWebRequest, deferredResult); + interceptor.afterCompletion(this.asyncWebRequest, deferredResult); + replay(interceptor); + + this.asyncManager.registerDeferredResultInterceptor("interceptor", interceptor); + this.asyncManager.startDeferredResultProcessing(deferredResult); + + this.asyncWebRequest.onTimeout(ASYNC_EVENT); + this.asyncWebRequest.onComplete(ASYNC_EVENT); + + assertFalse(this.asyncManager.hasConcurrentResult()); + assertEquals(DispatcherType.REQUEST, this.servletRequest.getDispatcherType()); + + verify(interceptor); + } + + @Test + public void startDeferredResultProcessingTimeoutAndResumeWithDefaultResult() throws Exception { + + DeferredResult deferredResult = new DeferredResult(null, 23); + + DeferredResultProcessingInterceptor interceptor = new DeferredResultProcessingInterceptorAdapter() { + public void afterTimeout(NativeWebRequest request, DeferredResult result) throws Exception { + result.setErrorResult("should not get here"); + } + }; + + this.asyncManager.registerDeferredResultInterceptor("interceptor", interceptor); + this.asyncManager.startDeferredResultProcessing(deferredResult); + + AsyncEvent event = null; + this.asyncWebRequest.onTimeout(event); + + assertEquals(23, this.asyncManager.getConcurrentResult()); + assertEquals(DispatcherType.ASYNC, this.servletRequest.getDispatcherType()); + } + + @Test + public void startDeferredResultProcessingTimeoutAndResumeWithInterceptor() throws Exception { + + DeferredResult deferredResult = new DeferredResult(); + + DeferredResultProcessingInterceptor interceptor = new DeferredResultProcessingInterceptorAdapter() { + public void afterTimeout(NativeWebRequest request, DeferredResult result) throws Exception { + result.setErrorResult(23); + } + }; + + this.asyncManager.registerDeferredResultInterceptor("interceptor", interceptor); + this.asyncManager.startDeferredResultProcessing(deferredResult); + + AsyncEvent event = null; + this.asyncWebRequest.onTimeout(event); + + assertEquals(23, this.asyncManager.getConcurrentResult()); + assertEquals(DispatcherType.ASYNC, this.servletRequest.getDispatcherType()); + } + + @Test + public void startDeferredResultProcessingAfterTimeoutException() throws Exception { + + DeferredResult deferredResult = new DeferredResult(); + final Exception exception = new Exception(); + + DeferredResultProcessingInterceptor interceptor = new DeferredResultProcessingInterceptorAdapter() { + public void afterTimeout(NativeWebRequest request, DeferredResult result) throws Exception { + throw exception; + } + }; + + this.asyncManager.registerDeferredResultInterceptor("interceptor", interceptor); + this.asyncManager.startDeferredResultProcessing(deferredResult); + + AsyncEvent event = null; + this.asyncWebRequest.onTimeout(event); + + assertEquals(exception, this.asyncManager.getConcurrentResult()); + assertEquals(DispatcherType.ASYNC, this.servletRequest.getDispatcherType()); + } + + + private final class StubCallable implements Callable { + public Object call() throws Exception { + return 21; + } + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java index f3cd6758a41f..dc3652b6f673 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java @@ -50,6 +50,7 @@ import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.request.async.CallableProcessingInterceptor; +import org.springframework.web.context.request.async.CallableProcessingInterceptorAdapter; import org.springframework.web.context.request.async.WebAsyncManager; import org.springframework.web.context.request.async.WebAsyncUtils; import org.springframework.web.context.support.ServletRequestHandledEvent; @@ -988,14 +989,13 @@ private void resetContextHolders(HttpServletRequest request, } private CallableProcessingInterceptor createRequestBindingInterceptor(final HttpServletRequest request) { - - return new CallableProcessingInterceptor() { - - public void preProcess(NativeWebRequest webRequest, Callable task) { + return new CallableProcessingInterceptorAdapter() { + @Override + public void preProcess(NativeWebRequest webRequest, Callable task) { initContextHolders(request, buildLocaleContext(request), new ServletRequestAttributes(request)); } - - public void postProcess(NativeWebRequest webRequest, Callable task, Object concurrentResult) { + @Override + public void postProcess(NativeWebRequest webRequest, Callable task, Object concurrentResult) { resetContextHolders(request, null, null); } };