Refactor Servlet 3 async support

As a result of the refactoring, the AsyncContext dispatch mechanism is
used much more centrally. Effectively every asynchronously processed
request involves one initial (container) thread, a second thread to
produce the handler return value asynchronously, and a third thread
as a result of a dispatch back to the container to resume processing
of the asynchronous resuilt.

Other updates include the addition of a MockAsyncContext and support
of related request method in the test packages of spring-web and
spring-webmvc. Also an upgrade of a Jetty test dependency required
to make tests pass.

Issue: SPR-9433
This commit is contained in:
Rossen Stoyanchev
2012-07-24 16:00:05 -04:00
parent 026ee846c7
commit 529e62921d
47 changed files with 1837 additions and 1741 deletions

View File

@@ -108,9 +108,6 @@ public class PathExtensionContentNegotiationStrategy extends AbstractMappingCont
@Override
protected void handleMatch(String extension, MediaType mediaType) {
if (logger.isDebugEnabled()) {
logger.debug("Requested media type is '" + mediaType + "' (based on file extension '" + extension + "')");
}
}
@Override

View File

@@ -28,6 +28,14 @@ import org.springframework.ui.ModelMap;
* Alternatively, a handler may also process the request completely, with no
* view to be rendered.
*
* <p>In an async processing scenario, the handler may be executed in a separate
* thread while the main thread exits without rendering or invoking the
* {@code postHandle} and {@code afterCompletion} callbacks. When concurrent
* handler execution completes, the request is dispatched back in order to
* proceed with rendering the model and all methods of this contract are invoked
* again. For further options and comments see
* {@code org.springframework.web.context.request.async.AsyncWebRequestInterceptor}
*
* <p>This interface is deliberately minimalistic to keep the dependencies of
* generic request interceptors as minimal as feasible.
*

View File

@@ -1,45 +0,0 @@
/*
* 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 used to form a chain of Callable instances.
* Instances of this class are typically registered via
* {@link AsyncExecutionChain#push(AbstractDelegatingCallable)} in which case
* there is no need to set the next Callable. Implementations can simply use
* {@link #getNext()} to delegate to the next Callable and assume it will be set.
*
* @author Rossen Stoyanchev
* @since 3.2
*
* @see AsyncExecutionChain
*/
public abstract class AbstractDelegatingCallable implements Callable<Object> {
private Callable<Object> next;
protected Callable<Object> getNext() {
return this.next;
}
public void setNext(Callable<Object> callable) {
this.next = callable;
}
}

View File

@@ -1,216 +0,0 @@
/*
* 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.ArrayDeque;
import java.util.Deque;
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.RequestAttributes;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.context.request.async.DeferredResult.DeferredResultHandler;
/**
* The central class for managing async request processing, mainly intended as
* an SPI and not typically used directly by application classes.
*
* <p>An async execution chain consists of a sequence of Callable instances that
* represent the work required to complete request processing in a separate thread.
* To construct the chain, each level of the call stack pushes an
* {@link AbstractDelegatingCallable} during the course of a normal request and
* pops (removes) it on the way out. If async processing has not started, the pop
* operation succeeds and the processing continues as normal, or otherwise if async
* processing has begun, the main processing thread must be exited.
*
* <p>For example the DispatcherServlet might contribute a Callable that completes
* view resolution or the HandlerAdapter might contribute a Callable that prepares a
* ModelAndView while the last Callable in the chain is usually associated with the
* application, e.g. the return value of 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 Deque<AbstractDelegatingCallable> callables = new ArrayDeque<AbstractDelegatingCallable>();
private Callable<Object> lastCallable;
private AsyncWebRequest asyncWebRequest;
private AsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor("MvcAsync");
/**
* Private constructor
* @see #getForCurrentRequest()
*/
private AsyncExecutionChain() {
}
/**
* Obtain the AsyncExecutionChain for the current request.
* Or if not found, create it 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;
}
/**
* Obtain the AsyncExecutionChain for the current request.
* Or if not found, create it and associate it with the request.
*/
public static AsyncExecutionChain getForCurrentRequest(WebRequest request) {
int scope = RequestAttributes.SCOPE_REQUEST;
AsyncExecutionChain chain = (AsyncExecutionChain) request.getAttribute(CALLABLE_CHAIN_ATTRIBUTE, scope);
if (chain == null) {
chain = new AsyncExecutionChain();
request.setAttribute(CALLABLE_CHAIN_ATTRIBUTE, chain, scope);
}
return chain;
}
/**
* Provide an instance of an AsyncWebRequest -- required for async processing.
*/
public void setAsyncWebRequest(AsyncWebRequest asyncRequest) {
Assert.state(!isAsyncStarted(), "Cannot set AsyncWebRequest after the start of async processing.");
this.asyncWebRequest = asyncRequest;
}
/**
* Provide an AsyncTaskExecutor for use with {@link #startCallableProcessing()}.
* <p>By default a {@link SimpleAsyncTaskExecutor} instance is used. Applications are
* advised to provide a TaskExecutor configured for production use.
* @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#setAsyncTaskExecutor
*/
public void setTaskExecutor(AsyncTaskExecutor taskExecutor) {
this.taskExecutor = taskExecutor;
}
/**
* Push an async Callable for the current stack level. This method should be
* invoked before delegating to the next level of the stack where async
* processing may start.
*/
public void push(AbstractDelegatingCallable callable) {
Assert.notNull(callable, "Async Callable is required");
this.callables.addFirst(callable);
}
/**
* Pop the Callable of the current stack level. Ensure this method is invoked
* after delegation to the next level of the stack where async processing may
* start. The pop operation succeeds if async processing did not start.
* @return {@code true} if the Callable was removed, or {@code false}
* otherwise (i.e. async started).
*/
public boolean pop() {
if (isAsyncStarted()) {
return false;
}
else {
this.callables.removeFirst();
return true;
}
}
/**
* Whether async request processing has started.
*/
public boolean isAsyncStarted() {
return ((this.asyncWebRequest != null) && this.asyncWebRequest.isAsyncStarted());
}
/**
* Set the last Callable, e.g. the one returned by the controller.
*/
public AsyncExecutionChain setLastCallable(Callable<Object> callable) {
Assert.notNull(callable, "Callable required");
this.lastCallable = callable;
return this;
}
/**
* Start async processing and execute the async chain with an AsyncTaskExecutor.
* This method returns immediately.
*/
public void startCallableProcessing() {
Assert.state(this.asyncWebRequest != null, "AsyncWebRequest was not set");
this.asyncWebRequest.startAsync();
this.taskExecutor.execute(new AsyncExecutionChainRunnable(this.asyncWebRequest, buildChain()));
}
private Callable<Object> buildChain() {
Assert.state(this.lastCallable != null, "The last Callable was not set");
AbstractDelegatingCallable head = new StaleAsyncRequestCheckingCallable(this.asyncWebRequest);
head.setNext(this.lastCallable);
for (AbstractDelegatingCallable callable : this.callables) {
callable.setNext(head);
head = callable;
}
return head;
}
/**
* Start async processing and initialize the given DeferredResult so when
* its value is set, the async chain is executed with an AsyncTaskExecutor.
*/
public void startDeferredResultProcessing(final DeferredResult<?> deferredResult) {
Assert.notNull(deferredResult, "DeferredResult is required");
Assert.state(this.asyncWebRequest != null, "AsyncWebRequest was not set");
this.asyncWebRequest.startAsync();
deferredResult.init(new DeferredResultHandler() {
public void handle(Object result) {
if (asyncWebRequest.isAsyncCompleted()) {
throw new StaleAsyncWebRequestException("Too late to set DeferredResult: " + result);
}
setLastCallable(new PassThroughCallable(result));
taskExecutor.execute(new AsyncExecutionChainRunnable(asyncWebRequest, buildChain()));
}
});
this.asyncWebRequest.setTimeoutHandler(deferredResult.getTimeoutHandler());
}
private static class PassThroughCallable implements Callable<Object> {
private final Object value;
public PassThroughCallable(Object value) {
this.value = value;
}
public Object call() throws Exception {
return this.value;
}
}
}

