Browse Source
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-9433pull/114/head
47 changed files with 1825 additions and 1729 deletions
@ -1,45 +0,0 @@
@@ -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 @@
@@ -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 @@
@@ -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(); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,60 @@
@@ -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; |
||||
} |
||||
|
||||
} |
||||
@ -1,50 +0,0 @@
@@ -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; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,319 @@
@@ -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(); |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,133 @@
@@ -0,0 +1,133 @@
|
||||
/* |
||||
* 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.mock.web; |
||||
|
||||
import java.io.IOException; |
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
import javax.servlet.AsyncContext; |
||||
import javax.servlet.AsyncEvent; |
||||
import javax.servlet.AsyncListener; |
||||
import javax.servlet.DispatcherType; |
||||
import javax.servlet.ServletContext; |
||||
import javax.servlet.ServletException; |
||||
import javax.servlet.ServletRequest; |
||||
import javax.servlet.ServletResponse; |
||||
|
||||
import org.springframework.beans.BeanUtils; |
||||
import org.springframework.web.util.WebUtils; |
||||
|
||||
/** |
||||
* Mock implementation of the {@link AsyncContext} interface. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 3.2 |
||||
*/ |
||||
public class MockAsyncContext implements AsyncContext { |
||||
|
||||
private final ServletRequest request; |
||||
|
||||
private final ServletResponse response; |
||||
|
||||
private final MockHttpServletRequest mockRequest; |
||||
|
||||
private final List<AsyncListener> listeners = new ArrayList<AsyncListener>(); |
||||
|
||||
private String dispatchPath; |
||||
|
||||
private long timeout = 10 * 60 * 1000L; |
||||
|
||||
public MockAsyncContext(ServletRequest request, ServletResponse response) { |
||||
this.request = request; |
||||
this.response = response; |
||||
this.mockRequest = WebUtils.getNativeRequest(request, MockHttpServletRequest.class); |
||||
} |
||||
|
||||
public ServletRequest getRequest() { |
||||
return this.request; |
||||
} |
||||
|
||||
public ServletResponse getResponse() { |
||||
return this.response; |
||||
} |
||||
|
||||
public boolean hasOriginalRequestAndResponse() { |
||||
return false; |
||||
} |
||||
|
||||
public String getDispatchPath() { |
||||
return this.dispatchPath; |
||||
} |
||||
|
||||
public void dispatch() { |
||||
dispatch(null); |
||||
} |
||||
|
||||
public void dispatch(String path) { |
||||
dispatch(null, path); |
||||
} |
||||
|
||||
public void dispatch(ServletContext context, String path) { |
||||
this.dispatchPath = path; |
||||
if (this.mockRequest != null) { |
||||
this.mockRequest.setDispatcherType(DispatcherType.ASYNC); |
||||
this.mockRequest.setAsyncStarted(false); |
||||
} |
||||
} |
||||
|
||||
public void complete() { |
||||
if (this.mockRequest != null) { |
||||
this.mockRequest.setAsyncStarted(false); |
||||
} |
||||
for (AsyncListener listener : this.listeners) { |
||||
try { |
||||
listener.onComplete(new AsyncEvent(this, this.request, this.response)); |
||||
} |
||||
catch (IOException e) { |
||||
throw new IllegalStateException("AsyncListener failure", e); |
||||
} |
||||
} |
||||
} |
||||
|
||||
public void start(Runnable run) { |
||||
} |
||||
|
||||
public List<AsyncListener> getListeners() { |
||||
return this.listeners; |
||||
} |
||||
|
||||
public void addListener(AsyncListener listener) { |
||||
this.listeners.add(listener); |
||||
} |
||||
|
||||
public void addListener(AsyncListener listener, ServletRequest request, ServletResponse response) { |
||||
this.listeners.add(listener); |
||||
} |
||||
|
||||
public <T extends AsyncListener> T createListener(Class<T> clazz) throws ServletException { |
||||
return BeanUtils.instantiateClass(clazz); |
||||
} |
||||
|
||||
public long getTimeout() { |
||||
return this.timeout; |
||||
} |
||||
|
||||
public void setTimeout(long timeout) { |
||||
this.timeout = timeout; |
||||
} |
||||
|
||||
} |
||||
@ -1,251 +0,0 @@
@@ -1,251 +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 static org.hamcrest.Matchers.containsString; |
||||
import static org.junit.Assert.assertEquals; |
||||
import static org.junit.Assert.assertFalse; |
||||
import static org.junit.Assert.assertNotNull; |
||||
import static org.junit.Assert.assertSame; |
||||
import static org.junit.Assert.assertThat; |
||||
import static org.junit.Assert.assertTrue; |
||||
import static org.junit.Assert.fail; |
||||
|
||||
import java.util.concurrent.Callable; |
||||
|
||||
import javax.servlet.http.HttpServletRequest; |
||||
import javax.servlet.http.HttpServletResponse; |
||||
|
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
import org.springframework.core.task.SimpleAsyncTaskExecutor; |
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.mock.web.MockHttpServletRequest; |
||||
import org.springframework.mock.web.MockHttpServletResponse; |
||||
import org.springframework.web.context.request.ServletWebRequest; |
||||
|
||||
|
||||
/** |
||||
* Test fixture with an AsyncExecutionChain. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
*/ |
||||
public class AsyncExecutionChainTests { |
||||
|
||||
private AsyncExecutionChain chain; |
||||
|
||||
private MockHttpServletRequest request; |
||||
|
||||
private SimpleAsyncWebRequest asyncWebRequest; |
||||
|
||||
private ResultSavingCallable resultSavingCallable; |
||||
|
||||
@Before |
||||
public void setUp() { |
||||
this.request = new MockHttpServletRequest(); |
||||
this.asyncWebRequest = new SimpleAsyncWebRequest(this.request, new MockHttpServletResponse()); |
||||
this.resultSavingCallable = new ResultSavingCallable(); |
||||
|
||||
this.chain = AsyncExecutionChain.getForCurrentRequest(this.request); |
||||
this.chain.setTaskExecutor(new SyncTaskExecutor()); |
||||
this.chain.setAsyncWebRequest(this.asyncWebRequest); |
||||
this.chain.push(this.resultSavingCallable); |
||||
} |
||||
|
||||
@Test |
||||
public void getForCurrentRequest() throws Exception { |
||||
assertNotNull(this.chain); |
||||
assertSame(this.chain, AsyncExecutionChain.getForCurrentRequest(this.request)); |
||||
assertSame(this.chain, this.request.getAttribute(AsyncExecutionChain.CALLABLE_CHAIN_ATTRIBUTE)); |
||||
} |
||||
|
||||
@Test |
||||
public void isAsyncStarted() { |
||||
assertFalse(this.chain.isAsyncStarted()); |
||||
|
||||
this.asyncWebRequest.startAsync(); |
||||
assertTrue(this.chain.isAsyncStarted()); |
||||
} |
||||
|
||||
@Test(expected=IllegalStateException.class) |
||||
public void setAsyncWebRequestAfterAsyncStarted() { |
||||
this.asyncWebRequest.startAsync(); |
||||
this.chain.setAsyncWebRequest(null); |
||||
} |
||||
|
||||
@Test |
||||
public void startCallableChainProcessing() throws Exception { |
||||
this.chain.push(new IntegerIncrementingCallable()); |
||||
this.chain.push(new IntegerIncrementingCallable()); |
||||
this.chain.setLastCallable(new Callable<Object>() { |
||||
public Object call() throws Exception { |
||||
return 1; |
||||
} |
||||
}); |
||||
|
||||
this.chain.startCallableProcessing(); |
||||
|
||||
assertEquals(3, this.resultSavingCallable.result); |
||||
} |
||||
|
||||
@Test |
||||
public void startCallableChainProcessing_staleRequest() { |
||||
this.chain.setLastCallable(new Callable<Object>() { |
||||
public Object call() throws Exception { |
||||
return 1; |
||||
} |
||||
}); |
||||
|
||||
this.asyncWebRequest.startAsync(); |
||||
this.asyncWebRequest.complete(); |
||||
this.chain.startCallableProcessing(); |
||||
Exception ex = this.resultSavingCallable.exception; |
||||
|
||||
assertNotNull(ex); |
||||
assertEquals(StaleAsyncWebRequestException.class, ex.getClass()); |
||||
} |
||||
|
||||
@Test |
||||
public void startCallableChainProcessing_requiredCallable() { |
||||
try { |
||||
this.chain.startCallableProcessing(); |
||||
fail("Expected exception"); |
||||
} |
||||
catch (IllegalStateException ex) { |
||||
assertEquals(ex.getMessage(), "The last Callable was not set"); |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
public void startCallableChainProcessing_requiredAsyncWebRequest() { |
||||
this.chain.setAsyncWebRequest(null); |
||||
try { |
||||
this.chain.startCallableProcessing(); |
||||
fail("Expected exception"); |
||||
} |
||||
catch (IllegalStateException ex) { |
||||
assertEquals(ex.getMessage(), "AsyncWebRequest was not set"); |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
public void startDeferredResultProcessing() throws Exception { |
||||
this.chain.push(new IntegerIncrementingCallable()); |
||||
this.chain.push(new IntegerIncrementingCallable()); |
||||
|
||||
DeferredResult<Integer> deferredResult = new DeferredResult<Integer>(); |
||||
this.chain.startDeferredResultProcessing(deferredResult); |
||||
|
||||
assertTrue(this.asyncWebRequest.isAsyncStarted()); |
||||
|
||||
deferredResult.set(1); |
||||
|
||||
assertEquals(3, this.resultSavingCallable.result); |
||||
} |
||||
|
||||
@Test(expected=StaleAsyncWebRequestException.class) |
||||
public void startDeferredResultProcessing_staleRequest() throws Exception { |
||||
this.asyncWebRequest.startAsync(); |
||||
this.asyncWebRequest.complete(); |
||||
|
||||
DeferredResult<Integer> deferredResult = new DeferredResult<Integer>(); |
||||
this.chain.startDeferredResultProcessing(deferredResult); |
||||
deferredResult.set(1); |
||||
} |
||||
|
||||
@Test |
||||
public void startDeferredResultProcessing_requiredDeferredResult() { |
||||
try { |
||||
this.chain.startDeferredResultProcessing(null); |
||||
fail("Expected exception"); |
||||
} |
||||
catch (IllegalArgumentException ex) { |
||||
assertThat(ex.getMessage(), containsString("DeferredResult is required")); |
||||
} |
||||
} |
||||
|
||||
|
||||
private static class SimpleAsyncWebRequest extends ServletWebRequest implements AsyncWebRequest { |
||||
|
||||
private boolean asyncStarted; |
||||
|
||||
private boolean asyncCompleted; |
||||
|
||||
public SimpleAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) { |
||||
super(request, response); |
||||
} |
||||
|
||||
public void setTimeout(Long timeout) { } |
||||
|
||||
public void setTimeoutHandler(Runnable runnable) { } |
||||
|
||||
public void startAsync() { |
||||
this.asyncStarted = true; |
||||
} |
||||
|
||||
public boolean isAsyncStarted() { |
||||
return this.asyncStarted; |
||||
} |
||||
|
||||
public void complete() { |
||||
this.asyncStarted = false; |
||||
this.asyncCompleted = true; |
||||
} |
||||
|
||||
public boolean isAsyncCompleted() { |
||||
return this.asyncCompleted; |
||||
} |
||||
|
||||
public void sendError(HttpStatus status, String message) { |
||||
} |
||||
} |
||||
|
||||
@SuppressWarnings("serial") |
||||
private static class SyncTaskExecutor extends SimpleAsyncTaskExecutor { |
||||
|
||||
@Override |
||||
public void execute(Runnable task, long startTimeout) { |
||||
task.run(); |
||||
} |
||||
} |
||||
|
||||
private static class ResultSavingCallable extends AbstractDelegatingCallable { |
||||
|
||||
Object result; |
||||
|
||||
Exception exception; |
||||
|
||||
public Object call() throws Exception { |
||||
try { |
||||
this.result = getNext().call(); |
||||
} |
||||
catch (Exception ex) { |
||||
this.exception = ex; |
||||
throw ex; |
||||
} |
||||
return this.result; |
||||
} |
||||
} |
||||
|
||||
private static class IntegerIncrementingCallable extends AbstractDelegatingCallable { |
||||
|
||||
public Object call() throws Exception { |
||||
return ((Integer) getNext().call() + 1); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -1,71 +0,0 @@
@@ -1,71 +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 static org.easymock.EasyMock.*; |
||||
|
||||
import java.util.concurrent.Callable; |
||||
|
||||
import org.easymock.EasyMock; |
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
|
||||
/** |
||||
* A test fixture with a {@link StaleAsyncRequestCheckingCallable}. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
*/ |
||||
public class StaleAsyncRequestCheckingCallableTests { |
||||
|
||||
private StaleAsyncRequestCheckingCallable callable; |
||||
|
||||
private AsyncWebRequest asyncWebRequest; |
||||
|
||||
@Before |
||||
public void setUp() { |
||||
this.asyncWebRequest = EasyMock.createMock(AsyncWebRequest.class); |
||||
this.callable = new StaleAsyncRequestCheckingCallable(asyncWebRequest); |
||||
this.callable.setNext(new Callable<Object>() { |
||||
public Object call() throws Exception { |
||||
return 1; |
||||
} |
||||
}); |
||||
} |
||||
|
||||
@Test |
||||
public void call_notStale() throws Exception { |
||||
expect(this.asyncWebRequest.isAsyncCompleted()).andReturn(false); |
||||
replay(this.asyncWebRequest); |
||||
|
||||
this.callable.call(); |
||||
|
||||
verify(this.asyncWebRequest); |
||||
} |
||||
|
||||
@Test(expected=StaleAsyncWebRequestException.class) |
||||
public void call_stale() throws Exception { |
||||
expect(this.asyncWebRequest.isAsyncCompleted()).andReturn(true); |
||||
replay(this.asyncWebRequest); |
||||
|
||||
try { |
||||
this.callable.call(); |
||||
} |
||||
finally { |
||||
verify(this.asyncWebRequest); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,222 @@
@@ -0,0 +1,222 @@
|
||||
/* |
||||
* Copyright 2002-2012 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.web.context.request.async; |
||||
|
||||
import static org.hamcrest.Matchers.containsString; |
||||
import static org.junit.Assert.assertEquals; |
||||
import static org.junit.Assert.assertFalse; |
||||
import static org.junit.Assert.assertNotNull; |
||||
import static org.junit.Assert.assertSame; |
||||
import static org.junit.Assert.assertThat; |
||||
import static org.junit.Assert.assertTrue; |
||||
import static org.junit.Assert.fail; |
||||
|
||||
import java.util.concurrent.Callable; |
||||
|
||||
import javax.servlet.http.HttpServletRequest; |
||||
import javax.servlet.http.HttpServletResponse; |
||||
|
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
import org.springframework.core.task.SimpleAsyncTaskExecutor; |
||||
import org.springframework.mock.web.MockHttpServletRequest; |
||||
import org.springframework.mock.web.MockHttpServletResponse; |
||||
import org.springframework.web.context.request.ServletWebRequest; |
||||
|
||||
|
||||
/** |
||||
* Test fixture with an {@link WebAsyncManager}. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
*/ |
||||
public class WebAsyncManagerTests { |
||||
|
||||
private WebAsyncManager asyncManager; |
||||
|
||||
private MockHttpServletRequest request; |
||||
|
||||
private StubAsyncWebRequest stubAsyncWebRequest; |
||||
|
||||
@Before |
||||
public void setUp() { |
||||
this.request = new MockHttpServletRequest(); |
||||
this.stubAsyncWebRequest = new StubAsyncWebRequest(this.request, new MockHttpServletResponse()); |
||||
|
||||
this.asyncManager = AsyncWebUtils.getAsyncManager(this.request); |
||||
this.asyncManager.setTaskExecutor(new SyncTaskExecutor()); |
||||
this.asyncManager.setAsyncWebRequest(this.stubAsyncWebRequest); |
||||
} |
||||
|
||||
@Test |
||||
public void getForCurrentRequest() throws Exception { |
||||
assertNotNull(this.asyncManager); |
||||
assertSame(this.asyncManager, AsyncWebUtils.getAsyncManager(this.request)); |
||||
assertSame(this.asyncManager, this.request.getAttribute(AsyncWebUtils.WEB_ASYNC_MANAGER_ATTRIBUTE)); |
||||
} |
||||
|
||||
@Test |
||||
public void isConcurrentHandlingStarted() { |
||||
assertFalse(this.asyncManager.isConcurrentHandlingStarted()); |
||||
|
||||
this.stubAsyncWebRequest.startAsync(); |
||||
assertTrue(this.asyncManager.isConcurrentHandlingStarted()); |
||||
} |
||||
|
||||
@Test(expected=IllegalArgumentException.class) |
||||
public void setAsyncWebRequestAfterAsyncStarted() { |
||||
this.stubAsyncWebRequest.startAsync(); |
||||
this.asyncManager.setAsyncWebRequest(null); |
||||
} |
||||
|
||||
@Test |
||||
public void startCallableChainProcessing() throws Exception { |
||||
this.asyncManager.startCallableProcessing(new Callable<Object>() { |
||||
public Object call() throws Exception { |
||||
return 1; |
||||
} |
||||
}); |
||||
|
||||
assertTrue(this.asyncManager.isConcurrentHandlingStarted()); |
||||
assertTrue(this.stubAsyncWebRequest.isDispatched()); |
||||
} |
||||
|
||||
@Test |
||||
public void startCallableChainProcessingStaleRequest() { |
||||
this.stubAsyncWebRequest.setAsyncComplete(true); |
||||
this.asyncManager.startCallableProcessing(new Callable<Object>() { |
||||
public Object call() throws Exception { |
||||
return 1; |
||||
} |
||||
}); |
||||
|
||||
assertFalse(this.stubAsyncWebRequest.isDispatched()); |
||||
} |
||||
|
||||
@Test |
||||
public void startCallableChainProcessingCallableRequired() { |
||||
try { |
||||
this.asyncManager.startCallableProcessing(null); |
||||
fail("Expected exception"); |
||||
} |
||||
catch (IllegalArgumentException ex) { |
||||
assertEquals(ex.getMessage(), "Callable is required"); |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
public void startCallableChainProcessingAsyncWebRequestRequired() { |
||||
this.request.removeAttribute(AsyncWebUtils.WEB_ASYNC_MANAGER_ATTRIBUTE); |
||||
this.asyncManager = AsyncWebUtils.getAsyncManager(this.request); |
||||
try { |
||||
this.asyncManager.startCallableProcessing(new Callable<Object>() { |
||||
public Object call() throws Exception { |
||||
return null; |
||||
} |
||||
}); |
||||
fail("Expected exception"); |
||||
} |
||||
catch (IllegalStateException ex) { |
||||
assertEquals(ex.getMessage(), "AsyncWebRequest was not set"); |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
public void startDeferredResultProcessing() throws Exception { |
||||
DeferredResult<Integer> deferredResult = new DeferredResult<Integer>(); |
||||
this.asyncManager.startDeferredResultProcessing(deferredResult); |
||||
|
||||
assertTrue(this.asyncManager.isConcurrentHandlingStarted()); |
||||
|
||||
deferredResult.set(25); |
||||
assertEquals(25, this.asyncManager.getConcurrentResult()); |
||||
} |
||||
|
||||
@Test(expected=StaleAsyncWebRequestException.class) |
||||
public void startDeferredResultProcessing_staleRequest() throws Exception { |
||||
DeferredResult<Integer> deferredResult = new DeferredResult<Integer>(); |
||||
this.asyncManager.startDeferredResultProcessing(deferredResult); |
||||
|
||||
this.stubAsyncWebRequest.setAsyncComplete(true); |
||||
deferredResult.set(1); |
||||
} |
||||
|
||||
@Test |
||||
public void startDeferredResultProcessingDeferredResultRequired() { |
||||
try { |
||||
this.asyncManager.startDeferredResultProcessing(null); |
||||
fail("Expected exception"); |
||||
} |
||||
catch (IllegalArgumentException ex) { |
||||
assertThat(ex.getMessage(), containsString("DeferredResult is required")); |
||||
} |
||||
} |
||||
|
||||
|
||||
private static class StubAsyncWebRequest extends ServletWebRequest implements AsyncWebRequest { |
||||
|
||||
private boolean asyncStarted; |
||||
|
||||
private boolean dispatched; |
||||
|
||||
private boolean asyncComplete; |
||||
|
||||
public StubAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) { |
||||
super(request, response); |
||||
} |
||||
|
||||
public void setTimeout(Long timeout) { } |
||||
|
||||
public void setTimeoutHandler(Runnable runnable) { } |
||||
|
||||
public void startAsync() { |
||||
this.asyncStarted = true; |
||||
} |
||||
|
||||
public boolean isAsyncStarted() { |
||||
return this.asyncStarted; |
||||
} |
||||
|
||||
public void dispatch() { |
||||
this.dispatched = true; |
||||
} |
||||
|
||||
public boolean isDispatched() { |
||||
return dispatched; |
||||
} |
||||
|
||||
public void setAsyncComplete(boolean asyncComplete) { |
||||
this.asyncComplete = asyncComplete; |
||||
} |
||||
|
||||
public boolean isAsyncComplete() { |
||||
return this.asyncComplete; |
||||
} |
||||
|
||||
public void addCompletionHandler(Runnable runnable) { |
||||
} |
||||
} |
||||
|
||||
@SuppressWarnings("serial") |
||||
private static class SyncTaskExecutor extends SimpleAsyncTaskExecutor { |
||||
|
||||
@Override |
||||
public void execute(Runnable task, long startTimeout) { |
||||
task.run(); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,133 @@
@@ -0,0 +1,133 @@
|
||||
/* |
||||
* 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.mock.web; |
||||
|
||||
import java.io.IOException; |
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
import javax.servlet.AsyncContext; |
||||
import javax.servlet.AsyncEvent; |
||||
import javax.servlet.AsyncListener; |
||||
import javax.servlet.DispatcherType; |
||||
import javax.servlet.ServletContext; |
||||
import javax.servlet.ServletException; |
||||
import javax.servlet.ServletRequest; |
||||
import javax.servlet.ServletResponse; |
||||
|
||||
import org.springframework.beans.BeanUtils; |
||||
import org.springframework.web.util.WebUtils; |
||||
|
||||
/** |
||||
* Mock implementation of the {@link AsyncContext} interface. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 3.2 |
||||
*/ |
||||
public class MockAsyncContext implements AsyncContext { |
||||
|
||||
private final ServletRequest request; |
||||
|
||||
private final ServletResponse response; |
||||
|
||||
private final MockHttpServletRequest mockRequest; |
||||
|
||||
private final List<AsyncListener> listeners = new ArrayList<AsyncListener>(); |
||||
|
||||
private String dispatchPath; |
||||
|
||||
private long timeout = 10 * 60 * 1000L; |
||||
|
||||
public MockAsyncContext(ServletRequest request, ServletResponse response) { |
||||
this.request = request; |
||||
this.response = response; |
||||
this.mockRequest = WebUtils.getNativeRequest(request, MockHttpServletRequest.class); |
||||
} |
||||
|
||||
public ServletRequest getRequest() { |
||||
return this.request; |
||||
} |
||||
|
||||
public ServletResponse getResponse() { |
||||
return this.response; |
||||
} |
||||
|
||||
public boolean hasOriginalRequestAndResponse() { |
||||
return false; |
||||
} |
||||
|
||||
public String getDispatchPath() { |
||||
return this.dispatchPath; |
||||
} |
||||
|
||||
public void dispatch() { |
||||
dispatch(null); |
||||
} |
||||
|
||||
public void dispatch(String path) { |
||||
dispatch(null, path); |
||||
} |
||||
|
||||
public void dispatch(ServletContext context, String path) { |
||||
this.dispatchPath = path; |
||||
if (this.mockRequest != null) { |
||||
this.mockRequest.setDispatcherType(DispatcherType.ASYNC); |
||||
this.mockRequest.setAsyncStarted(false); |
||||
} |
||||
} |
||||
|
||||
public void complete() { |
||||
if (this.mockRequest != null) { |
||||
this.mockRequest.setAsyncStarted(false); |
||||
} |
||||
for (AsyncListener listener : this.listeners) { |
||||
try { |
||||
listener.onComplete(new AsyncEvent(this, this.request, this.response)); |
||||
} |
||||
catch (IOException e) { |
||||
throw new IllegalStateException("AsyncListener failure", e); |
||||
} |
||||
} |
||||
} |
||||
|
||||
public void start(Runnable run) { |
||||
} |
||||
|
||||
public List<AsyncListener> getListeners() { |
||||
return this.listeners; |
||||
} |
||||
|
||||
public void addListener(AsyncListener listener) { |
||||
this.listeners.add(listener); |
||||
} |
||||
|
||||
public void addListener(AsyncListener listener, ServletRequest request, ServletResponse response) { |
||||
this.listeners.add(listener); |
||||
} |
||||
|
||||
public <T extends AsyncListener> T createListener(Class<T> clazz) throws ServletException { |
||||
return BeanUtils.instantiateClass(clazz); |
||||
} |
||||
|
||||
public long getTimeout() { |
||||
return this.timeout; |
||||
} |
||||
|
||||
public void setTimeout(long timeout) { |
||||
this.timeout = timeout; |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue