Initial cut of Servlet 3.0 based async support

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

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

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

From an SPI perspective, there are several new types:

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

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

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

View File

@ -21,6 +21,7 @@ import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.Callable;
/**
* Annotation for mapping web requests onto specific handler classes and/or
@ -180,12 +181,18 @@ import java.lang.annotation.Target;
* be converted to the response stream using
* {@linkplain org.springframework.http.converter.HttpMessageConverter message
* converters}.
* <li>A {@link org.springframework.http.HttpEntity HttpEntity&lt;?&gt;} or
* <li>An {@link org.springframework.http.HttpEntity HttpEntity&lt;?&gt;} or
* {@link org.springframework.http.ResponseEntity ResponseEntity&lt;?&gt;} object
* (Servlet-only) to access to the Servlet response HTTP headers and contents.
* The entity body will be converted to the response stream using
* {@linkplain org.springframework.http.converter.HttpMessageConverter message
* converters}.
* <li>A {@link Callable} which is used by Spring MVC to obtain the return
* value asynchronously in a separate thread transparently managed by Spring MVC
* on behalf of the application.
* <li>A {@code org.springframework.web.context.request.async.DeferredResult}
* which the application uses to produce a return value in a separate
* thread of its own choosing, as an alternative to returning a Callable.
* <li><code>void</code> if the method handles the response itself (by
* writing the response content directly, declaring an argument of type
* {@link javax.servlet.ServletResponse} / {@link javax.servlet.http.HttpServletResponse}

View File

@ -0,0 +1,54 @@
/*
* Copyright 2002-2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.context.request.async;
import java.util.concurrent.Callable;
/**
* A base class for a Callable that can be used in a chain of Callable instances.
*
* <p>Typical use for async request processing scenarios involves:
* <ul>
* <li>Create an instance of this type and register it via
* {@link AsyncExecutionChain#addDelegatingCallable(AbstractDelegatingCallable)}
* (internally the nodes of the chain will be linked so no need to set up "next").
* <li>Provide an implementation of {@link Callable#call()} that contains the
* logic needed to complete request processing outside the main processing thread.
* <li>In the implementation, delegate to the next Callable to obtain
* its result, e.g. ModelAndView, and then do some post-processing, e.g. view
* resolution. In some cases both pre- and post-processing might be
* appropriate, e.g. setting up {@link ThreadLocal} storage.
* </ul>
*
* @author Rossen Stoyanchev
* @since 3.2
*
* @see AsyncExecutionChain
*/
public abstract class AbstractDelegatingCallable implements Callable<Object> {
private Callable<Object> next;
public void setNextCallable(Callable<Object> nextCallable) {
this.next = nextCallable;
}
protected Callable<Object> getNextCallable() {
return this.next;
}
}

View File

@ -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.
*
* <p>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<AbstractDelegatingCallable> delegatingCallables = new ArrayList<AbstractDelegatingCallable>();
private Callable<Object> 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.
* <p>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:
* <ul>
* <li>{@link #startCallableChainProcessing()}
* <li>{@link #startDeferredResultProcessing(DeferredResult)}
* </ul>
*/
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<Object> 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<Object> buildChain() {
Assert.state(this.callable != null, "The callable field is required to complete the chain");
this.delegatingCallables.add(new StaleAsyncRequestCheckingCallable(asyncWebRequest));
Callable<Object> 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}.
* <p>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<Object> getSimpleCallable(final Object value) {
return new Callable<Object>() {
public Object call() throws Exception {
return value;
}
};
}
}

View File

@ -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.
* <p>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();
}
}
}

View File

@ -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();
}

View File

@ -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.
*
* <p>The following sequence describes typical use of a DeferredResult:
* <ol>
* <li>Application method (e.g. controller method) returns a DeferredResult instance
* <li>The framework completes initialization of the returned DeferredResult in the same thread
* <li>The application calls {@link DeferredResult#set(Object)} from another thread
* <li>The framework completes request processing in the thread in which it is invoked
* </ol>
*
* <p><strong>Note:</strong> {@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:
*
* <pre>
* DeferredResult value = new DeferredResult();
* value.set(1); // blocks
* </pre>
*
* @author Rossen Stoyanchev
* @since 3.2
*/
public final class DeferredResult {
private final AtomicReference<Object> value = new AtomicReference<Object>();
private final BlockingQueue<DeferredResultHandler> handlers = new ArrayBlockingQueue<DeferredResultHandler>(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;
}
}

View File

@ -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");
}
}

View File

@ -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.
*
* <p>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;
}
}

View File

@ -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);
}
}

View File

@ -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}.
*
* <p>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 <async-support>true</async-support>} 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 \"<async-supported>true</async-supported>\" 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);
}
}

View File

@ -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();

View File

@ -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 <code>doFilter</code>, but guaranteed to be

View File

@ -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;
}
};
}
}

View File

@ -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);

View File

@ -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
*/

View File

@ -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<Object>() {
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<Object>() {
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);
}
}
}

View File

@ -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<Object>() {
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);
}
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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);
}
}

View File

@ -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)
-------------------------------------