View File

@@ -1,79 +0,0 @@
/*
* 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#startCallableProcessing()
* @see AsyncExecutionChain#startDeferredResultProcessing(DeferredResult)
*/
public class AsyncExecutionChainRunnable implements Runnable {
private static final Log logger = LogFactory.getLog(AsyncExecutionChainRunnable.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");
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 {
this.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("Completing async request processing");
this.asyncWebRequest.complete();
}
}
}

View File

@@ -16,12 +16,11 @@
package org.springframework.web.context.request.async;
import org.springframework.http.HttpStatus;
import org.springframework.web.context.request.NativeWebRequest;
/**
* Extend {@link NativeWebRequest} with methods for async request processing.
* Extends {@link NativeWebRequest} with methods for asynchronous request processing.
*
* @author Rossen Stoyanchev
* @since 3.2
@@ -29,50 +28,51 @@ import org.springframework.web.context.request.NativeWebRequest;
public interface AsyncWebRequest extends NativeWebRequest {
/**
* Set the timeout for asynchronous request processing in milliseconds.
* In Servlet 3 async request processing, the timeout begins when the
* main processing thread has exited.
* Set the time required for concurrent handling to complete.
* @param timeout amount of time in milliseconds
*/
void setTimeout(Long timeout);
/**
* Invoked on a timeout to complete the response instead of the default
* behavior that sets the status to 503 (SERVICE_UNAVAILABLE).
* Provide a Runnable to invoke on timeout.
*/
void setTimeoutHandler(Runnable runnable);
/**
* Mark the start of async request processing for example ensuring the
* request remains open in order to be completed in a separate thread.
* @throws IllegalStateException if async processing has started, if it is
* not supported, or if it has completed.
* Provide a Runnable to invoke at the end of asynchronous request processing.
*/
void addCompletionHandler(Runnable runnable);
/**
* Mark the start of asynchronous request processing so that when the main
* processing thread exits, the response remains open for further processing
* in another thread.
* @throws IllegalStateException if async processing has completed or is not supported
*/
void startAsync();
/**
* Whether async processing is in progress and has not yet completed.
* Whether the request is in asynchronous mode after a call to {@link #startAsync()}.
* Returns "false" if asynchronous processing never started, has completed, or the
* request was dispatched for further processing.
*/
boolean isAsyncStarted();
/**
* Complete async request processing making a best effort but without any
* effect if async request processing has already completed for any reason
* including a timeout.
* Dispatch the request to the container in order to resume processing after
* concurrent execution in an application thread.
*/
void complete();
void dispatch();
/**
* Whether async processing has completed either normally via calls to
* {@link #complete()} or for other reasons such as a timeout likely
* detected in a separate thread during async request processing.
* Whether the request was dispatched to the container.
*/
boolean isAsyncCompleted();
boolean isDispatched();
/**
* Send an error to the client making a best effort to do so but without any
* effect if async request processing has already completed, for example due
* to a timeout.
* Whether asynchronous processing has completed in which case the request
* response should no longer be used.
*/
void sendError(HttpStatus status, String message);
boolean isAsyncComplete();
}

