Initial cut of Servlet 3.0 based async support

From a programming model perspective, @RequestMapping methods now
support two new return value types:

* java.util.concurrent.Callable - used by Spring MVC to obtain the
return value asynchronously in a separate thread managed transparently
by Spring MVC on behalf of the application.
* org.springframework.web.context.request.async.DeferredResult - used
by the application to produce the return value asynchronously in a
separate thread of its own choosing.

The high-level idea is that whatever value a controller normally
returns, it can now provide it asynchronously, through a Callable or
through a DeferredResult, with all remaining processing --
@ResponseBody, view resolution, etc, working just the same but
completed outside the main request thread.

From an SPI perspective, there are several new types:

* AsyncExecutionChain - the central class for managing async request
processing through a sequence of Callable instances each representing
work required to complete request processing asynchronously.
* AsyncWebRequest - provides methods for starting, completing, and
configuring async request processing.
* StandardServletAsyncWebRequest - Servlet 3 based implementation.
* AsyncExecutionChainRunnable - the Runnable used for async request
execution.

All spring-web and spring-webmvc Filter implementations have been
updated to participate in async request execution.
The open-session-in-view Filter and interceptors implementations in
spring-orm will be updated in a separate pull request.

Issue: SPR-8517
This commit is contained in:
Rossen Stoyanchev
2012-04-12 23:20:35 -04:00
parent fdded0768e
commit 3642b0f365
27 changed files with 2257 additions and 363 deletions

View File

