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:
parent
fdded0768e
commit
3642b0f365
|
@ -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<?>} or
|
||||
* <li>An {@link org.springframework.http.HttpEntity HttpEntity<?>} or
|
||||
* {@link org.springframework.http.ResponseEntity ResponseEntity<?>} object
|
||||
* (Servlet-only) to access to the Servlet response HTTP headers and contents.
|
||||
* The entity body will be converted to the response stream using
|
||||
* {@linkplain org.springframework.http.converter.HttpMessageConverter message
|
||||
* converters}.
|
||||
* <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}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
-------------------------------------
|
||||
|
|
Loading…
Reference in New Issue