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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user