Browse Source

Set timeout value and task executor per async request

Methods returning DeferredResult can now specify a timeout value
through constructor arg while methods returning a Callable can wrap it
in an AsyncTask that also accepts a timeout and a specific task
executor.

Issue: SPR-9399
pull/120/head
Rossen Stoyanchev 14 years ago
parent
commit
cdab04a032
  1. 10
      spring-orm/src/main/java/org/springframework/orm/hibernate3/support/OpenSessionInViewFilter.java
  2. 10
      spring-orm/src/main/java/org/springframework/orm/hibernate3/support/OpenSessionInViewInterceptor.java
  3. 10
      spring-orm/src/main/java/org/springframework/orm/hibernate4/support/OpenSessionInViewFilter.java
  4. 10
      spring-orm/src/main/java/org/springframework/orm/hibernate4/support/OpenSessionInViewInterceptor.java
  5. 121
      spring-web/src/main/java/org/springframework/web/context/request/async/AsyncTask.java
  6. 17
      spring-web/src/main/java/org/springframework/web/context/request/async/AsyncWebRequest.java
  7. 32
      spring-web/src/main/java/org/springframework/web/context/request/async/AsyncWebUtils.java
  8. 43
      spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResult.java
  9. 4
      spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java
  10. 238
      spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java
  11. 10
      spring-web/src/main/java/org/springframework/web/context/request/async/package-info.java
  12. 4
      spring-web/src/test/java/org/springframework/web/context/request/async/DeferredResultTests.java
  13. 6
      spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java
  14. 192
      spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTests.java
  15. 6
      spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java
  16. 73
      spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AsyncMethodReturnValueHandler.java
  17. 60
      spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AsyncTaskMethodReturnValueHandler.java
  18. 52
      spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/CallableMethodReturnValueHandler.java
  19. 51
      spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultMethodReturnValueHandler.java
  20. 30
      spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java

10
spring-orm/src/main/java/org/springframework/orm/hibernate3/support/OpenSessionInViewFilter.java

