Refactor Servlet 3 async support

As a result of the refactoring, the AsyncContext dispatch mechanism is
used much more centrally. Effectively every asynchronously processed
request involves one initial (container) thread, a second thread to
produce the handler return value asynchronously, and a third thread
as a result of a dispatch back to the container to resume processing
of the asynchronous resuilt.

Other updates include the addition of a MockAsyncContext and support
of related request method in the test packages of spring-web and
spring-webmvc. Also an upgrade of a Jetty test dependency required
to make tests pass.

Issue: SPR-9433
This commit is contained in:
Rossen Stoyanchev
2012-07-24 16:00:05 -04:00
parent 026ee846c7
commit 529e62921d
47 changed files with 1837 additions and 1741 deletions

View File

@@ -16,65 +16,45 @@
package org.springframework.web.servlet;
import java.util.concurrent.Callable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.context.request.async.AbstractDelegatingCallable;
/**
* Extends {@link HanderInterceptor} with lifecycle methods specific to async
* request processing.
* Extends the HandlerInterceptor contract for scenarios where a handler may be
* executed asynchronously. Since the handler will complete execution in another
* thread, the results are not available in the current thread, and therefore the
* DispatcherServlet exits quickly and on its way out invokes
* {@link #afterConcurrentHandlingStarted(HttpServletRequest, HttpServletResponse)}
* instead of {@code postHandle} and {@code afterCompletion}.
* When the async handler execution completes, and the request is dispatched back
* for further processing, the DispatcherServlet will invoke {@code preHandle}
* again, as well as {@code postHandle} and {@code afterCompletion}.
*
* <p>This is the sequence of events on the main thread in an async scenario:
* <ol>
* <li>{@link #preHandle(WebRequest)}
* <li>{@link #getAsyncCallable(WebRequest)}
* <li>... <em>handler execution</em>
* <li>{@link #postHandleAsyncStarted(WebRequest)}
* </ol>
*
* <p>This is the sequence of events on the async thread:
* <ol>
* <li>Async {@link Callable#call()} (the {@code Callable} returned by {@code getAsyncCallable})
* <li>... <em>async handler execution</em>
* <li>{@link #postHandle(WebRequest, org.springframework.ui.ModelMap)}
* <li>{@link #afterCompletion(WebRequest, Exception)}
* </ol>
* <p>Existing implementations should consider the fact that {@code preHandle} may
* be invoked twice before {@code postHandle} and {@code afterCompletion} are
* called if they don't implement this contract. Once before the start of concurrent
* handling and a second time as part of an asynchronous dispatch after concurrent
* handling is done. This may be not important in most cases but when some work
* needs to be done after concurrent handling starts (e.g. clearing thread locals)
* then this contract can be implemented.
*
* @author Rossen Stoyanchev
* @since 3.2
*
* @see org.springframework.web.context.request.async.WebAsyncManager
*/
public interface AsyncHandlerInterceptor extends HandlerInterceptor {
/**
* Invoked <em>after</em> {@link #preHandle(WebRequest)} and <em>before</em>
* the handler is executed. The returned {@link Callable} is used only if
* handler execution leads to teh start of async processing. It is invoked
* the async thread before the request is handled fro.
* <p>Implementations can use this <code>Callable</code> to initialize
* ThreadLocal attributes on the async thread.
* @param request current HTTP request
* @param response current HTTP response
* @param handler chosen handler to execute, for type and/or instance examination
* @return a {@link Callable} instance or <code>null</code>
* Called instead of {@code postHandle} and {@code afterCompletion}, when the
* a handler is being executed concurrently. Implementations may use the provided
* request and response but should avoid modifying them in ways that would
* conflict with the concurrent execution of the handler. A typical use of
* this method would be to clean thread local variables.
*
* @param request the current request
* @param response the current response
*/
AbstractDelegatingCallable getAsyncCallable(HttpServletRequest request, HttpServletResponse response, Object handler);
/**
* 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
* is invoked instead of {@link #postHandle(WebRequest, org.springframework.ui.ModelMap)}
* on the way out of the main processing thread allowing implementations
* to ensure ThreadLocal attributes are cleared. The <code>postHandle</code>
* invocation is effectively delayed until after async processing when the
* request has actually been handled.
* @param request current HTTP request
* @param response current HTTP response
* @param handler chosen handler to execute, for type and/or instance examination
*/
void postHandleAfterAsyncStarted(HttpServletRequest request, HttpServletResponse response, Object handler);
void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response);
}