View File

@@ -13,61 +13,43 @@
* 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.springframework.web.context.request.WebRequest;
import org.springframework.web.context.request.WebRequestInterceptor;
/**
* Extends {@link WebRequestInterceptor} with lifecycle methods specific to async
* request processing.
* Extends the WebRequestInterceptor contract for scenarios where a handler may be
* executed asynchronously. Since the handler will complete execution in another
* thread, the results are not available in the current thread, and therefore the
* DispatcherServlet exits quickly and on its way out invokes
* {@link #afterConcurrentHandlingStarted(WebRequest)} instead of
* {@code postHandle} and {@code afterCompletion}.
* When the async handler execution completes, and the request is dispatched back
* for further processing, the DispatcherServlet will invoke {@code preHandle}
* again, as well as {@code postHandle} and {@code afterCompletion}.
*
* <p>This is the sequence of events on the main thread in an async scenario:
* <ol>
* <li>{@link #preHandle(WebRequest)}
* <li>{@link #getAsyncCallable(WebRequest)}
* <li>... <em>handler execution</em>
* <li>{@link #postHandleAsyncStarted(WebRequest)}
* </ol>
*
* <p>This is the sequence of events on the async thread:
* <ol>
* <li>Async {@link Callable#call()} (the {@code Callable} returned by {@code getAsyncCallable})
* <li>... <em>async handler execution</em>
* <li>{@link #postHandle(WebRequest, org.springframework.ui.ModelMap)}
* <li>{@link #afterCompletion(WebRequest, Exception)}
* </ol>
* <p>Existing implementations should consider the fact that {@code preHandle} may
* be invoked twice before {@code postHandle} and {@code afterCompletion} are
* called if they don't implement this contract. Once before the start of concurrent
* handling and a second time as part of an asynchronous dispatch after concurrent
* handling is done. This may be not important in most cases but when some work
* needs to be done after concurrent handling starts (e.g. clearing thread locals)
* then this contract can be implemented.
*
* @author Rossen Stoyanchev
* @since 3.2
*
* @see WebAsyncManager
*/
public interface AsyncWebRequestInterceptor extends WebRequestInterceptor {
public interface AsyncWebRequestInterceptor extends WebRequestInterceptor{
/**
* Invoked <em>after</em> {@link #preHandle(WebRequest)} and <em>before</em>
* the handler is executed. The returned {@link Callable} is used only if
* handler execution leads to teh start of async processing. It is invoked
* the async thread before the request is handled fro.
* <p>Implementations can use this <code>Callable</code> to initialize
* ThreadLocal attributes on the async thread.
* @return a {@link Callable} instance or <code>null</code>
* Called instead of {@code postHandle} and {@code afterCompletion}, when the
* the handler started handling the request concurrently.
*
* @param request the current request
*/
AbstractDelegatingCallable getAsyncCallable(WebRequest request);
/**
* Invoked <em>after</em> the execution of a handler if the handler started
* async processing instead of handling the request. Effectively this method
* is invoked on the way out of the main processing thread instead of
* {@link #postHandle(WebRequest, org.springframework.ui.ModelMap)}. The
* <code>postHandle</code> method is invoked after the request is handled
* in the async thread.
* <p>Implementations of this method can ensure ThreadLocal attributes bound
* to the main thread are cleared and also prepare for binding them to the
* async thread.
*/
void postHandleAsyncStarted(WebRequest request);
void afterConcurrentHandlingStarted(WebRequest request);
}

View File

@@ -0,0 +1,60 @@
/*
* 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.ServletRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.WebRequest;
/**
* Utility methods related to processing asynchronous web requests.
*
* @author Rossen Stoyanchev
* @since 3.2
*/
public abstract class AsyncWebUtils {
public static final String WEB_ASYNC_MANAGER_ATTRIBUTE = WebAsyncManager.class.getName() + ".WEB_ASYNC_MANAGER";
/**
* Obtain the {@link WebAsyncManager} for the current request, or if not
* found, create and associate it with the request.
*/
public static WebAsyncManager getAsyncManager(ServletRequest servletRequest) {
WebAsyncManager asyncManager = (WebAsyncManager) servletRequest.getAttribute(WEB_ASYNC_MANAGER_ATTRIBUTE);
if (asyncManager == null) {
asyncManager = new WebAsyncManager();
servletRequest.setAttribute(WEB_ASYNC_MANAGER_ATTRIBUTE, asyncManager);
}
return asyncManager;
}
/**
* Obtain the {@link WebAsyncManager} for the current request, or if not
* found, create and associate it with the request.
*/
public static WebAsyncManager getAsyncManager(WebRequest webRequest) {
int scope = RequestAttributes.SCOPE_REQUEST;
WebAsyncManager asyncManager = (WebAsyncManager) webRequest.getAttribute(WEB_ASYNC_MANAGER_ATTRIBUTE, scope);
if (asyncManager == null) {
asyncManager = new WebAsyncManager();
webRequest.setAttribute(WEB_ASYNC_MANAGER_ATTRIBUTE, asyncManager, scope);
}
return asyncManager;
}
}

View File

@@ -20,6 +20,8 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.util.Assert;
@@ -49,6 +51,8 @@ import org.springframework.util.Assert;
*/
public final class DeferredResult<V> {
private static final Log logger = LogFactory.getLog(DeferredResult.class);
private V result;
private DeferredResultHandler resultHandler;
@@ -123,19 +127,28 @@ public final class DeferredResult<V> {
Assert.isNull(this.result, "A deferred result can be set once only");
this.result = result;
this.timeoutValueUsed = (this.timeoutValueSet && (this.result == this.timeoutValue));
try {
this.initializationLatch.await(10, TimeUnit.SECONDS);
}
catch (InterruptedException e) {
if (!await()) {
throw new IllegalStateException(
"Gave up on waiting for DeferredResult to be initialized. " +
"Are you perhaps creating and setting a DeferredResult in the same thread? " +
"The DeferredResult must be fully initialized before you can set it. " +
"See the class javadoc for more details");
}
if (this.timeoutValueUsed) {
logger.debug("Using default timeout value");
}
this.resultHandler.handle(result);
}
private boolean await() {
try {
return this.initializationLatch.await(10, TimeUnit.SECONDS);
}
catch (InterruptedException e) {
return false;
}
}
/**
* Return a handler to use to complete processing using the default timeout value
* provided via {@link #DeferredResult(Object)} or {@code null} if no timeout

View File

@@ -19,47 +19,51 @@ 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}.
* An implementation of {@link AsyncWebRequest} to use when no underlying support is available.
* The methods {@link #startAsync()} and {@link #dispatch()} raise {@link UnsupportedOperationException}.
*
* @author Rossen Stoyanchev
* @since 3.2
*/
public class NoOpAsyncWebRequest extends ServletWebRequest implements AsyncWebRequest {
public class NoSupportAsyncWebRequest extends ServletWebRequest implements AsyncWebRequest {
public NoOpAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) {
public NoSupportAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) {
super(request, response);
}
public void addCompletionHandler(Runnable runnable) {
// ignored
}
public void setTimeout(Long timeout) {
// ignored
}
public void setTimeoutHandler(Runnable runnable) {
// ignored
}
public boolean isAsyncStarted() {
return false;
}
public boolean isAsyncCompleted() {
public boolean isDispatched() {
return false;
}
public boolean isAsyncComplete() {
throw new UnsupportedOperationException("No async support in a pre-Servlet 3.0 runtime");
}
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");
public void dispatch() {
throw new UnsupportedOperationException("No async support in a pre-Servlet 3.0 runtime");
}
}

View File

@@ -1,50 +0,0 @@
/*
* 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 = getNext().call();
if (this.asyncWebRequest.isAsyncCompleted()) {
throw new StaleAsyncWebRequestException(
"Async request no longer available due to a timeout or a (client) error");
}
return result;
}
}

View File

@@ -21,9 +21,6 @@ package org.springframework.web.context.request.async;
*
* @author Rossen Stoyanchev
* @since 3.2
*
* @see DeferredResult#set(Object)
* @see AsyncExecutionChainRunnable#run()
*/
@SuppressWarnings("serial")
public class StaleAsyncWebRequestException extends RuntimeException {

View File

@@ -17,11 +17,14 @@
package org.springframework.web.context.request.async;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.DispatcherType;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -48,27 +51,49 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
private AtomicBoolean asyncCompleted = new AtomicBoolean(false);
private Runnable timeoutHandler;
private Runnable timeoutHandler = new DefaultTimeoutHandler();
private final List<Runnable> completionHandlers = new ArrayList<Runnable>();
public StandardServletAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) {
super(request, response);
}
/**
* {@inheritDoc}
* <p>The timeout period begins when the main processing thread has exited.
*/
public void setTimeout(Long timeout) {
this.timeout = timeout;
}
public void setTimeoutHandler(Runnable timeoutHandler) {
if (timeoutHandler != null) {
this.timeoutHandler = timeoutHandler;
}
}
public void addCompletionHandler(Runnable runnable) {
this.completionHandlers.add(runnable);
}
public boolean isAsyncStarted() {
return ((this.asyncContext != null) && getRequest().isAsyncStarted());
}
public boolean isAsyncCompleted() {
public boolean isDispatched() {
return (DispatcherType.ASYNC.equals(getRequest().getDispatcherType()));
}
/**
* Whether async request processing has completed.
* <p>It is important to avoid use of request and response objects after async
* processing has completed. Servlet containers often re-use them.
*/
public boolean isAsyncComplete() {
return this.asyncCompleted.get();
}
public void setTimeoutHandler(Runnable timeoutHandler) {
this.timeoutHandler = timeoutHandler;
}
public void startAsync() {
Assert.state(getRequest().isAsyncSupported(),
@@ -76,8 +101,10 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
"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.");
Assert.state(!isAsyncStarted(), "Async processing already started");
Assert.state(!isAsyncCompleted(), "Cannot use async request that has completed");
Assert.state(!isAsyncComplete(), "Async processing has already completed");
if (isAsyncStarted()) {
return;
}
this.asyncContext = getRequest().startAsync(getRequest(), getResponse());
this.asyncContext.addListener(this);
if (this.timeout != null) {
@@ -85,52 +112,44 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
}
}
public void complete() {
if (!isAsyncCompleted()) {
this.asyncContext.complete();
completeInternal();
}
}
private void completeInternal() {
this.asyncContext = null;
this.asyncCompleted.set(true);
}
public void sendError(HttpStatus status, String message) {
try {
if (!isAsyncCompleted()) {
getResponse().sendError(500, message);
}
}
catch (IOException ioEx) {
// absorb
}
public void dispatch() {
Assert.notNull(this.asyncContext, "Cannot dispatch without an AsyncContext");
this.asyncContext.dispatch();
}
// ---------------------------------------------------------------------
// Implementation of AsyncListener methods
// ---------------------------------------------------------------------
public void onTimeout(AsyncEvent event) throws IOException {
if (this.timeoutHandler == null) {
getResponse().sendError(HttpStatus.SERVICE_UNAVAILABLE.value());
}
else {
this.timeoutHandler.run();
}
completeInternal();
}
public void onError(AsyncEvent event) throws IOException {
completeInternal();
}
public void onStartAsync(AsyncEvent event) throws IOException {
}
public void onError(AsyncEvent event) throws IOException {
}
public void onTimeout(AsyncEvent event) throws IOException {
this.timeoutHandler.run();
}
public void onComplete(AsyncEvent event) throws IOException {
completeInternal();
for (Runnable runnable : this.completionHandlers) {
runnable.run();
}
this.asyncContext = null;
this.asyncCompleted.set(true);
}
private class DefaultTimeoutHandler implements Runnable {
public void run() {
try {
getResponse().sendError(HttpStatus.SERVICE_UNAVAILABLE.value());
}
catch (IOException ex) {
// ignore
}
}
}
}

View File

@@ -0,0 +1,319 @@
/*
* 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.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.util.Assert;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.async.DeferredResult.DeferredResultHandler;
import org.springframework.web.util.UrlPathHelper;
/**
* The central class for managing asynchronous request processing, mainly intended
* as an SPI and not typically used directly by application classes.
*
* <p>An async scenario starts with request processing as usual in a thread (T1).
* When a handler decides to handle the request concurrently, it calls
* {@linkplain #startCallableProcessing(Callable, Object...) startCallableProcessing} or
* {@linkplain #startDeferredResultProcessing(DeferredResult, Object...) startDeferredResultProcessing}
* both of which will process in a separate thread (T2).
* After the start of concurrent handling {@link #isConcurrentHandlingStarted()}
* returns "true" and this can be used by classes involved in processing on the
* main thread (T1) quickly and with very minimal processing.
*
* <p>When the concurrent handling completes in a separate thread (T2), both
* {@code startCallableProcessing} and {@code startDeferredResultProcessing}
* save the results and dispatched to the container, essentially to the
* same request URI as the one that started concurrent handling. This allows for
* further processing of the concurrent results. Classes in the dispatched
* thread (T3), can access the results via {@link #getConcurrentResult()} or
* detect their presence via {@link #hasConcurrentResult()}. Also in the
* dispatched thread {@link #isConcurrentHandlingStarted()} will return "false"
* unless concurrent handling is started once again.
*
* TODO .. mention Servlet 3 configuration
*
* @author Rossen Stoyanchev
* @since 3.2
*
* @see org.springframework.web.context.request.async.AsyncWebRequestInterceptor
* @see org.springframework.web.servlet.AsyncHandlerInterceptor
* @see org.springframework.web.filter.OncePerRequestFilter#shouldFilterAsyncDispatches
* @see org.springframework.web.filter.OncePerRequestFilter#isAsyncDispatch
* @see org.springframework.web.filter.OncePerRequestFilter#isLastRequestThread
*/
public final class WebAsyncManager {
private static final Object RESULT_NONE = new Object();
private static final Log logger = LogFactory.getLog(WebAsyncManager.class);
private AsyncWebRequest asyncWebRequest;
private AsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(this.getClass().getSimpleName());
private final Map<Object, AsyncThreadInitializer> threadInitializers = new LinkedHashMap<Object, AsyncThreadInitializer>();
private Object concurrentResult = RESULT_NONE;
private Object[] concurrentResultContext;
private static final UrlPathHelper urlPathHelper = new UrlPathHelper();
/**
* Package private constructor
* @see AsyncWebUtils
*/
WebAsyncManager() {
}
/**
* Configure an AsyncTaskExecutor for use with {@link #startCallableProcessing(Callable)}.
* <p>By default a {@link SimpleAsyncTaskExecutor} instance is used. Applications
* are advised to provide a TaskExecutor configured for production use.
* @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#setAsyncTaskExecutor
*/
public void setTaskExecutor(AsyncTaskExecutor taskExecutor) {
this.taskExecutor = taskExecutor;
}
/**
* Provide an {@link AsyncWebRequest} to use to start and to dispatch request.
* This property must be set before the start of concurrent handling.
* @param asyncWebRequest the request to use
*/
public void setAsyncWebRequest(final AsyncWebRequest asyncWebRequest) {
Assert.notNull(asyncWebRequest, "Expected AsyncWebRequest");
Assert.state(!isConcurrentHandlingStarted(), "Can't set AsyncWebRequest with concurrent handling in progress");
this.asyncWebRequest = asyncWebRequest;
this.asyncWebRequest.addCompletionHandler(new Runnable() {
public void run() {
asyncWebRequest.removeAttribute(AsyncWebUtils.WEB_ASYNC_MANAGER_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
}
});
}
/**
* Whether the handler for the current request is executed concurrently.
* Once concurrent handling is done, the result is saved, and the request
* dispatched again to resume processing where the result of concurrent
* handling is available via {@link #getConcurrentResult()}.
*/
public boolean isConcurrentHandlingStarted() {
return ((this.asyncWebRequest != null) && (this.asyncWebRequest.isAsyncStarted()));
}
/**
* Whether the current thread was dispatched to continue processing the result
* of concurrent handler execution.
*/
public boolean hasConcurrentResult() {
// TODO:
// Add check for asyncWebRequest.isDispatched() once Apache id=53632 is fixed.
// That ensure "true" is returned in the dispatched thread only.
return this.concurrentResult != RESULT_NONE;
}
/**
* Return the result of concurrent handler execution. This may be an Object
* value on successful return or an {@code Exception} or {@code Throwable}.
*/
public Object getConcurrentResult() {
return this.concurrentResult;
}
/**
* Return the processing context saved at the start of concurrent handling.
*/
public Object[] getConcurrentResultContext() {
return this.concurrentResultContext;
}
/**
* Reset the {@linkplain #getConcurrentResult() concurrentResult} and the
* {@linkplain #getConcurrentResultContext() concurrentResultContext}.
*/
public void resetConcurrentResult() {
this.concurrentResult = RESULT_NONE;
this.concurrentResultContext = null;
}
/**
* Register an {@link AsyncThreadInitializer} with the WebAsyncManager instance
* for the current request. It may later be accessed and applied via
* {@link #applyAsyncThreadInitializer(String)} and will also be used to
* initialize and reset threads for concurrent handler execution.
* @param key a unique the key under which to keep the initializer
* @param initializer the initializer instance
*/
public void registerAsyncThreadInitializer(Object key, AsyncThreadInitializer initializer) {
Assert.notNull(initializer, "An AsyncThreadInitializer instance is required");
this.threadInitializers.put(key, initializer);
}
/**
* Invoke the {@linkplain AsyncThreadInitializer#initialize() initialize()}
* method of the named {@link AsyncThreadInitializer}.
* @param key the key under which the initializer was registered
* @return whether an initializer was found and applied
*/
public boolean applyAsyncThreadInitializer(Object key) {
AsyncThreadInitializer initializer = this.threadInitializers.get(key);
if (initializer != null) {
initializer.initialize();
return true;
}
return false;
}
/**
* Submit a request handling task for concurrent execution. Returns immediately
* and subsequent calls to {@link #isConcurrentHandlingStarted()} return "true".
* <p>When concurrent handling is done, the resulting value, which may be an
* Object or a raised {@code Exception} or {@code Throwable}, is saved and the
* request is dispatched for further processing of that result. In the dispatched
* thread, the result can be accessed via {@link #getConcurrentResult()} while
* {@link #hasConcurrentResult()} returns "true" and
* {@link #isConcurrentHandlingStarted()} is back to returning "false".
*
* @param callable a unit of work to be executed asynchronously
* @param processingContext additional context to save for later access via
* {@link #getConcurrentResultContext()}
*/
public void startCallableProcessing(final Callable<?> callable, Object... processingContext) {
Assert.notNull(callable, "Callable is required");
startAsyncProcessing(processingContext);
this.taskExecutor.submit(new Runnable() {
public void run() {
List<AsyncThreadInitializer> initializers =
new ArrayList<AsyncThreadInitializer>(threadInitializers.values());
try {
for (AsyncThreadInitializer initializer : initializers) {
initializer.initialize();
}
concurrentResult = callable.call();
}
catch (Throwable t) {
concurrentResult = t;
}
finally {
Collections.reverse(initializers);
for (AsyncThreadInitializer initializer : initializers) {
initializer.reset();
}
}
if (logger.isDebugEnabled()) {
logger.debug("Concurrent result value [" + concurrentResult + "]");
}
if (asyncWebRequest.isAsyncComplete()) {
logger.error("Could not complete processing due to a timeout or network error");
return;
}
logger.debug("Dispatching request to continue processing");
asyncWebRequest.dispatch();
}
});
}
/**
* Initialize the given given {@link DeferredResult} so that whenever the
* DeferredResult is set, the resulting value, which may be an Object or a
* raised {@code Exception} or {@code Throwable}, is saved and the request
* is dispatched for further processing of the result. In the dispatch
* thread, the result value can be accessed via {@link #getConcurrentResult()}.
* <p>The method returns immediately and it's up to the caller to set the
* DeferredResult. Subsequent calls to {@link #isConcurrentHandlingStarted()}
* return "true" until after the dispatch when {@link #hasConcurrentResult()}
* returns "true" and {@link #isConcurrentHandlingStarted()} is back to "false".
*
* @param deferredResult the DeferredResult instance to initialize
* @param processingContext additional context to save for later access via
* {@link #getConcurrentResultContext()}
*/
public void startDeferredResultProcessing(final DeferredResult<?> deferredResult, Object... processingContext) {
Assert.notNull(deferredResult, "DeferredResult is required");
startAsyncProcessing(processingContext);
deferredResult.init(new DeferredResultHandler() {
public void handle(Object result) {
concurrentResult = result;
if (logger.isDebugEnabled()) {
logger.debug("Deferred result value [" + concurrentResult + "]");
}
if (asyncWebRequest.isAsyncComplete()) {
throw new StaleAsyncWebRequestException("Could not complete processing due to a timeout or network error");
}
logger.debug("Dispatching request to complete processing");
asyncWebRequest.dispatch();
}
});
this.asyncWebRequest.setTimeoutHandler(deferredResult.getTimeoutHandler());
}
private void startAsyncProcessing(Object... context) {
Assert.state(this.asyncWebRequest != null, "AsyncWebRequest was not set");
this.asyncWebRequest.startAsync();
this.concurrentResult = null;
this.concurrentResultContext = context;
if (logger.isDebugEnabled()) {
HttpServletRequest request = asyncWebRequest.getNativeRequest(HttpServletRequest.class);
String requestUri = urlPathHelper.getRequestUri(request);
logger.debug("Concurrent handling starting for " + request.getMethod() + " [" + requestUri + "]");
}
}
/**
* A contract for initializing and resetting a thread.
*/
public interface AsyncThreadInitializer {
void initialize();
void reset();
}
}

View File

@@ -32,8 +32,6 @@ 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;
/**
@@ -179,6 +177,16 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter
this.afterMessageSuffix = afterMessageSuffix;
}
/**
* The default value is "true" so that the filter may log a "before" message
* at the start of request processing and an "after" message at the end from
* when the last asynchronously dispatched thread is exiting.
*/
@Override
protected boolean shouldFilterAsyncDispatches() {
return true;
}
/**
* Forwards the request to the next filter in the chain and delegates down to the subclasses to perform the actual
* request logging both before and after the request is processed.
@@ -190,22 +198,28 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
boolean isAsyncDispatch = isAsyncDispatch(request);
if (isIncludePayload()) {
request = new RequestCachingRequestWrapper(request);
if (isAsyncDispatch) {
request = WebUtils.getNativeRequest(request, RequestCachingRequestWrapper.class);
Assert.notNull(request, "Expected wrapped request");
}
else {
request = new RequestCachingRequestWrapper(request);
}
}
beforeRequest(request, getBeforeMessage(request));
AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(request);
chain.push(getAsyncCallable(request));
if (!isAsyncDispatch) {
beforeRequest(request, getBeforeMessage(request));
}
try {
filterChain.doFilter(request, response);
}
finally {
if (!chain.pop()) {
return;
if (isLastRequestThread(request)) {
afterRequest(request, getAfterMessage(request));
}
afterRequest(request, getAfterMessage(request));
}
}
@@ -290,19 +304,6 @@ 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 {
getNext().call();
afterRequest(request, getAfterMessage(request));
return null;
}
};
}
private static class RequestCachingRequestWrapper extends HttpServletRequestWrapper {

View File

@@ -25,14 +25,19 @@ 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;
import org.springframework.web.context.request.async.AsyncWebUtils;
/**
* Filter base class that guarantees to be just executed once per request,
* on any servlet container. It provides a {@link #doFilterInternal}
* method with HttpServletRequest and HttpServletResponse arguments.
*
* <p>In an async scenario a filter may be invoked again in additional threads
* as part of an {@linkplain javax.servlet.DispatcherType.ASYNC ASYNC} dispatch.
* Sub-classes may decide whether to be invoked once per request or once per
* request thread for as long as the same request is being processed.
* See {@link #shouldFilterAsyncDispatches()}.
*
* <p>The {@link #getAlreadyFilteredAttributeName} method determines how
* to identify that a request is already filtered. The default implementation
* is based on the configured name of the concrete filter instance.
@@ -68,26 +73,27 @@ public abstract class OncePerRequestFilter extends GenericFilterBean {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
boolean processAsyncRequestThread = isAsyncDispatch(httpRequest) && shouldFilterAsyncDispatches();
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
if (request.getAttribute(alreadyFilteredAttributeName) != null || shouldNotFilter(httpRequest)) {
boolean hasAlreadyFilteredAttribute = request.getAttribute(alreadyFilteredAttributeName) != null;
if ((hasAlreadyFilteredAttribute && (!processAsyncRequestThread)) || shouldNotFilter(httpRequest)) {
// Proceed without invoking this filter...
filterChain.doFilter(request, response);
}
else {
AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(request);
chain.push(getAsyncCallable(request, alreadyFilteredAttributeName));
// Do invoke this filter...
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
try {
doFilterInternal(httpRequest, httpResponse, filterChain);
}
finally {
if (!chain.pop()) {
return;
if (isLastRequestThread(httpRequest)) {
// Remove the "already filtered" request attribute for this request.
request.removeAttribute(alreadyFilteredAttributeName);
}
// Remove the "already filtered" request attribute for this request.
request.removeAttribute(alreadyFilteredAttributeName);
}
}
}
@@ -122,25 +128,62 @@ public abstract class OncePerRequestFilter extends GenericFilterBean {
}
/**
* Create a Callable to use to complete processing in an async execution chain.
* Whether to filter once per request or once per request thread. The dispatcher
* type {@code javax.servlet.DispatcherType.ASYNC} introduced in Servlet 3.0
* means a filter can be invoked in more than one thread (and exited) over the
* course of a single request. Some filters only need to filter the initial
* thread (e.g. request wrapping) while others may need to be invoked at least
* once in each additional thread for example for setting up thread locals or
* to perform final processing at the very end.
* <p>Note that although a filter can be mapped to handle specific dispatcher
* types via {@code web.xml} or in Java through the {@code ServletContext},
* servlet containers may enforce different defaults with regards to dispatcher
* types. This flag enforces the design intent of the filter.
* <p>The default setting is "false", which means the filter will be invoked
* once only per request and only on the initial request thread. If "true", the
* filter will also be invoked once only on each additional thread.
*
* @see org.springframework.web.context.request.async.WebAsyncManager
*/
private AbstractDelegatingCallable getAsyncCallable(final ServletRequest request,
final String alreadyFilteredAttributeName) {
protected boolean shouldFilterAsyncDispatches() {
return false;
}
return new AbstractDelegatingCallable() {
public Object call() throws Exception {
getNext().call();
request.removeAttribute(alreadyFilteredAttributeName);
return null;
}
};
/**
* Whether the request was dispatched to complete processing of results produced
* in another thread. This aligns with the Servlet 3.0 dispatcher type
* {@code javax.servlet.DispatcherType.ASYNC} and can be used by filters that
* return "true" from {@link #shouldFilterAsyncDispatches()} to detect when
* the filter is being invoked subsequently in additional thread(s).
*
* @see org.springframework.web.context.request.async.WebAsyncManager
*/
protected final boolean isAsyncDispatch(HttpServletRequest request) {
return AsyncWebUtils.getAsyncManager(request).hasConcurrentResult();
}
/**
* Whether this is the last thread processing the request. Note the returned
* value may change from {@code true} to {@code false} if the method is
* invoked before and after delegating to the next filter, since the next filter
* or servlet may begin concurrent processing. Therefore this method is most
* useful after delegation for final, end-of-request type processing.
* @param request the current request
* @return {@code true} if the response will be committed when the current
* thread exits; {@code false} if the response will remain open.
*
* @see org.springframework.web.context.request.async.WebAsyncManager
*/
protected final boolean isLastRequestThread(HttpServletRequest request) {
return (!AsyncWebUtils.getAsyncManager(request).isConcurrentHandlingStarted());
}
/**
* Same contract as for <code>doFilter</code>, but guaranteed to be
* just invoked once per request. Provides HttpServletRequest and
* HttpServletResponse arguments instead of the default ServletRequest
* and ServletResponse ones.
* just invoked once per request or once per request thread.
* See {@link #shouldFilterAsyncDispatches()} for details.
* <p>Provides HttpServletRequest and HttpServletResponse arguments instead of the
* default ServletRequest and ServletResponse ones.
*/
protected abstract void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)

View File

@@ -26,8 +26,6 @@ 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,
@@ -72,6 +70,15 @@ public class RequestContextFilter extends OncePerRequestFilter {
}
/**
* The default value is "true" in which case the filter will set up the request
* context in each asynchronously dispatched thread.
*/
@Override
protected boolean shouldFilterAsyncDispatches() {
return true;
}
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
@@ -80,21 +87,15 @@ public class RequestContextFilter extends OncePerRequestFilter {
ServletRequestAttributes attributes = new ServletRequestAttributes(request);
initContextHolders(request, attributes);
AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(request);
chain.push(getChainedCallable(request, attributes));
try {
filterChain.doFilter(request, response);
}
finally {
resetContextHolders();
if (!chain.pop()) {
return;
}
attributes.requestCompleted();
if (logger.isDebugEnabled()) {
logger.debug("Cleared thread-bound request context: " + request);
}
attributes.requestCompleted();
}
}
@@ -111,28 +112,4 @@ public class RequestContextFilter extends OncePerRequestFilter {
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 {
getNext().call();
}
finally {
resetContextHolders();
requestAttributes.requestCompleted();
if (logger.isDebugEnabled()) {
logger.debug("Cleared thread-bound request context: " + request);
}
}
return null;
}
};
}
}

View File

@@ -29,10 +29,9 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import org.springframework.util.Assert;
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;
/**
@@ -54,37 +53,34 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter {
private static String HEADER_IF_NONE_MATCH = "If-None-Match";
/**
* The default value is "true" so that the filter may delay the generation of
* an ETag until the last asynchronously dispatched thread.
*/
@Override
protected boolean shouldFilterAsyncDispatches() {
return true;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
ShallowEtagResponseWrapper responseWrapper = new ShallowEtagResponseWrapper(response);
ShallowEtagResponseWrapper responseWrapper;
AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(request);
chain.push(getAsyncCallable(request, response, responseWrapper));
if (isAsyncDispatch(request)) {
responseWrapper = WebUtils.getNativeResponse(response, ShallowEtagResponseWrapper.class);
Assert.notNull(responseWrapper, "Expected wrapped response");
}
else {
responseWrapper = new ShallowEtagResponseWrapper(response);
}
filterChain.doFilter(request, responseWrapper);
if (!chain.pop()) {
return;
if (isLastRequestThread(request)) {
updateResponse(request, response, responseWrapper);
}
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 {
getNext().call();
updateResponse(request, response, responseWrapper);
return null;
}
};
}
private void updateResponse(HttpServletRequest request, HttpServletResponse response,