@ -34,7 +34,7 @@ import org.springframework.util.Assert;
import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.async.AsyncWebUtils; import org.springframework.web.context.request.async.AsyncWebUtils;
import org.springframework.web.context.request.async.WebAsyncManager; import org.springframework.web.context.request.async.WebAsyncManager;
import org.springframework.web.context.request.async.WebAsyncManager.AsyncThreadInitializer; import org.springframework.web.context.request.async.WebAsyncManager.WebAsyncThreadInitializer;
import org.springframework.web.context.support.WebApplicationContextUtils; import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
@ -195,13 +195,13 @@ public class OpenSessionInViewFilter extends OncePerRequestFilter {
participate = true; participate = true;
} }
else { else {
if (!isAsyncDispatch(request) || !asyncManager.applyAsyncThreadInitializer(key)) { if (!isAsyncDispatch(request) || !asyncManager.initializeAsyncThread(key)) {
logger.debug("Opening single Hibernate Session in OpenSessionInViewFilter"); logger.debug("Opening single Hibernate Session in OpenSessionInViewFilter");
Session session = getSession(sessionFactory); Session session = getSession(sessionFactory);
SessionHolder sessionHolder = new SessionHolder(session); SessionHolder sessionHolder = new SessionHolder(session);
TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder); TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder);
AsyncThreadInitializer initializer = createAsyncThreadInitializer(sessionFactory, sessionHolder); WebAsyncThreadInitializer initializer = createAsyncThreadInitializer(sessionFactory, sessionHolder);
asyncManager.registerAsyncThreadInitializer(key, initializer); asyncManager.registerAsyncThreadInitializer(key, initializer);
} }
} }
@ -240,10 +240,10 @@ public class OpenSessionInViewFilter extends OncePerRequestFilter {
} }
} }
private AsyncThreadInitializer createAsyncThreadInitializer(final SessionFactory sessionFactory, private WebAsyncThreadInitializer createAsyncThreadInitializer(final SessionFactory sessionFactory,
final SessionHolder sessionHolder) { final SessionHolder sessionHolder) {
return new AsyncThreadInitializer() { return new WebAsyncThreadInitializer() {
public void initialize() { public void initialize() {
TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder); TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder);
} }

10
spring-orm/src/main/java/org/springframework/orm/hibernate3/support/OpenSessionInViewInterceptor.java

@ -28,7 +28,7 @@ import org.springframework.web.context.request.WebRequest;
import org.springframework.web.context.request.async.AsyncWebRequestInterceptor; import org.springframework.web.context.request.async.AsyncWebRequestInterceptor;
import org.springframework.web.context.request.async.AsyncWebUtils; import org.springframework.web.context.request.async.AsyncWebUtils;
import org.springframework.web.context.request.async.WebAsyncManager; import org.springframework.web.context.request.async.WebAsyncManager;
import org.springframework.web.context.request.async.WebAsyncManager.AsyncThreadInitializer; import org.springframework.web.context.request.async.WebAsyncManager.WebAsyncThreadInitializer;
/** /**
* Spring web request interceptor that binds a Hibernate <code>Session</code> to the * Spring web request interceptor that binds a Hibernate <code>Session</code> to the
@ -147,7 +147,7 @@ public class OpenSessionInViewInterceptor extends HibernateAccessor implements A
String participateAttributeName = getParticipateAttributeName(); String participateAttributeName = getParticipateAttributeName();
if (asyncManager.hasConcurrentResult()) { if (asyncManager.hasConcurrentResult()) {
if (asyncManager.applyAsyncThreadInitializer(participateAttributeName)) { if (asyncManager.initializeAsyncThread(participateAttributeName)) {
return; return;
} }
} }
@ -169,7 +169,7 @@ public class OpenSessionInViewInterceptor extends HibernateAccessor implements A
SessionHolder sessionHolder = new SessionHolder(session); SessionHolder sessionHolder = new SessionHolder(session);
TransactionSynchronizationManager.bindResource(getSessionFactory(), sessionHolder); TransactionSynchronizationManager.bindResource(getSessionFactory(), sessionHolder);
AsyncThreadInitializer asyncThreadInitializer = createThreadInitializer(sessionHolder); WebAsyncThreadInitializer asyncThreadInitializer = createThreadInitializer(sessionHolder);
asyncManager.registerAsyncThreadInitializer(participateAttributeName, asyncThreadInitializer); asyncManager.registerAsyncThreadInitializer(participateAttributeName, asyncThreadInitializer);
} }
else { else {
@ -261,8 +261,8 @@ public class OpenSessionInViewInterceptor extends HibernateAccessor implements A
return getSessionFactory().toString() + PARTICIPATE_SUFFIX; return getSessionFactory().toString() + PARTICIPATE_SUFFIX;
} }
private AsyncThreadInitializer createThreadInitializer(final SessionHolder sessionHolder) { private WebAsyncThreadInitializer createThreadInitializer(final SessionHolder sessionHolder) {
return new AsyncThreadInitializer() { return new WebAsyncThreadInitializer() {
public void initialize() { public void initialize() {
TransactionSynchronizationManager.bindResource(getSessionFactory(), sessionHolder); TransactionSynchronizationManager.bindResource(getSessionFactory(), sessionHolder);
} }

10
spring-orm/src/main/java/org/springframework/orm/hibernate4/support/OpenSessionInViewFilter.java

@ -34,7 +34,7 @@ import org.springframework.transaction.support.TransactionSynchronizationManager
import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.async.AsyncWebUtils; import org.springframework.web.context.request.async.AsyncWebUtils;
import org.springframework.web.context.request.async.WebAsyncManager; import org.springframework.web.context.request.async.WebAsyncManager;
import org.springframework.web.context.request.async.WebAsyncManager.AsyncThreadInitializer; import org.springframework.web.context.request.async.WebAsyncManager.WebAsyncThreadInitializer;
import org.springframework.web.context.support.WebApplicationContextUtils; import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
@ -126,13 +126,13 @@ public class OpenSessionInViewFilter extends OncePerRequestFilter {
participate = true; participate = true;
} }
else { else {
if (!isAsyncDispatch(request) || !asyncManager.applyAsyncThreadInitializer(key)) { if (!isAsyncDispatch(request) || !asyncManager.initializeAsyncThread(key)) {
logger.debug("Opening Hibernate Session in OpenSessionInViewFilter"); logger.debug("Opening Hibernate Session in OpenSessionInViewFilter");
Session session = openSession(sessionFactory); Session session = openSession(sessionFactory);
SessionHolder sessionHolder = new SessionHolder(session); SessionHolder sessionHolder = new SessionHolder(session);
TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder); TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder);
AsyncThreadInitializer initializer = createAsyncThreadInitializer(sessionFactory, sessionHolder); WebAsyncThreadInitializer initializer = createAsyncThreadInitializer(sessionFactory, sessionHolder);
asyncManager.registerAsyncThreadInitializer(key, initializer); asyncManager.registerAsyncThreadInitializer(key, initializer);
} }
} }
@ -153,10 +153,10 @@ public class OpenSessionInViewFilter extends OncePerRequestFilter {
} }
} }
private AsyncThreadInitializer createAsyncThreadInitializer(final SessionFactory sessionFactory, private WebAsyncThreadInitializer createAsyncThreadInitializer(final SessionFactory sessionFactory,
final SessionHolder sessionHolder) { final SessionHolder sessionHolder) {
return new AsyncThreadInitializer() { return new WebAsyncThreadInitializer() {
public void initialize() { public void initialize() {
TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder); TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder);
} }

10
spring-orm/src/main/java/org/springframework/orm/hibernate4/support/OpenSessionInViewInterceptor.java

@ -32,7 +32,7 @@ import org.springframework.web.context.request.WebRequest;
import org.springframework.web.context.request.async.AsyncWebRequestInterceptor; import org.springframework.web.context.request.async.AsyncWebRequestInterceptor;
import org.springframework.web.context.request.async.AsyncWebUtils; import org.springframework.web.context.request.async.AsyncWebUtils;
import org.springframework.web.context.request.async.WebAsyncManager; import org.springframework.web.context.request.async.WebAsyncManager;
import org.springframework.web.context.request.async.WebAsyncManager.AsyncThreadInitializer; import org.springframework.web.context.request.async.WebAsyncManager.WebAsyncThreadInitializer;
/** /**
* Spring web request interceptor that binds a Hibernate <code>Session</code> to the * Spring web request interceptor that binds a Hibernate <code>Session</code> to the
@ -109,7 +109,7 @@ public class OpenSessionInViewInterceptor implements AsyncWebRequestInterceptor
String participateAttributeName = getParticipateAttributeName(); String participateAttributeName = getParticipateAttributeName();
if (asyncManager.hasConcurrentResult()) { if (asyncManager.hasConcurrentResult()) {
if (asyncManager.applyAsyncThreadInitializer(participateAttributeName)) { if (asyncManager.initializeAsyncThread(participateAttributeName)) {
return; return;
} }
} }
@ -126,7 +126,7 @@ public class OpenSessionInViewInterceptor implements AsyncWebRequestInterceptor
SessionHolder sessionHolder = new SessionHolder(session); SessionHolder sessionHolder = new SessionHolder(session);
TransactionSynchronizationManager.bindResource(getSessionFactory(), sessionHolder); TransactionSynchronizationManager.bindResource(getSessionFactory(), sessionHolder);
AsyncThreadInitializer asyncThreadInitializer = createThreadInitializer(sessionHolder); WebAsyncThreadInitializer asyncThreadInitializer = createThreadInitializer(sessionHolder);
asyncManager.registerAsyncThreadInitializer(participateAttributeName, asyncThreadInitializer); asyncManager.registerAsyncThreadInitializer(participateAttributeName, asyncThreadInitializer);
} }
} }
@ -200,8 +200,8 @@ public class OpenSessionInViewInterceptor implements AsyncWebRequestInterceptor
return getSessionFactory().toString() + PARTICIPATE_SUFFIX; return getSessionFactory().toString() + PARTICIPATE_SUFFIX;
} }
private AsyncThreadInitializer createThreadInitializer(final SessionHolder sessionHolder) { private WebAsyncThreadInitializer createThreadInitializer(final SessionHolder sessionHolder) {
return new AsyncThreadInitializer() { return new WebAsyncThreadInitializer() {
public void initialize() { public void initialize() {
TransactionSynchronizationManager.bindResource(getSessionFactory(), sessionHolder); TransactionSynchronizationManager.bindResource(getSessionFactory(), sessionHolder);
} }

121
spring-web/src/main/java/org/springframework/web/context/request/async/AsyncTask.java

@ -0,0 +1,121 @@
/*
* 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.springframework.beans.factory.BeanFactory;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.util.Assert;
/**
* Holder for a {@link Callable}, a timeout value, and a task executor.
*
* @author Rossen Stoyanchev
* @since 3.2
*/
public class AsyncTask {
private final Callable<?> callable;
private final Long timeout;
private final String executorName;
private final AsyncTaskExecutor executor;
private BeanFactory beanFactory;
/**
* Create an AsyncTask with a timeout value and a Callable.
* @param timeout timeout value in milliseconds
* @param callable the callable for concurrent handling
*/
public AsyncTask(long timeout, Callable<?> callable) {
this(timeout, null, null, callable);
Assert.notNull(timeout, "Timeout must not be null");
}
/**
* Create an AsyncTask with a timeout value, an executor name, and a Callable.
* @param timeout timeout value in milliseconds; ignored if {@code null}
* @param callable the callable for concurrent handling
*/
public AsyncTask(Long timeout, String executorName, Callable<?> callable) {
this(timeout, null, executorName, callable);
Assert.notNull(executor, "Executor name must not be null");
}
/**
* Create an AsyncTask with a timeout value, an executor instance, and a Callable.
* @param timeout timeout value in milliseconds; ignored if {@code null}
* @param callable the callable for concurrent handling
*/
public AsyncTask(Long timeout, AsyncTaskExecutor executor, Callable<?> callable) {
this(timeout, executor, null, callable);
Assert.notNull(executor, "Executor must not be null");
}
private AsyncTask(Long timeout, AsyncTaskExecutor executor, String executorName, Callable<?> callable) {
Assert.notNull(callable, "Callable must not be null");
this.callable = callable;
this.timeout = timeout;
this.executor = executor;
this.executorName = executorName;
}
/**
* Return the {@link Callable} to use for concurrent handling, never {@code null}.
*/
public Callable<?> getCallable() {
return this.callable;
}
/**
* Return the timeout value in milliseconds or {@code null} if not value is set.
*/
public Long getTimeout() {
return this.timeout;
}
/**
* Return the AsyncTaskExecutor to use for concurrent handling, or {@code null}.
*/
public AsyncTaskExecutor getExecutor() {
if (this.executor != null) {
return this.executor;
}
else if (this.executorName != null) {
Assert.state(this.beanFactory != null, "A BeanFactory is required to look up an task executor bean");
return this.beanFactory.getBean(this.executorName, AsyncTaskExecutor.class);
}
else {
return null;
}
}
/**
* A {@link BeanFactory} to use to resolve an executor name. Applications are
* not expected to have to set this property when AsyncTask is used in a
* Spring MVC controller.
*/
public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
}

17
spring-web/src/main/java/org/springframework/web/context/request/async/AsyncWebRequest.java

@ -29,20 +29,18 @@ public interface AsyncWebRequest extends NativeWebRequest {
/** /**
* Set the time required for concurrent handling to complete. * Set the time required for concurrent handling to complete.
* @param timeout amount of time in milliseconds * This property should not be set when concurrent handling is in progress,
* i.e. when {@link #isAsyncStarted()} is {@code true}.
* @param timeout amount of time in milliseconds; {@code null} means no
* timeout, i.e. rely on the default timeout of the container.
*/ */
void setTimeout(Long timeout); void setTimeout(Long timeout);
/** /**
* Provide a Runnable to invoke on timeout. * Set a handler to be invoked if concurrent processing times out.
*/ */
void setTimeoutHandler(Runnable runnable); void setTimeoutHandler(Runnable runnable);
/**
* 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 * Mark the start of asynchronous request processing so that when the main
* processing thread exits, the response remains open for further processing * processing thread exits, the response remains open for further processing
@ -70,6 +68,11 @@ public interface AsyncWebRequest extends NativeWebRequest {
*/ */
boolean isDispatched(); boolean isDispatched();
/**
* Add a Runnable to be invoked when request processing completes.
*/
void addCompletionHandler(Runnable runnable);
/** /**
* Whether asynchronous processing has completed. * Whether asynchronous processing has completed.
*/ */

32
spring-web/src/main/java/org/springframework/web/context/request/async/AsyncWebUtils.java

@ -15,8 +15,14 @@
*/ */
package org.springframework.web.context.request.async; package org.springframework.web.context.request.async;
import java.lang.reflect.Constructor;
import javax.servlet.ServletRequest; import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.BeanUtils;
import org.springframework.util.ClassUtils;
import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.WebRequest; import org.springframework.web.context.request.WebRequest;
@ -57,4 +63,30 @@ public abstract class AsyncWebUtils {
return asyncManager; return asyncManager;
} }
/**
* Create an AsyncWebRequest instance.
* <p>By default an instance of {@link StandardServletAsyncWebRequest} is created
* if running in Servlet 3.0 (or higher) environment or as a fallback option an
* instance of {@link NoSupportAsyncWebRequest} is returned.
* @param request the current request
* @param response the current response
* @return an AsyncWebRequest instance, never {@code null}
*/
public static AsyncWebRequest createAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) {
return ClassUtils.hasMethod(ServletRequest.class, "startAsync") ?
createStandardServletAsyncWebRequest(request, response) : new NoSupportAsyncWebRequest(request, response);
}
private static AsyncWebRequest createStandardServletAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) {
try {
String className = "org.springframework.web.context.request.async.StandardServletAsyncWebRequest";
Class<?> clazz = ClassUtils.forName(className, AsyncWebUtils.class.getClassLoader());
Constructor<?> constructor = clazz.getConstructor(HttpServletRequest.class, HttpServletResponse.class);
return (AsyncWebRequest) BeanUtils.instantiateClass(constructor, request, response);
}
catch (Throwable t) {
throw new IllegalStateException("Failed to instantiate StandardServletAsyncWebRequest", t);
}
}
} }