View File

@@ -50,8 +50,8 @@ import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.async.AbstractDelegatingCallable;
import org.springframework.web.context.request.async.AsyncExecutionChain;
import org.springframework.web.context.request.async.WebAsyncManager;
import org.springframework.web.context.request.async.AsyncWebUtils;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.multipart.MultipartResolver;
@@ -819,8 +819,9 @@ public class DispatcherServlet extends FrameworkServlet {
if (logger.isDebugEnabled()) {
String requestUri = urlPathHelper.getRequestUri(request);
logger.debug("DispatcherServlet with name '" + getServletName() + "' processing " + request.getMethod() +
" request for [" + requestUri + "]");
String resumed = AsyncWebUtils.getAsyncManager(request).hasConcurrentResult() ? " resumed" : "";
logger.debug("DispatcherServlet with name '" + getServletName() + "'" + resumed +
" processing " + request.getMethod() + " request for [" + requestUri + "]");
}
// Keep a snapshot of the request attributes in case of an include,
@@ -851,14 +852,11 @@ public class DispatcherServlet extends FrameworkServlet {
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
AsyncExecutionChain asyncChain = AsyncExecutionChain.getForCurrentRequest(request);
asyncChain.push(getServiceAsyncCallable(request, attributesSnapshot));
try {
doDispatch(request, response);
}
finally {
if (!asyncChain.pop()) {
if (AsyncWebUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
return;
}
// Restore the original attribute snapshot, in case of an include.
@@ -868,27 +866,6 @@ public class DispatcherServlet extends FrameworkServlet {
}
}
/**
* Create a Callable to complete doService() processing asynchronously.
*/
private AbstractDelegatingCallable getServiceAsyncCallable(
final HttpServletRequest request, final Map<String, Object> attributesSnapshot) {
return new AbstractDelegatingCallable() {
public Object call() throws Exception {
if (logger.isDebugEnabled()) {
logger.debug("Resuming asynchronous processing of " + request.getMethod() +
" request for [" + urlPathHelper.getRequestUri(request) + "]");
}
getNext().call();
if (attributesSnapshot != null) {
restoreAttributesAfterInclude(request, attributesSnapshot);
}
return null;
}
};
}
/**
* Process the actual dispatching to the handler.
* <p>The handler will be obtained by applying the servlet's HandlerMappings in order.
@@ -903,9 +880,9 @@ public class DispatcherServlet extends FrameworkServlet {
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
AsyncExecutionChain asyncChain = AsyncExecutionChain.getForCurrentRequest(request);
boolean asyncStarted = false;
WebAsyncManager asyncManager = AsyncWebUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
@@ -913,6 +890,7 @@ public class DispatcherServlet extends FrameworkServlet {
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = processedRequest != request;
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest, false);
@@ -942,18 +920,12 @@ public class DispatcherServlet extends FrameworkServlet {
return;
}
mappedHandler.pushInterceptorCallables(processedRequest, response);
asyncChain.push(getDispatchAsyncCallable(mappedHandler, request, response, processedRequest));
try {
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
}
finally {
asyncStarted = !asyncChain.pop();
mappedHandler.popInterceptorCallables(processedRequest, response, asyncStarted);
if (asyncStarted) {
logger.debug("Exiting request thread and leaving the response open");
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
}
@@ -973,11 +945,13 @@ public class DispatcherServlet extends FrameworkServlet {
triggerAfterCompletionWithError(processedRequest, response, mappedHandler, err);
}
finally {
if (asyncStarted) {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
return;
}
// Clean up any resources used by a multipart request.
if (processedRequest != request) {
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
@@ -1027,50 +1001,16 @@ public class DispatcherServlet extends FrameworkServlet {
}
}
if (AsyncWebUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}
if (mappedHandler != null) {
mappedHandler.triggerAfterCompletion(request, response, null);
}
}
/**
* Create a Callable to complete doDispatch processing asynchronously.
*/
private AbstractDelegatingCallable getDispatchAsyncCallable(
final HandlerExecutionChain mappedHandler,
final HttpServletRequest request, final HttpServletResponse response,
final HttpServletRequest processedRequest) throws Exception {
return new AbstractDelegatingCallable() {
public Object call() throws Exception {
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
mv = (ModelAndView) getNext().call();
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(request, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Error err) {
triggerAfterCompletionWithError(processedRequest, response, mappedHandler, err);
}
finally {
if (processedRequest != request) {
cleanupMultipart(processedRequest);
}
}
return null;
}
};
}
/**
* Build a LocaleContext for the given request, exposing the request's primary locale as current locale.
* <p>The default implementation uses the dispatcher's LocaleResolver to obtain the current locale,
@@ -1113,12 +1053,13 @@ public class DispatcherServlet extends FrameworkServlet {
/**
* Clean up any resources used by the given multipart request (if any).
* @param request current HTTP request
* @param servletRequest current HTTP request
* @see MultipartResolver#cleanupMultipart
*/
protected void cleanupMultipart(HttpServletRequest request) {
if (request instanceof MultipartHttpServletRequest) {
this.multipartResolver.cleanupMultipart((MultipartHttpServletRequest) request);
protected void cleanupMultipart(HttpServletRequest servletRequest) {
MultipartHttpServletRequest req = WebUtils.getNativeRequest(servletRequest, MultipartHttpServletRequest.class);
if (req != null) {
this.multipartResolver.cleanupMultipart(req);
}
}

View File

@@ -47,8 +47,9 @@ import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.request.async.AbstractDelegatingCallable;
import org.springframework.web.context.request.async.AsyncExecutionChain;
import org.springframework.web.context.request.async.AsyncWebUtils;
import org.springframework.web.context.request.async.WebAsyncManager;
import org.springframework.web.context.request.async.WebAsyncManager.AsyncThreadInitializer;
import org.springframework.web.context.support.ServletRequestHandledEvent;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.context.support.XmlWebApplicationContext;
@@ -195,6 +196,7 @@ public abstract class FrameworkServlet extends HttpServletBean {
private ArrayList<ApplicationContextInitializer<ConfigurableApplicationContext>> contextInitializers =
new ArrayList<ApplicationContextInitializer<ConfigurableApplicationContext>>();
/**
* Create a new {@code FrameworkServlet} that will create its own internal web
* application context based on defaults and values provided through servlet
@@ -905,22 +907,55 @@ public abstract class FrameworkServlet extends HttpServletBean {
initContextHolders(request, localeContext, requestAttributes);
AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(request);
chain.push(getAsyncCallable(startTime, request, response,
previousLocaleContext, previousAttributes, localeContext, requestAttributes));
WebAsyncManager asyncManager = AsyncWebUtils.getAsyncManager(request);
asyncManager.registerAsyncThreadInitializer(this.getClass().getName(), createAsyncThreadInitializer(request));
try {
doService(request, response);
}
catch (Throwable t) {
failureCause = t;
catch (ServletException ex) {
failureCause = ex;
throw ex;
}
catch (IOException ex) {
failureCause = ex;
throw ex;
}
catch (Throwable ex) {
failureCause = ex;
throw new NestedServletException("Request processing failed", ex);
}
finally {
resetContextHolders(request, previousLocaleContext, previousAttributes);
if (!chain.pop()) {
return;
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
if (logger.isDebugEnabled()) {
if (failureCause != null) {
this.logger.debug("Could not complete request", failureCause);
} else {
if (asyncManager.isConcurrentHandlingStarted()) {
if (logger.isDebugEnabled()) {
logger.debug("Leaving response open for concurrent processing");
}
}
else {
this.logger.debug("Successfully completed request");
}
}
}
if (this.publishEvents) {
// Whether or not we succeeded, publish an event.
long processingTime = System.currentTimeMillis() - startTime;
this.webApplicationContext.publishEvent(
new ServletRequestHandledEvent(this,
request.getRequestURI(), request.getRemoteAddr(),
request.getMethod(), getServletConfig().getServletName(),
WebUtils.getSessionId(request), getUsernameForRequest(request),
processingTime, failureCause));
}
finalizeProcessing(startTime, request, response, requestAttributes, failureCause);
}
}
@@ -956,78 +991,14 @@ public abstract class FrameworkServlet extends HttpServletBean {
}
}
/**
* Log and re-throw unhandled exceptions, publish a ServletRequestHandledEvent, etc.
*/
private void finalizeProcessing(long startTime, HttpServletRequest request, HttpServletResponse response,
ServletRequestAttributes requestAttributes, Throwable t) throws ServletException, IOException {
private AsyncThreadInitializer createAsyncThreadInitializer(final HttpServletRequest request) {
Throwable failureCause = null;
try {
if (t != null) {
if (t instanceof ServletException) {
failureCause = t;
throw (ServletException) t;
}
else if (t instanceof IOException) {
failureCause = t;
throw (IOException) t;
}
else {
NestedServletException ex = new NestedServletException("Request processing failed", t);
failureCause = ex;
throw ex;
}
return new AsyncThreadInitializer() {
public void initialize() {
initContextHolders(request, buildLocaleContext(request), new ServletRequestAttributes(request));
}
}
finally {
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
if (logger.isDebugEnabled()) {
if (failureCause != null) {
this.logger.debug("Could not complete request", failureCause);
}
else {
this.logger.debug("Successfully completed request");
}
}
if (this.publishEvents) {
// Whether or not we succeeded, publish an event.
long processingTime = System.currentTimeMillis() - startTime;
this.webApplicationContext.publishEvent(
new ServletRequestHandledEvent(this,
request.getRequestURI(), request.getRemoteAddr(),
request.getMethod(), getServletConfig().getServletName(),
WebUtils.getSessionId(request), getUsernameForRequest(request),
processingTime, failureCause));
}
}
}
/**
* Create a Callable to use to complete processing in an async execution chain.
*/
private AbstractDelegatingCallable getAsyncCallable(final long startTime,
final HttpServletRequest request, final HttpServletResponse response,
final LocaleContext previousLocaleContext, final RequestAttributes previousAttributes,
final LocaleContext localeContext, final ServletRequestAttributes requestAttributes) {
return new AbstractDelegatingCallable() {
public Object call() throws Exception {
initContextHolders(request, localeContext, requestAttributes);
Throwable unhandledFailure = null;
try {
getNext().call();
}
catch (Throwable t) {
unhandledFailure = t;
}
finally {
resetContextHolders(request, previousLocaleContext, previousAttributes);
finalizeProcessing(startTime, request, response, requestAttributes, unhandledFailure);
}
return null;
public void reset() {
resetContextHolders(request, null, null);
}
};
}
@@ -1061,6 +1032,7 @@ public abstract class FrameworkServlet extends HttpServletBean {
protected abstract void doService(HttpServletRequest request, HttpServletResponse response)
throws Exception;
/**
* Close the WebApplicationContext of this servlet.
* @see org.springframework.context.ConfigurableApplicationContext#close()
@@ -1073,6 +1045,7 @@ public abstract class FrameworkServlet extends HttpServletBean {
}
}
/**
* ApplicationListener endpoint that receives events from this servlet's WebApplicationContext
* only, delegating to <code>onApplicationEvent</code> on the FrameworkServlet instance.

View File

@@ -26,8 +26,6 @@ import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.request.async.AbstractDelegatingCallable;
import org.springframework.web.context.request.async.AsyncExecutionChain;
/**
* Handler execution chain, consisting of handler object and any handler interceptors.
@@ -49,7 +47,6 @@ public class HandlerExecutionChain {
private int interceptorIndex = -1;
private int pushedCallableCount;
/**
* Create a new HandlerExecutionChain.
@@ -140,27 +137,6 @@ public class HandlerExecutionChain {
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.
*/
@@ -174,34 +150,6 @@ public class HandlerExecutionChain {
}
}
/**
* Remove pushed callables and apply postHandleAsyncStarted callbacks.
*/
void popInterceptorCallables(HttpServletRequest request, HttpServletResponse response,
boolean asyncStarted) throws Exception {
if (getInterceptors() == null) {
return;
}
AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(request);
for ( ; this.pushedCallableCount > 0; this.pushedCallableCount--) {
chain.pop();
}
if (asyncStarted) {
for (int i = getInterceptors().length - 1; i >= 0; i--) {
HandlerInterceptor interceptor = getInterceptors()[i];
if (interceptor instanceof AsyncHandlerInterceptor) {
try {
((AsyncHandlerInterceptor) interceptor).postHandleAfterAsyncStarted(request, response, this.handler);
}
catch (Throwable ex) {
logger.error("HandlerInterceptor.postHandleAsyncStarted(..) failed", ex);
}
}
}
}
}
/**
* Trigger afterCompletion callbacks on the mapped HandlerInterceptors.
* Will just invoke afterCompletion for all interceptors whose preHandle invocation
@@ -224,6 +172,25 @@ public class HandlerExecutionChain {
}
}
/**
* Apply afterConcurrentHandlerStarted callback on mapped AsyncHandlerInterceptors.
*/
void applyAfterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response) {
if (getInterceptors() == null) {
return;
}
for (int i = getInterceptors().length - 1; i >= 0; i--) {
if (interceptors[i] instanceof AsyncHandlerInterceptor) {
try {
AsyncHandlerInterceptor asyncInterceptor = (AsyncHandlerInterceptor) interceptors[i];
asyncInterceptor.afterConcurrentHandlingStarted(request, response);
}
catch (Throwable ex) {
logger.error("Interceptor [" + interceptors[i] + "] failed in afterConcurrentHandlingStarted", ex);
}
}
}
}
/**
* Delegates to the handler's <code>toString()</code>.

View File

@@ -31,6 +31,14 @@ import javax.servlet.http.HttpServletResponse;
* or common handler behavior like locale or theme changes. Its main purpose
* is to allow for factoring out repetitive handler code.
*
* <p>In an async processing scenario, the handler may be executed in a separate
* thread while the main thread exits without rendering or invoking the
* {@code postHandle} and {@code afterCompletion} callbacks. When concurrent
* handler execution completes, the request is dispatched back in order to
* proceed with rendering the model and all methods of this contract are invoked
* again. For further options and comments see
* {@code org.springframework.web.servlet.HandlerInterceptor}
*
* <p>Typically an interceptor chain is defined per HandlerMapping bean,
* sharing its granularity. To be able to apply a certain interceptor chain
* to a group of handlers, one needs to map the desired handlers via one

View File

@@ -21,7 +21,6 @@ import javax.servlet.http.HttpServletResponse;
import org.springframework.util.Assert;
import org.springframework.web.context.request.WebRequestInterceptor;
import org.springframework.web.context.request.async.AbstractDelegatingCallable;
import org.springframework.web.context.request.async.AsyncWebRequestInterceptor;
import org.springframework.web.servlet.AsyncHandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
@@ -57,25 +56,6 @@ public class WebRequestHandlerInterceptorAdapter implements AsyncHandlerIntercep
return true;
}
public AbstractDelegatingCallable getAsyncCallable(HttpServletRequest request,
HttpServletResponse response, Object handler) {
if (this.requestInterceptor instanceof AsyncWebRequestInterceptor) {
AsyncWebRequestInterceptor asyncInterceptor = (AsyncWebRequestInterceptor) this.requestInterceptor;
DispatcherServletWebRequest webRequest = new DispatcherServletWebRequest(request, response);
return asyncInterceptor.getAsyncCallable(webRequest);
}
return null;
}
public void postHandleAfterAsyncStarted(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (this.requestInterceptor instanceof AsyncWebRequestInterceptor) {
AsyncWebRequestInterceptor asyncInterceptor = (AsyncWebRequestInterceptor) this.requestInterceptor;
DispatcherServletWebRequest webRequest = new DispatcherServletWebRequest(request, response);
asyncInterceptor.postHandleAsyncStarted(webRequest);
}
}
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
throws Exception {
@@ -89,4 +69,12 @@ public class WebRequestHandlerInterceptorAdapter implements AsyncHandlerIntercep
this.requestInterceptor.afterCompletion(new DispatcherServletWebRequest(request, response), ex);
}
public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response) {
if (this.requestInterceptor instanceof AsyncWebRequestInterceptor) {
AsyncWebRequestInterceptor asyncInterceptor = (AsyncWebRequestInterceptor) this.requestInterceptor;
DispatcherServletWebRequest webRequest = new DispatcherServletWebRequest(request, response);
asyncInterceptor.afterConcurrentHandlingStarted(webRequest);
}
}
}

View File

@@ -24,8 +24,9 @@ import javax.servlet.ServletRequest;
import org.springframework.core.MethodParameter;
import org.springframework.util.Assert;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.async.AsyncExecutionChain;
import org.springframework.web.context.request.async.DeferredResult;
import org.springframework.web.context.request.async.WebAsyncManager;
import org.springframework.web.context.request.async.AsyncWebUtils;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
@@ -52,18 +53,15 @@ public class AsyncMethodReturnValueHandler implements HandlerMethodReturnValueHa
Assert.notNull(returnValue, "A Callable or a DeferredValue is required");
mavContainer.setRequestHandled(true);
Class<?> paramType = returnType.getParameterType();
ServletRequest servletRequest = webRequest.getNativeRequest(ServletRequest.class);
AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(servletRequest);
WebAsyncManager asyncManager = AsyncWebUtils.getAsyncManager(servletRequest);
if (Callable.class.isAssignableFrom(paramType)) {
chain.setLastCallable((Callable<Object>) returnValue);
chain.startCallableProcessing();
asyncManager.startCallableProcessing((Callable<Object>) returnValue, mavContainer);
}
else if (DeferredResult.class.isAssignableFrom(paramType)) {
chain.startDeferredResultProcessing((DeferredResult<?>) returnValue);
asyncManager.startDeferredResultProcessing((DeferredResult<?>) returnValue, mavContainer);
}
else {
// should never happen..

View File

@@ -61,10 +61,10 @@ import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.context.request.async.AbstractDelegatingCallable;
import org.springframework.web.context.request.async.AsyncExecutionChain;
import org.springframework.web.context.request.async.AsyncWebRequest;
import org.springframework.web.context.request.async.NoOpAsyncWebRequest;
import org.springframework.web.context.request.async.NoSupportAsyncWebRequest;
import org.springframework.web.context.request.async.WebAsyncManager;
import org.springframework.web.context.request.async.AsyncWebUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.HandlerMethodSelector;
import org.springframework.web.method.annotation.ErrorsMethodArgumentResolver;
@@ -146,7 +146,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
private final Map<Class<?>, Set<Method>> modelFactoryCache = new ConcurrentHashMap<Class<?>, Set<Method>>();
private AsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor();
private AsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor("MvcAsync");
private Long asyncRequestTimeout;
@@ -652,48 +652,36 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
modelFactory.initModel(webRequest, mavContainer, requestMappingMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(request);
chain.setAsyncWebRequest(createAsyncWebRequest(request, response));
chain.setTaskExecutor(this.taskExecutor);
chain.push(getAsyncCallable(mavContainer, modelFactory, webRequest));
AsyncWebRequest asyncWebRequest = createAsyncWebRequest(request, response);
asyncWebRequest.setTimeout(this.asyncRequestTimeout);
try {
requestMappingMethod.invokeAndHandle(webRequest, mavContainer);
}
finally {
if (!chain.pop()) {
return null;
final WebAsyncManager asyncManager = AsyncWebUtils.getAsyncManager(request);
asyncManager.setTaskExecutor(this.taskExecutor);
asyncManager.setAsyncWebRequest(asyncWebRequest);
if (asyncManager.hasConcurrentResult()) {
Object result = asyncManager.getConcurrentResult();
mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
asyncManager.resetConcurrentResult();
if (logger.isDebugEnabled()) {
logger.debug("Found concurrent result value [" + result + "]");
}
requestMappingMethod = requestMappingMethod.wrapConcurrentProcessingResult(result);
}
requestMappingMethod.invokeAndHandle(webRequest, mavContainer);
if (asyncManager.isConcurrentHandlingStarted()) {
return null;
}
return getModelAndView(mavContainer, modelFactory, webRequest);
}
private AsyncWebRequest createAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) {
if (ClassUtils.hasMethod(ServletRequest.class, "startAsync")) {
AsyncWebRequest asyncRequest = instantiateStandardServletAsyncWebRequest(request, response);
asyncRequest.setTimeout(this.asyncRequestTimeout);
return asyncRequest;
}
else {
return new NoOpAsyncWebRequest(request, response);
}
}
private ServletInvocableHandlerMethod createRequestMappingMethod(
HandlerMethod handlerMethod, WebDataBinderFactory binderFactory) {
private AsyncWebRequest instantiateStandardServletAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) {
String className = "org.springframework.web.context.request.async.StandardServletAsyncWebRequest";
try {
Class<?> clazz = ClassUtils.forName(className, this.getClass().getClassLoader());
Constructor<?> constructor = clazz.getConstructor(HttpServletRequest.class, HttpServletResponse.class);
return (AsyncWebRequest) BeanUtils.instantiateClass(constructor , request, response);
}
catch (Throwable t) {
throw new IllegalStateException("Failed to instantiate StandardServletAsyncWebRequest", t);
}
}
private ServletInvocableHandlerMethod createRequestMappingMethod(HandlerMethod handlerMethod,
WebDataBinderFactory binderFactory) {
ServletInvocableHandlerMethod requestMethod;
requestMethod = new ServletInvocableHandlerMethod(handlerMethod.getBean(), handlerMethod.getMethod());
requestMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
@@ -753,18 +741,21 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
return new ServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer());
}
/**
* Create a Callable to produce a ModelAndView asynchronously.
*/
private AbstractDelegatingCallable getAsyncCallable(final ModelAndViewContainer mavContainer,
final ModelFactory modelFactory, final NativeWebRequest webRequest) {
private AsyncWebRequest createAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) {
return ClassUtils.hasMethod(ServletRequest.class, "startAsync") ?
createStandardServletAsyncWebRequest(request, response) : new NoSupportAsyncWebRequest(request, response);
}
return new AbstractDelegatingCallable() {
public Object call() throws Exception {
getNext().call();
return getModelAndView(mavContainer, modelFactory, webRequest);
}
};
private AsyncWebRequest createStandardServletAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) {
try {
String className = "org.springframework.web.context.request.async.StandardServletAsyncWebRequest";
Class<?> clazz = ClassUtils.forName(className, this.getClass().getClassLoader());
Constructor<?> constructor = clazz.getConstructor(HttpServletRequest.class, HttpServletResponse.class);
return (AsyncWebRequest) BeanUtils.instantiateClass(constructor, request, response);
}
catch (Throwable t) {
throw new IllegalStateException("Failed to instantiate StandardServletAsyncWebRequest", t);
}
}
private ModelAndView getModelAndView(ModelAndViewContainer mavContainer,

View File

@@ -26,13 +26,12 @@ import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.async.AbstractDelegatingCallable;
import org.springframework.web.context.request.async.AsyncExecutionChain;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite;
import org.springframework.web.method.support.InvocableHandlerMethod;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.View;
import org.springframework.web.util.NestedServletException;
/**
* Extends {@link InvocableHandlerMethod} with the ability to handle return
@@ -108,9 +107,6 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
mavContainer.setRequestHandled(false);
AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(webRequest.getRequest());
chain.push(geAsyncCallable(webRequest, mavContainer, providedArgs));
try {
this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
@@ -120,33 +116,6 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
}
throw ex;
}
finally {
chain.pop();
}
}
/**
* Create a Callable to populate the ModelAndViewContainer asynchronously.
*/
private AbstractDelegatingCallable geAsyncCallable(final ServletWebRequest webRequest,
final ModelAndViewContainer mavContainer, final Object... providedArgs) {
return new AbstractDelegatingCallable() {
public Object call() throws Exception {
mavContainer.setRequestHandled(false);
new CallableHandlerMethod(getNext()).invokeAndHandle(webRequest, mavContainer, providedArgs);
return null;
}
};
}
private String getReturnValueHandlingErrorMessage(String message, Object returnValue) {
StringBuilder sb = new StringBuilder(message);
if (returnValue != null) {
sb.append(" [type=" + returnValue.getClass().getName() + "] ");
}
sb.append("[value=" + returnValue + "]");
return getDetailedErrorMessage(sb.toString());
}
/**
@@ -184,11 +153,39 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
return responseStatus != null;
}
private String getReturnValueHandlingErrorMessage(String message, Object returnValue) {
StringBuilder sb = new StringBuilder(message);
if (returnValue != null) {
sb.append(" [type=" + returnValue.getClass().getName() + "] ");
}
sb.append("[value=" + returnValue + "]");
return getDetailedErrorMessage(sb.toString());
}
/**
* Wraps the Callable returned from a HandlerMethod so may be invoked just
* like the HandlerMethod with the same return value handling guarantees.
* Method-level annotations must be on the HandlerMethod, not the Callable.
* Return a ServletInvocableHandlerMethod that will process the value returned
* from an async operation essentially either applying return value handling or
* raising an exception if the end result is an Exception.
*/
ServletInvocableHandlerMethod wrapConcurrentProcessingResult(final Object result) {
return new CallableHandlerMethod(new Callable<Object>() {
public Object call() throws Exception {
if (result instanceof Exception) {
throw (Exception) result;
}
else if (result instanceof Throwable) {
throw new NestedServletException("Async processing failed", (Throwable) result);
}
return result;
}
});
}
/**
* Wrap a Callable as a ServletInvocableHandlerMethod inheriting method-level annotations.
*/
private class CallableHandlerMethod extends ServletInvocableHandlerMethod {