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