From 3642b0f365f90b14061d00d8fb1c39786f08d911 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 12 Apr 2012 23:20:35 -0400 Subject: [PATCH] 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 --- .../web/bind/annotation/RequestMapping.java | 9 +- .../async/AbstractDelegatingCallable.java | 54 ++++ .../request/async/AsyncExecutionChain.java | 189 +++++++++++++ .../async/AsyncExecutionChainRunnable.java | 81 ++++++ .../request/async/AsyncWebRequest.java | 74 +++++ .../context/request/async/DeferredResult.java | 91 +++++++ .../request/async/NoOpAsyncWebRequest.java | 62 +++++ .../StaleAsyncRequestCheckingCallable.java | 50 ++++ .../async/StaleAsyncWebRequestException.java | 35 +++ .../async/StandardServletAsyncWebRequest.java | 124 +++++++++ .../filter/AbstractRequestLoggingFilter.java | 28 +- .../web/filter/OncePerRequestFilter.java | 26 +- .../web/filter/RequestContextFilter.java | 59 +++- .../web/filter/ShallowEtagHeaderFilter.java | 36 ++- .../support/InvocableHandlerMethod.java | 4 +- .../async/AsyncExecutionChainTests.java | 246 +++++++++++++++++ ...taleAsyncRequestCheckingCallableTests.java | 71 +++++ .../StandardServletAsyncWebRequestTests.java | 165 ++++++++++++ .../filter/CharacterEncodingFilterTests.java | 138 +++++----- .../web/servlet/DispatcherServlet.java | 247 +++++++++++------ .../web/servlet/FrameworkServlet.java | 142 +++++++--- .../web/servlet/HandlerExecutionChain.java | 68 ++++- .../AsyncMethodReturnValueHandler.java | 75 ++++++ .../RequestMappingHandlerAdapter.java | 253 +++++++++++------- .../ServletInvocableHandlerMethod.java | 134 ++++++---- .../servlet/HandlerExecutionChainTests.java | 158 +++++++++++ src/dist/changelog.txt | 1 + 27 files changed, 2257 insertions(+), 363 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/web/context/request/async/AbstractDelegatingCallable.java create mode 100644 spring-web/src/main/java/org/springframework/web/context/request/async/AsyncExecutionChain.java create mode 100644 spring-web/src/main/java/org/springframework/web/context/request/async/AsyncExecutionChainRunnable.java create mode 100644 spring-web/src/main/java/org/springframework/web/context/request/async/AsyncWebRequest.java create mode 100644 spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResult.java create mode 100644 spring-web/src/main/java/org/springframework/web/context/request/async/NoOpAsyncWebRequest.java create mode 100644 spring-web/src/main/java/org/springframework/web/context/request/async/StaleAsyncRequestCheckingCallable.java create mode 100644 spring-web/src/main/java/org/springframework/web/context/request/async/StaleAsyncWebRequestException.java create mode 100644 spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java create mode 100644 spring-web/src/test/java/org/springframework/web/context/request/async/AsyncExecutionChainTests.java create mode 100644 spring-web/src/test/java/org/springframework/web/context/request/async/StaleAsyncRequestCheckingCallableTests.java create mode 100644 spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AsyncMethodReturnValueHandler.java create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/HandlerExecutionChainTests.java diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java index 00ad724649..2f4970b8e8 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java @@ -21,6 +21,7 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.concurrent.Callable; /** * Annotation for mapping web requests onto specific handler classes and/or @@ -180,12 +181,18 @@ import java.lang.annotation.Target; * be converted to the response stream using * {@linkplain org.springframework.http.converter.HttpMessageConverter message * converters}. - *
  • A {@link org.springframework.http.HttpEntity HttpEntity<?>} or + *
  • An {@link org.springframework.http.HttpEntity HttpEntity<?>} or * {@link org.springframework.http.ResponseEntity ResponseEntity<?>} object * (Servlet-only) to access to the Servlet response HTTP headers and contents. * The entity body will be converted to the response stream using * {@linkplain org.springframework.http.converter.HttpMessageConverter message * converters}. + *
  • A {@link Callable} which is used by Spring MVC to obtain the return + * value asynchronously in a separate thread transparently managed by Spring MVC + * on behalf of the application. + *
  • A {@code org.springframework.web.context.request.async.DeferredResult} + * which the application uses to produce a return value in a separate + * thread of its own choosing, as an alternative to returning a Callable. *
  • void if the method handles the response itself (by * writing the response content directly, declaring an argument of type * {@link javax.servlet.ServletResponse} / {@link javax.servlet.http.HttpServletResponse} diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/AbstractDelegatingCallable.java b/spring-web/src/main/java/org/springframework/web/context/request/async/AbstractDelegatingCallable.java new file mode 100644 index 0000000000..352616c722 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/AbstractDelegatingCallable.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.context.request.async; + +import java.util.concurrent.Callable; + +/** + * A base class for a Callable that can be used in a chain of Callable instances. + * + *

    Typical use for async request processing scenarios involves: + *

    + * + * @author Rossen Stoyanchev + * @since 3.2 + * + * @see AsyncExecutionChain + */ +public abstract class AbstractDelegatingCallable implements Callable { + + private Callable next; + + public void setNextCallable(Callable nextCallable) { + this.next = nextCallable; + } + + protected Callable getNextCallable() { + return this.next; + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/AsyncExecutionChain.java b/spring-web/src/main/java/org/springframework/web/context/request/async/AsyncExecutionChain.java new file mode 100644 index 0000000000..268b337801 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/AsyncExecutionChain.java @@ -0,0 +1,189 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.context.request.async; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; + +import javax.servlet.ServletRequest; + +import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.util.Assert; +import org.springframework.web.context.request.async.DeferredResult.DeferredResultHandler; + +/** + * The central class for managing async request processing, mainly intended as + * an SPI and typically not by non-framework classes. + * + *

    An async execution chain consists of a sequence of Callable instances and + * represents the work required to complete request processing in a separate + * thread. To construct the chain, each layer in the call stack of a normal + * request (e.g. filter, servlet) may contribute an + * {@link AbstractDelegatingCallable} when a request is being processed. + * For example the DispatcherServlet might contribute a Callable that + * performs view resolution while a HandlerAdapter might contribute a Callable + * that returns the ModelAndView, etc. The last Callable is the one that + * actually produces an application-specific value, for example the Callable + * returned by an {@code @RequestMapping} method. + * + * @author Rossen Stoyanchev + * @since 3.2 + */ +public final class AsyncExecutionChain { + + public static final String CALLABLE_CHAIN_ATTRIBUTE = AsyncExecutionChain.class.getName() + ".CALLABLE_CHAIN"; + + private final List delegatingCallables = new ArrayList(); + + private Callable callable; + + private AsyncWebRequest asyncWebRequest; + + private AsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor("AsyncExecutionChain"); + + /** + * Private constructor + * @see #getForCurrentRequest() + */ + private AsyncExecutionChain() { + } + + /** + * Obtain the AsyncExecutionChain for the current request. + * Or if not found, create an instance and associate it with the request. + */ + public static AsyncExecutionChain getForCurrentRequest(ServletRequest request) { + AsyncExecutionChain chain = (AsyncExecutionChain) request.getAttribute(CALLABLE_CHAIN_ATTRIBUTE); + if (chain == null) { + chain = new AsyncExecutionChain(); + request.setAttribute(CALLABLE_CHAIN_ATTRIBUTE, chain); + } + return chain; + } + + /** + * Provide an instance of an AsyncWebRequest. + * This property must be set before async request processing can begin. + */ + public void setAsyncWebRequest(AsyncWebRequest asyncRequest) { + this.asyncWebRequest = asyncRequest; + } + + /** + * Provide an AsyncTaskExecutor to use when + * {@link #startCallableChainProcessing()} is invoked, for example when a + * controller method returns a Callable. + *

    By default a {@link SimpleAsyncTaskExecutor} instance is used. + */ + public void setTaskExecutor(AsyncTaskExecutor taskExecutor) { + this.taskExecutor = taskExecutor; + } + + /** + * Whether async request processing has started through one of: + *

      + *
    • {@link #startCallableChainProcessing()} + *
    • {@link #startDeferredResultProcessing(DeferredResult)} + *
    + */ + public boolean isAsyncStarted() { + return ((this.asyncWebRequest != null) && this.asyncWebRequest.isAsyncStarted()); + } + + /** + * Add a Callable with logic required to complete request processing in a + * separate thread. See {@link AbstractDelegatingCallable} for details. + */ + public void addDelegatingCallable(AbstractDelegatingCallable callable) { + Assert.notNull(callable, "Callable required"); + this.delegatingCallables.add(callable); + } + + /** + * Add the last Callable, for example the one returned by the controller. + * This property must be set prior to invoking + * {@link #startCallableChainProcessing()}. + */ + public AsyncExecutionChain setCallable(Callable callable) { + Assert.notNull(callable, "Callable required"); + this.callable = callable; + return this; + } + + /** + * Start the async execution chain by submitting an + * {@link AsyncExecutionChainRunnable} instance to the TaskExecutor provided via + * {@link #setTaskExecutor(AsyncTaskExecutor)} and returning immediately. + * @see AsyncExecutionChainRunnable + */ + public void startCallableChainProcessing() { + startAsync(); + this.taskExecutor.execute(new AsyncExecutionChainRunnable(this.asyncWebRequest, buildChain())); + } + + private void startAsync() { + Assert.state(this.asyncWebRequest != null, "An AsyncWebRequest is required to start async processing"); + this.asyncWebRequest.startAsync(); + } + + private Callable buildChain() { + Assert.state(this.callable != null, "The callable field is required to complete the chain"); + this.delegatingCallables.add(new StaleAsyncRequestCheckingCallable(asyncWebRequest)); + Callable result = this.callable; + for (int i = this.delegatingCallables.size() - 1; i >= 0; i--) { + AbstractDelegatingCallable callable = this.delegatingCallables.get(i); + callable.setNextCallable(result); + result = callable; + } + return result; + } + + /** + * Mark the start of async request processing accepting the provided + * DeferredResult and initializing it such that if + * {@link DeferredResult#set(Object)} is called (from another thread), + * the set Object value will be processed with the execution chain by + * invoking {@link AsyncExecutionChainRunnable}. + *

    The resulting processing from this method is identical to + * {@link #startCallableChainProcessing()}. The main difference is in + * the threading model, i.e. whether a TaskExecutor is used. + * @see DeferredResult + */ + public void startDeferredResultProcessing(DeferredResult deferredResult) { + Assert.notNull(deferredResult, "A DeferredResult is required"); + startAsync(); + deferredResult.setValueProcessor(new DeferredResultHandler() { + public void handle(Object value) { + if (asyncWebRequest.isAsyncCompleted()) { + throw new StaleAsyncWebRequestException("Async request processing already completed"); + } + setCallable(getSimpleCallable(value)); + new AsyncExecutionChainRunnable(asyncWebRequest, buildChain()).run(); + } + }); + } + + private Callable getSimpleCallable(final Object value) { + return new Callable() { + public Object call() throws Exception { + return value; + } + }; + } +} diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/AsyncExecutionChainRunnable.java b/spring-web/src/main/java/org/springframework/web/context/request/async/AsyncExecutionChainRunnable.java new file mode 100644 index 0000000000..5b48f9279b --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/AsyncExecutionChainRunnable.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.context.request.async; + +import java.util.concurrent.Callable; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.http.HttpStatus; +import org.springframework.util.Assert; + +/** + * A Runnable for invoking a chain of Callable instances and completing async + * request processing while also dealing with any unhandled exceptions. + * + * @author Rossen Stoyanchev + * @since 3.2 + * + * @see AsyncExecutionChain#startCallableChainProcessing() + * @see AsyncExecutionChain#startDeferredResultProcessing(DeferredResult) + */ +public class AsyncExecutionChainRunnable implements Runnable { + + private static final Log logger = LogFactory.getLog(AsyncWebRequest.class); + + private final AsyncWebRequest asyncWebRequest; + + private final Callable callable; + + /** + * Class constructor. + * @param asyncWebRequest the async request + * @param callable the async execution chain + */ + public AsyncExecutionChainRunnable(AsyncWebRequest asyncWebRequest, Callable callable) { + Assert.notNull(asyncWebRequest, "An AsyncWebRequest is required"); + Assert.notNull(callable, "A Callable is required"); + Assert.state(asyncWebRequest.isAsyncStarted(), "Not an async request"); + this.asyncWebRequest = asyncWebRequest; + this.callable = callable; + } + + /** + * Run the async execution chain and complete the async request. + *

    A {@link StaleAsyncWebRequestException} is logged at debug level and + * absorbed while any other unhandled {@link Exception} results in a 500 + * response code. + */ + public void run() { + try { + logger.debug("Starting async execution chain"); + callable.call(); + } + catch (StaleAsyncWebRequestException ex) { + logger.trace("Could not complete async request", ex); + } + catch (Exception ex) { + logger.trace("Could not complete async request", ex); + this.asyncWebRequest.sendError(HttpStatus.INTERNAL_SERVER_ERROR, ex.getMessage()); + } + finally { + logger.debug("Exiting async execution chain"); + asyncWebRequest.complete(); + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/AsyncWebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/async/AsyncWebRequest.java new file mode 100644 index 0000000000..8ed1a4ec6e --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/AsyncWebRequest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.context.request.async; + +import org.springframework.http.HttpStatus; +import org.springframework.web.context.request.NativeWebRequest; + + +/** + * Extends {@link NativeWebRequest} with methods for starting, completing, and + * configuring async request processing. Abstract underlying mechanisms such as + * the Servlet 3.0 AsyncContext. + * + * @author Rossen Stoyanchev + * @since 3.2 + */ +public interface AsyncWebRequest extends NativeWebRequest { + + /** + * Set the timeout for asynchronous request processing. When the timeout + * begins depends on the underlying 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. + */ + void setTimeout(Long timeout); + + /** + * Marks the start of async request processing for example. Ensures the + * request remains open to be completed in a separate thread. + */ + void startAsync(); + + /** + * Return {@code true} if async processing has started following a call to + * {@link #startAsync()} and before it has completed. + */ + boolean isAsyncStarted(); + + /** + * Complete async request processing finalizing the underlying request. + */ + void complete(); + + /** + * Send an error to the client. + */ + void sendError(HttpStatus status, String message); + + /** + * Return {@code true} if async processing completed either normally or for + * any other reason such as a timeout or an error. Note that a timeout or a + * (client) error may occur in a separate thread while async processing is + * still in progress in its own thread. Hence the underlying async request + * may become stale and this method may return {@code true} even if + * {@link #complete()} was never actually called. + * @see StaleAsyncWebRequestException + */ + boolean isAsyncCompleted(); + +} diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResult.java b/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResult.java new file mode 100644 index 0000000000..16c5843574 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResult.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.context.request.async; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.atomic.AtomicReference; + +import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.util.Assert; + +/** + * DeferredResult provides an alternative to using a Callable to complete async + * request processing. Whereas with a Callable the framework manages a thread on + * behalf of the application through an {@link AsyncTaskExecutor}, with a + * DeferredResult the application can produce a value using a thread of its choice. + * + *

    The following sequence describes typical use of a DeferredResult: + *

      + *
    1. Application method (e.g. controller method) returns a DeferredResult instance + *
    2. The framework completes initialization of the returned DeferredResult in the same thread + *
    3. The application calls {@link DeferredResult#set(Object)} from another thread + *
    4. The framework completes request processing in the thread in which it is invoked + *
    + * + *

    Note: {@link DeferredResult#set(Object)} will block if + * called before the DeferredResult is fully initialized (by the framework). + * Application code should never create a DeferredResult and set it immediately: + * + *

    + * DeferredResult value = new DeferredResult();
    + * value.set(1);  // blocks
    + * 
    + * + * @author Rossen Stoyanchev + * @since 3.2 + */ +public final class DeferredResult { + + private final AtomicReference value = new AtomicReference(); + + private final BlockingQueue handlers = new ArrayBlockingQueue(1); + + /** + * Provide a value to use to complete async request processing. + * This method should be invoked only once and usually from a separate + * thread to allow the framework to fully initialize the created + * DeferrredValue. See the class level documentation for more details. + * + * @throws StaleAsyncWebRequestException if the underlying async request + * ended due to a timeout or an error before the value was set. + */ + public void set(Object value) throws StaleAsyncWebRequestException { + Assert.isNull(this.value.get(), "Value already set"); + this.value.set(value); + try { + this.handlers.take().handle(value); + } + catch (InterruptedException e) { + throw new IllegalStateException("Failed to process deferred return value: " + value, e); + } + } + + void setValueProcessor(DeferredResultHandler handler) { + this.handlers.add(handler); + } + + + /** + * Puts the set value through processing wiht the async execution chain. + */ + interface DeferredResultHandler { + + void handle(Object result) throws StaleAsyncWebRequestException; + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/NoOpAsyncWebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/async/NoOpAsyncWebRequest.java new file mode 100644 index 0000000000..d51bbd67ce --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/NoOpAsyncWebRequest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.context.request.async; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.HttpStatus; +import org.springframework.web.context.request.ServletWebRequest; + +/** + * An implementation of {@link AsyncWebRequest} used when no underlying support + * for async request processing is available in which case {@link #startAsync()} + * results in an {@link UnsupportedOperationException}. + * + * @author Rossen Stoyanchev + * @since 3.2 + */ +public class NoOpAsyncWebRequest extends ServletWebRequest implements AsyncWebRequest { + + public NoOpAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) { + super(request, response); + } + + public void setTimeout(Long timeout) { + } + + public boolean isAsyncStarted() { + return false; + } + + public boolean isAsyncCompleted() { + return false; + } + + public void startAsync() { + throw new UnsupportedOperationException("No async support in a pre-Servlet 3.0 runtime"); + } + + public void complete() { + throw new UnsupportedOperationException("No async support in a pre-Servlet 3.0 environment"); + } + + public void sendError(HttpStatus status, String message) { + throw new UnsupportedOperationException("No async support in a pre-Servlet 3.0 environment"); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/StaleAsyncRequestCheckingCallable.java b/spring-web/src/main/java/org/springframework/web/context/request/async/StaleAsyncRequestCheckingCallable.java new file mode 100644 index 0000000000..0126a5de26 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/StaleAsyncRequestCheckingCallable.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.context.request.async; + + +/** + * Invokes the next Callable in a chain and then checks if the AsyncWebRequest + * provided to the constructor has ended before returning. Since a timeout or a + * (client) error may occur in a separate thread while async request processing + * is still in progress in its own thread, inserting this Callable in the chain + * protects against use of stale async requests. + * + *

    If an async request was terminated while the next Callable was still + * processing, a {@link StaleAsyncWebRequestException} is raised. + * + * @author Rossen Stoyanchev + * @since 3.2 + */ +public class StaleAsyncRequestCheckingCallable extends AbstractDelegatingCallable { + + private final AsyncWebRequest asyncWebRequest; + + public StaleAsyncRequestCheckingCallable(AsyncWebRequest asyncWebRequest) { + this.asyncWebRequest = asyncWebRequest; + } + + public Object call() throws Exception { + Object result = getNextCallable().call(); + if (this.asyncWebRequest.isAsyncCompleted()) { + throw new StaleAsyncWebRequestException( + "Async request no longer available due to a timed out or a (client) error"); + } + return result; + } + +} \ No newline at end of file diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/StaleAsyncWebRequestException.java b/spring-web/src/main/java/org/springframework/web/context/request/async/StaleAsyncWebRequestException.java new file mode 100644 index 0000000000..fbbb2f46cf --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/StaleAsyncWebRequestException.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.context.request.async; + +/** + * Raised if a stale AsyncWebRequest is detected during async request processing. + * + * @author Rossen Stoyanchev + * @since 3.2 + * + * @see DeferredResult#set(Object) + * @see AsyncExecutionChainRunnable#run() + */ +@SuppressWarnings("serial") +public class StaleAsyncWebRequestException extends RuntimeException { + + public StaleAsyncWebRequestException(String message) { + super(message); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java new file mode 100644 index 0000000000..76d9f66a66 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.context.request.async; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.servlet.AsyncContext; +import javax.servlet.AsyncEvent; +import javax.servlet.AsyncListener; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.HttpStatus; +import org.springframework.util.Assert; +import org.springframework.web.context.request.ServletWebRequest; + +/** + * A Servlet 3.0 implementation of {@link AsyncWebRequest}. + * + *

    The servlet processing an async request as well as all filters involved + * must async support enabled. This can be done in Java using the Servlet API + * or by adding an {@code true} element to + * servlet and filter declarations in web.xml + * + * @author Rossen Stoyanchev + * @since 3.2 + */ +public class StandardServletAsyncWebRequest extends ServletWebRequest implements AsyncWebRequest, AsyncListener { + + private Long timeout; + + private AsyncContext asyncContext; + + private AtomicBoolean asyncCompleted = new AtomicBoolean(false); + + public StandardServletAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) { + super(request, response); + } + + public void setTimeout(Long timeout) { + this.timeout = timeout; + } + + public boolean isAsyncStarted() { + assertNotStale(); + return ((this.asyncContext != null) && getRequest().isAsyncStarted()); + } + + public boolean isAsyncCompleted() { + return this.asyncCompleted.get(); + } + + public void startAsync() { + Assert.state(getRequest().isAsyncSupported(), + "Async support must be enabled on a servlet and for all filters involved " + + "in async request processing. This is done in Java code using the Servlet API " + + "or by adding \"true\" to servlet and " + + "filter declarations in web.xml."); + assertNotStale(); + Assert.state(!isAsyncStarted(), "Async processing already started"); + this.asyncContext = getRequest().startAsync(getRequest(), getResponse()); + this.asyncContext.addListener(this); + if (this.timeout != null) { + this.asyncContext.setTimeout(this.timeout); + } + } + + public void complete() { + assertNotStale(); + if (!isAsyncCompleted()) { + this.asyncContext.complete(); + } + } + + public void sendError(HttpStatus status, String message) { + try { + if (!isAsyncCompleted()) { + getResponse().sendError(500, message); + } + } + catch (IOException ioEx) { + // absorb + } + } + + private void assertNotStale() { + Assert.state(!isAsyncCompleted(), "Cannot use async request after completion"); + } + + // --------------------------------------------------------------------- + // Implementation of AsyncListener methods + // --------------------------------------------------------------------- + + public void onTimeout(AsyncEvent event) throws IOException { + this.asyncCompleted.set(true); + } + + public void onError(AsyncEvent event) throws IOException { + this.asyncCompleted.set(true); + } + + public void onStartAsync(AsyncEvent event) throws IOException { + } + + public void onComplete(AsyncEvent event) throws IOException { + this.asyncCompleted.set(true); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/filter/AbstractRequestLoggingFilter.java b/spring-web/src/main/java/org/springframework/web/filter/AbstractRequestLoggingFilter.java index c36bc87332..0f6ef8c341 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/AbstractRequestLoggingFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/AbstractRequestLoggingFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2008 the original author or authors. + * Copyright 2002-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; + import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletInputStream; @@ -31,6 +32,8 @@ import javax.servlet.http.HttpSession; import org.springframework.util.Assert; import org.springframework.util.StringUtils; +import org.springframework.web.context.request.async.AbstractDelegatingCallable; +import org.springframework.web.context.request.async.AsyncExecutionChain; import org.springframework.web.util.WebUtils; /** @@ -51,6 +54,7 @@ import org.springframework.web.util.WebUtils; * * @author Rob Harrop * @author Juergen Hoeller + * @author Rossen Stoyanchev * @see #beforeRequest * @see #afterRequest * @since 1.2.5 @@ -185,14 +189,22 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + if (isIncludePayload()) { request = new RequestCachingRequestWrapper(request); } beforeRequest(request, getBeforeMessage(request)); + + AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(request); + chain.addDelegatingCallable(getAsyncCallable(request)); + try { filterChain.doFilter(request, response); } finally { + if (chain.isAsyncStarted()) { + return; + } afterRequest(request, getAfterMessage(request)); } } @@ -278,6 +290,20 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter */ protected abstract void afterRequest(HttpServletRequest request, String message); + /** + * Create a Callable to use to complete processing in an async execution chain. + */ + private AbstractDelegatingCallable getAsyncCallable(final HttpServletRequest request) { + return new AbstractDelegatingCallable() { + public Object call() throws Exception { + getNextCallable().call(); + afterRequest(request, getAfterMessage(request)); + return null; + } + }; + } + + private static class RequestCachingRequestWrapper extends HttpServletRequestWrapper { private final ByteArrayOutputStream bos = new ByteArrayOutputStream(); diff --git a/spring-web/src/main/java/org/springframework/web/filter/OncePerRequestFilter.java b/spring-web/src/main/java/org/springframework/web/filter/OncePerRequestFilter.java index 2f53f6be1b..3b8378d771 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/OncePerRequestFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/OncePerRequestFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2008 the original author or authors. + * Copyright 2002-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,9 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.springframework.web.context.request.async.AbstractDelegatingCallable; +import org.springframework.web.context.request.async.AsyncExecutionChain; + /** * Filter base class that guarantees to be just executed once per request, * on any servlet container. It provides a {@link #doFilterInternal} @@ -35,6 +38,7 @@ import javax.servlet.http.HttpServletResponse; * is based on the configured name of the concrete filter instance. * * @author Juergen Hoeller + * @author Rossen Stoyanchev * @since 06.12.2003 */ public abstract class OncePerRequestFilter extends GenericFilterBean { @@ -70,12 +74,18 @@ public abstract class OncePerRequestFilter extends GenericFilterBean { filterChain.doFilter(request, response); } else { + AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(request); + chain.addDelegatingCallable(getAsyncCallable(request, alreadyFilteredAttributeName)); + // Do invoke this filter... request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE); try { doFilterInternal(httpRequest, httpResponse, filterChain); } finally { + if (chain.isAsyncStarted()) { + return; + } // Remove the "already filtered" request attribute for this request. request.removeAttribute(alreadyFilteredAttributeName); } @@ -111,6 +121,20 @@ public abstract class OncePerRequestFilter extends GenericFilterBean { return false; } + /** + * Create a Callable to use to complete processing in an async execution chain. + */ + private AbstractDelegatingCallable getAsyncCallable(final ServletRequest request, + final String alreadyFilteredAttributeName) { + + return new AbstractDelegatingCallable() { + public Object call() throws Exception { + getNextCallable().call(); + request.removeAttribute(alreadyFilteredAttributeName); + return null; + } + }; + } /** * Same contract as for doFilter, but guaranteed to be diff --git a/spring-web/src/main/java/org/springframework/web/filter/RequestContextFilter.java b/spring-web/src/main/java/org/springframework/web/filter/RequestContextFilter.java index 725907e03e..3ce5e4e18a 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/RequestContextFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/RequestContextFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 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. @@ -17,6 +17,7 @@ package org.springframework.web.filter; import java.io.IOException; + import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -25,6 +26,8 @@ import javax.servlet.http.HttpServletResponse; import org.springframework.context.i18n.LocaleContextHolder; 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; /** * Servlet 2.3 Filter that exposes the request to the current thread, @@ -40,6 +43,7 @@ import org.springframework.web.context.request.ServletRequestAttributes; * * @author Juergen Hoeller * @author Rod Johnson + * @author Rossen Stoyanchev * @since 2.0 * @see org.springframework.context.i18n.LocaleContextHolder * @see org.springframework.web.context.request.RequestContextHolder @@ -74,17 +78,19 @@ public class RequestContextFilter extends OncePerRequestFilter { throws ServletException, IOException { ServletRequestAttributes attributes = new ServletRequestAttributes(request); - LocaleContextHolder.setLocale(request.getLocale(), this.threadContextInheritable); - RequestContextHolder.setRequestAttributes(attributes, this.threadContextInheritable); - if (logger.isDebugEnabled()) { - logger.debug("Bound request context to thread: " + request); - } + initContextHolders(request, attributes); + + AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(request); + chain.addDelegatingCallable(getChainedCallable(request, attributes)); + try { filterChain.doFilter(request, response); } finally { - LocaleContextHolder.resetLocaleContext(); - RequestContextHolder.resetRequestAttributes(); + resetContextHolders(); + if (chain.isAsyncStarted()) { + return; + } attributes.requestCompleted(); if (logger.isDebugEnabled()) { logger.debug("Cleared thread-bound request context: " + request); @@ -92,4 +98,41 @@ public class RequestContextFilter extends OncePerRequestFilter { } } + private void initContextHolders(HttpServletRequest request, ServletRequestAttributes requestAttributes) { + LocaleContextHolder.setLocale(request.getLocale(), this.threadContextInheritable); + RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable); + if (logger.isDebugEnabled()) { + logger.debug("Bound request context to thread: " + request); + } + } + + private void resetContextHolders() { + LocaleContextHolder.resetLocaleContext(); + RequestContextHolder.resetRequestAttributes(); + } + + /** + * Create a Callable to use to complete processing in an async execution chain. + */ + private AbstractDelegatingCallable getChainedCallable(final HttpServletRequest request, + final ServletRequestAttributes requestAttributes) { + + return new AbstractDelegatingCallable() { + public Object call() throws Exception { + initContextHolders(request, requestAttributes); + try { + getNextCallable().call(); + } + finally { + resetContextHolders(); + requestAttributes.requestCompleted(); + if (logger.isDebugEnabled()) { + logger.debug("Cleared thread-bound request context: " + request); + } + } + return null; + } + }; + } + } diff --git a/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java b/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java index d214561959..e88948cce3 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 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. @@ -21,6 +21,7 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; + import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; @@ -30,6 +31,8 @@ import javax.servlet.http.HttpServletResponseWrapper; import org.springframework.util.DigestUtils; import org.springframework.util.FileCopyUtils; +import org.springframework.web.context.request.async.AbstractDelegatingCallable; +import org.springframework.web.context.request.async.AsyncExecutionChain; import org.springframework.web.util.WebUtils; /** @@ -41,6 +44,7 @@ import org.springframework.web.util.WebUtils; * is still rendered. As such, this filter only saves bandwidth, not server performance. * * @author Arjen Poutsma + * @author Rossen Stoyanchev * @since 3.0 */ public class ShallowEtagHeaderFilter extends OncePerRequestFilter { @@ -55,8 +59,37 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter { throws ServletException, IOException { ShallowEtagResponseWrapper responseWrapper = new ShallowEtagResponseWrapper(response); + + AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(request); + chain.addDelegatingCallable(getAsyncCallable(request, response, responseWrapper)); + filterChain.doFilter(request, responseWrapper); + if (chain.isAsyncStarted()) { + return; + } + + updateResponse(request, response, responseWrapper); + } + + /** + * Create a Callable to use to complete processing in an async execution chain. + */ + private AbstractDelegatingCallable getAsyncCallable(final HttpServletRequest request, + final HttpServletResponse response, final ShallowEtagResponseWrapper responseWrapper) { + + return new AbstractDelegatingCallable() { + public Object call() throws Exception { + getNextCallable().call(); + updateResponse(request, response, responseWrapper); + return null; + } + }; + } + + private void updateResponse(HttpServletRequest request, HttpServletResponse response, + ShallowEtagResponseWrapper responseWrapper) throws IOException { + byte[] body = responseWrapper.toByteArray(); int statusCode = responseWrapper.getStatusCode(); @@ -149,6 +182,7 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter { this.statusCode = sc; } + @SuppressWarnings("deprecation") @Override public void setStatus(int sc, String sm) { super.setStatus(sc, sm); diff --git a/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java b/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java index 1ce366f52a..a9a2a4b463 100644 --- a/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java +++ b/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java @@ -106,8 +106,8 @@ public class InvocableHandlerMethod extends HandlerMethod { * a thrown exception instance. Provided argument values are checked before argument resolvers. * * @param request the current request - * @param mavContainer the {@link ModelAndViewContainer} for the current request - * @param providedArgs argument values to try to use without view resolution + * @param mavContainer the ModelAndViewContainer for this request + * @param providedArgs "given" arguments matched by type, not resolved * @return the raw value returned by the invoked method * @exception Exception raised if no suitable argument resolver can be found, or the method raised an exception */ diff --git a/spring-web/src/test/java/org/springframework/web/context/request/async/AsyncExecutionChainTests.java b/spring-web/src/test/java/org/springframework/web/context/request/async/AsyncExecutionChainTests.java new file mode 100644 index 0000000000..bb70bfc5f0 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/context/request/async/AsyncExecutionChainTests.java @@ -0,0 +1,246 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.context.request.async; + +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.concurrent.Callable; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.web.context.request.ServletWebRequest; + + +/** + * Test fixture with an AsyncExecutionChain. + * + * @author Rossen Stoyanchev + */ +public class AsyncExecutionChainTests { + + private AsyncExecutionChain chain; + + private MockHttpServletRequest request; + + private SimpleAsyncWebRequest asyncWebRequest; + + private ResultSavingCallable resultSavingCallable; + + @Before + public void setUp() { + this.request = new MockHttpServletRequest(); + this.asyncWebRequest = new SimpleAsyncWebRequest(this.request, new MockHttpServletResponse()); + this.resultSavingCallable = new ResultSavingCallable(); + + this.chain = AsyncExecutionChain.getForCurrentRequest(this.request); + this.chain.setTaskExecutor(new SyncTaskExecutor()); + this.chain.setAsyncWebRequest(this.asyncWebRequest); + this.chain.addDelegatingCallable(this.resultSavingCallable); + } + + @Test + public void getForCurrentRequest() throws Exception { + assertNotNull(this.chain); + assertSame(this.chain, AsyncExecutionChain.getForCurrentRequest(this.request)); + assertSame(this.chain, this.request.getAttribute(AsyncExecutionChain.CALLABLE_CHAIN_ATTRIBUTE)); + } + + @Test + public void isAsyncStarted() { + assertFalse(this.chain.isAsyncStarted()); + + this.asyncWebRequest.startAsync(); + assertTrue(this.chain.isAsyncStarted()); + + this.chain.setAsyncWebRequest(null); + assertFalse(this.chain.isAsyncStarted()); + } + + @Test + public void startCallableChainProcessing() throws Exception { + this.chain.addDelegatingCallable(new IntegerIncrementingCallable()); + this.chain.addDelegatingCallable(new IntegerIncrementingCallable()); + this.chain.setCallable(new Callable() { + public Object call() throws Exception { + return 1; + } + }); + + this.chain.startCallableChainProcessing(); + + assertEquals(3, this.resultSavingCallable.result); + } + + @Test + public void startCallableChainProcessing_staleRequest() { + this.chain.setCallable(new Callable() { + public Object call() throws Exception { + return 1; + } + }); + + this.asyncWebRequest.startAsync(); + this.asyncWebRequest.complete(); + this.chain.startCallableChainProcessing(); + Exception ex = this.resultSavingCallable.exception; + + assertNotNull(ex); + assertEquals(StaleAsyncWebRequestException.class, ex.getClass()); + } + + @Test + public void startCallableChainProcessing_requiredCallable() { + try { + this.chain.startCallableChainProcessing(); + fail("Expected exception"); + } + catch (IllegalStateException ex) { + assertThat(ex.getMessage(), containsString("The callable field is required")); + } + } + + @Test + public void startCallableChainProcessing_requiredAsyncWebRequest() { + this.chain.setAsyncWebRequest(null); + try { + this.chain.startCallableChainProcessing(); + fail("Expected exception"); + } + catch (IllegalStateException ex) { + assertThat(ex.getMessage(), containsString("AsyncWebRequest is required")); + } + } + + @Test + public void startDeferredValueProcessing() throws Exception { + this.chain.addDelegatingCallable(new IntegerIncrementingCallable()); + this.chain.addDelegatingCallable(new IntegerIncrementingCallable()); + + DeferredResult deferredValue = new DeferredResult(); + this.chain.startDeferredResultProcessing(deferredValue); + + assertTrue(this.asyncWebRequest.isAsyncStarted()); + + deferredValue.set(1); + + assertEquals(3, this.resultSavingCallable.result); + } + + @Test(expected=StaleAsyncWebRequestException.class) + public void startDeferredValueProcessing_staleRequest() throws Exception { + this.asyncWebRequest.startAsync(); + this.asyncWebRequest.complete(); + + DeferredResult deferredValue = new DeferredResult(); + this.chain.startDeferredResultProcessing(deferredValue); + deferredValue.set(1); + } + + @Test + public void startDeferredValueProcessing_requiredDeferredValue() { + try { + this.chain.startDeferredResultProcessing(null); + fail("Expected exception"); + } + catch (IllegalArgumentException ex) { + assertThat(ex.getMessage(), containsString("A DeferredValue is required")); + } + } + + + private static class SimpleAsyncWebRequest extends ServletWebRequest implements AsyncWebRequest { + + private boolean asyncStarted; + + private boolean asyncCompleted; + + public SimpleAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) { + super(request, response); + } + + public void startAsync() { + this.asyncStarted = true; + } + + public boolean isAsyncStarted() { + return this.asyncStarted; + } + + public void setTimeout(Long timeout) { } + + public void complete() { + this.asyncStarted = false; + this.asyncCompleted = true; + } + + public boolean isAsyncCompleted() { + return this.asyncCompleted; + } + + public void sendError(HttpStatus status, String message) { + } + } + + @SuppressWarnings("serial") + private static class SyncTaskExecutor extends SimpleAsyncTaskExecutor { + + @Override + public void execute(Runnable task, long startTimeout) { + task.run(); + } + } + + private static class ResultSavingCallable extends AbstractDelegatingCallable { + + Object result; + + Exception exception; + + public Object call() throws Exception { + try { + this.result = getNextCallable().call(); + } + catch (Exception ex) { + this.exception = ex; + throw ex; + } + return this.result; + } + } + + private static class IntegerIncrementingCallable extends AbstractDelegatingCallable { + + public Object call() throws Exception { + return ((Integer) getNextCallable().call() + 1); + } + } + +} diff --git a/spring-web/src/test/java/org/springframework/web/context/request/async/StaleAsyncRequestCheckingCallableTests.java b/spring-web/src/test/java/org/springframework/web/context/request/async/StaleAsyncRequestCheckingCallableTests.java new file mode 100644 index 0000000000..c676e5a47d --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/context/request/async/StaleAsyncRequestCheckingCallableTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.context.request.async; + +import static org.easymock.EasyMock.*; + +import java.util.concurrent.Callable; + +import org.easymock.EasyMock; +import org.junit.Before; +import org.junit.Test; + +/** + * A test fixture with a {@link StaleAsyncRequestCheckingCallable}. + * + * @author Rossen Stoyanchev + */ +public class StaleAsyncRequestCheckingCallableTests { + + private StaleAsyncRequestCheckingCallable callable; + + private AsyncWebRequest asyncWebRequest; + + @Before + public void setUp() { + this.asyncWebRequest = EasyMock.createMock(AsyncWebRequest.class); + this.callable = new StaleAsyncRequestCheckingCallable(asyncWebRequest); + this.callable.setNextCallable(new Callable() { + public Object call() throws Exception { + return 1; + } + }); + } + + @Test + public void call_notStale() throws Exception { + expect(this.asyncWebRequest.isAsyncCompleted()).andReturn(false); + replay(this.asyncWebRequest); + + this.callable.call(); + + verify(this.asyncWebRequest); + } + + @Test(expected=StaleAsyncWebRequestException.class) + public void call_stale() throws Exception { + expect(this.asyncWebRequest.isAsyncCompleted()).andReturn(true); + replay(this.asyncWebRequest); + + try { + this.callable.call(); + } + finally { + verify(this.asyncWebRequest); + } + } +} diff --git a/spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java b/spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java new file mode 100644 index 0000000000..bc10bedd0c --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java @@ -0,0 +1,165 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.context.request.async; + +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.reset; +import static org.easymock.EasyMock.verify; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import javax.servlet.AsyncContext; +import javax.servlet.AsyncEvent; +import javax.servlet.http.HttpServletRequest; + +import org.easymock.EasyMock; +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletResponse; + +/** + * A test fixture with a {@link StandardServletAsyncWebRequest}. + * + * @author Rossen Stoyanchev + */ +public class StandardServletAsyncWebRequestTests { + + private StandardServletAsyncWebRequest asyncRequest; + + private HttpServletRequest request; + + private MockHttpServletResponse response; + + @Before + public void setup() { + this.request = EasyMock.createMock(HttpServletRequest.class); + this.response = new MockHttpServletResponse(); + this.asyncRequest = new StandardServletAsyncWebRequest(this.request, this.response); + this.asyncRequest.setTimeout(60*1000L); + } + + @Test + public void isAsyncStarted() throws Exception { + assertEquals(false, this.asyncRequest.isAsyncStarted()); + + startAsync(); + + reset(this.request); + expect(this.request.isAsyncStarted()).andReturn(true); + replay(this.request); + + assertTrue(this.asyncRequest.isAsyncStarted()); + } + + @Test + public void isAsyncStarted_stale() throws Exception { + this.asyncRequest.onComplete(new AsyncEvent(null)); + try { + this.asyncRequest.isAsyncStarted(); + fail("expected exception"); + } + catch (IllegalStateException ex) { + assertStaleRequestMessage(ex); + } + } + + @Test + public void startAsync() throws Exception { + AsyncContext asyncContext = EasyMock.createMock(AsyncContext.class); + + expect(this.request.isAsyncSupported()).andReturn(true); + expect(this.request.startAsync(this.request, this.response)).andStubReturn(asyncContext); + replay(this.request); + + asyncContext.addListener(this.asyncRequest); + asyncContext.setTimeout(60*1000); + replay(asyncContext); + + this.asyncRequest.startAsync(); + + verify(this.request); + } + + @Test + public void startAsync_alreadyStarted() throws Exception { + startAsync(); + + reset(this.request); + + expect(this.request.isAsyncSupported()).andReturn(true); + expect(this.request.isAsyncStarted()).andReturn(true); + replay(this.request); + + try { + this.asyncRequest.startAsync(); + fail("expected exception"); + } + catch (IllegalStateException ex) { + assertEquals("Async processing already started", ex.getMessage()); + } + + verify(this.request); + } + + @Test + public void startAsync_stale() throws Exception { + expect(this.request.isAsyncSupported()).andReturn(true); + replay(this.request); + this.asyncRequest.onComplete(new AsyncEvent(null)); + try { + this.asyncRequest.startAsync(); + fail("expected exception"); + } + catch (IllegalStateException ex) { + assertStaleRequestMessage(ex); + } + } + + @Test + public void complete_stale() throws Exception { + this.asyncRequest.onComplete(new AsyncEvent(null)); + try { + this.asyncRequest.complete(); + fail("expected exception"); + } + catch (IllegalStateException ex) { + assertStaleRequestMessage(ex); + } + } + + @Test + public void sendError() throws Exception { + this.asyncRequest.sendError(HttpStatus.INTERNAL_SERVER_ERROR, "error"); + assertEquals(500, this.response.getStatus()); + } + + @Test + public void sendError_requestAlreadyCompleted() throws Exception { + this.asyncRequest.onComplete(new AsyncEvent(null)); + this.asyncRequest.sendError(HttpStatus.INTERNAL_SERVER_ERROR, "error"); + assertEquals(200, this.response.getStatus()); + } + + + private void assertStaleRequestMessage(IllegalStateException ex) { + assertEquals("Cannot use async request after completion", ex.getMessage()); + } + +} diff --git a/spring-web/src/test/java/org/springframework/web/filter/CharacterEncodingFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/CharacterEncodingFilterTests.java index 801c05440c..8e30c50109 100644 --- a/spring-web/src/test/java/org/springframework/web/filter/CharacterEncodingFilterTests.java +++ b/spring-web/src/test/java/org/springframework/web/filter/CharacterEncodingFilterTests.java @@ -16,16 +16,23 @@ package org.springframework.web.filter; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.notNull; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.same; +import static org.easymock.EasyMock.verify; + import javax.servlet.FilterChain; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import junit.framework.TestCase; -import org.easymock.MockControl; import org.springframework.mock.web.MockFilterConfig; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockServletContext; +import org.springframework.web.context.request.async.AsyncExecutionChain; /** * @author Rick Evans @@ -39,28 +46,22 @@ public class CharacterEncodingFilterTests extends TestCase { public void testForceAlwaysSetsEncoding() throws Exception { - MockControl mockRequest = MockControl.createControl(HttpServletRequest.class); - HttpServletRequest request = (HttpServletRequest) mockRequest.getMock(); + HttpServletRequest request = createMock(HttpServletRequest.class); + expect(request.getAttribute(AsyncExecutionChain.CALLABLE_CHAIN_ATTRIBUTE)).andReturn(null); + request.setAttribute(same(AsyncExecutionChain.CALLABLE_CHAIN_ATTRIBUTE), notNull()); request.setCharacterEncoding(ENCODING); - mockRequest.setVoidCallable(); - request.getAttribute(FILTER_NAME + OncePerRequestFilter.ALREADY_FILTERED_SUFFIX); - mockRequest.setReturnValue(null); + expect(request.getAttribute(FILTER_NAME + OncePerRequestFilter.ALREADY_FILTERED_SUFFIX)).andReturn(null); request.setAttribute(FILTER_NAME + OncePerRequestFilter.ALREADY_FILTERED_SUFFIX, Boolean.TRUE); - mockRequest.setVoidCallable(); request.removeAttribute(FILTER_NAME + OncePerRequestFilter.ALREADY_FILTERED_SUFFIX); - mockRequest.setVoidCallable(); - mockRequest.replay(); + replay(request); - MockControl mockResponse = MockControl.createControl(HttpServletResponse.class); - HttpServletResponse response = (HttpServletResponse) mockResponse.getMock(); + HttpServletResponse response = createMock(HttpServletResponse.class); response.setCharacterEncoding(ENCODING); - mockResponse.setVoidCallable(); - mockResponse.replay(); + replay(response); - MockControl mockFilter = MockControl.createControl(FilterChain.class); - FilterChain filterChain = (FilterChain) mockFilter.getMock(); + FilterChain filterChain = createMock(FilterChain.class); filterChain.doFilter(request, response); - mockFilter.replay(); + replay(filterChain); CharacterEncodingFilter filter = new CharacterEncodingFilter(); filter.setForceEncoding(true); @@ -68,32 +69,27 @@ public class CharacterEncodingFilterTests extends TestCase { filter.init(new MockFilterConfig(FILTER_NAME)); filter.doFilter(request, response, filterChain); - mockRequest.verify(); - mockResponse.verify(); - mockFilter.verify(); + verify(request); + verify(response); + verify(filterChain); } public void testEncodingIfEmptyAndNotForced() throws Exception { - MockControl mockRequest = MockControl.createControl(HttpServletRequest.class); - HttpServletRequest request = (HttpServletRequest) mockRequest.getMock(); - request.getCharacterEncoding(); - mockRequest.setReturnValue(null); + HttpServletRequest request = createMock(HttpServletRequest.class); + expect(request.getAttribute(AsyncExecutionChain.CALLABLE_CHAIN_ATTRIBUTE)).andReturn(null); + request.setAttribute(same(AsyncExecutionChain.CALLABLE_CHAIN_ATTRIBUTE), notNull()); + expect(request.getCharacterEncoding()).andReturn(null); request.setCharacterEncoding(ENCODING); - mockRequest.setVoidCallable(); - request.getAttribute(FILTER_NAME + OncePerRequestFilter.ALREADY_FILTERED_SUFFIX); - mockRequest.setReturnValue(null); + expect(request.getAttribute(FILTER_NAME + OncePerRequestFilter.ALREADY_FILTERED_SUFFIX)).andReturn(null); request.setAttribute(FILTER_NAME + OncePerRequestFilter.ALREADY_FILTERED_SUFFIX, Boolean.TRUE); - mockRequest.setVoidCallable(); request.removeAttribute(FILTER_NAME + OncePerRequestFilter.ALREADY_FILTERED_SUFFIX); - mockRequest.setVoidCallable(); - mockRequest.replay(); + replay(request); MockHttpServletResponse response = new MockHttpServletResponse(); - MockControl mockFilter = MockControl.createControl(FilterChain.class); - FilterChain filterChain = (FilterChain) mockFilter.getMock(); + FilterChain filterChain = createMock(FilterChain.class); filterChain.doFilter(request, response); - mockFilter.replay(); + replay(filterChain); CharacterEncodingFilter filter = new CharacterEncodingFilter(); filter.setForceEncoding(false); @@ -101,60 +97,51 @@ public class CharacterEncodingFilterTests extends TestCase { filter.init(new MockFilterConfig(FILTER_NAME)); filter.doFilter(request, response, filterChain); - mockRequest.verify(); - mockFilter.verify(); + verify(request); + verify(filterChain); } public void testDoesNowtIfEncodingIsNotEmptyAndNotForced() throws Exception { - MockControl mockRequest = MockControl.createControl(HttpServletRequest.class); - HttpServletRequest request = (HttpServletRequest) mockRequest.getMock(); - request.getCharacterEncoding(); - mockRequest.setReturnValue(ENCODING); - request.getAttribute(FILTER_NAME + OncePerRequestFilter.ALREADY_FILTERED_SUFFIX); - mockRequest.setReturnValue(null); + HttpServletRequest request = createMock(HttpServletRequest.class); + expect(request.getAttribute(AsyncExecutionChain.CALLABLE_CHAIN_ATTRIBUTE)).andReturn(null); + request.setAttribute(same(AsyncExecutionChain.CALLABLE_CHAIN_ATTRIBUTE), notNull()); + expect(request.getCharacterEncoding()).andReturn(ENCODING); + expect(request.getAttribute(FILTER_NAME + OncePerRequestFilter.ALREADY_FILTERED_SUFFIX)).andReturn(null); request.setAttribute(FILTER_NAME + OncePerRequestFilter.ALREADY_FILTERED_SUFFIX, Boolean.TRUE); - mockRequest.setVoidCallable(); request.removeAttribute(FILTER_NAME + OncePerRequestFilter.ALREADY_FILTERED_SUFFIX); - mockRequest.setVoidCallable(); - mockRequest.replay(); + replay(request); MockHttpServletResponse response = new MockHttpServletResponse(); - MockControl mockFilter = MockControl.createControl(FilterChain.class); - FilterChain filterChain = (FilterChain) mockFilter.getMock(); + FilterChain filterChain = createMock(FilterChain.class); filterChain.doFilter(request, response); - mockFilter.replay(); + replay(filterChain); CharacterEncodingFilter filter = new CharacterEncodingFilter(); filter.setEncoding(ENCODING); filter.init(new MockFilterConfig(FILTER_NAME)); filter.doFilter(request, response, filterChain); - mockRequest.verify(); - mockFilter.verify(); + verify(request); + verify(filterChain); } public void testWithBeanInitialization() throws Exception { - MockControl mockRequest = MockControl.createControl(HttpServletRequest.class); - HttpServletRequest request = (HttpServletRequest) mockRequest.getMock(); - request.getCharacterEncoding(); - mockRequest.setReturnValue(null); + HttpServletRequest request = createMock(HttpServletRequest.class); + expect(request.getAttribute(AsyncExecutionChain.CALLABLE_CHAIN_ATTRIBUTE)).andReturn(null); + request.setAttribute(same(AsyncExecutionChain.CALLABLE_CHAIN_ATTRIBUTE), notNull()); + expect(request.getCharacterEncoding()).andReturn(null); request.setCharacterEncoding(ENCODING); - mockRequest.setVoidCallable(); - request.getAttribute(FILTER_NAME + OncePerRequestFilter.ALREADY_FILTERED_SUFFIX); - mockRequest.setReturnValue(null); + expect(request.getAttribute(FILTER_NAME + OncePerRequestFilter.ALREADY_FILTERED_SUFFIX)).andReturn(null); request.setAttribute(FILTER_NAME + OncePerRequestFilter.ALREADY_FILTERED_SUFFIX, Boolean.TRUE); - mockRequest.setVoidCallable(); request.removeAttribute(FILTER_NAME + OncePerRequestFilter.ALREADY_FILTERED_SUFFIX); - mockRequest.setVoidCallable(); - mockRequest.replay(); + replay(request); MockHttpServletResponse response = new MockHttpServletResponse(); - MockControl mockFilter = MockControl.createControl(FilterChain.class); - FilterChain filterChain = (FilterChain) mockFilter.getMock(); + FilterChain filterChain = createMock(FilterChain.class); filterChain.doFilter(request, response); - mockFilter.replay(); + replay(filterChain); CharacterEncodingFilter filter = new CharacterEncodingFilter(); filter.setEncoding(ENCODING); @@ -162,38 +149,33 @@ public class CharacterEncodingFilterTests extends TestCase { filter.setServletContext(new MockServletContext()); filter.doFilter(request, response, filterChain); - mockRequest.verify(); - mockFilter.verify(); + verify(request); + verify(filterChain); } public void testWithIncompleteInitialization() throws Exception { - MockControl mockRequest = MockControl.createControl(HttpServletRequest.class); - HttpServletRequest request = (HttpServletRequest) mockRequest.getMock(); - request.getCharacterEncoding(); - mockRequest.setReturnValue(null); + HttpServletRequest request = createMock(HttpServletRequest.class); + expect(request.getAttribute(AsyncExecutionChain.CALLABLE_CHAIN_ATTRIBUTE)).andReturn(null); + request.setAttribute(same(AsyncExecutionChain.CALLABLE_CHAIN_ATTRIBUTE), notNull()); + expect(request.getCharacterEncoding()).andReturn(null); request.setCharacterEncoding(ENCODING); - mockRequest.setVoidCallable(); - request.getAttribute(CharacterEncodingFilter.class.getName() + OncePerRequestFilter.ALREADY_FILTERED_SUFFIX); - mockRequest.setReturnValue(null); + expect(request.getAttribute(CharacterEncodingFilter.class.getName() + OncePerRequestFilter.ALREADY_FILTERED_SUFFIX)).andReturn(null); request.setAttribute(CharacterEncodingFilter.class.getName() + OncePerRequestFilter.ALREADY_FILTERED_SUFFIX, Boolean.TRUE); - mockRequest.setVoidCallable(); request.removeAttribute(CharacterEncodingFilter.class.getName() + OncePerRequestFilter.ALREADY_FILTERED_SUFFIX); - mockRequest.setVoidCallable(); - mockRequest.replay(); + replay(request); MockHttpServletResponse response = new MockHttpServletResponse(); - MockControl mockFilter = MockControl.createControl(FilterChain.class); - FilterChain filterChain = (FilterChain) mockFilter.getMock(); + FilterChain filterChain = createMock(FilterChain.class); filterChain.doFilter(request, response); - mockFilter.replay(); + replay(filterChain); CharacterEncodingFilter filter = new CharacterEncodingFilter(); filter.setEncoding(ENCODING); filter.doFilter(request, response, filterChain); - mockRequest.verify(); - mockFilter.verify(); + verify(request); + verify(filterChain); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java index d6eddaf7d7..19c561a04c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java @@ -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 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. - *

    If no implementation is configured then we default to + *

    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 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. *

    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. *

    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 null 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; } /** diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java index f44b130484..d8916287c9 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java @@ -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> contextInitializers = new ArrayList>(); - /** * 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. *

    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 onApplicationEvent on the FrameworkServlet instance. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerExecutionChain.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerExecutionChain.java index 9c8e6d8949..c66836c638 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerExecutionChain.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerExecutionChain.java @@ -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 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 true 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 toString(). diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AsyncMethodReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AsyncMethodReturnValueHandler.java new file mode 100644 index 0000000000..5152ac19fc --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AsyncMethodReturnValueHandler.java @@ -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}. + * + *

    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) 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); + } + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java index c2e030e9e6..25abdccb91 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -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}. - * + * *

    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 customArgumentResolvers; private List customReturnValueHandlers; - + private List modelAndViewResolvers; private List> 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, SessionAttributesHandler> sessionAttributesHandlerCache = new ConcurrentHashMap, SessionAttributesHandler>(); private HandlerMethodArgumentResolverComposite argumentResolvers; private HandlerMethodArgumentResolverComposite initBinderArgumentResolvers; - + private HandlerMethodReturnValueHandlerComposite returnValueHandlers; private final Map, Set> dataBinderFactoryCache = new ConcurrentHashMap, Set>(); private final Map, Set> modelFactoryCache = new ConcurrentHashMap, Set>(); + 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 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 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. - *

    Note: This method is available for backwards - * compatibility only. However, it is recommended to re-write a + * Provide custom {@link ModelAndViewResolver}s. + *

    Note: 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. - *

    A {@code HandlerMethodReturnValueHandler} provides better access to + *

    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> 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 @SessionAttributes annotated handlers * for the given number of seconds. Default is 0, preventing caching completely. - *

    In contrast to the "cacheSeconds" property which will apply to all general - * handlers (but not to @SessionAttributes annotated handlers), + *

    In contrast to the "cacheSeconds" property which will apply to all general + * handlers (but not to @SessionAttributes annotated handlers), * this setting will apply to @SessionAttributes 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. - *

    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 + *

    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. - *

    The default setting is {@code false} but new applications should + *

    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. + *

    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. + *

    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} - *

    A {@link ConfigurableBeanFactory} is expected for resolving + *

    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 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 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 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 flashAttributes = ((RedirectAttributes) model).getFlashAttributes(); + HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); + RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes); + } + return mav; + } + + /** * MethodFilter that matches {@link InitBinder @InitBinder} methods. */ diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java index ea6ff054c0..d2b86bc8d1 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java @@ -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. * - *

    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. + *

    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}. - *

    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: - *

      - *
    • A {@link HandlerMethodArgumentResolver} has set the {@link ModelAndViewContainer#setRequestHandled(boolean)} - * flag to {@code false} -- e.g. method arguments providing access to the response. - *
    • 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. - *
    • The status on the response is set due to a @{@link ResponseStatus} instruction. - *
    - *

    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 getMethodAnnotation(Class annotationType) { + return ServletInvocableHandlerMethod.this.getMethodAnnotation(annotationType); + } + } + } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/HandlerExecutionChainTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/HandlerExecutionChainTests.java new file mode 100644 index 0000000000..550afaa6b8 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/HandlerExecutionChainTests.java @@ -0,0 +1,158 @@ +/* + * 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; + +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.junit.Assert.assertSame; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +/** + * A test fixture with HandlerExecutionChain and mock handler interceptors. + * + * @author Rossen Stoyanchev + */ +public class HandlerExecutionChainTests { + + private HandlerExecutionChain chain; + + private Object handler; + + private MockHttpServletRequest request; + + private MockHttpServletResponse response; + + private HandlerInterceptor interceptor1; + + private HandlerInterceptor interceptor2; + + private HandlerInterceptor interceptor3; + + @Before + public void setup() { + this.request = new MockHttpServletRequest(); + this.response= new MockHttpServletResponse() ; + + this.handler = new Object(); + this.chain = new HandlerExecutionChain(this.handler); + + this.interceptor1 = createMock(HandlerInterceptor.class); + this.interceptor2 = createMock(HandlerInterceptor.class); + this.interceptor3 = createMock(HandlerInterceptor.class); + + this.chain.addInterceptor(this.interceptor1); + this.chain.addInterceptor(this.interceptor2); + this.chain.addInterceptor(this.interceptor3); + } + + @Test + public void successScenario() throws Exception { + ModelAndView mav = new ModelAndView(); + + expect(this.interceptor1.preHandle(this.request, this.response, this.handler)).andReturn(true); + expect(this.interceptor2.preHandle(this.request, this.response, this.handler)).andReturn(true); + expect(this.interceptor3.preHandle(this.request, this.response, this.handler)).andReturn(true); + + this.interceptor1.postHandle(this.request, this.response, this.handler, mav); + this.interceptor2.postHandle(this.request, this.response, this.handler, mav); + this.interceptor3.postHandle(this.request, this.response, this.handler, mav); + + this.interceptor3.afterCompletion(this.request, this.response, this.handler, null); + this.interceptor2.afterCompletion(this.request, this.response, this.handler, null); + this.interceptor1.afterCompletion(this.request, this.response, this.handler, null); + + replay(this.interceptor1, this.interceptor2, this.interceptor3); + + this.chain.applyPreHandle(request, response); + this.chain.applyPostHandle(request, response, mav); + this.chain.triggerAfterCompletion(this.request, this.response, null); + + verify(this.interceptor1, this.interceptor2, this.interceptor3); + } + + @Test + public void earlyExit() throws Exception { + expect(this.interceptor1.preHandle(this.request, this.response, this.handler)).andReturn(true); + expect(this.interceptor2.preHandle(this.request, this.response, this.handler)).andReturn(false); + + this.interceptor1.afterCompletion(this.request, this.response, this.handler, null); + + replay(this.interceptor1, this.interceptor2, this.interceptor3); + + this.chain.applyPreHandle(request, response); + + verify(this.interceptor1, this.interceptor2, this.interceptor3); + } + + @Test + public void exceptionBeforePreHandle() throws Exception { + replay(this.interceptor1, this.interceptor2, this.interceptor3); + + this.chain.triggerAfterCompletion(this.request, this.response, null); + + verify(this.interceptor1, this.interceptor2, this.interceptor3); + } + + @Test + public void exceptionDuringPreHandle() throws Exception { + Exception ex = new Exception(""); + + expect(this.interceptor1.preHandle(this.request, this.response, this.handler)).andReturn(true); + expect(this.interceptor2.preHandle(this.request, this.response, this.handler)).andThrow(ex); + + this.interceptor1.afterCompletion(this.request, this.response, this.handler, ex); + + replay(this.interceptor1, this.interceptor2, this.interceptor3); + + try { + this.chain.applyPreHandle(request, response); + } + catch (Exception actual) { + assertSame(ex, actual); + } + this.chain.triggerAfterCompletion(this.request, this.response, ex); + + verify(this.interceptor1, this.interceptor2, this.interceptor3); + } + + @Test + public void exceptionAfterPreHandle() throws Exception { + Exception ex = new Exception(""); + + expect(this.interceptor1.preHandle(this.request, this.response, this.handler)).andReturn(true); + expect(this.interceptor2.preHandle(this.request, this.response, this.handler)).andReturn(true); + expect(this.interceptor3.preHandle(this.request, this.response, this.handler)).andReturn(true); + + this.interceptor3.afterCompletion(this.request, this.response, this.handler, ex); + this.interceptor2.afterCompletion(this.request, this.response, this.handler, ex); + this.interceptor1.afterCompletion(this.request, this.response, this.handler, ex); + + replay(this.interceptor1, this.interceptor2, this.interceptor3); + + this.chain.applyPreHandle(request, response); + this.chain.triggerAfterCompletion(this.request, this.response, ex); + + verify(this.interceptor1, this.interceptor2, this.interceptor3); + } + +} diff --git a/src/dist/changelog.txt b/src/dist/changelog.txt index 42257de507..fdd01c8ef3 100644 --- a/src/dist/changelog.txt +++ b/src/dist/changelog.txt @@ -11,6 +11,7 @@ Changes in version 3.2 M1 * fix issue with combining identical controller and method level request mapping paths * fix concurrency issue in AnnotationMethodHandlerExceptionResolver * fix case-sensitivity issue with some containers on access to 'Content-Disposition' header +* add Servlet 3.0 based async support Changes in version 3.1.1 (2012-02-16) -------------------------------------