Ensure async Callables are in sync with the call stack

After this change each call stack level pushes and pops an async
Callable to ensure the AsyncExecutionChain is in sync with the
call stack. Before this change, a controller returning a "forward:"
prefixed string caused the AsyncExecutionChain to contain a
extra Callables that did not match the actual call stack.

Issue: SPR-9611
This commit is contained in:
Rossen Stoyanchev 2012-07-20 10:54:58 -04:00
parent 33a3681975
commit 6cc512b51c
27 changed files with 240 additions and 239 deletions

View File

@ -187,7 +187,7 @@ public class OpenSessionInViewFilter extends OncePerRequestFilter {
SessionHolder sessionHolder = new SessionHolder(session); SessionHolder sessionHolder = new SessionHolder(session);
TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder); TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder);
chain.addDelegatingCallable(getAsyncCallable(request, sessionFactory, sessionHolder)); chain.push(getAsyncCallable(request, sessionFactory, sessionHolder));
} }
} }
else { else {
@ -204,21 +204,20 @@ public class OpenSessionInViewFilter extends OncePerRequestFilter {
try { try {
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
} }
finally { finally {
if (!participate) { if (!participate) {
if (isSingleSession()) { if (isSingleSession()) {
// single session mode // single session mode
SessionHolder sessionHolder = SessionHolder sessionHolder =
(SessionHolder) TransactionSynchronizationManager.unbindResource(sessionFactory); (SessionHolder) TransactionSynchronizationManager.unbindResource(sessionFactory);
if (chain.isAsyncStarted()) { if (!chain.pop()) {
return; return;
} }
logger.debug("Closing single Hibernate Session in OpenSessionInViewFilter"); logger.debug("Closing single Hibernate Session in OpenSessionInViewFilter");
closeSession(sessionHolder.getSession(), sessionFactory); closeSession(sessionHolder.getSession(), sessionFactory);
} }
else { else {
if (chain.isAsyncStarted()) { if (!chain.pop()) {
throw new IllegalStateException("Deferred close is not supported with async requests."); throw new IllegalStateException("Deferred close is not supported with async requests.");
} }
// deferred close mode // deferred close mode
@ -303,7 +302,7 @@ public class OpenSessionInViewFilter extends OncePerRequestFilter {
public Object call() throws Exception { public Object call() throws Exception {
TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder); TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder);
try { try {
getNextCallable().call(); getNext().call();
} }
finally { finally {
SessionHolder sessionHolder = SessionHolder sessionHolder =

View File

@ -181,7 +181,7 @@ public class OpenSessionInViewInterceptor extends HibernateAccessor implements A
return new AbstractDelegatingCallable() { return new AbstractDelegatingCallable() {
public Object call() throws Exception { public Object call() throws Exception {
TransactionSynchronizationManager.bindResource(getSessionFactory(), sessionHolder); TransactionSynchronizationManager.bindResource(getSessionFactory(), sessionHolder);
getNextCallable().call(); getNext().call();
return null; return null;
} }
}; };

View File

@ -119,7 +119,7 @@ public class OpenSessionInViewFilter extends OncePerRequestFilter {
SessionHolder sessionHolder = new SessionHolder(session); SessionHolder sessionHolder = new SessionHolder(session);
TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder); TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder);
chain.addDelegatingCallable(getAsyncCallable(request, sessionFactory, sessionHolder)); chain.push(getAsyncCallable(request, sessionFactory, sessionHolder));
} }
try { try {
@ -130,7 +130,7 @@ public class OpenSessionInViewFilter extends OncePerRequestFilter {
if (!participate) { if (!participate) {
SessionHolder sessionHolder = SessionHolder sessionHolder =
(SessionHolder) TransactionSynchronizationManager.unbindResource(sessionFactory); (SessionHolder) TransactionSynchronizationManager.unbindResource(sessionFactory);
if (chain.isAsyncStarted()) { if (!chain.pop()) {
return; return;
} }
logger.debug("Closing Hibernate Session in OpenSessionInViewFilter"); logger.debug("Closing Hibernate Session in OpenSessionInViewFilter");
@ -198,7 +198,7 @@ public class OpenSessionInViewFilter extends OncePerRequestFilter {
public Object call() throws Exception { public Object call() throws Exception {
TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder); TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder);
try { try {
getNextCallable().call(); getNext().call();
} }
finally { finally {
SessionHolder sessionHolder = SessionHolder sessionHolder =

View File

@ -137,7 +137,7 @@ public class OpenSessionInViewInterceptor implements AsyncWebRequestInterceptor
return new AbstractDelegatingCallable() { return new AbstractDelegatingCallable() {
public Object call() throws Exception { public Object call() throws Exception {
TransactionSynchronizationManager.bindResource(getSessionFactory(), sessionHolder); TransactionSynchronizationManager.bindResource(getSessionFactory(), sessionHolder);
getNextCallable().call(); getNext().call();
return null; return null;
} }
}; };

View File

@ -176,7 +176,7 @@ public class OpenSessionInViewTests {
verify(sf); verify(sf);
verify(session); verify(session);
asyncCallable.setNextCallable(new Callable<Object>() { asyncCallable.setNext(new Callable<Object>() {
public Object call() { public Object call() {
return null; return null;
} }
@ -484,7 +484,7 @@ public class OpenSessionInViewTests {
verify(asyncWebRequest); verify(asyncWebRequest);
chain.setTaskExecutor(new SyncTaskExecutor()); chain.setTaskExecutor(new SyncTaskExecutor());
chain.setCallable(new Callable<Object>() { chain.setLastCallable(new Callable<Object>() {
public Object call() { public Object call() {
assertTrue(TransactionSynchronizationManager.hasResource(sf)); assertTrue(TransactionSynchronizationManager.hasResource(sf));
return null; return null;
@ -503,7 +503,7 @@ public class OpenSessionInViewTests {
replay(sf); replay(sf);
replay(session); replay(session);
chain.startCallableChainProcessing(); chain.startCallableProcessing();
assertFalse(TransactionSynchronizationManager.hasResource(sf)); assertFalse(TransactionSynchronizationManager.hasResource(sf));
verify(sf); verify(sf);

View File

@ -19,20 +19,11 @@ package org.springframework.web.context.request.async;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
/** /**
* A base class for a Callable that can be used in a chain of Callable instances. * A base class for a Callable used to form a chain of Callable instances.
* * Instances of this class are typically registered via
* <p>Typical use for async request processing scenarios involves: * {@link AsyncExecutionChain#push(AbstractDelegatingCallable)} in which case
* <ul> * there is no need to set the next Callable. Implementations can simply use
* <li>Create an instance of this type and register it via * {@link #getNext()} to delegate to the next Callable and assume it will be set.
* {@link AsyncExecutionChain#addDelegatingCallable(AbstractDelegatingCallable)}
* (internally the nodes of the chain will be linked so no need to set up "next").
* <li>Provide an implementation of {@link Callable#call()} that contains the
* logic needed to complete request processing outside the main processing thread.
* <li>In the implementation, delegate to the next Callable to obtain
* its result, e.g. ModelAndView, and then do some post-processing, e.g. view
* resolution. In some cases both pre- and post-processing might be
* appropriate, e.g. setting up {@link ThreadLocal} storage.
* </ul>
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 3.2 * @since 3.2
@ -43,12 +34,12 @@ public abstract class AbstractDelegatingCallable implements Callable<Object> {
private Callable<Object> next; private Callable<Object> next;
public void setNextCallable(Callable<Object> nextCallable) { protected Callable<Object> getNext() {
this.next = nextCallable;
}
protected Callable<Object> getNextCallable() {
return this.next; return this.next;
} }
public void setNext(Callable<Object> callable) {
this.next = callable;
}
} }

View File

@ -16,8 +16,8 @@
package org.springframework.web.context.request.async; package org.springframework.web.context.request.async;
import java.util.ArrayList; import java.util.ArrayDeque;
import java.util.List; import java.util.Deque;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import javax.servlet.ServletRequest; import javax.servlet.ServletRequest;
@ -31,18 +31,20 @@ import org.springframework.web.context.request.async.DeferredResult.DeferredResu
/** /**
* The central class for managing async request processing, mainly intended as * The central class for managing async request processing, mainly intended as
* an SPI and typically not by non-framework classes. * an SPI and not typically used directly by application classes.
* *
* <p>An async execution chain consists of a sequence of Callable instances and * <p>An async execution chain consists of a sequence of Callable instances that
* represents the work required to complete request processing in a separate * represent the work required to complete request processing in a separate thread.
* thread. To construct the chain, each layer in the call stack of a normal * To construct the chain, each level of the call stack pushes an
* request (e.g. filter, servlet) may contribute an * {@link AbstractDelegatingCallable} during the course of a normal request and
* {@link AbstractDelegatingCallable} when a request is being processed. * pops (removes) it on the way out. If async processing has not started, the pop
* For example the DispatcherServlet might contribute a Callable that * operation succeeds and the processing continues as normal, or otherwise if async
* performs view resolution while a HandlerAdapter might contribute a Callable * processing has begun, the main processing thread must be exited.
* that returns the ModelAndView, etc. The last Callable is the one that *
* actually produces an application-specific value, for example the Callable * <p>For example the DispatcherServlet might contribute a Callable that completes
* returned by an {@code @RequestMapping} method. * view resolution or the HandlerAdapter might contribute a Callable that prepares a
* ModelAndView while the last Callable in the chain is usually associated with the
* application, e.g. the return value of an {@code @RequestMapping} method.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 3.2 * @since 3.2
@ -51,13 +53,13 @@ public final class AsyncExecutionChain {
public static final String CALLABLE_CHAIN_ATTRIBUTE = AsyncExecutionChain.class.getName() + ".CALLABLE_CHAIN"; public static final String CALLABLE_CHAIN_ATTRIBUTE = AsyncExecutionChain.class.getName() + ".CALLABLE_CHAIN";
private final List<AbstractDelegatingCallable> delegatingCallables = new ArrayList<AbstractDelegatingCallable>(); private final Deque<AbstractDelegatingCallable> callables = new ArrayDeque<AbstractDelegatingCallable>();
private Callable<Object> callable; private Callable<Object> lastCallable;
private AsyncWebRequest asyncWebRequest; private AsyncWebRequest asyncWebRequest;
private AsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor("AsyncExecutionChain"); private AsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor("MvcAsync");
/** /**
* Private constructor * Private constructor
@ -68,7 +70,7 @@ public final class AsyncExecutionChain {
/** /**
* Obtain the AsyncExecutionChain for the current request. * Obtain the AsyncExecutionChain for the current request.
* Or if not found, create an instance and associate it with the request. * Or if not found, create it and associate it with the request.
*/ */
public static AsyncExecutionChain getForCurrentRequest(ServletRequest request) { public static AsyncExecutionChain getForCurrentRequest(ServletRequest request) {
AsyncExecutionChain chain = (AsyncExecutionChain) request.getAttribute(CALLABLE_CHAIN_ATTRIBUTE); AsyncExecutionChain chain = (AsyncExecutionChain) request.getAttribute(CALLABLE_CHAIN_ATTRIBUTE);
@ -81,7 +83,7 @@ public final class AsyncExecutionChain {
/** /**
* Obtain the AsyncExecutionChain for the current request. * Obtain the AsyncExecutionChain for the current request.
* Or if not found, create an instance and associate it with the request. * Or if not found, create it and associate it with the request.
*/ */
public static AsyncExecutionChain getForCurrentRequest(WebRequest request) { public static AsyncExecutionChain getForCurrentRequest(WebRequest request) {
int scope = RequestAttributes.SCOPE_REQUEST; int scope = RequestAttributes.SCOPE_REQUEST;
@ -94,105 +96,106 @@ public final class AsyncExecutionChain {
} }
/** /**
* Provide an instance of an AsyncWebRequest. * Provide an instance of an AsyncWebRequest -- required for async processing.
* This property must be set before async request processing can begin.
*/ */
public void setAsyncWebRequest(AsyncWebRequest asyncRequest) { public void setAsyncWebRequest(AsyncWebRequest asyncRequest) {
Assert.state(!isAsyncStarted(), "Cannot set AsyncWebRequest after the start of async processing.");
this.asyncWebRequest = asyncRequest; this.asyncWebRequest = asyncRequest;
} }
/** /**
* Provide an AsyncTaskExecutor to use when * Provide an AsyncTaskExecutor for use with {@link #startCallableProcessing()}.
* {@link #startCallableChainProcessing()} is invoked, for example when a * <p>By default a {@link SimpleAsyncTaskExecutor} instance is used. Applications are
* controller method returns a Callable. * advised to provide a TaskExecutor configured for production use.
* <p>By default a {@link SimpleAsyncTaskExecutor} instance is used. * @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#setAsyncTaskExecutor
*/ */
public void setTaskExecutor(AsyncTaskExecutor taskExecutor) { public void setTaskExecutor(AsyncTaskExecutor taskExecutor) {
this.taskExecutor = taskExecutor; this.taskExecutor = taskExecutor;
} }
/** /**
* Whether async request processing has started through one of: * Push an async Callable for the current stack level. This method should be
* <ul> * invoked before delegating to the next level of the stack where async
* <li>{@link #startCallableChainProcessing()} * processing may start.
* <li>{@link #startDeferredResultProcessing(DeferredResult)} */
* </ul> public void push(AbstractDelegatingCallable callable) {
Assert.notNull(callable, "Async Callable is required");
this.callables.addFirst(callable);
}
/**
* Pop the Callable of the current stack level. Ensure this method is invoked
* after delegation to the next level of the stack where async processing may
* start. The pop operation succeeds if async processing did not start.
* @return {@code true} if the Callable was removed, or {@code false}
* otherwise (i.e. async started).
*/
public boolean pop() {
if (isAsyncStarted()) {
return false;
}
else {
this.callables.removeFirst();
return true;
}
}
/**
* Whether async request processing has started.
*/ */
public boolean isAsyncStarted() { public boolean isAsyncStarted() {
return ((this.asyncWebRequest != null) && this.asyncWebRequest.isAsyncStarted()); return ((this.asyncWebRequest != null) && this.asyncWebRequest.isAsyncStarted());
} }
/** /**
* Add a Callable with logic required to complete request processing in a * Set the last Callable, e.g. the one returned by the controller.
* separate thread. See {@link AbstractDelegatingCallable} for details.
*/ */
public void addDelegatingCallable(AbstractDelegatingCallable callable) { public AsyncExecutionChain setLastCallable(Callable<Object> callable) {
Assert.notNull(callable, "Callable required"); Assert.notNull(callable, "Callable required");
this.delegatingCallables.add(callable); this.lastCallable = callable;
}
/**
* Add the last Callable, for example the one returned by the controller.
* This property must be set prior to invoking
* {@link #startCallableChainProcessing()}.
*/
public AsyncExecutionChain setCallable(Callable<Object> callable) {
Assert.notNull(callable, "Callable required");
this.callable = callable;
return this; return this;
} }
/** /**
* Start the async execution chain by submitting an * Start async processing and execute the async chain with an AsyncTaskExecutor.
* {@link AsyncExecutionChainRunnable} instance to the TaskExecutor provided via * This method returns immediately.
* {@link #setTaskExecutor(AsyncTaskExecutor)} and returning immediately.
* @see AsyncExecutionChainRunnable
*/ */
public void startCallableChainProcessing() { public void startCallableProcessing() {
startAsync(); Assert.state(this.asyncWebRequest != null, "AsyncWebRequest was not set");
this.asyncWebRequest.startAsync();
this.taskExecutor.execute(new AsyncExecutionChainRunnable(this.asyncWebRequest, buildChain())); this.taskExecutor.execute(new AsyncExecutionChainRunnable(this.asyncWebRequest, buildChain()));
} }
private void startAsync() {
Assert.state(this.asyncWebRequest != null, "An AsyncWebRequest is required to start async processing");
this.asyncWebRequest.startAsync();
}
private Callable<Object> buildChain() { private Callable<Object> buildChain() {
Assert.state(this.callable != null, "The last callable is required to build the async chain"); Assert.state(this.lastCallable != null, "The last Callable was not set");
this.delegatingCallables.add(new StaleAsyncRequestCheckingCallable(asyncWebRequest)); AbstractDelegatingCallable head = new StaleAsyncRequestCheckingCallable(this.asyncWebRequest);
Callable<Object> result = this.callable; head.setNext(this.lastCallable);
for (int i = this.delegatingCallables.size() - 1; i >= 0; i--) { for (AbstractDelegatingCallable callable : this.callables) {
AbstractDelegatingCallable callable = this.delegatingCallables.get(i); callable.setNext(head);
callable.setNextCallable(result); head = callable;
result = callable;
} }
return result; return head;
} }
/** /**
* Mark the start of async request processing accepting the provided * Start async processing and initialize the given DeferredResult so when
* DeferredResult and initializing it such that if * its value is set, the async chain is executed with an AsyncTaskExecutor.
* {@link DeferredResult#set(Object)} is called (from another thread),
* the set Object value will be processed with the execution chain by
* invoking {@link AsyncExecutionChainRunnable}.
* <p>The resulting processing from this method is identical to
* {@link #startCallableChainProcessing()}. The main difference is in
* the threading model, i.e. whether a TaskExecutor is used.
* @see DeferredResult
*/ */
public void startDeferredResultProcessing(final DeferredResult<?> deferredResult) { public void startDeferredResultProcessing(final DeferredResult<?> deferredResult) {
Assert.notNull(deferredResult, "DeferredResult is required"); Assert.notNull(deferredResult, "DeferredResult is required");
startAsync(); Assert.state(this.asyncWebRequest != null, "AsyncWebRequest was not set");
this.asyncWebRequest.startAsync();
deferredResult.init(new DeferredResultHandler() { deferredResult.init(new DeferredResultHandler() {
public void handle(Object result) { public void handle(Object result) {
if (asyncWebRequest.isAsyncCompleted()) { if (asyncWebRequest.isAsyncCompleted()) {
throw new StaleAsyncWebRequestException("Async request processing already completed"); throw new StaleAsyncWebRequestException("Too late to set DeferredResult: " + result);
} }
setCallable(new PassThroughCallable(result)); setLastCallable(new PassThroughCallable(result));
new AsyncExecutionChainRunnable(asyncWebRequest, buildChain()).run(); taskExecutor.execute(new AsyncExecutionChainRunnable(asyncWebRequest, buildChain()));
} }
}); });
this.asyncWebRequest.setTimeoutHandler(deferredResult.getTimeoutHandler()); this.asyncWebRequest.setTimeoutHandler(deferredResult.getTimeoutHandler());
} }

View File

@ -30,7 +30,7 @@ import org.springframework.util.Assert;
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 3.2 * @since 3.2
* *
* @see AsyncExecutionChain#startCallableChainProcessing() * @see AsyncExecutionChain#startCallableProcessing()
* @see AsyncExecutionChain#startDeferredResultProcessing(DeferredResult) * @see AsyncExecutionChain#startDeferredResultProcessing(DeferredResult)
*/ */
public class AsyncExecutionChainRunnable implements Runnable { public class AsyncExecutionChainRunnable implements Runnable {

View File

@ -39,7 +39,7 @@ public class StaleAsyncRequestCheckingCallable extends AbstractDelegatingCallabl
} }
public Object call() throws Exception { public Object call() throws Exception {
Object result = getNextCallable().call(); Object result = getNext().call();
if (this.asyncWebRequest.isAsyncCompleted()) { if (this.asyncWebRequest.isAsyncCompleted()) {
throw new StaleAsyncWebRequestException( throw new StaleAsyncWebRequestException(
"Async request no longer available due to a timeout or a (client) error"); "Async request no longer available due to a timeout or a (client) error");

View File

@ -76,8 +76,8 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
"in async request processing. This is done in Java code using the Servlet API " + "in async request processing. This is done in Java code using the Servlet API " +
"or by adding \"<async-supported>true</async-supported>\" to servlet and " + "or by adding \"<async-supported>true</async-supported>\" to servlet and " +
"filter declarations in web.xml."); "filter declarations in web.xml.");
assertNotStale();
Assert.state(!isAsyncStarted(), "Async processing already started"); Assert.state(!isAsyncStarted(), "Async processing already started");
Assert.state(!isAsyncCompleted(), "Cannot use async request that has completed");
this.asyncContext = getRequest().startAsync(getRequest(), getResponse()); this.asyncContext = getRequest().startAsync(getRequest(), getResponse());
this.asyncContext.addListener(this); this.asyncContext.addListener(this);
if (this.timeout != null) { if (this.timeout != null) {
@ -108,10 +108,6 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
} }
} }
private void assertNotStale() {
Assert.state(!isAsyncCompleted(), "Cannot use async request after completion");
}
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
// Implementation of AsyncListener methods // Implementation of AsyncListener methods
// --------------------------------------------------------------------- // ---------------------------------------------------------------------

View File

@ -196,13 +196,13 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter
beforeRequest(request, getBeforeMessage(request)); beforeRequest(request, getBeforeMessage(request));
AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(request); AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(request);
chain.addDelegatingCallable(getAsyncCallable(request)); chain.push(getAsyncCallable(request));
try { try {
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
} }
finally { finally {
if (chain.isAsyncStarted()) { if (!chain.pop()) {
return; return;
} }
afterRequest(request, getAfterMessage(request)); afterRequest(request, getAfterMessage(request));
@ -296,7 +296,7 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter
private AbstractDelegatingCallable getAsyncCallable(final HttpServletRequest request) { private AbstractDelegatingCallable getAsyncCallable(final HttpServletRequest request) {
return new AbstractDelegatingCallable() { return new AbstractDelegatingCallable() {
public Object call() throws Exception { public Object call() throws Exception {
getNextCallable().call(); getNext().call();
afterRequest(request, getAfterMessage(request)); afterRequest(request, getAfterMessage(request));
return null; return null;
} }

View File

@ -75,7 +75,7 @@ public abstract class OncePerRequestFilter extends GenericFilterBean {
} }
else { else {
AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(request); AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(request);
chain.addDelegatingCallable(getAsyncCallable(request, alreadyFilteredAttributeName)); chain.push(getAsyncCallable(request, alreadyFilteredAttributeName));
// Do invoke this filter... // Do invoke this filter...
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE); request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
@ -83,7 +83,7 @@ public abstract class OncePerRequestFilter extends GenericFilterBean {
doFilterInternal(httpRequest, httpResponse, filterChain); doFilterInternal(httpRequest, httpResponse, filterChain);
} }
finally { finally {
if (chain.isAsyncStarted()) { if (!chain.pop()) {
return; return;
} }
// Remove the "already filtered" request attribute for this request. // Remove the "already filtered" request attribute for this request.
@ -129,7 +129,7 @@ public abstract class OncePerRequestFilter extends GenericFilterBean {
return new AbstractDelegatingCallable() { return new AbstractDelegatingCallable() {
public Object call() throws Exception { public Object call() throws Exception {
getNextCallable().call(); getNext().call();
request.removeAttribute(alreadyFilteredAttributeName); request.removeAttribute(alreadyFilteredAttributeName);
return null; return null;
} }

View File

@ -81,14 +81,14 @@ public class RequestContextFilter extends OncePerRequestFilter {
initContextHolders(request, attributes); initContextHolders(request, attributes);
AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(request); AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(request);
chain.addDelegatingCallable(getChainedCallable(request, attributes)); chain.push(getChainedCallable(request, attributes));
try { try {
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
} }
finally { finally {
resetContextHolders(); resetContextHolders();
if (chain.isAsyncStarted()) { if (!chain.pop()) {
return; return;
} }
attributes.requestCompleted(); attributes.requestCompleted();
@ -121,7 +121,7 @@ public class RequestContextFilter extends OncePerRequestFilter {
public Object call() throws Exception { public Object call() throws Exception {
initContextHolders(request, requestAttributes); initContextHolders(request, requestAttributes);
try { try {
getNextCallable().call(); getNext().call();
} }
finally { finally {
resetContextHolders(); resetContextHolders();

View File

@ -61,11 +61,11 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter {
ShallowEtagResponseWrapper responseWrapper = new ShallowEtagResponseWrapper(response); ShallowEtagResponseWrapper responseWrapper = new ShallowEtagResponseWrapper(response);
AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(request); AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(request);
chain.addDelegatingCallable(getAsyncCallable(request, response, responseWrapper)); chain.push(getAsyncCallable(request, response, responseWrapper));
filterChain.doFilter(request, responseWrapper); filterChain.doFilter(request, responseWrapper);
if (chain.isAsyncStarted()) { if (!chain.pop()) {
return; return;
} }
@ -80,7 +80,7 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter {
return new AbstractDelegatingCallable() { return new AbstractDelegatingCallable() {
public Object call() throws Exception { public Object call() throws Exception {
getNextCallable().call(); getNext().call();
updateResponse(request, response, responseWrapper); updateResponse(request, response, responseWrapper);
return null; return null;
} }

View File

@ -63,7 +63,7 @@ public class AsyncExecutionChainTests {
this.chain = AsyncExecutionChain.getForCurrentRequest(this.request); this.chain = AsyncExecutionChain.getForCurrentRequest(this.request);
this.chain.setTaskExecutor(new SyncTaskExecutor()); this.chain.setTaskExecutor(new SyncTaskExecutor());
this.chain.setAsyncWebRequest(this.asyncWebRequest); this.chain.setAsyncWebRequest(this.asyncWebRequest);
this.chain.addDelegatingCallable(this.resultSavingCallable); this.chain.push(this.resultSavingCallable);
} }
@Test @Test
@ -79,29 +79,32 @@ public class AsyncExecutionChainTests {
this.asyncWebRequest.startAsync(); this.asyncWebRequest.startAsync();
assertTrue(this.chain.isAsyncStarted()); assertTrue(this.chain.isAsyncStarted());
}
@Test(expected=IllegalStateException.class)
public void setAsyncWebRequestAfterAsyncStarted() {
this.asyncWebRequest.startAsync();
this.chain.setAsyncWebRequest(null); this.chain.setAsyncWebRequest(null);
assertFalse(this.chain.isAsyncStarted());
} }
@Test @Test
public void startCallableChainProcessing() throws Exception { public void startCallableChainProcessing() throws Exception {
this.chain.addDelegatingCallable(new IntegerIncrementingCallable()); this.chain.push(new IntegerIncrementingCallable());
this.chain.addDelegatingCallable(new IntegerIncrementingCallable()); this.chain.push(new IntegerIncrementingCallable());
this.chain.setCallable(new Callable<Object>() { this.chain.setLastCallable(new Callable<Object>() {
public Object call() throws Exception { public Object call() throws Exception {
return 1; return 1;
} }
}); });
this.chain.startCallableChainProcessing(); this.chain.startCallableProcessing();
assertEquals(3, this.resultSavingCallable.result); assertEquals(3, this.resultSavingCallable.result);
} }
@Test @Test
public void startCallableChainProcessing_staleRequest() { public void startCallableChainProcessing_staleRequest() {
this.chain.setCallable(new Callable<Object>() { this.chain.setLastCallable(new Callable<Object>() {
public Object call() throws Exception { public Object call() throws Exception {
return 1; return 1;
} }
@ -109,7 +112,7 @@ public class AsyncExecutionChainTests {
this.asyncWebRequest.startAsync(); this.asyncWebRequest.startAsync();
this.asyncWebRequest.complete(); this.asyncWebRequest.complete();
this.chain.startCallableChainProcessing(); this.chain.startCallableProcessing();
Exception ex = this.resultSavingCallable.exception; Exception ex = this.resultSavingCallable.exception;
assertNotNull(ex); assertNotNull(ex);
@ -119,11 +122,11 @@ public class AsyncExecutionChainTests {
@Test @Test
public void startCallableChainProcessing_requiredCallable() { public void startCallableChainProcessing_requiredCallable() {
try { try {
this.chain.startCallableChainProcessing(); this.chain.startCallableProcessing();
fail("Expected exception"); fail("Expected exception");
} }
catch (IllegalStateException ex) { catch (IllegalStateException ex) {
assertThat(ex.getMessage(), containsString("last callable is required")); assertEquals(ex.getMessage(), "The last Callable was not set");
} }
} }
@ -131,18 +134,18 @@ public class AsyncExecutionChainTests {
public void startCallableChainProcessing_requiredAsyncWebRequest() { public void startCallableChainProcessing_requiredAsyncWebRequest() {
this.chain.setAsyncWebRequest(null); this.chain.setAsyncWebRequest(null);
try { try {
this.chain.startCallableChainProcessing(); this.chain.startCallableProcessing();
fail("Expected exception"); fail("Expected exception");
} }
catch (IllegalStateException ex) { catch (IllegalStateException ex) {
assertThat(ex.getMessage(), containsString("AsyncWebRequest is required")); assertEquals(ex.getMessage(), "AsyncWebRequest was not set");
} }
} }
@Test @Test
public void startDeferredResultProcessing() throws Exception { public void startDeferredResultProcessing() throws Exception {
this.chain.addDelegatingCallable(new IntegerIncrementingCallable()); this.chain.push(new IntegerIncrementingCallable());
this.chain.addDelegatingCallable(new IntegerIncrementingCallable()); this.chain.push(new IntegerIncrementingCallable());
DeferredResult<Integer> deferredResult = new DeferredResult<Integer>(); DeferredResult<Integer> deferredResult = new DeferredResult<Integer>();
this.chain.startDeferredResultProcessing(deferredResult); this.chain.startDeferredResultProcessing(deferredResult);
@ -228,7 +231,7 @@ public class AsyncExecutionChainTests {
public Object call() throws Exception { public Object call() throws Exception {
try { try {
this.result = getNextCallable().call(); this.result = getNext().call();
} }
catch (Exception ex) { catch (Exception ex) {
this.exception = ex; this.exception = ex;
@ -241,7 +244,7 @@ public class AsyncExecutionChainTests {
private static class IntegerIncrementingCallable extends AbstractDelegatingCallable { private static class IntegerIncrementingCallable extends AbstractDelegatingCallable {
public Object call() throws Exception { public Object call() throws Exception {
return ((Integer) getNextCallable().call() + 1); return ((Integer) getNext().call() + 1);
} }
} }

View File

@ -39,7 +39,7 @@ public class StaleAsyncRequestCheckingCallableTests {
public void setUp() { public void setUp() {
this.asyncWebRequest = EasyMock.createMock(AsyncWebRequest.class); this.asyncWebRequest = EasyMock.createMock(AsyncWebRequest.class);
this.callable = new StaleAsyncRequestCheckingCallable(asyncWebRequest); this.callable = new StaleAsyncRequestCheckingCallable(asyncWebRequest);
this.callable.setNextCallable(new Callable<Object>() { this.callable.setNext(new Callable<Object>() {
public Object call() throws Exception { public Object call() throws Exception {
return 1; return 1;
} }

View File

@ -141,7 +141,7 @@ public class StandardServletAsyncWebRequestTests {
fail("expected exception"); fail("expected exception");
} }
catch (IllegalStateException ex) { catch (IllegalStateException ex) {
assertEquals("Cannot use async request after completion", ex.getMessage()); assertEquals("Cannot use async request that has completed", ex.getMessage());
} }
} }

View File

@ -64,19 +64,17 @@ public interface AsyncHandlerInterceptor extends HandlerInterceptor {
AbstractDelegatingCallable getAsyncCallable(HttpServletRequest request, HttpServletResponse response, Object handler); AbstractDelegatingCallable getAsyncCallable(HttpServletRequest request, HttpServletResponse response, Object handler);
/** /**
* Invoked <em>after</em> the execution of a handler if the handler started * Invoked <em>after</em> the execution of a handler but only if the handler started
* async processing instead of handling the request. Effectively this method * async processing instead of handling the request. Effectively this method
* is invoked on the way out of the main processing thread instead of * is invoked instead of {@link #postHandle(WebRequest, org.springframework.ui.ModelMap)}
* {@link #postHandle(WebRequest, org.springframework.ui.ModelMap)}. The * on the way out of the main processing thread allowing implementations
* <code>postHandle</code> method is invoked after the request is handled * to ensure ThreadLocal attributes are cleared. The <code>postHandle</code>
* in the async thread. * invocation is effectively delayed until after async processing when the
* <p>Implementations of this method can ensure ThreadLocal attributes bound * request has actually been handled.
* to the main thread are cleared and also prepare for binding them to the
* async thread.
* @param request current HTTP request * @param request current HTTP request
* @param response current HTTP response * @param response current HTTP response
* @param handler chosen handler to execute, for type and/or instance examination * @param handler chosen handler to execute, for type and/or instance examination
*/ */
void postHandleAsyncStarted(HttpServletRequest request, HttpServletResponse response, Object handler); void postHandleAfterAsyncStarted(HttpServletRequest request, HttpServletResponse response, Object handler);
} }

View File

@ -817,8 +817,6 @@ public class DispatcherServlet extends FrameworkServlet {
@Override @Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception { protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
AsyncExecutionChain asyncChain = AsyncExecutionChain.getForCurrentRequest(request);
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
String requestUri = urlPathHelper.getRequestUri(request); String requestUri = urlPathHelper.getRequestUri(request);
logger.debug("DispatcherServlet with name '" + getServletName() + "' processing " + request.getMethod() + logger.debug("DispatcherServlet with name '" + getServletName() + "' processing " + request.getMethod() +
@ -853,13 +851,14 @@ public class DispatcherServlet extends FrameworkServlet {
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap()); request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager); request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
asyncChain.addDelegatingCallable(getServiceAsyncCallable(request, attributesSnapshot)); AsyncExecutionChain asyncChain = AsyncExecutionChain.getForCurrentRequest(request);
asyncChain.push(getServiceAsyncCallable(request, attributesSnapshot));
try { try {
doDispatch(request, response); doDispatch(request, response);
} }
finally { finally {
if (asyncChain.isAsyncStarted()) { if (!asyncChain.pop()) {
return; return;
} }
// Restore the original attribute snapshot, in case of an include. // Restore the original attribute snapshot, in case of an include.
@ -881,7 +880,7 @@ public class DispatcherServlet extends FrameworkServlet {
logger.debug("Resuming asynchronous processing of " + request.getMethod() + logger.debug("Resuming asynchronous processing of " + request.getMethod() +
" request for [" + urlPathHelper.getRequestUri(request) + "]"); " request for [" + urlPathHelper.getRequestUri(request) + "]");
} }
getNextCallable().call(); getNext().call();
if (attributesSnapshot != null) { if (attributesSnapshot != null) {
restoreAttributesAfterInclude(request, attributesSnapshot); restoreAttributesAfterInclude(request, attributesSnapshot);
} }
@ -904,7 +903,9 @@ public class DispatcherServlet extends FrameworkServlet {
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request; HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null; HandlerExecutionChain mappedHandler = null;
AsyncExecutionChain asyncChain = AsyncExecutionChain.getForCurrentRequest(request); AsyncExecutionChain asyncChain = AsyncExecutionChain.getForCurrentRequest(request);
boolean asyncStarted = false;
try { try {
ModelAndView mv = null; ModelAndView mv = null;
@ -941,22 +942,23 @@ public class DispatcherServlet extends FrameworkServlet {
return; return;
} }
mappedHandler.addDelegatingCallables(processedRequest, response); mappedHandler.pushInterceptorCallables(processedRequest, response);
asyncChain.push(getDispatchAsyncCallable(mappedHandler, request, response, processedRequest));
asyncChain.addDelegatingCallable( try {
getDispatchAsyncCallable(mappedHandler, request, response, processedRequest)); // Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// Actually invoke the handler. }
mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); finally {
asyncStarted = !asyncChain.pop();
if (asyncChain.isAsyncStarted()) { mappedHandler.popInterceptorCallables(processedRequest, response, asyncStarted);
mappedHandler.applyPostHandleAsyncStarted(processedRequest, response); if (asyncStarted) {
logger.debug("Exiting request thread and leaving the response open"); logger.debug("Exiting request thread and leaving the response open");
return; return;
}
} }
applyDefaultViewName(request, mv); applyDefaultViewName(request, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv); mappedHandler.applyPostHandle(processedRequest, response, mv);
} }
catch (Exception ex) { catch (Exception ex) {
@ -971,7 +973,7 @@ public class DispatcherServlet extends FrameworkServlet {
triggerAfterCompletionWithError(processedRequest, response, mappedHandler, err); triggerAfterCompletionWithError(processedRequest, response, mappedHandler, err);
} }
finally { finally {
if (asyncChain.isAsyncStarted()) { if (asyncStarted) {
return; return;
} }
// Clean up any resources used by a multipart request. // Clean up any resources used by a multipart request.
@ -1044,7 +1046,7 @@ public class DispatcherServlet extends FrameworkServlet {
ModelAndView mv = null; ModelAndView mv = null;
Exception dispatchException = null; Exception dispatchException = null;
try { try {
mv = (ModelAndView) getNextCallable().call(); mv = (ModelAndView) getNext().call();
applyDefaultViewName(processedRequest, mv); applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(request, response, mv); mappedHandler.applyPostHandle(request, response, mv);
} }

View File

@ -906,7 +906,7 @@ public abstract class FrameworkServlet extends HttpServletBean {
initContextHolders(request, localeContext, requestAttributes); initContextHolders(request, localeContext, requestAttributes);
AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(request); AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(request);
chain.addDelegatingCallable(getAsyncCallable(startTime, request, response, chain.push(getAsyncCallable(startTime, request, response,
previousLocaleContext, previousAttributes, localeContext, requestAttributes)); previousLocaleContext, previousAttributes, localeContext, requestAttributes));
try { try {
@ -917,7 +917,7 @@ public abstract class FrameworkServlet extends HttpServletBean {
} }
finally { finally {
resetContextHolders(request, previousLocaleContext, previousAttributes); resetContextHolders(request, previousLocaleContext, previousAttributes);
if (chain.isAsyncStarted()) { if (!chain.pop()) {
return; return;
} }
finalizeProcessing(startTime, request, response, requestAttributes, failureCause); finalizeProcessing(startTime, request, response, requestAttributes, failureCause);
@ -1018,7 +1018,7 @@ public abstract class FrameworkServlet extends HttpServletBean {
initContextHolders(request, localeContext, requestAttributes); initContextHolders(request, localeContext, requestAttributes);
Throwable unhandledFailure = null; Throwable unhandledFailure = null;
try { try {
getNextCallable().call(); getNext().call();
} }
catch (Throwable t) { catch (Throwable t) {
unhandledFailure = t; unhandledFailure = t;

View File

@ -49,6 +49,8 @@ public class HandlerExecutionChain {
private int interceptorIndex = -1; private int interceptorIndex = -1;
private int pushedCallableCount;
/** /**
* Create a new HandlerExecutionChain. * Create a new HandlerExecutionChain.
* @param handler the handler object to execute * @param handler the handler object to execute
@ -124,9 +126,7 @@ public class HandlerExecutionChain {
* next interceptor or the handler itself. Else, DispatcherServlet assumes * next interceptor or the handler itself. Else, DispatcherServlet assumes
* that this interceptor has already dealt with the response itself. * that this interceptor has already dealt with the response itself.
*/ */
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
throws Exception {
if (getInterceptors() != null) { if (getInterceptors() != null) {
for (int i = 0; i < getInterceptors().length; i++) { for (int i = 0; i < getInterceptors().length; i++) {
HandlerInterceptor interceptor = getInterceptors()[i]; HandlerInterceptor interceptor = getInterceptors()[i];
@ -140,12 +140,31 @@ public class HandlerExecutionChain {
return true; return true;
} }
void pushInterceptorCallables(HttpServletRequest request, HttpServletResponse response) {
if (getInterceptors() == null) {
return;
}
for (HandlerInterceptor interceptor : getInterceptors()) {
if (interceptor instanceof AsyncHandlerInterceptor) {
try {
AsyncHandlerInterceptor asyncInterceptor = (AsyncHandlerInterceptor) interceptor;
AbstractDelegatingCallable callable = asyncInterceptor.getAsyncCallable(request, response, this.handler);
if (callable != null) {
AsyncExecutionChain.getForCurrentRequest(request).push(callable);
this.pushedCallableCount++;
}
}
catch (Throwable ex) {
logger.error("HandlerInterceptor failed to return an async Callable", ex);
}
}
}
}
/** /**
* Apply postHandle methods of registered interceptors. * Apply postHandle methods of registered interceptors.
*/ */
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, ModelAndView mv) void applyPostHandle(HttpServletRequest request, HttpServletResponse response, ModelAndView mv) throws Exception {
throws Exception {
if (getInterceptors() == null) { if (getInterceptors() == null) {
return; return;
} }
@ -156,50 +175,28 @@ public class HandlerExecutionChain {
} }
/** /**
* Add delegating, async Callable instances to the {@link AsyncExecutionChain} * Remove pushed callables and apply postHandleAsyncStarted callbacks.
* for use in case of asynchronous request processing.
*/ */
void addDelegatingCallables(HttpServletRequest request, HttpServletResponse response) void popInterceptorCallables(HttpServletRequest request, HttpServletResponse response,
throws Exception { boolean asyncStarted) throws Exception {
if (getInterceptors() == null) { if (getInterceptors() == null) {
return; return;
} }
for (int i = getInterceptors().length - 1; i >= 0; i--) { AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(request);
HandlerInterceptor interceptor = getInterceptors()[i]; for ( ; this.pushedCallableCount > 0; this.pushedCallableCount--) {
if (interceptor instanceof AsyncHandlerInterceptor) { chain.pop();
try { }
AsyncHandlerInterceptor asyncInterceptor = (AsyncHandlerInterceptor) interceptor; if (asyncStarted) {
AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(request); for (int i = getInterceptors().length - 1; i >= 0; i--) {
AbstractDelegatingCallable callable = asyncInterceptor.getAsyncCallable(request, response, this.handler); HandlerInterceptor interceptor = getInterceptors()[i];
if (callable != null) { if (interceptor instanceof AsyncHandlerInterceptor) {
chain.addDelegatingCallable(callable); try {
((AsyncHandlerInterceptor) interceptor).postHandleAfterAsyncStarted(request, response, this.handler);
}
catch (Throwable ex) {
logger.error("HandlerInterceptor.postHandleAsyncStarted(..) failed", ex);
} }
}
catch (Throwable ex) {
logger.error("HandlerInterceptor.addAsyncCallables threw exception", ex);
}
}
}
}
/**
* Trigger postHandleAsyncStarted callbacks on the mapped HandlerInterceptors.
*/
void applyPostHandleAsyncStarted(HttpServletRequest request, HttpServletResponse response)
throws Exception {
if (getInterceptors() == null) {
return;
}
for (int i = getInterceptors().length - 1; i >= 0; i--) {
HandlerInterceptor interceptor = getInterceptors()[i];
if (interceptor instanceof AsyncHandlerInterceptor) {
try {
((AsyncHandlerInterceptor) interceptor).postHandleAsyncStarted(request, response, this.handler);
}
catch (Throwable ex) {
logger.error("HandlerInterceptor.postHandleAsyncStarted threw exception", ex);
} }
} }
} }

View File

@ -68,7 +68,7 @@ public class WebRequestHandlerInterceptorAdapter implements AsyncHandlerIntercep
return null; return null;
} }
public void postHandleAsyncStarted(HttpServletRequest request, HttpServletResponse response, Object handler) { public void postHandleAfterAsyncStarted(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (this.requestInterceptor instanceof AsyncWebRequestInterceptor) { if (this.requestInterceptor instanceof AsyncWebRequestInterceptor) {
AsyncWebRequestInterceptor asyncInterceptor = (AsyncWebRequestInterceptor) this.requestInterceptor; AsyncWebRequestInterceptor asyncInterceptor = (AsyncWebRequestInterceptor) this.requestInterceptor;
DispatcherServletWebRequest webRequest = new DispatcherServletWebRequest(request, response); DispatcherServletWebRequest webRequest = new DispatcherServletWebRequest(request, response);

View File

@ -59,8 +59,8 @@ public class AsyncMethodReturnValueHandler implements HandlerMethodReturnValueHa
AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(servletRequest); AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(servletRequest);
if (Callable.class.isAssignableFrom(paramType)) { if (Callable.class.isAssignableFrom(paramType)) {
chain.setCallable((Callable<Object>) returnValue); chain.setLastCallable((Callable<Object>) returnValue);
chain.startCallableChainProcessing(); chain.startCallableProcessing();
} }
else if (DeferredResult.class.isAssignableFrom(paramType)) { else if (DeferredResult.class.isAssignableFrom(paramType)) {
chain.startDeferredResultProcessing((DeferredResult<?>) returnValue); chain.startDeferredResultProcessing((DeferredResult<?>) returnValue);

View File

@ -653,14 +653,17 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect); mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(request); AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(request);
chain.addDelegatingCallable(getAsyncCallable(mavContainer, modelFactory, webRequest));
chain.setAsyncWebRequest(createAsyncWebRequest(request, response)); chain.setAsyncWebRequest(createAsyncWebRequest(request, response));
chain.setTaskExecutor(this.taskExecutor); chain.setTaskExecutor(this.taskExecutor);
chain.push(getAsyncCallable(mavContainer, modelFactory, webRequest));
requestMappingMethod.invokeAndHandle(webRequest, mavContainer); try {
requestMappingMethod.invokeAndHandle(webRequest, mavContainer);
if (chain.isAsyncStarted()) { }
return null; finally {
if (!chain.pop()) {
return null;
}
} }
return getModelAndView(mavContainer, modelFactory, webRequest); return getModelAndView(mavContainer, modelFactory, webRequest);
@ -758,7 +761,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
return new AbstractDelegatingCallable() { return new AbstractDelegatingCallable() {
public Object call() throws Exception { public Object call() throws Exception {
getNextCallable().call(); getNext().call();
return getModelAndView(mavContainer, modelFactory, webRequest); return getModelAndView(mavContainer, modelFactory, webRequest);
} }
}; };

View File

@ -91,9 +91,6 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
public final void invokeAndHandle(ServletWebRequest webRequest, public final void invokeAndHandle(ServletWebRequest webRequest,
ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(webRequest.getRequest());
chain.addDelegatingCallable(geAsyncCallable(webRequest, mavContainer, providedArgs));
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
setResponseStatus(webRequest); setResponseStatus(webRequest);
@ -111,15 +108,21 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
mavContainer.setRequestHandled(false); mavContainer.setRequestHandled(false);
AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(webRequest.getRequest());
chain.push(geAsyncCallable(webRequest, mavContainer, providedArgs));
try { try {
this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest); this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
} catch (Exception ex) { catch (Exception ex) {
if (logger.isTraceEnabled()) { if (logger.isTraceEnabled()) {
logger.trace(getReturnValueHandlingErrorMessage("Error handling return value", returnValue), ex); logger.trace(getReturnValueHandlingErrorMessage("Error handling return value", returnValue), ex);
} }
throw ex; throw ex;
} }
finally {
chain.pop();
}
} }
/** /**
@ -131,7 +134,7 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
return new AbstractDelegatingCallable() { return new AbstractDelegatingCallable() {
public Object call() throws Exception { public Object call() throws Exception {
mavContainer.setRequestHandled(false); mavContainer.setRequestHandled(false);
new CallableHandlerMethod(getNextCallable()).invokeAndHandle(webRequest, mavContainer, providedArgs); new CallableHandlerMethod(getNext()).invokeAndHandle(webRequest, mavContainer, providedArgs);
return null; return null;
} }
}; };

View File

@ -74,6 +74,10 @@ public class HandlerExecutionChainTests {
expect(this.interceptor2.preHandle(this.request, this.response, this.handler)).andReturn(true); expect(this.interceptor2.preHandle(this.request, this.response, this.handler)).andReturn(true);
expect(this.interceptor3.preHandle(this.request, this.response, this.handler)).andReturn(true); expect(this.interceptor3.preHandle(this.request, this.response, this.handler)).andReturn(true);
expect(this.interceptor1.getAsyncCallable(request, response, this.handler)).andReturn(new TestAsyncCallable());
expect(this.interceptor2.getAsyncCallable(request, response, this.handler)).andReturn(new TestAsyncCallable());
expect(this.interceptor3.getAsyncCallable(request, response, this.handler)).andReturn(new TestAsyncCallable());
this.interceptor1.postHandle(this.request, this.response, this.handler, mav); this.interceptor1.postHandle(this.request, this.response, this.handler, mav);
this.interceptor2.postHandle(this.request, this.response, this.handler, mav); this.interceptor2.postHandle(this.request, this.response, this.handler, mav);
this.interceptor3.postHandle(this.request, this.response, this.handler, mav); this.interceptor3.postHandle(this.request, this.response, this.handler, mav);
@ -85,6 +89,7 @@ public class HandlerExecutionChainTests {
replay(this.interceptor1, this.interceptor2, this.interceptor3); replay(this.interceptor1, this.interceptor2, this.interceptor3);
this.chain.applyPreHandle(request, response); this.chain.applyPreHandle(request, response);
this.chain.pushInterceptorCallables(request, response);
this.chain.applyPostHandle(request, response, mav); this.chain.applyPostHandle(request, response, mav);
this.chain.triggerAfterCompletion(this.request, this.response, null); this.chain.triggerAfterCompletion(this.request, this.response, null);
@ -103,9 +108,9 @@ public class HandlerExecutionChainTests {
expect(this.interceptor2.getAsyncCallable(request, response, this.handler)).andReturn(new TestAsyncCallable()); expect(this.interceptor2.getAsyncCallable(request, response, this.handler)).andReturn(new TestAsyncCallable());
expect(this.interceptor3.getAsyncCallable(request, response, this.handler)).andReturn(new TestAsyncCallable()); expect(this.interceptor3.getAsyncCallable(request, response, this.handler)).andReturn(new TestAsyncCallable());
this.interceptor1.postHandleAsyncStarted(request, response, this.handler); this.interceptor1.postHandleAfterAsyncStarted(request, response, this.handler);
this.interceptor2.postHandleAsyncStarted(request, response, this.handler); this.interceptor2.postHandleAfterAsyncStarted(request, response, this.handler);
this.interceptor3.postHandleAsyncStarted(request, response, this.handler); this.interceptor3.postHandleAfterAsyncStarted(request, response, this.handler);
this.interceptor1.postHandle(this.request, this.response, this.handler, mav); this.interceptor1.postHandle(this.request, this.response, this.handler, mav);
this.interceptor2.postHandle(this.request, this.response, this.handler, mav); this.interceptor2.postHandle(this.request, this.response, this.handler, mav);
@ -118,8 +123,8 @@ public class HandlerExecutionChainTests {
replay(this.interceptor1, this.interceptor2, this.interceptor3); replay(this.interceptor1, this.interceptor2, this.interceptor3);
this.chain.applyPreHandle(request, response); this.chain.applyPreHandle(request, response);
this.chain.addDelegatingCallables(request, response); this.chain.pushInterceptorCallables(request, response);
this.chain.applyPostHandleAsyncStarted(request, response); this.chain.popInterceptorCallables(request, response, true);
this.chain.applyPostHandle(request, response, mav); this.chain.applyPostHandle(request, response, mav);
this.chain.triggerAfterCompletion(this.request, this.response, null); this.chain.triggerAfterCompletion(this.request, this.response, null);

View File

@ -24,6 +24,7 @@ Changes in version 3.2 M2 (2012-08-xx)
* handle BindException in DefaultHandlerExceptionResolver * handle BindException in DefaultHandlerExceptionResolver
* parameterize DeferredResult type * parameterize DeferredResult type
* use reflection to instantiate StandardServletAsyncWebRequest * use reflection to instantiate StandardServletAsyncWebRequest
* fix issue with forward before async request processing
Changes in version 3.2 M1 (2012-05-28) Changes in version 3.2 M1 (2012-05-28)