43
spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResult.java

@ -24,10 +24,10 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
/** /**
* {@code DeferredResult} provides an alternative to returning a {@link Callable} * {@code DeferredResult} provides an alternative to using a {@link Callable}
* for asynchronous request processing. While with a Callable, a thread is used * for asynchronous request processing. While a Callable is executed concurrently
* to execute it on behalf of the application, with a DeferredResult the application * on behalf of the application, with a DeferredResult the application can produce
* sets the result whenever it needs to from a thread of its choice. * the result from a thread of its choice.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 3.2 * @since 3.2
@ -38,32 +38,53 @@ public final class DeferredResult<T> {
private static final Object RESULT_NONE = new Object(); private static final Object RESULT_NONE = new Object();
private Object result = RESULT_NONE;
private final Object timeoutResult; private final Object timeoutResult;
private final AtomicBoolean expired = new AtomicBoolean(false); private final Long timeout;
private DeferredResultHandler resultHandler; private DeferredResultHandler resultHandler;
private Object result = RESULT_NONE;
private final AtomicBoolean expired = new AtomicBoolean(false);
private final Object lock = new Object(); private final Object lock = new Object();
private final CountDownLatch latch = new CountDownLatch(1); private final CountDownLatch latch = new CountDownLatch(1);
/** /**
* Create a DeferredResult instance. * Create a DeferredResult.
*/ */
public DeferredResult() { public DeferredResult() {
this(RESULT_NONE); this(null, RESULT_NONE);
} }
/** /**
* Create a DeferredResult with a default result to use in case of a timeout. * Create a DeferredResult with a timeout.
* @param timeoutResult the result to use * @param timeout timeout value in milliseconds
*/ */
public DeferredResult(Object timeoutResult) { public DeferredResult(long timeout) {
this(timeout, RESULT_NONE);
}
/**
* Create a DeferredResult with a timeout and a default result to use on timeout.
* @param timeout timeout value in milliseconds; ignored if {@code null}
* @param timeoutResult the result to use, possibly {@code null}
*/
public DeferredResult(Long timeout, Object timeoutResult) {
this.timeoutResult = timeoutResult; this.timeoutResult = timeoutResult;
this.timeout = timeout;
}
/**
* Return the configured timeout value in milliseconds.
*/
public Long getTimeoutMilliseconds() {
return this.timeout;
} }
/** /**

4
spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java

@ -70,6 +70,7 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
* <p>The timeout period begins when the main processing thread has exited. * <p>The timeout period begins when the main processing thread has exited.
*/ */
public void setTimeout(Long timeout) { public void setTimeout(Long timeout) {
Assert.state(!isAsyncStarted(), "Cannot change the timeout with concurrent handling in progress");
this.timeout = timeout; this.timeout = timeout;
} }
@ -145,6 +146,9 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
} }
/**
* Sends a SERVICE_UNAVAILABLE (503).
*/
private class DefaultTimeoutHandler implements Runnable { private class DefaultTimeoutHandler implements Runnable {
public void run() { public void run() {

238
spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java

@ -38,25 +38,16 @@ import org.springframework.web.util.UrlPathHelper;
* as an SPI and not typically used directly by application classes. * 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). * <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 * Concurrent request handling can be innitiated by calling
* {@linkplain #startCallableProcessing(Callable, Object...) startCallableProcessing} or * {@linkplain #startCallableProcessing(Callable, Object...) startCallableProcessing} or
* {@linkplain #startDeferredResultProcessing(DeferredResult, Object...) startDeferredResultProcessing} * {@linkplain #startDeferredResultProcessing(DeferredResult, Object...) startDeferredResultProcessing}
* both of which will process in a separate thread (T2). * both of which produce a result in a separate thread (T2). The result is saved
* After the start of concurrent handling {@link #isConcurrentHandlingStarted()} * and the request dispatched to the container, to resume processing with the saved
* returns "true" and this can be used by classes involved in processing on the * result in a third thread (T3). Within the dispatched thread (T3), the saved
* main thread (T1) quickly and with very minimal processing. * result can be accessed via {@link #getConcurrentResult()} or its presence
* detected via {@link #hasConcurrentResult()}.
* *
* <p>When the concurrent handling completes in a separate thread (T2), both * <p>TODO .. Servlet 3 config
* {@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 * @author Rossen Stoyanchev
* @since 3.2 * @since 3.2
@ -73,42 +64,38 @@ public final class WebAsyncManager {
private static final Log logger = LogFactory.getLog(WebAsyncManager.class); private static final Log logger = LogFactory.getLog(WebAsyncManager.class);
private AsyncWebRequest asyncWebRequest; private AsyncWebRequest asyncWebRequest;
private AsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(this.getClass().getSimpleName()); 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 concurrentResult = RESULT_NONE;
private Object[] concurrentResultContext; private Object[] concurrentResultContext;
private final Map<Object, WebAsyncThreadInitializer> threadInitializers = new LinkedHashMap<Object, WebAsyncThreadInitializer>();
private static final UrlPathHelper urlPathHelper = new UrlPathHelper(); private static final UrlPathHelper urlPathHelper = new UrlPathHelper();
/**
* Package private constructor
* @see AsyncWebUtils
*/
WebAsyncManager() {
}
/** /**
* Configure an AsyncTaskExecutor for use with {@link #startCallableProcessing(Callable)}. * Package private constructor.
* <p>By default a {@link SimpleAsyncTaskExecutor} instance is used. Applications * @see AsyncWebUtils#getAsyncManager(javax.servlet.ServletRequest)
* are advised to provide a TaskExecutor configured for production use. * @see AsyncWebUtils#getAsyncManager(org.springframework.web.context.request.WebRequest)
* @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#setAsyncTaskExecutor
*/ */
public void setTaskExecutor(AsyncTaskExecutor taskExecutor) { WebAsyncManager() {
this.taskExecutor = taskExecutor;
} }
/** /**
* Provide an {@link AsyncWebRequest} to use to start and to dispatch request. * Configure the {@link AsyncWebRequest} to use. This property may be
* This property must be set before the start of concurrent handling. * set more than once during a single request to accurately reflect the
* @param asyncWebRequest the request to use * current state of the request (e.g. following a forward, request/response
* wrapping, etc). However, it should not be set while concurrent handling is
* in progress, i.e. while {@link #isConcurrentHandlingStarted()} is {@code true}.
* @param asyncWebRequest the web request to use
*/ */
public void setAsyncWebRequest(final AsyncWebRequest asyncWebRequest) { public void setAsyncWebRequest(final AsyncWebRequest asyncWebRequest) {
Assert.notNull(asyncWebRequest, "Expected AsyncWebRequest"); Assert.notNull(asyncWebRequest, "AsyncWebRequest must not be null");
Assert.state(!isConcurrentHandlingStarted(), "Can't set AsyncWebRequest with concurrent handling in progress"); Assert.state(!isConcurrentHandlingStarted(), "Can't set AsyncWebRequest with concurrent handling in progress");
this.asyncWebRequest = asyncWebRequest; this.asyncWebRequest = asyncWebRequest;
this.asyncWebRequest.addCompletionHandler(new Runnable() { this.asyncWebRequest.addCompletionHandler(new Runnable() {
@ -119,18 +106,27 @@ public final class WebAsyncManager {
} }
/** /**
* Whether the handler for the current request is executed concurrently. * Configure an AsyncTaskExecutor for use with concurrent processing via
* Once concurrent handling is done, the result is saved, and the request * {@link #startCallableProcessing(Callable, Object...)}.
* dispatched again to resume processing where the result of concurrent * <p>By default a {@link SimpleAsyncTaskExecutor} instance is used.
* handling is available via {@link #getConcurrentResult()}. */
public void setTaskExecutor(AsyncTaskExecutor taskExecutor) {
this.taskExecutor = taskExecutor;
}
/**
* Whether the target handler chose to handle the request asynchronously.
* A return value of "true" indicates concurrent handling is under way and the
* response will remain open. A return value of "false" will be returned again after concurrent
* handling produces a result and the request is dispatched to resume processing.
*/ */
public boolean isConcurrentHandlingStarted() { public boolean isConcurrentHandlingStarted() {
return ((this.asyncWebRequest != null) && (this.asyncWebRequest.isAsyncStarted())); return ((this.asyncWebRequest != null) && (this.asyncWebRequest.isAsyncStarted()));
} }
/** /**
* Whether the current thread was dispatched to continue processing the result * Whether the request was dispatched to resume processing the result of
* of concurrent handler execution. * concurrent handling.
*/ */
public boolean hasConcurrentResult() { public boolean hasConcurrentResult() {
@ -142,84 +138,58 @@ public final class WebAsyncManager {
} }
/** /**
* Return the result of concurrent handler execution. This may be an Object * Provides access to the result from concurrent handling.
* value on successful return or an {@code Exception} or {@code Throwable}. * @return an Object, possibly an {@code Exception} or {@code Throwable} if
* concurrent handling raised one.
*/ */
public Object getConcurrentResult() { public Object getConcurrentResult() {
return this.concurrentResult; return this.concurrentResult;
} }
/** /**
* Return the processing context saved at the start of concurrent handling. * Provides access to additional processing context saved at the start of
* concurrent handling.
*/ */
public Object[] getConcurrentResultContext() { public Object[] getConcurrentResultContext() {
return this.concurrentResultContext; return this.concurrentResultContext;
} }
/** /**
* Reset the {@linkplain #getConcurrentResult() concurrentResult} and the * Clear {@linkplain #getConcurrentResult() concurrentResult} and
* {@linkplain #getConcurrentResultContext() concurrentResultContext}. * {@linkplain #getConcurrentResultContext() concurrentResultContext}.
*/ */
public void resetConcurrentResult() { public void clearConcurrentResult() {
this.concurrentResult = RESULT_NONE; this.concurrentResult = RESULT_NONE;
this.concurrentResultContext = null; this.concurrentResultContext = null;
} }
/** /**
* Register an {@link AsyncThreadInitializer} with the WebAsyncManager instance * Start concurrent request processing and execute the given task with an
* for the current request. It may later be accessed and applied via * {@link #setTaskExecutor(AsyncTaskExecutor) AsyncTaskExecutor}. The result
* {@link #applyAsyncThreadInitializer(String)} and will also be used to * from the task execution is saved and the request dispatched in order to
* initialize and reset threads for concurrent handler execution. * resume processing of that result. If the task raises an Exception then
* @param key a unique the key under which to keep the initializer * the saved result will be the raised Exception.
* @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 callable a unit of work to be executed asynchronously
* @param processingContext additional context to save for later access via * @param processingContext additional context to save that can be accessed
* {@link #getConcurrentResultContext()} * via {@link #getConcurrentResultContext()}
*
* @see #getConcurrentResult()
* @see #getConcurrentResultContext()
*/ */
public void startCallableProcessing(final Callable<?> callable, Object... processingContext) { public void startCallableProcessing(final Callable<?> callable, Object... processingContext) {
Assert.notNull(callable, "Callable is required"); Assert.notNull(callable, "Callable must not be null");
startAsyncProcessing(processingContext); startAsyncProcessing(processingContext);
this.taskExecutor.submit(new Runnable() { this.taskExecutor.submit(new Runnable() {
public void run() { public void run() {
List<AsyncThreadInitializer> initializers = List<WebAsyncThreadInitializer> initializers =
new ArrayList<AsyncThreadInitializer>(threadInitializers.values()); new ArrayList<WebAsyncThreadInitializer>(threadInitializers.values());
try { try {
for (AsyncThreadInitializer initializer : initializers) { for (WebAsyncThreadInitializer initializer : initializers) {
initializer.initialize(); initializer.initialize();
} }
concurrentResult = callable.call(); concurrentResult = callable.call();
@ -229,7 +199,7 @@ public final class WebAsyncManager {
} }
finally { finally {
Collections.reverse(initializers); Collections.reverse(initializers);
for (AsyncThreadInitializer initializer : initializers) { for (WebAsyncThreadInitializer initializer : initializers) {
initializer.reset(); initializer.reset();
} }
} }
@ -250,24 +220,51 @@ public final class WebAsyncManager {
} }
/** /**
* Initialize the given given {@link DeferredResult} so that whenever the * Use the given {@link AsyncTask} to configure the task executor as well as
* DeferredResult is set, the resulting value, which may be an Object or a * the timeout value of the {@code AsyncWebRequest} before delegating to
* raised {@code Exception} or {@code Throwable}, is saved and the request * {@link #startCallableProcessing(Callable, Object...)}.
* is dispatched for further processing of the result. In the dispatch * @param asyncTask an asyncTask containing the target {@code Callable}
* thread, the result value can be accessed via {@link #getConcurrentResult()}. * @param processingContext additional context to save that can be accessed
* <p>The method returns immediately and it's up to the caller to set the * via {@link #getConcurrentResultContext()}
* DeferredResult. Subsequent calls to {@link #isConcurrentHandlingStarted()} */
* return "true" until after the dispatch when {@link #hasConcurrentResult()} public void startCallableProcessing(AsyncTask asyncTask, Object... processingContext) {
* returns "true" and {@link #isConcurrentHandlingStarted()} is back to "false". Assert.notNull(asyncTask, "AsyncTask must not be null");
Long timeout = asyncTask.getTimeout();
if (timeout != null) {
this.asyncWebRequest.setTimeout(timeout);
}
AsyncTaskExecutor executor = asyncTask.getExecutor();
if (executor != null) {
this.taskExecutor = executor;
}
startCallableProcessing(asyncTask.getCallable(), processingContext);
}
/**
* Start concurrent request processing and initialize the given {@link DeferredResult}
* with a {@link DeferredResultHandler} that saves the result and dispatches
* the request to resume processing of that result.
* The {@code AsyncWebRequest} is also updated with a completion handler that
* expires the {@code DeferredResult} and a timeout handler assuming the
* {@code DeferredResult} has a default timeout result.
* *
* @param deferredResult the DeferredResult instance to initialize * @param deferredResult the DeferredResult instance to initialize
* @param processingContext additional context to save for later access via * @param processingContext additional context to save that can be accessed
* {@link #getConcurrentResultContext()} * via {@link #getConcurrentResultContext()}
*
* @see #getConcurrentResult()
* @see #getConcurrentResultContext()
*/ */
public void startDeferredResultProcessing(final DeferredResult<?> deferredResult, Object... processingContext) { public void startDeferredResultProcessing(final DeferredResult<?> deferredResult, Object... processingContext) {
Assert.notNull(deferredResult, "DeferredResult is required"); Assert.notNull(deferredResult, "DeferredResult must not be null");
startAsyncProcessing(processingContext); Long timeout = deferredResult.getTimeoutMilliseconds();
if (timeout != null) {
this.asyncWebRequest.setTimeout(timeout);
}
this.asyncWebRequest.addCompletionHandler(new Runnable() { this.asyncWebRequest.addCompletionHandler(new Runnable() {
public void run() { public void run() {
@ -283,6 +280,8 @@ public final class WebAsyncManager {
}); });
} }
startAsyncProcessing(processingContext);
deferredResult.setResultHandler(new DeferredResultHandler() { deferredResult.setResultHandler(new DeferredResultHandler() {
public void handleResult(Object result) { public void handleResult(Object result) {
@ -300,13 +299,13 @@ public final class WebAsyncManager {
}); });
} }
private void startAsyncProcessing(Object... context) { private void startAsyncProcessing(Object[] processingContext) {
Assert.state(this.asyncWebRequest != null, "AsyncWebRequest was not set"); Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null");
this.asyncWebRequest.startAsync(); this.asyncWebRequest.startAsync();
this.concurrentResult = null; this.concurrentResult = null;
this.concurrentResultContext = context; this.concurrentResultContext = processingContext;
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
HttpServletRequest request = asyncWebRequest.getNativeRequest(HttpServletRequest.class); HttpServletRequest request = asyncWebRequest.getNativeRequest(HttpServletRequest.class);
@ -315,11 +314,38 @@ public final class WebAsyncManager {
} }
} }
/**
* Register an {@link WebAsyncThreadInitializer} for the current request. It may
* later be accessed and applied via {@link #initializeAsyncThread(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, WebAsyncThreadInitializer initializer) {
Assert.notNull(initializer, "WebAsyncThreadInitializer must not be null");
this.threadInitializers.put(key, initializer);
}
/**
* Invoke the {@linkplain WebAsyncThreadInitializer#initialize() initialize()}
* method of the named {@link WebAsyncThreadInitializer}.
* @param key the key under which the initializer was registered
* @return whether an initializer was found and applied
*/
public boolean initializeAsyncThread(Object key) {
WebAsyncThreadInitializer initializer = this.threadInitializers.get(key);
if (initializer != null) {
initializer.initialize();
return true;
}
return false;
}
/** /**
* A contract for initializing and resetting a thread. * Initialize and reset thread-bound variables.
*/ */
public interface AsyncThreadInitializer { public interface WebAsyncThreadInitializer {
void initialize(); void initialize();

10
spring-web/src/main/java/org/springframework/web/context/request/async/package-info.java

@ -0,0 +1,10 @@
/**
*
* Support for asynchronous request processing.
*
* @since 3.2
*
*/
package org.springframework.web.context.request.async;

4
spring-web/src/test/java/org/springframework/web/context/request/async/DeferredResultTests.java

@ -108,7 +108,7 @@ public class DeferredResultTests {
@Test @Test
public void hasTimeout() { public void hasTimeout() {
assertFalse(new DeferredResult<String>().hasTimeoutResult()); assertFalse(new DeferredResult<String>().hasTimeoutResult());
assertTrue(new DeferredResult<String>("timed out").hasTimeoutResult()); assertTrue(new DeferredResult<String>(null, "timed out").hasTimeoutResult());
} }
@Test @Test
@ -117,7 +117,7 @@ public class DeferredResultTests {
handler.handleResult("timed out"); handler.handleResult("timed out");
replay(handler); replay(handler);
DeferredResult<String> result = new DeferredResult<String>("timed out"); DeferredResult<String> result = new DeferredResult<String>(null, "timed out");
result.setResultHandler(handler); result.setResultHandler(handler);
assertTrue(result.applyTimeoutResult()); assertTrue(result.applyTimeoutResult());

6
spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java

@ -135,4 +135,10 @@ public class StandardServletAsyncWebRequestTests {
verify(timeoutHandler); verify(timeoutHandler);
} }
@Test(expected=IllegalStateException.class)
public void setTimeoutDuringConcurrentHandling() {
this.asyncRequest.startAsync();
this.asyncRequest.setTimeout(25L);
}
} }

192
spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTests.java

@ -16,26 +16,26 @@
package org.springframework.web.context.request.async; package org.springframework.web.context.request.async;
import static org.hamcrest.Matchers.containsString; import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.createStrictMock;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.notNull;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.reset;
import static org.easymock.EasyMock.verify;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; 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.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.web.context.request.async.WebAsyncManager.WebAsyncThreadInitializer;
import org.springframework.web.context.request.ServletWebRequest;
/** /**
@ -47,169 +47,141 @@ public class WebAsyncManagerTests {
private WebAsyncManager asyncManager; private WebAsyncManager asyncManager;
private MockHttpServletRequest request; private AsyncWebRequest asyncWebRequest;
private StubAsyncWebRequest stubAsyncWebRequest;
@Before @Before
public void setUp() { public void setUp() {
this.request = new MockHttpServletRequest(); this.asyncManager = AsyncWebUtils.getAsyncManager(new MockHttpServletRequest());
this.stubAsyncWebRequest = new StubAsyncWebRequest(this.request, new MockHttpServletResponse());
this.asyncManager = AsyncWebUtils.getAsyncManager(this.request);
this.asyncManager.setTaskExecutor(new SyncTaskExecutor()); this.asyncManager.setTaskExecutor(new SyncTaskExecutor());
this.asyncManager.setAsyncWebRequest(this.stubAsyncWebRequest);
}
@Test this.asyncWebRequest = createStrictMock(AsyncWebRequest.class);
public void getForCurrentRequest() throws Exception { this.asyncWebRequest.addCompletionHandler((Runnable) notNull());
assertNotNull(this.asyncManager); replay(this.asyncWebRequest);
assertSame(this.asyncManager, AsyncWebUtils.getAsyncManager(this.request));
assertSame(this.asyncManager, this.request.getAttribute(AsyncWebUtils.WEB_ASYNC_MANAGER_ATTRIBUTE)); this.asyncManager.setAsyncWebRequest(this.asyncWebRequest);
verify(this.asyncWebRequest);
reset(this.asyncWebRequest);
} }
@Test @Test
public void isConcurrentHandlingStarted() { public void isConcurrentHandlingStarted() {
expect(this.asyncWebRequest.isAsyncStarted()).andReturn(false);
replay(this.asyncWebRequest);
assertFalse(this.asyncManager.isConcurrentHandlingStarted()); assertFalse(this.asyncManager.isConcurrentHandlingStarted());
this.stubAsyncWebRequest.startAsync(); verify(this.asyncWebRequest);
reset(this.asyncWebRequest);
expect(this.asyncWebRequest.isAsyncStarted()).andReturn(true);
replay(this.asyncWebRequest);
assertTrue(this.asyncManager.isConcurrentHandlingStarted()); assertTrue(this.asyncManager.isConcurrentHandlingStarted());
verify(this.asyncWebRequest);
} }
@Test(expected=IllegalArgumentException.class) @Test(expected=IllegalArgumentException.class)
public void setAsyncWebRequestAfterAsyncStarted() { public void setAsyncWebRequestAfterAsyncStarted() {
this.stubAsyncWebRequest.startAsync(); this.asyncWebRequest.startAsync();
this.asyncManager.setAsyncWebRequest(null); this.asyncManager.setAsyncWebRequest(null);
} }
@Test @Test
public void startCallableChainProcessing() throws Exception { public void startCallableProcessing() throws Exception {
WebAsyncThreadInitializer initializer = createStrictMock(WebAsyncThreadInitializer.class);
initializer.initialize();
initializer.reset();
replay(initializer);
this.asyncWebRequest.startAsync();
expect(this.asyncWebRequest.isAsyncComplete()).andReturn(false);
this.asyncWebRequest.dispatch();
replay(this.asyncWebRequest);
this.asyncManager.registerAsyncThreadInitializer("testInitializer", initializer);
this.asyncManager.startCallableProcessing(new Callable<Object>() { this.asyncManager.startCallableProcessing(new Callable<Object>() {
public Object call() throws Exception { public Object call() throws Exception {
return 1; return 1;
} }
}); });
assertTrue(this.asyncManager.isConcurrentHandlingStarted()); verify(initializer, this.asyncWebRequest);
assertTrue(this.stubAsyncWebRequest.isDispatched());
} }
@Test @Test
public void startCallableChainProcessingStaleRequest() { public void startCallableProcessingAsyncTask() {
this.stubAsyncWebRequest.setAsyncComplete(true);
this.asyncManager.startCallableProcessing(new Callable<Object>() { AsyncTaskExecutor executor = createMock(AsyncTaskExecutor.class);
public Object call() throws Exception { expect(executor.submit((Runnable) notNull())).andReturn(null);
return 1; replay(executor);
}
}); this.asyncWebRequest.setTimeout(1000L);
this.asyncWebRequest.startAsync();
replay(this.asyncWebRequest);
AsyncTask asyncTask = new AsyncTask(1000L, executor, createMock(Callable.class));
this.asyncManager.startCallableProcessing(asyncTask);
assertFalse(this.stubAsyncWebRequest.isDispatched()); verify(executor, this.asyncWebRequest);
} }
@Test @Test
public void startCallableChainProcessingCallableRequired() { public void startCallableProcessingNullCallable() {
try { try {
this.asyncManager.startCallableProcessing(null); this.asyncManager.startCallableProcessing((Callable<?>) null);
fail("Expected exception"); fail("Expected exception");
} }
catch (IllegalArgumentException ex) { catch (IllegalArgumentException ex) {
assertEquals(ex.getMessage(), "Callable is required"); assertEquals(ex.getMessage(), "Callable must not be null");
} }
} }
@Test @Test
public void startCallableChainProcessingAsyncWebRequestRequired() { public void startCallableProcessingNullRequest() {
this.request.removeAttribute(AsyncWebUtils.WEB_ASYNC_MANAGER_ATTRIBUTE); WebAsyncManager manager = AsyncWebUtils.getAsyncManager(new MockHttpServletRequest());
this.asyncManager = AsyncWebUtils.getAsyncManager(this.request);
try { try {
this.asyncManager.startCallableProcessing(new Callable<Object>() { manager.startCallableProcessing(new Callable<Object>() {
public Object call() throws Exception { public Object call() throws Exception {
return null; return 1;
} }
}); });
fail("Expected exception"); fail("Expected exception");
} }
catch (IllegalStateException ex) { catch (IllegalStateException ex) {
assertEquals(ex.getMessage(), "AsyncWebRequest was not set"); assertEquals(ex.getMessage(), "AsyncWebRequest must not be null");
} }
} }
@Test @Test
public void startDeferredResultProcessing() throws Exception { public void startDeferredResultProcessing() throws Exception {
DeferredResult<Integer> deferredResult = new DeferredResult<Integer>();
this.asyncManager.startDeferredResultProcessing(deferredResult);
assertTrue(this.asyncManager.isConcurrentHandlingStarted());
deferredResult.setResult(25); this.asyncWebRequest.setTimeout(1000L);
assertEquals(25, this.asyncManager.getConcurrentResult()); this.asyncWebRequest.addCompletionHandler((Runnable) notNull());
} this.asyncWebRequest.setTimeoutHandler((Runnable) notNull());
this.asyncWebRequest.startAsync();
replay(this.asyncWebRequest);
@Test DeferredResult<Integer> deferredResult = new DeferredResult<Integer>(1000L, 10);
public void startDeferredResultProcessingStaleRequest() throws Exception {
DeferredResult<Integer> deferredResult = new DeferredResult<Integer>();
this.asyncManager.startDeferredResultProcessing(deferredResult); this.asyncManager.startDeferredResultProcessing(deferredResult);
this.stubAsyncWebRequest.setAsyncComplete(true); verify(this.asyncWebRequest);
assertFalse(deferredResult.setResult(1)); reset(this.asyncWebRequest);
}
@Test expect(this.asyncWebRequest.isAsyncComplete()).andReturn(false);
public void startDeferredResultProcessingDeferredResultRequired() { this.asyncWebRequest.dispatch();
try { replay(this.asyncWebRequest);
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() { deferredResult.setResult(25);
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) { assertEquals(25, this.asyncManager.getConcurrentResult());
} verify(this.asyncWebRequest);
} }
@SuppressWarnings("serial") @SuppressWarnings("serial")
private static class SyncTaskExecutor extends SimpleAsyncTaskExecutor { private static class SyncTaskExecutor extends SimpleAsyncTaskExecutor {

6
spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java

@ -49,7 +49,7 @@ import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.request.async.AsyncWebUtils; import org.springframework.web.context.request.async.AsyncWebUtils;
import org.springframework.web.context.request.async.WebAsyncManager; import org.springframework.web.context.request.async.WebAsyncManager;
import org.springframework.web.context.request.async.WebAsyncManager.AsyncThreadInitializer; import org.springframework.web.context.request.async.WebAsyncManager.WebAsyncThreadInitializer;
import org.springframework.web.context.support.ServletRequestHandledEvent; import org.springframework.web.context.support.ServletRequestHandledEvent;
import org.springframework.web.context.support.WebApplicationContextUtils; import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.context.support.XmlWebApplicationContext; import org.springframework.web.context.support.XmlWebApplicationContext;
@ -991,9 +991,9 @@ public abstract class FrameworkServlet extends HttpServletBean {
} }
} }
private AsyncThreadInitializer createAsyncThreadInitializer(final HttpServletRequest request) { private WebAsyncThreadInitializer createAsyncThreadInitializer(final HttpServletRequest request) {
return new AsyncThreadInitializer() { return new WebAsyncThreadInitializer() {
public void initialize() { public void initialize() {
initContextHolders(request, buildLocaleContext(request), new ServletRequestAttributes(request)); initContextHolders(request, buildLocaleContext(request), new ServletRequestAttributes(request));
} }

73
spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AsyncMethodReturnValueHandler.java

@ -1,73 +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.servlet.mvc.method.annotation;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;
import javax.servlet.ServletRequest;
import org.springframework.core.MethodParameter;
import org.springframework.util.Assert;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.async.DeferredResult;
import org.springframework.web.context.request.async.WebAsyncManager;
import org.springframework.web.context.request.async.AsyncWebUtils;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* Handles return values of type {@link Callable} and {@link DeferredResult}.
*
* <p>This handler does not have a defined behavior for {@code null} return
* values and will raise an {@link IllegalArgumentException}.
*
* @author Rossen Stoyanchev
* @since 3.2
*/
public class AsyncMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
public boolean supportsReturnType(MethodParameter returnType) {
Class<?> paramType = returnType.getParameterType();
return Callable.class.isAssignableFrom(paramType) || DeferredResult.class.isAssignableFrom(paramType);
}
@SuppressWarnings("unchecked")
public void handleReturnValue(Object returnValue,
MethodParameter returnType, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest) throws Exception {
Assert.notNull(returnValue, "A Callable or a DeferredValue is required");
Class<?> paramType = returnType.getParameterType();
ServletRequest servletRequest = webRequest.getNativeRequest(ServletRequest.class);
WebAsyncManager asyncManager = AsyncWebUtils.getAsyncManager(servletRequest);
if (Callable.class.isAssignableFrom(paramType)) {
asyncManager.startCallableProcessing((Callable<Object>) returnValue, mavContainer);
}
else if (DeferredResult.class.isAssignableFrom(paramType)) {
asyncManager.startDeferredResultProcessing((DeferredResult<?>) returnValue, mavContainer);
}
else {
// should never happen..
Method method = returnType.getMethod();
throw new UnsupportedOperationException("Unknown return value: " + paramType + " in method: " + method);
}
}
}

60
spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AsyncTaskMethodReturnValueHandler.java

@ -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.servlet.mvc.method.annotation;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.core.MethodParameter;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.async.AsyncTask;
import org.springframework.web.context.request.async.AsyncWebUtils;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* Handles return values of type {@link AsyncTask}.
*
* @author Rossen Stoyanchev
* @since 3.2
*/
public class AsyncTaskMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
private final BeanFactory beanFactory;
public AsyncTaskMethodReturnValueHandler(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
public boolean supportsReturnType(MethodParameter returnType) {
Class<?> paramType = returnType.getParameterType();
return AsyncTask.class.isAssignableFrom(paramType);
}
public void handleReturnValue(Object returnValue,
MethodParameter returnType, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest) throws Exception {
if (returnValue == null) {
return;
}
AsyncTask asyncTask = (AsyncTask) returnValue;
asyncTask.setBeanFactory(this.beanFactory);
AsyncWebUtils.getAsyncManager(webRequest).startCallableProcessing(asyncTask.getCallable(), mavContainer);
}
}

52
spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/CallableMethodReturnValueHandler.java

@ -0,0 +1,52 @@
/*
* Copyright 2002-2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.mvc.method.annotation;
import java.util.concurrent.Callable;
import org.springframework.core.MethodParameter;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.async.AsyncWebUtils;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* Handles return values of type {@link Callable}.
*
* @author Rossen Stoyanchev
* @since 3.2
*/
public class CallableMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
public boolean supportsReturnType(MethodParameter returnType) {
Class<?> paramType = returnType.getParameterType();
return Callable.class.isAssignableFrom(paramType);
}
public void handleReturnValue(Object returnValue,
MethodParameter returnType, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest) throws Exception {
if (returnValue == null) {
return;
}
Callable<?> callable = (Callable<?>) returnValue;
AsyncWebUtils.getAsyncManager(webRequest).startCallableProcessing(callable, mavContainer);
}
}

51
spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultMethodReturnValueHandler.java

@ -0,0 +1,51 @@
/*
* Copyright 2002-2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.mvc.method.annotation;
import org.springframework.core.MethodParameter;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.async.AsyncWebUtils;
import org.springframework.web.context.request.async.DeferredResult;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* Handles return values of type {@link DeferredResult}.
*
* @author Rossen Stoyanchev
* @since 3.2
*/
public class DeferredResultMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
public boolean supportsReturnType(MethodParameter returnType) {
Class<?> paramType = returnType.getParameterType();
return DeferredResult.class.isAssignableFrom(paramType);
}
public void handleReturnValue(Object returnValue,
MethodParameter returnType, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest) throws Exception {
if (returnValue == null) {
return;
}
DeferredResult<?> deferredResult = (DeferredResult<?>) returnValue;
AsyncWebUtils.getAsyncManager(webRequest).startDeferredResultProcessing(deferredResult, mavContainer);
}
}

30
spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java

@ -16,7 +16,6 @@
package org.springframework.web.servlet.mvc.method.annotation; package org.springframework.web.servlet.mvc.method.annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -27,13 +26,11 @@ import java.util.Map.Entry;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSession;
import javax.xml.transform.Source; import javax.xml.transform.Source;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
@ -50,7 +47,6 @@ import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.xml.SourceHttpMessageConverter; import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter; import org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter;
import org.springframework.ui.ModelMap; import org.springframework.ui.ModelMap;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils.MethodFilter; import org.springframework.util.ReflectionUtils.MethodFilter;
import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.accept.ContentNegotiationManager;
@ -68,7 +64,6 @@ import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest; import org.springframework.web.context.request.WebRequest;
import org.springframework.web.context.request.async.AsyncWebRequest; import org.springframework.web.context.request.async.AsyncWebRequest;
import org.springframework.web.context.request.async.AsyncWebUtils; import org.springframework.web.context.request.async.AsyncWebUtils;
import org.springframework.web.context.request.async.NoSupportAsyncWebRequest;
import org.springframework.web.context.request.async.WebAsyncManager; import org.springframework.web.context.request.async.WebAsyncManager;
import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.HandlerMethodSelector; import org.springframework.web.method.HandlerMethodSelector;
@ -552,7 +547,9 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
handlers.add(new ModelMethodProcessor()); handlers.add(new ModelMethodProcessor());
handlers.add(new ViewMethodReturnValueHandler()); handlers.add(new ViewMethodReturnValueHandler());
handlers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.contentNegotiationManager)); handlers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.contentNegotiationManager));
handlers.add(new AsyncMethodReturnValueHandler()); handlers.add(new CallableMethodReturnValueHandler());
handlers.add(new DeferredResultMethodReturnValueHandler());
handlers.add(new AsyncTaskMethodReturnValueHandler(this.beanFactory));
// Annotation-based return value types // Annotation-based return value types
handlers.add(new ModelAttributeMethodProcessor(false)); handlers.add(new ModelAttributeMethodProcessor(false));
@ -690,7 +687,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
modelFactory.initModel(webRequest, mavContainer, requestMappingMethod); modelFactory.initModel(webRequest, mavContainer, requestMappingMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect); mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
AsyncWebRequest asyncWebRequest = createAsyncWebRequest(request, response); AsyncWebRequest asyncWebRequest = AsyncWebUtils.createAsyncWebRequest(request, response);
asyncWebRequest.setTimeout(this.asyncRequestTimeout); asyncWebRequest.setTimeout(this.asyncRequestTimeout);
final WebAsyncManager asyncManager = AsyncWebUtils.getAsyncManager(request); final WebAsyncManager asyncManager = AsyncWebUtils.getAsyncManager(request);
@ -700,7 +697,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
if (asyncManager.hasConcurrentResult()) { if (asyncManager.hasConcurrentResult()) {
Object result = asyncManager.getConcurrentResult(); Object result = asyncManager.getConcurrentResult();
mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0]; mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
asyncManager.resetConcurrentResult(); asyncManager.clearConcurrentResult();
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("Found concurrent result value [" + result + "]"); logger.debug("Found concurrent result value [" + result + "]");
@ -802,23 +799,6 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
return new ServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer()); return new ServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer());
} }
private AsyncWebRequest createAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) {
return ClassUtils.hasMethod(ServletRequest.class, "startAsync") ?
createStandardServletAsyncWebRequest(request, response) : new NoSupportAsyncWebRequest(request, response);
}
private AsyncWebRequest createStandardServletAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) {
try {
String className = "org.springframework.web.context.request.async.StandardServletAsyncWebRequest";
Class<?> clazz = ClassUtils.forName(className, this.getClass().getClassLoader());
Constructor<?> constructor = clazz.getConstructor(HttpServletRequest.class, HttpServletResponse.class);
return (AsyncWebRequest) BeanUtils.instantiateClass(constructor, request, response);
}
catch (Throwable t) {
throw new IllegalStateException("Failed to instantiate StandardServletAsyncWebRequest", t);
}
}
private ModelAndView getModelAndView(ModelAndViewContainer mavContainer, private ModelAndView getModelAndView(ModelAndViewContainer mavContainer,
ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception { ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception {

Loading…
Cancel
Save