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:
+ *
+ * Application method (e.g. controller method) returns a DeferredResult instance
+ * The framework completes initialization of the returned DeferredResult in the same thread
+ * The application calls {@link DeferredResult#set(Object)} from another thread
+ * 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)
-------------------------------------