@@ -50,6 +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.multipart.MultipartException;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.multipart.MultipartResolver;
@@ -130,6 +132,7 @@ import org.springframework.web.util.WebUtils;
* @author Juergen Hoeller
* @author Rob Harrop
* @author Chris Beams
* @author Rossen Stoyanchev
* @see org.springframework.web.HttpRequestHandler
* @see org.springframework.web.servlet.mvc.Controller
* @see org.springframework.web.context.ContextLoaderListener
@@ -297,11 +300,10 @@ public class DispatcherServlet extends FrameworkServlet {
/** FlashMapManager used by this servlet */
private FlashMapManager flashMapManager;
/** List of ViewResolvers used by this servlet */
private List<ViewResolver> viewResolvers;
/**
* Create a new {@code DispatcherServlet} that will create its own internal web
* application context based on defaults and values provided through servlet
@@ -691,7 +693,7 @@ public class DispatcherServlet extends FrameworkServlet {
/**
* Initialize the {@link FlashMapManager} used by this servlet instance.
* <p>If no implementation is configured then we default to
* <p>If no implementation is configured then we default to
* {@code org.springframework.web.servlet.support.DefaultFlashMapManager}.
*/
private void initFlashMapManager(ApplicationContext context) {
@@ -814,6 +816,9 @@ public class DispatcherServlet extends FrameworkServlet {
*/
@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
AsyncExecutionChain asyncChain = AsyncExecutionChain.getForCurrentRequest(request);
if (logger.isDebugEnabled()) {
String requestUri = urlPathHelper.getRequestUri(request);
logger.debug("DispatcherServlet with name '" + getServletName() + "' processing " + request.getMethod() +
@@ -848,10 +853,15 @@ public class DispatcherServlet extends FrameworkServlet {
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
asyncChain.addDelegatingCallable(getServiceAsyncCallable(request, attributesSnapshot));
try {
doDispatch(request, response);
}
finally {
if (asyncChain.isAsyncStarted()) {
return;
}
// Restore the original attribute snapshot, in case of an include.
if (attributesSnapshot != null) {
restoreAttributesAfterInclude(request, attributesSnapshot);
@@ -859,6 +869,27 @@ 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) + "]");
}
getNextCallable().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.
@@ -873,11 +904,11 @@ public class DispatcherServlet extends FrameworkServlet {
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
int interceptorIndex = -1;
AsyncExecutionChain asyncChain = AsyncExecutionChain.getForCurrentRequest(request);
try {
ModelAndView mv;
boolean errorView = false;
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
@@ -892,7 +923,7 @@ public class DispatcherServlet extends FrameworkServlet {
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
@@ -906,76 +937,40 @@ public class DispatcherServlet extends FrameworkServlet {
}
}
// Apply preHandle methods of registered interceptors.
HandlerInterceptor[] interceptors = mappedHandler.getInterceptors();
if (interceptors != null) {
for (int i = 0; i < interceptors.length; i++) {
HandlerInterceptor interceptor = interceptors[i];
if (!interceptor.preHandle(processedRequest, response, mappedHandler.getHandler())) {
triggerAfterCompletion(mappedHandler, interceptorIndex, processedRequest, response, null);
return;
}
interceptorIndex = i;
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
asyncChain.addDelegatingCallable(
getDispatchAsyncCallable(mappedHandler, request, response, processedRequest));
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// Do we need view name translation?
if (mv != null && !mv.hasView()) {
mv.setViewName(getDefaultViewName(request));
if (asyncChain.isAsyncStarted()) {
logger.debug("Exiting request thread and leaving the response open");
return;
}
// Apply postHandle methods of registered interceptors.
if (interceptors != null) {
for (int i = interceptors.length - 1; i >= 0; i--) {
HandlerInterceptor interceptor = interceptors[i];
interceptor.postHandle(processedRequest, response, mappedHandler.getHandler(), mv);
}
}
}
catch (ModelAndViewDefiningException ex) {
logger.debug("ModelAndViewDefiningException encountered", ex);
mv = ex.getModelAndView();
applyDefaultViewName(request, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(processedRequest, response, handler, ex);
errorView = (mv != null);
dispatchException = ex;
}
// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
render(mv, processedRequest, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + getServletName() +
"': assuming HandlerAdapter completed request handling");
}
}
// Trigger after-completion for successful outcome.
triggerAfterCompletion(mappedHandler, interceptorIndex, processedRequest, response, null);
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
// Trigger after-completion for thrown exception.
triggerAfterCompletion(mappedHandler, interceptorIndex, processedRequest, response, ex);
throw ex;
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Error err) {
ServletException ex = new NestedServletException("Handler processing failed", err);
// Trigger after-completion for thrown exception.
triggerAfterCompletion(mappedHandler, interceptorIndex, processedRequest, response, ex);
throw ex;
triggerAfterCompletionWithError(processedRequest, response, mappedHandler, err);
}
finally {
if (asyncChain.isAsyncStarted()) {
return;
}
// Clean up any resources used by a multipart request.
if (processedRequest != request) {
cleanupMultipart(processedRequest);
@@ -983,6 +978,94 @@ public class DispatcherServlet extends FrameworkServlet {
}
}
/**
* Do we need view name translation?
*/
private void applyDefaultViewName(HttpServletRequest request, ModelAndView mv) throws Exception {
if (mv != null && !mv.hasView()) {
mv.setViewName(getDefaultViewName(request));
}
}
/**
* Handle the result of handler selection and handler invocation, which is
* either a ModelAndView or an Exception to be resolved to a ModelAndView.
*/
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {
boolean errorView = false;
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + getServletName() +
"': assuming HandlerAdapter completed request handling");
}
}
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) getNextCallable().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,
@@ -996,7 +1079,6 @@ public class DispatcherServlet extends FrameworkServlet {
public Locale getLocale() {
return localeResolver.resolveLocale(request);
}
@Override
public String toString() {
return getLocale().toString();
}
@@ -1216,36 +1298,23 @@ public class DispatcherServlet extends FrameworkServlet {
return null;
}
/**
* Trigger afterCompletion callbacks on the mapped HandlerInterceptors.
* Will just invoke afterCompletion for all interceptors whose preHandle invocation
* has successfully completed and returned true.
* @param mappedHandler the mapped HandlerExecutionChain
* @param interceptorIndex index of last interceptor that successfully completed
* @param ex Exception thrown on handler execution, or <code>null</code> if none
* @see HandlerInterceptor#afterCompletion
*/
private void triggerAfterCompletion(HandlerExecutionChain mappedHandler,
int interceptorIndex,
HttpServletRequest request,
HttpServletResponse response,
Exception ex) throws Exception {
private void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response,
HandlerExecutionChain mappedHandler, Exception ex) throws Exception {
// Apply afterCompletion methods of registered interceptors.
if (mappedHandler != null) {
HandlerInterceptor[] interceptors = mappedHandler.getInterceptors();
if (interceptors != null) {
for (int i = interceptorIndex; i >= 0; i--) {
HandlerInterceptor interceptor = interceptors[i];
try {
interceptor.afterCompletion(request, response, mappedHandler.getHandler(), ex);
}
catch (Throwable ex2) {
logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
}
}
}
mappedHandler.triggerAfterCompletion(request, response, ex);
}
throw ex;
}
private void triggerAfterCompletionWithError(HttpServletRequest request, HttpServletResponse response,
HandlerExecutionChain mappedHandler, Error error) throws Exception, ServletException {
ServletException ex = new NestedServletException("Handler processing failed", error);
if (mappedHandler != null) {
mappedHandler.triggerAfterCompletion(request, response, ex);
}
throw ex;
}
/**

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@ import java.io.IOException;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collections;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
@@ -45,6 +46,8 @@ 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.support.ServletRequestHandledEvent;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.context.support.XmlWebApplicationContext;
@@ -112,6 +115,7 @@ import org.springframework.web.util.WebUtils;
* @author Juergen Hoeller
* @author Sam Brannen
* @author Chris Beams
* @author Rossen Stoyanchev
* @see #doService
* @see #setContextClass
* @see #setContextConfigLocation
@@ -190,7 +194,6 @@ 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
@@ -850,7 +853,6 @@ public abstract class FrameworkServlet extends HttpServletBean {
super.doTrace(request, response);
}
/**
* Process this request, publishing an event regardless of the outcome.
* <p>The actual event handling is performed by the abstract
@@ -862,49 +864,96 @@ public abstract class FrameworkServlet extends HttpServletBean {
long startTime = System.currentTimeMillis();
Throwable failureCause = null;
// Expose current LocaleResolver and request as LocaleContext.
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContextHolder.setLocaleContext(buildLocaleContext(request), this.threadContextInheritable);
LocaleContext localeContext = buildLocaleContext(request);
// Expose current RequestAttributes to current thread.
RequestAttributes previousRequestAttributes = RequestContextHolder.getRequestAttributes();
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = null;
if (previousRequestAttributes == null || previousRequestAttributes.getClass().equals(ServletRequestAttributes.class)) {
if (previousAttributes == null || previousAttributes.getClass().equals(ServletRequestAttributes.class)) {
requestAttributes = new ServletRequestAttributes(request);
RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
}
if (logger.isTraceEnabled()) {
logger.trace("Bound request context to thread: " + request);
}
initContextHolders(request, localeContext, requestAttributes);
AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(request);
chain.addDelegatingCallable(getAsyncCallable(startTime, request, response,
previousLocaleContext, previousAttributes, localeContext, requestAttributes));
try {
doService(request, response);
}
catch (ServletException ex) {
failureCause = ex;
throw ex;
catch (Throwable t) {
failureCause = t;
}
finally {
resetContextHolders(request, previousLocaleContext, previousAttributes);
if (chain.isAsyncStarted()) {
return;
}
finalizeProcessing(startTime, request, response, requestAttributes, failureCause);
}
}
/**
* Build a LocaleContext for the given request, exposing the request's
* primary locale as current locale.
* @param request current HTTP request
* @return the corresponding LocaleContext
*/
protected LocaleContext buildLocaleContext(HttpServletRequest request) {
return new SimpleLocaleContext(request.getLocale());
}
private void initContextHolders(HttpServletRequest request,
LocaleContext localeContext, RequestAttributes attributes) {
LocaleContextHolder.setLocaleContext(localeContext, this.threadContextInheritable);
if (attributes != null) {
RequestContextHolder.setRequestAttributes(attributes, this.threadContextInheritable);
}
if (logger.isTraceEnabled()) {
logger.trace("Bound request context to thread: " + request);
}
}
private void resetContextHolders(HttpServletRequest request,
LocaleContext prevLocaleContext, RequestAttributes previousAttributes) {
LocaleContextHolder.setLocaleContext(prevLocaleContext, this.threadContextInheritable);
RequestContextHolder.setRequestAttributes(previousAttributes, this.threadContextInheritable);
if (logger.isTraceEnabled()) {
logger.trace("Cleared thread-bound request context: " + request);
}
}
/**
* 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 {
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;
}
}
}
catch (IOException ex) {
failureCause = ex;
throw ex;
}
catch (Throwable ex) {
failureCause = ex;
throw new NestedServletException("Request processing failed", ex);
}
finally {
// Clear request attributes and reset thread-bound context.
LocaleContextHolder.setLocaleContext(previousLocaleContext, this.threadContextInheritable);
if (requestAttributes != null) {
RequestContextHolder.setRequestAttributes(previousRequestAttributes, this.threadContextInheritable);
requestAttributes.requestCompleted();
}
if (logger.isTraceEnabled()) {
logger.trace("Cleared thread-bound request context: " + request);
}
if (logger.isDebugEnabled()) {
if (failureCause != null) {
this.logger.debug("Could not complete request", failureCause);
@@ -927,13 +976,30 @@ public abstract class FrameworkServlet extends HttpServletBean {
}
/**
* Build a LocaleContext for the given request, exposing the request's
* primary locale as current locale.
* @param request current HTTP request
* @return the corresponding LocaleContext
* Create a Callable to use to complete processing in an async execution chain.
*/
protected LocaleContext buildLocaleContext(HttpServletRequest request) {
return new SimpleLocaleContext(request.getLocale());
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 {
getNextCallable().call();
}
catch (Throwable t) {
unhandledFailure = t;
}
finally {
resetContextHolders(request, previousLocaleContext, previousAttributes);
finalizeProcessing(startTime, request, response, requestAttributes, unhandledFailure);
}
return null;
}
};
}
/**
@@ -965,7 +1031,6 @@ 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()
@@ -978,7 +1043,6 @@ 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

@@ -20,6 +20,11 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.CollectionUtils;
/**
@@ -32,12 +37,15 @@ import org.springframework.util.CollectionUtils;
*/
public class HandlerExecutionChain {
private static final Log logger = LogFactory.getLog(HandlerExecutionChain.class);
private final Object handler;
private HandlerInterceptor[] interceptors;
private List<HandlerInterceptor> interceptorList;
private int interceptorIndex = -1;
/**
* Create a new HandlerExecutionChain.
@@ -67,7 +75,6 @@ public class HandlerExecutionChain {
}
}
/**
* Return the handler object to execute.
* @return the handler object
@@ -109,6 +116,65 @@ public class HandlerExecutionChain {
return this.interceptors;
}
/**
* Apply preHandle methods of registered interceptors.
* @return <code>true</code> if the execution chain should proceed with the
* next interceptor or the handler itself. Else, DispatcherServlet assumes
* that this interceptor has already dealt with the response itself.
*/
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response)
throws Exception {
if (getInterceptors() != null) {
for (int i = 0; i < getInterceptors().length; i++) {
HandlerInterceptor interceptor = getInterceptors()[i];
if (!interceptor.preHandle(request, response, this.handler)) {
triggerAfterCompletion(request, response, null);
return false;
}
this.interceptorIndex = i;
}
}
return true;
}
/**
* Apply preHandle methods of registered interceptors.
*/
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, ModelAndView mv)
throws Exception {
if (getInterceptors() == null) {
return;
}
for (int i = getInterceptors().length - 1; i >= 0; i--) {
HandlerInterceptor interceptor = getInterceptors()[i];
interceptor.postHandle(request, response, this.handler, mv);
}
}
/**
* Trigger afterCompletion callbacks on the mapped HandlerInterceptors.
* Will just invoke afterCompletion for all interceptors whose preHandle invocation
* has successfully completed and returned true.
*/
void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, Exception ex)
throws Exception {
if (getInterceptors() == null) {
return;
}
for (int i = this.interceptorIndex; i >= 0; i--) {
HandlerInterceptor interceptor = getInterceptors()[i];
try {
interceptor.afterCompletion(request, response, this.handler, ex);
}
catch (Throwable ex2) {
logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
}
}
}
/**
* Delegates to the handler's <code>toString()</code>.

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2002-2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.mvc.method.annotation;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;
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.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* Handles return values of type {@link Callable} and {@link DeferredResult}.
*
* <p>This handler does not have a defined behavior for {@code null} return
* values and will raise an {@link IllegalArgumentException}.
*
* @author Rossen Stoyanchev
* @since 3.2
*/
public class AsyncMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
public boolean supportsReturnType(MethodParameter returnType) {
Class<?> paramType = returnType.getParameterType();
return Callable.class.isAssignableFrom(paramType) || DeferredResult.class.isAssignableFrom(paramType);
}
@SuppressWarnings("unchecked")
public void handleReturnValue(Object returnValue,
MethodParameter returnType, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest) throws Exception {
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);
if (Callable.class.isAssignableFrom(paramType)) {
chain.setCallable((Callable<Object>) returnValue);
chain.startCallableChainProcessing();
}
else if (DeferredResult.class.isAssignableFrom(paramType)) {
chain.startDeferredResultProcessing((DeferredResult) returnValue);
}
else {
// should never happen..
Method method = returnType.getMethod();
throw new UnsupportedOperationException("Unknown return value: " + paramType + " in method: " + method);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@@ -33,15 +34,17 @@ import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter;
import org.springframework.ui.ModelMap;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils.MethodFilter;
import org.springframework.web.bind.annotation.InitBinder;
@@ -52,22 +55,27 @@ import org.springframework.web.bind.support.DefaultSessionAttributeStore;
import org.springframework.web.bind.support.SessionAttributeStore;
import org.springframework.web.bind.support.WebBindingInitializer;
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.method.HandlerMethod;
import org.springframework.web.method.HandlerMethodSelector;
import org.springframework.web.method.annotation.ErrorsMethodArgumentResolver;
import org.springframework.web.method.annotation.ExpressionValueMethodArgumentResolver;
import org.springframework.web.method.annotation.MapMethodProcessor;
import org.springframework.web.method.annotation.ModelAttributeMethodProcessor;
import org.springframework.web.method.annotation.ModelFactory;
import org.springframework.web.method.annotation.ModelMethodProcessor;
import org.springframework.web.method.annotation.RequestHeaderMapMethodArgumentResolver;
import org.springframework.web.method.annotation.RequestHeaderMethodArgumentResolver;
import org.springframework.web.method.annotation.RequestParamMapMethodArgumentResolver;
import org.springframework.web.method.annotation.RequestParamMethodArgumentResolver;
import org.springframework.web.method.annotation.SessionAttributesHandler;
import org.springframework.web.method.annotation.SessionStatusMethodArgumentResolver;
import org.springframework.web.method.annotation.ExpressionValueMethodArgumentResolver;
import org.springframework.web.method.annotation.RequestHeaderMethodArgumentResolver;
import org.springframework.web.method.annotation.RequestParamMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodArgumentResolverComposite;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
@@ -84,9 +92,9 @@ import org.springframework.web.util.WebUtils;
/**
* An {@link AbstractHandlerMethodAdapter} that supports {@link HandlerMethod}s
* with the signature -- method argument and return types, defined in
* with the signature -- method argument and return types, defined in
* {@code @RequestMapping}.
*
*
* <p>Support for custom argument and return value types can be added via
* {@link #setCustomArgumentResolvers} and {@link #setCustomReturnValueHandlers}.
* Or alternatively to re-configure all argument and return value types use
@@ -103,7 +111,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
private List<HandlerMethodArgumentResolver> customArgumentResolvers;
private List<HandlerMethodReturnValueHandler> customReturnValueHandlers;
private List<ModelAndViewResolver> modelAndViewResolvers;
private List<HttpMessageConverter<?>> messageConverters;
@@ -115,31 +123,35 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
private boolean synchronizeOnSession = false;
private ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
private ConfigurableBeanFactory beanFactory;
private SessionAttributeStore sessionAttributeStore = new DefaultSessionAttributeStore();
private boolean ignoreDefaultModelOnRedirect = false;
private final Map<Class<?>, SessionAttributesHandler> sessionAttributesHandlerCache =
new ConcurrentHashMap<Class<?>, SessionAttributesHandler>();
private HandlerMethodArgumentResolverComposite argumentResolvers;
private HandlerMethodArgumentResolverComposite initBinderArgumentResolvers;
private HandlerMethodReturnValueHandlerComposite returnValueHandlers;
private final Map<Class<?>, Set<Method>> dataBinderFactoryCache = new ConcurrentHashMap<Class<?>, Set<Method>>();
private final Map<Class<?>, Set<Method>> modelFactoryCache = new ConcurrentHashMap<Class<?>, Set<Method>>();
private AsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor();
private Long asyncRequestTimeout;
/**
* Default constructor.
*/
public RequestMappingHandlerAdapter() {
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
stringHttpMessageConverter.setWriteAcceptCharset(false); // See SPR-7316
@@ -152,7 +164,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
/**
* Provide resolvers for custom argument types. Custom resolvers are ordered
* after built-in ones. To override the built-in support for argument
* after built-in ones. To override the built-in support for argument
* resolution use {@link #setArgumentResolvers} instead.
*/
public void setCustomArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
@@ -179,9 +191,9 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
this.argumentResolvers.addResolvers(argumentResolvers);
}
}
/**
* Return the configured argument resolvers, or possibly {@code null} if
* Return the configured argument resolvers, or possibly {@code null} if
* not initialized yet via {@link #afterPropertiesSet()}.
*/
public HandlerMethodArgumentResolverComposite getArgumentResolvers() {
@@ -226,7 +238,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
}
/**
* Configure the complete list of supported return value types thus
* Configure the complete list of supported return value types thus
* overriding handlers that would otherwise be configured by default.
*/
public void setReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) {
@@ -240,7 +252,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
}
/**
* Return the configured handlers, or possibly {@code null} if not
* Return the configured handlers, or possibly {@code null} if not
* initialized yet via {@link #afterPropertiesSet()}.
*/
public HandlerMethodReturnValueHandlerComposite getReturnValueHandlers() {
@@ -248,16 +260,16 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
}
/**
* Provide custom {@link ModelAndViewResolver}s.
* <p><strong>Note:</strong> This method is available for backwards
* compatibility only. However, it is recommended to re-write a
* Provide custom {@link ModelAndViewResolver}s.
* <p><strong>Note:</strong> This method is available for backwards
* compatibility only. However, it is recommended to re-write a
* {@code ModelAndViewResolver} as {@link HandlerMethodReturnValueHandler}.
* An adapter between the two interfaces is not possible since the
* An adapter between the two interfaces is not possible since the
* {@link HandlerMethodReturnValueHandler#supportsReturnType} method
* cannot be implemented. Hence {@code ModelAndViewResolver}s are limited
* to always being invoked at the end after all other return value
* to always being invoked at the end after all other return value
* handlers have been given a chance.
* <p>A {@code HandlerMethodReturnValueHandler} provides better access to
* <p>A {@code HandlerMethodReturnValueHandler} provides better access to
* the return type and controller method information and can be ordered
* freely relative to other return value handlers.
*/
@@ -273,8 +285,8 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
}
/**
* Provide the converters to use in argument resolvers and return value
* handlers that support reading and/or writing to the body of the
* Provide the converters to use in argument resolvers and return value
* handlers that support reading and/or writing to the body of the
* request and response.
*/
public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
@@ -289,7 +301,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
}
/**
* Provide a WebBindingInitializer with "global" initialization to apply
* Provide a WebBindingInitializer with "global" initialization to apply
* to every DataBinder instance.
*/
public void setWebBindingInitializer(WebBindingInitializer webBindingInitializer) {
@@ -304,20 +316,20 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
}
/**
* Specify the strategy to store session attributes with. The default is
* Specify the strategy to store session attributes with. The default is
* {@link org.springframework.web.bind.support.DefaultSessionAttributeStore},
* storing session attributes in the HttpSession with the same attribute
* storing session attributes in the HttpSession with the same attribute
* name as in the model.
*/
public void setSessionAttributeStore(SessionAttributeStore sessionAttributeStore) {
this.sessionAttributeStore = sessionAttributeStore;
}
/**
* Cache content produced by <code>@SessionAttributes</code> annotated handlers
* for the given number of seconds. Default is 0, preventing caching completely.
* <p>In contrast to the "cacheSeconds" property which will apply to all general
* handlers (but not to <code>@SessionAttributes</code> annotated handlers),
* <p>In contrast to the "cacheSeconds" property which will apply to all general
* handlers (but not to <code>@SessionAttributes</code> annotated handlers),
* this setting will apply to <code>@SessionAttributes</code> handlers only.
* @see #setCacheSeconds
* @see org.springframework.web.bind.annotation.SessionAttributes
@@ -349,25 +361,25 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
}
/**
* Set the ParameterNameDiscoverer to use for resolving method parameter
* names if needed (e.g. for default attribute names). Default is a
* Set the ParameterNameDiscoverer to use for resolving method parameter
* names if needed (e.g. for default attribute names). Default is a
* {@link org.springframework.core.LocalVariableTableParameterNameDiscoverer}.
*/
public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
this.parameterNameDiscoverer = parameterNameDiscoverer;
}
/**
* By default the content of the "default" model is used both during
* rendering and redirect scenarios. Alternatively a controller method
* By default the content of the "default" model is used both during
* rendering and redirect scenarios. Alternatively a controller method
* can declare a {@link RedirectAttributes} argument and use it to provide
* attributes for a redirect.
* <p>Setting this flag to {@code true} guarantees the "default" model is
* never used in a redirect scenario even if a RedirectAttributes argument
* is not declared. Setting it to {@code false} means the "default" model
* may be used in a redirect if the controller method doesn't declare a
* <p>Setting this flag to {@code true} guarantees the "default" model is
* never used in a redirect scenario even if a RedirectAttributes argument
* is not declared. Setting it to {@code false} means the "default" model
* may be used in a redirect if the controller method doesn't declare a
* RedirectAttributes argument.
* <p>The default setting is {@code false} but new applications should
* <p>The default setting is {@code false} but new applications should
* consider setting it to {@code true}.
* @see RedirectAttributes
*/
@@ -375,9 +387,31 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
this.ignoreDefaultModelOnRedirect = ignoreDefaultModelOnRedirect;
}
/**
* Set an AsyncTaskExecutor to use when a controller method returns a Callable.
* <p>The default is a {@link SimpleAsyncTaskExecutor}
*
* TODO... need a better default
*/
public void setAsyncTaskExecutor(AsyncTaskExecutor taskExecutor) {
this.taskExecutor = taskExecutor;
}
/**
* Set the timeout for asynchronous request processing. When the timeout
* begins depends on the underlying async technology. With the Servlet 3
* async support the timeout begins after the main processing thread has
* exited and has been returned to the container pool.
* <p>If a value is not provided, the default timeout of the underlying
* async technology is used (10 seconds on Tomcat with Servlet 3 async).
*/
public void setAsyncRequestTimeout(long asyncRequestTimeout) {
this.asyncRequestTimeout = asyncRequestTimeout;
}
/**
* {@inheritDoc}
* <p>A {@link ConfigurableBeanFactory} is expected for resolving
* <p>A {@link ConfigurableBeanFactory} is expected for resolving
* expressions in method argument default values.
*/
public void setBeanFactory(BeanFactory beanFactory) {
@@ -387,7 +421,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
}
/**
* Return the owning factory of this bean instance, or {@code null}.
* Return the owning factory of this bean instance, or {@code null}.
*/
protected ConfigurableBeanFactory getBeanFactory() {
return this.beanFactory;
@@ -451,7 +485,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
}
/**
* Return the list of argument resolvers to use for {@code @InitBinder}
* Return the list of argument resolvers to use for {@code @InitBinder}
* methods including built-in and custom resolvers.
*/
private List<HandlerMethodArgumentResolver> getDefaultInitBinderArgumentResolvers() {
@@ -474,12 +508,12 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
// Catch-all
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
return resolvers;
}
/**
* Return the list of return value handlers to use including built-in and
* Return the list of return value handlers to use including built-in and
* custom handlers provided via {@link #setReturnValueHandlers}.
*/
private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
@@ -490,6 +524,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
handlers.add(new ModelMethodProcessor());
handlers.add(new ViewMethodReturnValueHandler());
handlers.add(new HttpEntityMethodProcessor(getMessageConverters()));
handlers.add(new AsyncMethodReturnValueHandler());
// Annotation-based return value types
handlers.add(new ModelAttributeMethodProcessor(false));
@@ -516,33 +551,22 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
}
/**
* Return {@code true} if all arguments and the return value of the given
* HandlerMethod are supported by the configured resolvers and handlers.
* Always return {@code true} since any method argument and return value
* type will be processed in some way. A method argument not recognized
* by any HandlerMethodArgumentResolver is interpreted as a request parameter
* if it is a simple type, or as a model attribute otherwise. A return value
* not recognized by any HandlerMethodReturnValueHandler will be interpreted
* as a model attribute.
*/
@Override
protected boolean supportsInternal(HandlerMethod handlerMethod) {
return supportsMethodParameters(handlerMethod.getMethodParameters()) &&
supportsReturnType(handlerMethod.getReturnType());
}
private boolean supportsMethodParameters(MethodParameter[] methodParameters) {
for (MethodParameter methodParameter : methodParameters) {
if (! this.argumentResolvers.supportsParameter(methodParameter)) {
return false;
}
}
return true;
}
private boolean supportsReturnType(MethodParameter methodReturnType) {
return (this.returnValueHandlers.supportsReturnType(methodReturnType) ||
Void.TYPE.equals(methodReturnType.getParameterType()));
}
/**
* This implementation always returns -1. An {@code @RequestMapping}
* method can calculate the lastModified value, call
* {@link WebRequest#checkNotModified(long)}, and return {@code null}
* This implementation always returns -1. An {@code @RequestMapping}
* method can calculate the lastModified value, call
* {@link WebRequest#checkNotModified(long)}, and return {@code null}
* if the result of that call is {@code true}.
*/
@Override
@@ -552,8 +576,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
@Override
protected final ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response,
HandlerMethod handlerMethod) throws Exception {
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
// Always prevent caching in case of session attribute management.
@@ -563,23 +586,23 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
// Uses configured default cacheSeconds setting.
checkAndPrepare(request, response, true);
}
// Execute invokeHandlerMethod in synchronized block if required.
if (this.synchronizeOnSession) {
HttpSession session = request.getSession(false);
if (session != null) {
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
return invokeHandlerMethod(request, response, handlerMethod);
return invokeHandleMethod(request, response, handlerMethod);
}
}
}
return invokeHandlerMethod(request, response, handlerMethod);
return invokeHandleMethod(request, response, handlerMethod);
}
/**
* Return the {@link SessionAttributesHandler} instance for the given
* Return the {@link SessionAttributesHandler} instance for the given
* handler type, never {@code null}.
*/
private SessionAttributesHandler getSessionAttributesHandler(HandlerMethod handlerMethod) {
@@ -600,9 +623,9 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
/**
* Invoke the {@link RequestMapping} handler method preparing a {@link ModelAndView} if view resolution is required.
*/
private ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response,
HandlerMethod handlerMethod) throws Exception {
private ModelAndView invokeHandleMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ServletWebRequest webRequest = new ServletWebRequest(request, response);
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
@@ -614,27 +637,33 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
modelFactory.initModel(webRequest, mavContainer, requestMappingMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
requestMappingMethod.invokeAndHandle(webRequest, mavContainer);
modelFactory.updateModel(webRequest, mavContainer);
AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(request);
chain.addDelegatingCallable(getAsyncCallable(mavContainer, modelFactory, webRequest));
chain.setAsyncWebRequest(createAsyncWebRequest(request, response));
chain.setTaskExecutor(this.taskExecutor);
if (mavContainer.isRequestHandled()) {
requestMappingMethod.invokeAndHandle(webRequest, mavContainer);
if (chain.isAsyncStarted()) {
return null;
}
else {
ModelMap model = mavContainer.getModel();
ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model);
if (!mavContainer.isViewReference()) {
mav.setView((View) mavContainer.getView());
}
if (model instanceof RedirectAttributes) {
Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
}
return mav;
}
return getModelAndView(mavContainer, modelFactory, webRequest);
}
private ServletInvocableHandlerMethod createRequestMappingMethod(HandlerMethod handlerMethod,
private AsyncWebRequest createAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) {
AsyncWebRequest asyncRequest;
if (ClassUtils.hasMethod(ServletRequest.class, "startAsync")) {
asyncRequest = new org.springframework.web.context.request.async.StandardServletAsyncWebRequest(request, response);
asyncRequest.setTimeout(this.asyncRequestTimeout);
}
else {
asyncRequest = new NoOpAsyncWebRequest(request, response);
}
return asyncRequest;
}
private ServletInvocableHandlerMethod createRequestMappingMethod(HandlerMethod handlerMethod,
WebDataBinderFactory binderFactory) {
ServletInvocableHandlerMethod requestMethod;
requestMethod = new ServletInvocableHandlerMethod(handlerMethod.getBean(), handlerMethod.getMethod());
@@ -644,7 +673,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
requestMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
return requestMethod;
}
private ModelFactory getModelFactory(HandlerMethod handlerMethod, WebDataBinderFactory binderFactory) {
SessionAttributesHandler sessionAttrHandler = getSessionAttributesHandler(handlerMethod);
Class<?> handlerType = handlerMethod.getBeanType();
@@ -695,6 +724,42 @@ 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) {
return new AbstractDelegatingCallable() {
public Object call() throws Exception {
getNextCallable().call();
return getModelAndView(mavContainer, modelFactory, webRequest);
}
};
}
private ModelAndView getModelAndView(ModelAndViewContainer mavContainer,
ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception {
modelFactory.updateModel(webRequest, mavContainer);
if (mavContainer.isRequestHandled()) {
return null;
}
ModelMap model = mavContainer.getModel();
ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model);
if (!mavContainer.isViewReference()) {
mav.setView((View) mavContainer.getView());
}
if (model instanceof RedirectAttributes) {
Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
}
return mav;
}
/**
* MethodFilter that matches {@link InitBinder @InitBinder} methods.
*/

View File

@@ -17,14 +17,17 @@
package org.springframework.web.servlet.mvc.method.annotation;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;
import org.springframework.http.HttpStatus;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
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;
@@ -32,16 +35,19 @@ import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.View;
/**
* Extends {@link InvocableHandlerMethod} with the ability to handle the value returned from the method through
* a registered {@link HandlerMethodArgumentResolver} that supports the given return value type.
* Return value handling may include writing to the response or updating the {@link ModelAndViewContainer} structure.
* Extends {@link InvocableHandlerMethod} with the ability to handle return
* values through a registered {@link HandlerMethodReturnValueHandler} and
* also supports setting the response status based on a method-level
* {@code @ResponseStatus} annotation.
*
* <p>If the underlying method has a {@link ResponseStatus} instruction, the status on the response is set
* accordingly after the method is invoked but before the return value is handled.
* <p>A {@code null} return value (including void) may be interpreted as the
* end of request processing in combination with a {@code @ResponseStatus}
* annotation, a not-modified check condition
* (see {@link ServletWebRequest#checkNotModified(long)}), or
* a method argument that provides access to the response stream.
*
* @author Rossen Stoyanchev
* @since 3.1
* @see #invokeAndHandle(NativeWebRequest, ModelAndViewContainer, Object...)
*/
public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
@@ -51,10 +57,6 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
private HandlerMethodReturnValueHandlerComposite returnValueHandlers;
public void setHandlerMethodReturnValueHandlers(HandlerMethodReturnValueHandlerComposite returnValueHandlers) {
this.returnValueHandlers = returnValueHandlers;
}
/**
* Creates a {@link ServletInvocableHandlerMethod} instance with the given bean and method.
* @param handler the object handler
@@ -71,34 +73,33 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
}
/**
* Invokes the method and handles the return value through a registered {@link HandlerMethodReturnValueHandler}.
* <p>Return value handling may be skipped entirely when the method returns {@code null} (also possibly due
* to a {@code void} return type) and one of the following additional conditions is true:
* <ul>
* <li>A {@link HandlerMethodArgumentResolver} has set the {@link ModelAndViewContainer#setRequestHandled(boolean)}
* flag to {@code false} -- e.g. method arguments providing access to the response.
* <li>The request qualifies as "not modified" as defined in {@link ServletWebRequest#checkNotModified(long)}
* and {@link ServletWebRequest#checkNotModified(String)}. In this case a response with "not modified" response
* headers will be automatically generated without the need for return value handling.
* <li>The status on the response is set due to a @{@link ResponseStatus} instruction.
* </ul>
* <p>After the return value is handled, callers of this method can use the {@link ModelAndViewContainer}
* to gain access to model attributes, view selection choices, and to check if view resolution is even needed.
*
* @param request the current request
* @param mavContainer the {@link ModelAndViewContainer} for the current request
* @param providedArgs argument values to try to use without the need for view resolution
* Register {@link HandlerMethodReturnValueHandler} instances to use to
* handle return values.
*/
public final void invokeAndHandle(
NativeWebRequest request, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
public void setHandlerMethodReturnValueHandlers(HandlerMethodReturnValueHandlerComposite returnValueHandlers) {
this.returnValueHandlers = returnValueHandlers;
}
Object returnValue = invokeForRequest(request, mavContainer, providedArgs);
/**
* Invokes the method and handles the return value through a registered
* {@link HandlerMethodReturnValueHandler}.
*
* @param webRequest the current request
* @param mavContainer the ModelAndViewContainer for this request
* @param providedArgs "given" arguments matched by type, not resolved
*/
public final void invokeAndHandle(ServletWebRequest webRequest,
ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
setResponseStatus((ServletWebRequest) request);
AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(webRequest.getRequest());
chain.addDelegatingCallable(geAsyncCallable(webRequest, mavContainer, providedArgs));
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
setResponseStatus(webRequest);
if (returnValue == null) {
if (isRequestNotModified(request) || hasResponseStatus() || mavContainer.isRequestHandled()) {
if (isRequestNotModified(webRequest) || hasResponseStatus() || mavContainer.isRequestHandled()) {
mavContainer.setRequestHandled(true);
return;
}
@@ -107,7 +108,8 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
mavContainer.setRequestHandled(false);
try {
returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, request);
this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
} catch (Exception ex) {
if (logger.isTraceEnabled()) {
logger.trace(getReturnValueHandlingErrorMessage("Error handling return value", returnValue), ex);
@@ -116,6 +118,20 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
}
}
/**
* 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 {
new CallableHandlerMethod(getNextCallable()).invokeAndHandle(webRequest, mavContainer, providedArgs);
return null;
}
};
}
private String getReturnValueHandlingErrorMessage(String message, Object returnValue) {
StringBuilder sb = new StringBuilder(message);
if (returnValue != null) {
@@ -129,17 +145,19 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
* Set the response status according to the {@link ResponseStatus} annotation.
*/
private void setResponseStatus(ServletWebRequest webRequest) throws IOException {
if (this.responseStatus != null) {
if (StringUtils.hasText(this.responseReason)) {
webRequest.getResponse().sendError(this.responseStatus.value(), this.responseReason);
}
else {
webRequest.getResponse().setStatus(this.responseStatus.value());
}
// to be picked up by the RedirectView
webRequest.getRequest().setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, this.responseStatus);
if (this.responseStatus == null) {
return;
}
if (StringUtils.hasText(this.responseReason)) {
webRequest.getResponse().sendError(this.responseStatus.value(), this.responseReason);
}
else {
webRequest.getResponse().setStatus(this.responseStatus.value());
}
// to be picked up by the RedirectView
webRequest.getRequest().setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, this.responseStatus);
}
/**
@@ -147,8 +165,8 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
* @see ServletWebRequest#checkNotModified(long)
* @see ServletWebRequest#checkNotModified(String)
*/
private boolean isRequestNotModified(NativeWebRequest request) {
return ((ServletWebRequest) request).isNotModified();
private boolean isRequestNotModified(ServletWebRequest webRequest) {
return webRequest.isNotModified();
}
/**
@@ -157,4 +175,24 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
private boolean hasResponseStatus() {
return responseStatus != null;
}
/**
* 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.
*/
private class CallableHandlerMethod extends ServletInvocableHandlerMethod {
public CallableHandlerMethod(Callable<?> callable) {
super(callable, ClassUtils.getMethod(callable.getClass(), "call"));
this.setHandlerMethodReturnValueHandlers(ServletInvocableHandlerMethod.this.returnValueHandlers);
}
@Override
public <A extends Annotation> A getMethodAnnotation(Class<A> annotationType) {
return ServletInvocableHandlerMethod.this.getMethodAnnotation(annotationType);
}
}
}