Add interceptors for async processing

This change introduces two new interceptors with callback methods
for concurrent request handling. These interfaces are
CallableProcessingInterceptor and DeferredResultProcessingInterceptor.

Unlike a HandlerInterceptor, and its AsyncHandlerInterceptor sub-type,
which intercepts the invocation of a handler in he main request
processing thread, the two new interfaces are aimed at intercepting the
asynchronous execution of a Callable or a DeferredResult.

This allows for the registration of thread initialization logic in the
case of Callable executed with an AsyncTaskExecutor, or for centralized
tracking of the completion and/or expiration of a DeferredResult.
This commit is contained in:
Rossen Stoyanchev
2012-09-19 09:25:50 -04:00
parent 30bce7fa16
commit 57c36dd395
24 changed files with 735 additions and 267 deletions

View File

@@ -17,6 +17,7 @@
package org.springframework.orm.hibernate3.support;
import java.io.IOException;
import java.util.concurrent.Callable;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
@@ -32,9 +33,10 @@ import org.springframework.orm.hibernate3.SessionHolder;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.Assert;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.async.WebAsyncUtils;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.async.CallableProcessingInterceptor;
import org.springframework.web.context.request.async.WebAsyncManager;
import org.springframework.web.context.request.async.WebAsyncManager.WebAsyncThreadInitializer;
import org.springframework.web.context.request.async.WebAsyncUtils;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.filter.OncePerRequestFilter;
@@ -195,14 +197,14 @@ public class OpenSessionInViewFilter extends OncePerRequestFilter {
participate = true;
}
else {
if (isFirstRequest || !asyncManager.initializeAsyncThread(key)) {
if (isFirstRequest || !applySessionBindingInterceptor(asyncManager, key)) {
logger.debug("Opening single Hibernate Session in OpenSessionInViewFilter");
Session session = getSession(sessionFactory);
SessionHolder sessionHolder = new SessionHolder(session);
TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder);
WebAsyncThreadInitializer initializer = createAsyncThreadInitializer(sessionFactory, sessionHolder);
asyncManager.registerAsyncThreadInitializer(key, initializer);
asyncManager.registerCallableInterceptor(key,
new SessionBindingCallableInterceptor(sessionFactory, sessionHolder));
}
}
}
@@ -304,17 +306,40 @@ public class OpenSessionInViewFilter extends OncePerRequestFilter {
SessionFactoryUtils.closeSession(session);
}
private WebAsyncThreadInitializer createAsyncThreadInitializer(final SessionFactory sessionFactory,
final SessionHolder sessionHolder) {
private boolean applySessionBindingInterceptor(WebAsyncManager asyncManager, String key) {
if (asyncManager.getCallableInterceptor(key) == null) {
return false;
}
((SessionBindingCallableInterceptor) asyncManager.getCallableInterceptor(key)).initializeThread();
return true;
}
return new WebAsyncThreadInitializer() {
public void initialize() {
TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder);
}
public void reset() {
TransactionSynchronizationManager.unbindResource(sessionFactory);
}
};
/**
* Bind and unbind the Hibernate {@code Session} to the current thread.
*/
private static class SessionBindingCallableInterceptor implements CallableProcessingInterceptor {
private final SessionFactory sessionFactory;
private final SessionHolder sessionHolder;
public SessionBindingCallableInterceptor(SessionFactory sessionFactory, SessionHolder sessionHolder) {
this.sessionFactory = sessionFactory;
this.sessionHolder = sessionHolder;
}
public void preProcess(NativeWebRequest request, Callable<?> task) {
initializeThread();
}
private void initializeThread() {
TransactionSynchronizationManager.bindResource(this.sessionFactory, this.sessionHolder);
}
public void postProcess(NativeWebRequest request, Callable<?> task, Object concurrentResult) {
TransactionSynchronizationManager.unbindResource(this.sessionFactory);
}
}
}

View File

@@ -16,6 +16,8 @@
package org.springframework.orm.hibernate3.support;
import java.util.concurrent.Callable;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.springframework.dao.DataAccessException;
@@ -25,10 +27,11 @@ import org.springframework.orm.hibernate3.SessionHolder;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.ui.ModelMap;
import org.springframework.web.context.request.AsyncWebRequestInterceptor;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.context.request.async.WebAsyncUtils;
import org.springframework.web.context.request.async.CallableProcessingInterceptor;
import org.springframework.web.context.request.async.WebAsyncManager;
import org.springframework.web.context.request.async.WebAsyncManager.WebAsyncThreadInitializer;
import org.springframework.web.context.request.async.WebAsyncUtils;
/**
* Spring web request interceptor that binds a Hibernate <code>Session</code> to the
@@ -147,7 +150,7 @@ public class OpenSessionInViewInterceptor extends HibernateAccessor implements A
String participateAttributeName = getParticipateAttributeName();
if (asyncManager.hasConcurrentResult()) {
if (asyncManager.initializeAsyncThread(participateAttributeName)) {
if (applySessionBindingInterceptor(asyncManager, participateAttributeName)) {
return;
}
}
@@ -169,8 +172,8 @@ public class OpenSessionInViewInterceptor extends HibernateAccessor implements A
SessionHolder sessionHolder = new SessionHolder(session);
TransactionSynchronizationManager.bindResource(getSessionFactory(), sessionHolder);
WebAsyncThreadInitializer asyncThreadInitializer = createThreadInitializer(sessionHolder);
asyncManager.registerAsyncThreadInitializer(participateAttributeName, asyncThreadInitializer);
asyncManager.registerCallableInterceptor(participateAttributeName,
new SessionBindingCallableInterceptor(sessionHolder));
}
else {
// deferred close mode
@@ -261,15 +264,36 @@ public class OpenSessionInViewInterceptor extends HibernateAccessor implements A
return getSessionFactory().toString() + PARTICIPATE_SUFFIX;
}
private WebAsyncThreadInitializer createThreadInitializer(final SessionHolder sessionHolder) {
return new WebAsyncThreadInitializer() {
public void initialize() {
TransactionSynchronizationManager.bindResource(getSessionFactory(), sessionHolder);
}
public void reset() {
TransactionSynchronizationManager.unbindResource(getSessionFactory());
}
};
private boolean applySessionBindingInterceptor(WebAsyncManager asyncManager, String key) {
if (asyncManager.getCallableInterceptor(key) == null) {
return false;
}
((SessionBindingCallableInterceptor) asyncManager.getCallableInterceptor(key)).initializeThread();
return true;
}
/**
* Bind and unbind the Hibernate {@code Session} to the current thread.
*/
private class SessionBindingCallableInterceptor implements CallableProcessingInterceptor {
private final SessionHolder sessionHolder;
public SessionBindingCallableInterceptor(SessionHolder sessionHolder) {
this.sessionHolder = sessionHolder;
}
public void preProcess(NativeWebRequest request, Callable<?> task) {
initializeThread();
}
private void initializeThread() {
TransactionSynchronizationManager.bindResource(getSessionFactory(), this.sessionHolder);
}
public void postProcess(NativeWebRequest request, Callable<?> task, Object concurrentResult) {
TransactionSynchronizationManager.unbindResource(getSessionFactory());
}
}
}

View File

@@ -17,6 +17,7 @@
package org.springframework.orm.hibernate4.support;
import java.io.IOException;
import java.util.concurrent.Callable;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
@@ -32,9 +33,10 @@ import org.springframework.orm.hibernate4.SessionFactoryUtils;
import org.springframework.orm.hibernate4.SessionHolder;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.async.WebAsyncUtils;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.async.CallableProcessingInterceptor;
import org.springframework.web.context.request.async.WebAsyncManager;
import org.springframework.web.context.request.async.WebAsyncManager.WebAsyncThreadInitializer;
import org.springframework.web.context.request.async.WebAsyncUtils;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.filter.OncePerRequestFilter;
@@ -126,14 +128,14 @@ public class OpenSessionInViewFilter extends OncePerRequestFilter {
participate = true;
}
else {
if (isFirstRequest || !asyncManager.initializeAsyncThread(key)) {
if (isFirstRequest || !applySessionBindingInterceptor(asyncManager, key)) {
logger.debug("Opening Hibernate Session in OpenSessionInViewFilter");
Session session = openSession(sessionFactory);
SessionHolder sessionHolder = new SessionHolder(session);
TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder);
WebAsyncThreadInitializer initializer = createAsyncThreadInitializer(sessionFactory, sessionHolder);
asyncManager.registerAsyncThreadInitializer(key, initializer);
asyncManager.registerCallableInterceptor(key,
new SessionBindingCallableInterceptor(sessionFactory, sessionHolder));
}
}
@@ -201,17 +203,39 @@ public class OpenSessionInViewFilter extends OncePerRequestFilter {
}
}
private WebAsyncThreadInitializer createAsyncThreadInitializer(final SessionFactory sessionFactory,
final SessionHolder sessionHolder) {
return new WebAsyncThreadInitializer() {
public void initialize() {
TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder);
}
public void reset() {
TransactionSynchronizationManager.unbindResource(sessionFactory);
}
};
private boolean applySessionBindingInterceptor(WebAsyncManager asyncManager, String key) {
if (asyncManager.getCallableInterceptor(key) == null) {
return false;
}
((SessionBindingCallableInterceptor) asyncManager.getCallableInterceptor(key)).initializeThread();
return true;
}
/**
* Bind and unbind the Hibernate {@code Session} to the current thread.
*/
private static class SessionBindingCallableInterceptor implements CallableProcessingInterceptor {
private final SessionFactory sessionFactory;
private final SessionHolder sessionHolder;
public SessionBindingCallableInterceptor(SessionFactory sessionFactory, SessionHolder sessionHolder) {
this.sessionFactory = sessionFactory;
this.sessionHolder = sessionHolder;
}
public void preProcess(NativeWebRequest request, Callable<?> task) {
initializeThread();
}
private void initializeThread() {
TransactionSynchronizationManager.bindResource(this.sessionFactory, this.sessionHolder);
}
public void postProcess(NativeWebRequest request, Callable<?> task, Object concurrentResult) {
TransactionSynchronizationManager.unbindResource(this.sessionFactory);
}
}
}

View File

@@ -16,6 +16,8 @@
package org.springframework.orm.hibernate4.support;
import java.util.concurrent.Callable;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.FlushMode;
@@ -29,10 +31,11 @@ import org.springframework.orm.hibernate4.SessionHolder;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.ui.ModelMap;
import org.springframework.web.context.request.AsyncWebRequestInterceptor;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.context.request.async.WebAsyncUtils;
import org.springframework.web.context.request.async.CallableProcessingInterceptor;
import org.springframework.web.context.request.async.WebAsyncManager;
import org.springframework.web.context.request.async.WebAsyncManager.WebAsyncThreadInitializer;
import org.springframework.web.context.request.async.WebAsyncUtils;
/**
* Spring web request interceptor that binds a Hibernate <code>Session</code> to the
@@ -109,7 +112,7 @@ public class OpenSessionInViewInterceptor implements AsyncWebRequestInterceptor
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
if (asyncManager.hasConcurrentResult()) {
if (asyncManager.initializeAsyncThread(participateAttributeName)) {
if (applySessionBindingInterceptor(asyncManager, participateAttributeName)) {
return;
}
}
@@ -126,8 +129,8 @@ public class OpenSessionInViewInterceptor implements AsyncWebRequestInterceptor
SessionHolder sessionHolder = new SessionHolder(session);
TransactionSynchronizationManager.bindResource(getSessionFactory(), sessionHolder);
WebAsyncThreadInitializer asyncThreadInitializer = createThreadInitializer(sessionHolder);
asyncManager.registerAsyncThreadInitializer(participateAttributeName, asyncThreadInitializer);
asyncManager.registerCallableInterceptor(participateAttributeName,
new SessionBindingCallableInterceptor(sessionHolder));
}
}
@@ -200,15 +203,37 @@ public class OpenSessionInViewInterceptor implements AsyncWebRequestInterceptor
return getSessionFactory().toString() + PARTICIPATE_SUFFIX;
}
private WebAsyncThreadInitializer createThreadInitializer(final SessionHolder sessionHolder) {
return new WebAsyncThreadInitializer() {
public void initialize() {
TransactionSynchronizationManager.bindResource(getSessionFactory(), sessionHolder);
}
public void reset() {
TransactionSynchronizationManager.unbindResource(getSessionFactory());
}
};
private boolean applySessionBindingInterceptor(WebAsyncManager asyncManager, String key) {
if (asyncManager.getCallableInterceptor(key) == null) {
return false;
}
((SessionBindingCallableInterceptor) asyncManager.getCallableInterceptor(key)).initializeThread();
return true;
}
/**
* Bind and unbind the Hibernate {@code Session} to the current thread.
*/
private class SessionBindingCallableInterceptor implements CallableProcessingInterceptor {
private final SessionHolder sessionHolder;
public SessionBindingCallableInterceptor(SessionHolder sessionHolder) {
this.sessionHolder = sessionHolder;
}
public void preProcess(NativeWebRequest request, Callable<?> task) {
initializeThread();
}
private void initializeThread() {
TransactionSynchronizationManager.bindResource(getSessionFactory(), this.sessionHolder);
}
public void postProcess(NativeWebRequest request, Callable<?> task, Object concurrentResult) {
TransactionSynchronizationManager.unbindResource(getSessionFactory());
}
}
}

View File

@@ -17,6 +17,7 @@
package org.springframework.orm.jpa.support;
import java.io.IOException;
import java.util.concurrent.Callable;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
@@ -32,9 +33,10 @@ import org.springframework.orm.jpa.EntityManagerHolder;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.StringUtils;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.async.WebAsyncUtils;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.async.CallableProcessingInterceptor;
import org.springframework.web.context.request.async.WebAsyncManager;
import org.springframework.web.context.request.async.WebAsyncManager.WebAsyncThreadInitializer;
import org.springframework.web.context.request.async.WebAsyncUtils;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.filter.OncePerRequestFilter;
@@ -150,15 +152,14 @@ public class OpenEntityManagerInViewFilter extends OncePerRequestFilter {
participate = true;
}
else {
if (isFirstRequest || !asyncManager.initializeAsyncThread(key)) {
if (isFirstRequest || !applyEntityManagerBindingInterceptor(asyncManager, key)) {
logger.debug("Opening JPA EntityManager in OpenEntityManagerInViewFilter");
try {
EntityManager em = createEntityManager(emf);
EntityManagerHolder emHolder = new EntityManagerHolder(em);
TransactionSynchronizationManager.bindResource(emf, emHolder);
WebAsyncThreadInitializer initializer = createAsyncThreadInitializer(emf, emHolder);
asyncManager.registerAsyncThreadInitializer(key, initializer);
asyncManager.registerCallableInterceptor(key, new EntityManagerBindingCallableInterceptor(emf, emHolder));
}
catch (PersistenceException ex) {
throw new DataAccessResourceFailureException("Could not create JPA EntityManager", ex);
@@ -230,17 +231,40 @@ public class OpenEntityManagerInViewFilter extends OncePerRequestFilter {
return emf.createEntityManager();
}
private WebAsyncThreadInitializer createAsyncThreadInitializer(final EntityManagerFactory emFactory,
final EntityManagerHolder emHolder) {
private boolean applyEntityManagerBindingInterceptor(WebAsyncManager asyncManager, String key) {
if (asyncManager.getCallableInterceptor(key) == null) {
return false;
}
((EntityManagerBindingCallableInterceptor) asyncManager.getCallableInterceptor(key)).initializeThread();
return true;
}
return new WebAsyncThreadInitializer() {
public void initialize() {
TransactionSynchronizationManager.bindResource(emFactory, emHolder);
}
public void reset() {
TransactionSynchronizationManager.unbindResource(emFactory);
}
};
/**
* Bind and unbind the {@code EntityManager} to the current thread.
*/
private static class EntityManagerBindingCallableInterceptor implements CallableProcessingInterceptor {
private final EntityManagerFactory emFactory;
private final EntityManagerHolder emHolder;
public EntityManagerBindingCallableInterceptor(EntityManagerFactory emFactory, EntityManagerHolder emHolder) {
this.emFactory = emFactory;
this.emHolder = emHolder;
}
public void preProcess(NativeWebRequest request, Callable<?> task) {
initializeThread();
}
private void initializeThread() {
TransactionSynchronizationManager.bindResource(this.emFactory, this.emHolder);
}
public void postProcess(NativeWebRequest request, Callable<?> task, Object concurrentResult) {
TransactionSynchronizationManager.unbindResource(this.emFactory);
}
}
}

View File

@@ -16,6 +16,8 @@
package org.springframework.orm.jpa.support;
import java.util.concurrent.Callable;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceException;
@@ -27,10 +29,11 @@ import org.springframework.orm.jpa.EntityManagerHolder;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.ui.ModelMap;
import org.springframework.web.context.request.AsyncWebRequestInterceptor;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.context.request.async.WebAsyncUtils;
import org.springframework.web.context.request.async.CallableProcessingInterceptor;
import org.springframework.web.context.request.async.WebAsyncManager;
import org.springframework.web.context.request.async.WebAsyncManager.WebAsyncThreadInitializer;
import org.springframework.web.context.request.async.WebAsyncUtils;
/**
* Spring web request interceptor that binds a JPA EntityManager to the
@@ -76,7 +79,7 @@ public class OpenEntityManagerInViewInterceptor extends EntityManagerFactoryAcce
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
if (asyncManager.hasConcurrentResult()) {
if (asyncManager.initializeAsyncThread(participateAttributeName)) {
if (applyCallableInterceptor(asyncManager, participateAttributeName)) {
return;
}
}
@@ -94,8 +97,8 @@ public class OpenEntityManagerInViewInterceptor extends EntityManagerFactoryAcce
EntityManagerHolder emHolder = new EntityManagerHolder(em);
TransactionSynchronizationManager.bindResource(getEntityManagerFactory(), emHolder);
WebAsyncThreadInitializer asyncThreadInitializer = createThreadInitializer(emHolder);
asyncManager.registerAsyncThreadInitializer(participateAttributeName, asyncThreadInitializer);
asyncManager.registerCallableInterceptor(participateAttributeName,
new EntityManagerBindingCallableInterceptor(emHolder));
}
catch (PersistenceException ex) {
throw new DataAccessResourceFailureException("Could not create JPA EntityManager", ex);
@@ -147,15 +150,39 @@ public class OpenEntityManagerInViewInterceptor extends EntityManagerFactoryAcce
return getEntityManagerFactory().toString() + PARTICIPATE_SUFFIX;
}
private WebAsyncThreadInitializer createThreadInitializer(final EntityManagerHolder emHolder) {
return new WebAsyncThreadInitializer() {
public void initialize() {
TransactionSynchronizationManager.bindResource(getEntityManagerFactory(), emHolder);
}
public void reset() {
TransactionSynchronizationManager.unbindResource(getEntityManagerFactory());
}
};
private boolean applyCallableInterceptor(WebAsyncManager asyncManager, String key) {
if (asyncManager.getCallableInterceptor(key) == null) {
return false;
}
((EntityManagerBindingCallableInterceptor) asyncManager.getCallableInterceptor(key)).initializeThread();
return true;
}
/**
* Bind and unbind the Hibernate {@code Session} to the current thread.
*/
private class EntityManagerBindingCallableInterceptor implements CallableProcessingInterceptor {
private final EntityManagerHolder emHolder;
public EntityManagerBindingCallableInterceptor(EntityManagerHolder emHolder) {
this.emHolder = emHolder;
}
public void preProcess(NativeWebRequest request, Callable<?> task) {
initializeThread();
}
private void initializeThread() {
TransactionSynchronizationManager.bindResource(getEntityManagerFactory(), this.emHolder);
}
public void postProcess(NativeWebRequest request, Callable<?> task, Object concurrentResult) {
TransactionSynchronizationManager.unbindResource(getEntityManagerFactory());
}
}
}

View File

@@ -181,6 +181,7 @@ public class OpenSessionInViewTests {
replay(asyncWebRequest);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(this.request);
asyncManager.setTaskExecutor(new SyncTaskExecutor());
asyncManager.setAsyncWebRequest(asyncWebRequest);
asyncManager.startCallableProcessing(new Callable<String>() {
@@ -196,10 +197,6 @@ public class OpenSessionInViewTests {
// Async dispatch thread
reset(asyncWebRequest);
expect(asyncWebRequest.isDispatched()).andReturn(true);
replay(asyncWebRequest);
interceptor.preHandle(this.webRequest);
assertTrue("Session not bound to async thread", TransactionSynchronizationManager.hasResource(sf));
@@ -496,10 +493,10 @@ public class OpenSessionInViewTests {
asyncWebRequest.addCompletionHandler((Runnable) anyObject());
asyncWebRequest.startAsync();
expect(asyncWebRequest.isAsyncStarted()).andReturn(true).anyTimes();
expect(asyncWebRequest.isDispatched()).andReturn(false).anyTimes();
replay(asyncWebRequest);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(this.request);
asyncManager.setTaskExecutor(new SyncTaskExecutor());
asyncManager.setAsyncWebRequest(asyncWebRequest);
asyncManager.startCallableProcessing(new Callable<String>() {
public String call() throws Exception {
@@ -524,7 +521,6 @@ public class OpenSessionInViewTests {
expect(session.close()).andReturn(null);
expect(asyncWebRequest.isAsyncStarted()).andReturn(false).anyTimes();
expect(asyncWebRequest.isDispatched()).andReturn(true).anyTimes();
replay(sf);
replay(session);

View File

@@ -38,6 +38,7 @@ import javax.servlet.ServletResponse;
import junit.framework.TestCase;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.mock.web.MockFilterConfig;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
@@ -158,6 +159,7 @@ public class OpenEntityManagerInViewTests extends TestCase {
replay(asyncWebRequest);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(webRequest);
asyncManager.setTaskExecutor(new SyncTaskExecutor());
asyncManager.setAsyncWebRequest(asyncWebRequest);
asyncManager.startCallableProcessing(new Callable<String>() {
public String call() throws Exception {
@@ -172,10 +174,6 @@ public class OpenEntityManagerInViewTests extends TestCase {
// Async dispatch thread
reset(asyncWebRequest);
expect(asyncWebRequest.isDispatched()).andReturn(true);
replay(asyncWebRequest);
reset(manager, factory);
replay(manager, factory);
@@ -348,10 +346,10 @@ public class OpenEntityManagerInViewTests extends TestCase {
asyncWebRequest.addCompletionHandler((Runnable) anyObject());
asyncWebRequest.startAsync();
expect(asyncWebRequest.isAsyncStarted()).andReturn(true).anyTimes();
expect(asyncWebRequest.isDispatched()).andReturn(false).anyTimes();
replay(asyncWebRequest);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.setTaskExecutor(new SyncTaskExecutor());
asyncManager.setAsyncWebRequest(asyncWebRequest);
asyncManager.startCallableProcessing(new Callable<String>() {
public String call() throws Exception {
@@ -372,7 +370,6 @@ public class OpenEntityManagerInViewTests extends TestCase {
reset(asyncWebRequest);
expect(asyncWebRequest.isAsyncStarted()).andReturn(false).anyTimes();
expect(asyncWebRequest.isDispatched()).andReturn(true).anyTimes();
replay(asyncWebRequest);
assertFalse(TransactionSynchronizationManager.hasResource(factory));
@@ -389,4 +386,12 @@ public class OpenEntityManagerInViewTests extends TestCase {
wac.close();
}
@SuppressWarnings("serial")
private static class SyncTaskExecutor extends SimpleAsyncTaskExecutor {
@Override
public void execute(Runnable task, long startTimeout) {
task.run();
}
}
}

View File

@@ -41,6 +41,11 @@ public interface AsyncWebRequest extends NativeWebRequest {
*/
void setTimeoutHandler(Runnable runnable);
/**
* Add a Runnable to be invoked when request processing completes.
*/
void addCompletionHandler(Runnable runnable);
/**
* Mark the start of asynchronous request processing so that when the main
* processing thread exits, the response remains open for further processing
@@ -62,17 +67,6 @@ public interface AsyncWebRequest extends NativeWebRequest {
*/
void dispatch();
/**
* Whether the request was dispatched to the container in order to resume
* processing after concurrent execution in an application thread.
*/
boolean isDispatched();
/**
* Add a Runnable to be invoked when request processing completes.
*/
void addCompletionHandler(Runnable runnable);
/**
* Whether asynchronous processing has completed.
*/

View File

@@ -0,0 +1,64 @@
/*
* 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.Collection;
import java.util.List;
import java.util.concurrent.Callable;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.web.context.request.NativeWebRequest;
/**
* Assists with the invocation of {@link CallableProcessingInterceptor}'s.
*
* @author Rossen Stoyanchev
* @since 3.2
*/
class CallableInterceptorChain {
private static Log logger = LogFactory.getLog(CallableInterceptorChain.class);
private final List<CallableProcessingInterceptor> interceptors;
private int interceptorIndex = -1;
public CallableInterceptorChain(Collection<CallableProcessingInterceptor> interceptors) {
this.interceptors = new ArrayList<CallableProcessingInterceptor>(interceptors);
}
public void applyPreProcess(NativeWebRequest request, Callable<?> task) throws Exception {
for (int i = 0; i < this.interceptors.size(); i++) {
this.interceptors.get(i).preProcess(request, task);
this.interceptorIndex = i;
}
}
public void applyPostProcess(NativeWebRequest request, Callable<?> task, Object concurrentResult) {
for (int i = this.interceptorIndex; i >= 0; i--) {
try {
this.interceptors.get(i).postProcess(request, task, concurrentResult);
}
catch (Exception ex) {
logger.error("CallableProcessingInterceptor.postProcess threw exception", ex);
}
}
}
}

View File

@@ -0,0 +1,67 @@
/*
* 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.core.task.AsyncTaskExecutor;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.WebRequestInterceptor;
/**
* Intercepts concurrent request handling, where the concurrent result is
* obtained by executing a {@link Callable} on behalf of the application with an
* {@link AsyncTaskExecutor}.
* <p>
* A {@code CallableProcessingInterceptor} is invoked before and after the
* invocation of the {@code Callable} task in the asynchronous thread.
*
* <p>A {@code CallableProcessingInterceptor} may be registered as follows:
* <pre>
* CallableProcessingInterceptor interceptor = ... ;
* WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
* asyncManager.registerCallableInterceptor("key", interceptor);
* </pre>
*
* <p>To register an interceptor for every request, the above can be done through
* a {@link WebRequestInterceptor} during pre-handling.
*
* @author Rossen Stoyanchev
* @since 3.2
*/
public interface CallableProcessingInterceptor {
/**
* Invoked from the asynchronous thread in which the {@code Callable} is
* executed, before the {@code Callable} is invoked.
*
* @param request the current request
* @param task the task that will produce a result
*/
void preProcess(NativeWebRequest request, Callable<?> task) throws Exception;
/**
* Invoked from the asynchronous thread in which the {@code Callable} is
* executed, after the {@code Callable} returned a result.
*
* @param request the current request
* @param task the task that produced the result
* @param concurrentResult the result of concurrent processing, which could
* be a {@link Throwable} if the {@code Callable} raised an exception
*/
void postProcess(NativeWebRequest request, Callable<?> task, Object concurrentResult) throws Exception;
}

View File

@@ -25,9 +25,9 @@ import org.apache.commons.logging.LogFactory;
/**
* {@code DeferredResult} provides an alternative to using a {@link Callable}
* for asynchronous request processing. While a Callable is executed concurrently
* on behalf of the application, with a DeferredResult the application can produce
* the result from a thread of its choice.
* for asynchronous request processing. While a {@code Callable} is executed
* concurrently on behalf of the application, with a {@code DeferredResult} the
* application can produce the result from a thread of its choice.
*
* @author Rossen Stoyanchev
* @since 3.2
@@ -45,8 +45,6 @@ public final class DeferredResult<T> {
private DeferredResultHandler resultHandler;
private Object result = RESULT_NONE;
private final AtomicBoolean expired = new AtomicBoolean(false);
private final Object lock = new Object();
@@ -79,7 +77,6 @@ public final class DeferredResult<T> {
this.timeout = timeout;
}
/**
* Return the configured timeout value in milliseconds.
*/
@@ -88,8 +85,12 @@ public final class DeferredResult<T> {
}
/**
* Set a handler to handle the result when set. Normally applications do not
* use this method at runtime but may do so during testing.
* Set a handler to handle the result when set. There can be only handler
* for a {@code DeferredResult}. At runtime it will be set by the framework.
* However applications may set it when unit testing.
*
* <p>If you need to be called back when a {@code DeferredResult} is set or
* expires, register a {@link DeferredResultProcessingInterceptor} instead.
*/
public void setResultHandler(DeferredResultHandler resultHandler) {
this.resultHandler = resultHandler;
@@ -122,14 +123,14 @@ public final class DeferredResult<T> {
}
private boolean processResult(Object result) {
synchronized (this.lock) {
if (isSetOrExpired()) {
boolean wasExpired = getAndSetExpired();
if (wasExpired) {
return false;
}
this.result = result;
if (!awaitResultHandler()) {
throw new IllegalStateException("DeferredResultHandler not set");
}
@@ -156,15 +157,24 @@ public final class DeferredResult<T> {
}
/**
* Whether the DeferredResult can no longer be set either because the async
* request expired or because it was already set.
* Return {@code true} if this DeferredResult is no longer usable either
* because it was previously set or because the underlying request ended
* before it could be set.
* <p>
* The result may have been set with a call to {@link #setResult(Object)},
* or {@link #setErrorResult(Object)}, or following a timeout, assuming a
* timeout result was provided to the constructor. The request may before
* the result set due to a timeout or network error.
*/
public boolean isSetOrExpired() {
return (this.expired.get() || (this.result != RESULT_NONE));
return this.expired.get();
}
void setExpired() {
this.expired.set(true);
/**
* Atomically set the expired flag and return its previous value.
*/
boolean getAndSetExpired() {
return this.expired.getAndSet(true);
}
boolean hasTimeoutResult() {

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2002-2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.context.request.async;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.web.context.request.NativeWebRequest;
/**
* Assists with the invocation of {@link DeferredResultProcessingInterceptor}'s.
*
* @author Rossen Stoyanchev
* @since 3.2
*/
class DeferredResultInterceptorChain {
private static Log logger = LogFactory.getLog(DeferredResultInterceptorChain.class);
private final List<DeferredResultProcessingInterceptor> interceptors;
public DeferredResultInterceptorChain(Collection<DeferredResultProcessingInterceptor> interceptors) {
this.interceptors = new ArrayList<DeferredResultProcessingInterceptor>(interceptors);
}
public void applyPreProcess(NativeWebRequest request, DeferredResult<?> task) throws Exception {
for (DeferredResultProcessingInterceptor interceptor : this.interceptors) {
interceptor.preProcess(request, task);
}
}
public void applyPostProcess(NativeWebRequest request, DeferredResult<?> task, Object concurrentResult) {
for (int i = this.interceptors.size()-1; i >= 0; i--) {
try {
this.interceptors.get(i).postProcess(request, task, concurrentResult);
}
catch (Exception ex) {
logger.error("DeferredResultProcessingInterceptor.postProcess threw exception", ex);
}
}
}
public void triggerAfterExpiration(NativeWebRequest request, DeferredResult<?> task) {
for (int i = this.interceptors.size()-1; i >= 0; i--) {
try {
this.interceptors.get(i).afterExpiration(request, task);
}
catch (Exception ex) {
logger.error("DeferredResultProcessingInterceptor.afterExpiration threw exception", ex);
}
}
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2002-2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.context.request.async;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.WebRequestInterceptor;
/**
* Intercepts concurrent request handling, where the concurrent result is
* obtained by waiting for a {@link DeferredResult} to be set from a thread
* chosen by the application (e.g. in response to some external event).
*
* <p>A {@code DeferredResultProcessingInterceptor} is invoked before the start of
* asynchronous processing and either when the {@code DeferredResult} is set or
* when when the underlying request ends, whichever comes fist.
*
* <p>A {@code DeferredResultProcessingInterceptor} may be registered as follows:
* <pre>
* DeferredResultProcessingInterceptor interceptor = ... ;
* WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
* asyncManager.registerDeferredResultInterceptor("key", interceptor);
* </pre>
*
* <p>To register an interceptor for every request, the above can be done through
* a {@link WebRequestInterceptor} during pre-handling.
*
* @author Rossen Stoyanchev
* @since 3.2
*/
public interface DeferredResultProcessingInterceptor {
/**
* Invoked before the start of concurrent handling using a
* {@link DeferredResult}. The invocation occurs in the thread that
* initiated concurrent handling.
*
* @param request the current request
* @param deferredResult the DeferredResult instance
*/
void preProcess(NativeWebRequest request, DeferredResult<?> deferredResult) throws Exception;
/**
* Invoked when a {@link DeferredResult} is set either with a normal value
* or with a {@link DeferredResult#DeferredResult(Long, Object) timeout
* result}. The invocation occurs in the thread that set the result.
* <p>
* If the request ends before the {@code DeferredResult} is set, then
* {@link #afterExpiration(NativeWebRequest, DeferredResult)} is called.
*
* @param request the current request
* @param deferredResult the DeferredResult that has been set
* @param concurrentResult the result to which the {@code DeferredResult}
* was set
*/
void postProcess(NativeWebRequest request, DeferredResult<?> deferredResult,
Object concurrentResult) throws Exception;
/**
* Invoked when a {@link DeferredResult} expires before a result has been
* set possibly due to a timeout or a network error. This invocation occurs
* in the thread where the timeout or network error notification is
* processed.
*
* @param request the current request
* @param deferredResult the DeferredResult that has been set
*/
void afterExpiration(NativeWebRequest request, DeferredResult<?> deferredResult) throws Exception;
}

View File

@@ -49,10 +49,6 @@ public class NoSupportAsyncWebRequest extends ServletWebRequest implements Async
return false;
}
public boolean isDispatched() {
return false;
}
// Not supported
public void startAsync() {

View File

@@ -24,7 +24,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.DispatcherType;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -94,10 +93,6 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
return ((this.asyncContext != null) && getRequest().isAsyncStarted());
}
public boolean isDispatched() {
return (DispatcherType.ASYNC.equals(getRequest().getDispatcherType()));
}
/**
* Whether async request processing has completed.
* <p>It is important to avoid use of request and response objects after async

View File

@@ -15,10 +15,7 @@
*/
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;
@@ -73,7 +70,11 @@ public final class WebAsyncManager {
private Object[] concurrentResultContext;
private final Map<Object, WebAsyncThreadInitializer> threadInitializers = new LinkedHashMap<Object, WebAsyncThreadInitializer>();
private final Map<Object, CallableProcessingInterceptor> callableInterceptors =
new LinkedHashMap<Object, CallableProcessingInterceptor>();
private final Map<Object, DeferredResultProcessingInterceptor> deferredResultInterceptors =
new LinkedHashMap<Object, DeferredResultProcessingInterceptor>();
private static final UrlPathHelper urlPathHelper = new UrlPathHelper();
@@ -87,11 +88,13 @@ public final class WebAsyncManager {
}
/**
* Configure the {@link AsyncWebRequest} to use. This property may be
* set more than once during a single request to accurately reflect the
* 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}.
* Configure the {@link AsyncWebRequest} to use. This property may be set
* more than once during a single request to accurately reflect the 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) {
@@ -127,17 +130,18 @@ public final class WebAsyncManager {
}
/**
* Whether the request has been dispatched to process the result of
* concurrent handling.
* Whether a result value exists as a result of concurrent handling.
*/
public boolean hasConcurrentResult() {
return ((this.concurrentResult != RESULT_NONE) && this.asyncWebRequest.isDispatched());
return (this.concurrentResult != RESULT_NONE);
}
/**
* Provides access to the result from concurrent handling.
*
* @return an Object, possibly an {@code Exception} or {@code Throwable} if
* concurrent handling raised one.
* concurrent handling raised one.
* @see #clearConcurrentResult()
*/
public Object getConcurrentResult() {
return this.concurrentResult;
@@ -146,11 +150,46 @@ public final class WebAsyncManager {
/**
* Provides access to additional processing context saved at the start of
* concurrent handling.
*
* @see #clearConcurrentResult()
*/
public Object[] getConcurrentResultContext() {
return this.concurrentResultContext;
}
public CallableProcessingInterceptor getCallableInterceptor(Object key) {
return this.callableInterceptors.get(key);
}
public DeferredResultProcessingInterceptor getDeferredResultInterceptor(Object key) {
return this.deferredResultInterceptors.get(key);
}
/**
* Register a {@link CallableProcessingInterceptor} that will be applied
* when concurrent request handling with a {@link Callable} starts.
*
* @param key a unique the key under which to register the interceptor
* @param interceptor the interceptor to register
*/
public void registerCallableInterceptor(Object key, CallableProcessingInterceptor interceptor) {
Assert.notNull(interceptor, "interceptor is required");
this.callableInterceptors.put(key, interceptor);
}
/**
* Register a {@link DeferredResultProcessingInterceptor} that will be
* applied when concurrent request handling with a {@link DeferredResult}
* starts.
*
* @param key a unique the key under which to register the interceptor
* @param interceptor the interceptor to register
*/
public void registerDeferredResultInterceptor(Object key, DeferredResultProcessingInterceptor interceptor) {
Assert.notNull(interceptor, "interceptor is required");
this.deferredResultInterceptors.put(key, interceptor);
}
/**
* Clear {@linkplain #getConcurrentResult() concurrentResult} and
* {@linkplain #getConcurrentResultContext() concurrentResultContext}.
@@ -169,7 +208,7 @@ public final class WebAsyncManager {
*
* @param callable a unit of work to be executed asynchronously
* @param processingContext additional context to save that can be accessed
* via {@link #getConcurrentResultContext()}
* via {@link #getConcurrentResultContext()}
*
* @see #getConcurrentResult()
* @see #getConcurrentResultContext()
@@ -182,23 +221,19 @@ public final class WebAsyncManager {
this.taskExecutor.submit(new Runnable() {
public void run() {
List<WebAsyncThreadInitializer> initializers =
new ArrayList<WebAsyncThreadInitializer>(threadInitializers.values());
CallableInterceptorChain chain =
new CallableInterceptorChain(callableInterceptors.values());
try {
for (WebAsyncThreadInitializer initializer : initializers) {
initializer.initialize();
}
chain.applyPreProcess(asyncWebRequest, callable);
concurrentResult = callable.call();
}
catch (Throwable t) {
concurrentResult = t;
}
finally {
Collections.reverse(initializers);
for (WebAsyncThreadInitializer initializer : initializers) {
initializer.reset();
}
chain.applyPostProcess(asyncWebRequest, callable, concurrentResult);
}
if (logger.isDebugEnabled()) {
@@ -220,9 +255,10 @@ public final class WebAsyncManager {
* Use the given {@link AsyncTask} to configure the task executor as well as
* the timeout value of the {@code AsyncWebRequest} before delegating to
* {@link #startCallableProcessing(Callable, Object...)}.
*
* @param asyncTask an asyncTask containing the target {@code Callable}
* @param processingContext additional context to save that can be accessed
* via {@link #getConcurrentResultContext()}
* via {@link #getConcurrentResultContext()}
*/
public void startCallableProcessing(AsyncTask asyncTask, Object... processingContext) {
Assert.notNull(asyncTask, "AsyncTask must not be null");
@@ -241,21 +277,23 @@ public final class WebAsyncManager {
}
/**
* 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.
* 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 processingContext additional context to save that can be accessed
* via {@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) throws Exception {
Assert.notNull(deferredResult, "DeferredResult must not be null");
Long timeout = deferredResult.getTimeoutMilliseconds();
@@ -263,12 +301,6 @@ public final class WebAsyncManager {
this.asyncWebRequest.setTimeout(timeout);
}
this.asyncWebRequest.addCompletionHandler(new Runnable() {
public void run() {
deferredResult.setExpired();
}
});
if (deferredResult.hasTimeoutResult()) {
this.asyncWebRequest.setTimeoutHandler(new Runnable() {
public void run() {
@@ -277,6 +309,19 @@ public final class WebAsyncManager {
});
}
final DeferredResultInterceptorChain chain =
new DeferredResultInterceptorChain(this.deferredResultInterceptors.values());
chain.applyPreProcess(this.asyncWebRequest, deferredResult);
this.asyncWebRequest.addCompletionHandler(new Runnable() {
public void run() {
if (!deferredResult.getAndSetExpired()) {
chain.triggerAfterExpiration(asyncWebRequest, deferredResult);
}
}
});
startAsyncProcessing(processingContext);
deferredResult.setResultHandler(new DeferredResultHandler() {
@@ -287,8 +332,7 @@ public final class WebAsyncManager {
logger.debug("Deferred result value [" + concurrentResult + "]");
}
Assert.state(!asyncWebRequest.isAsyncComplete(),
"Cannot handle DeferredResult [ " + deferredResult + " ] due to a timeout or network error");
chain.applyPostProcess(asyncWebRequest, deferredResult, result);
logger.debug("Dispatching request to complete processing");
asyncWebRequest.dispatch();
@@ -298,12 +342,12 @@ public final class WebAsyncManager {
private void startAsyncProcessing(Object[] processingContext) {
clearConcurrentResult();
this.concurrentResultContext = processingContext;
Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null");
this.asyncWebRequest.startAsync();
this.concurrentResult = null;
this.concurrentResultContext = processingContext;
if (logger.isDebugEnabled()) {
HttpServletRequest request = asyncWebRequest.getNativeRequest(HttpServletRequest.class);
String requestUri = urlPathHelper.getRequestUri(request);
@@ -311,43 +355,4 @@ 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;
}
/**
* Initialize and reset thread-bound variables.
*/
public interface WebAsyncThreadInitializer {
void initialize();
void reset();
}
}

View File

@@ -64,10 +64,11 @@ public abstract class WebAsyncUtils {
}
/**
* 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.
* Create an AsyncWebRequest instance. By default an instance of
* {@link StandardServletAsyncWebRequest} is created if running in Servlet
* 3.0 (or higher) environment or as a fallback, an instance of
* {@link NoSupportAsyncWebRequest} is returned.
*
* @param request the current request
* @param response the current response
* @return an AsyncWebRequest instance, never {@code null}

View File

@@ -100,7 +100,7 @@ public class DeferredResultTests {
DeferredResult<String> result = new DeferredResult<String>();
assertFalse(result.isSetOrExpired());
result.setExpired();
result.getAndSetExpired();
assertTrue(result.isSetOrExpired());
assertFalse(result.setResult("hello"));
}

View File

@@ -35,7 +35,6 @@ import org.junit.Test;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.context.request.async.WebAsyncManager.WebAsyncThreadInitializer;
/**
@@ -92,24 +91,26 @@ public class WebAsyncManagerTests {
@Test
public void startCallableProcessing() throws Exception {
WebAsyncThreadInitializer initializer = createStrictMock(WebAsyncThreadInitializer.class);
initializer.initialize();
initializer.reset();
replay(initializer);
Callable<Object> task = new Callable<Object>() {
public Object call() throws Exception {
return 1;
}
};
CallableProcessingInterceptor interceptor = createStrictMock(CallableProcessingInterceptor.class);
interceptor.preProcess(this.asyncWebRequest, task);
interceptor.postProcess(this.asyncWebRequest, task, new Integer(1));
replay(interceptor);
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>() {
public Object call() throws Exception {
return 1;
}
});
this.asyncManager.registerCallableInterceptor("interceptor", interceptor);
this.asyncManager.startCallableProcessing(task);
verify(initializer, this.asyncWebRequest);
verify(interceptor, this.asyncWebRequest);
}
@Test
@@ -159,26 +160,34 @@ public class WebAsyncManagerTests {
@Test
public void startDeferredResultProcessing() throws Exception {
DeferredResult<Integer> deferredResult = new DeferredResult<Integer>(1000L, 10);
this.asyncWebRequest.setTimeout(1000L);
this.asyncWebRequest.addCompletionHandler((Runnable) notNull());
this.asyncWebRequest.setTimeoutHandler((Runnable) notNull());
this.asyncWebRequest.addCompletionHandler((Runnable) notNull());
this.asyncWebRequest.startAsync();
replay(this.asyncWebRequest);
DeferredResult<Integer> deferredResult = new DeferredResult<Integer>(1000L, 10);
DeferredResultProcessingInterceptor interceptor = createStrictMock(DeferredResultProcessingInterceptor.class);
interceptor.preProcess(this.asyncWebRequest, deferredResult);
replay(interceptor);
this.asyncManager.registerDeferredResultInterceptor("interceptor", interceptor);
this.asyncManager.startDeferredResultProcessing(deferredResult);
verify(this.asyncWebRequest);
reset(this.asyncWebRequest);
verify(this.asyncWebRequest, interceptor);
reset(this.asyncWebRequest, interceptor);
expect(this.asyncWebRequest.isAsyncComplete()).andReturn(false);
this.asyncWebRequest.dispatch();
replay(this.asyncWebRequest);
interceptor.postProcess(asyncWebRequest, deferredResult, 25);
replay(interceptor);
deferredResult.setResult(25);
assertEquals(25, this.asyncManager.getConcurrentResult());
verify(this.asyncWebRequest);
verify(this.asyncWebRequest, interceptor);
}

View File

@@ -19,18 +19,19 @@ package org.springframework.web.servlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.method.HandlerMethod;
/**
* Extends {@code HandlerInterceptor} with a callback method invoked during
* asynchronous request handling.
*
* <p>When a handler starts asynchronous request handling, the DispatcherServlet
* exits without invoking {@code postHandle} and {@code afterCompletion}, as it
* normally does, since the results of request handling (e.g. ModelAndView) are
* not available in the current thread and handling is not yet complete.
* In such scenarios, the
* normally does, since the results of request handling (e.g. ModelAndView)
* will. be produced concurrently in another thread. In such scenarios,
* {@link #afterConcurrentHandlingStarted(HttpServletRequest, HttpServletResponse)}
* method is invoked instead allowing implementations to perform tasks such as
* cleaning up thread bound attributes.
* is invoked instead allowing implementations to perform tasks such as cleaning
* up thread bound attributes.
*
* <p>When asynchronous handling completes, the request is dispatched to the
* container for further processing. At this stage the DispatcherServlet invokes
@@ -40,20 +41,26 @@ import javax.servlet.http.HttpServletResponse;
* @since 3.2
*
* @see org.springframework.web.context.request.async.WebAsyncManager
* @see org.springframework.web.context.request.async.CallableProcessingInterceptor
* @see org.springframework.web.context.request.async.DeferredResultProcessingInterceptor
*/
public interface AsyncHandlerInterceptor extends HandlerInterceptor {
/**
* Called instead of {@code postHandle} and {@code afterCompletion}, when the
* a handler is being executed concurrently. Implementations may use the provided
* request and response but should avoid modifying them in ways that would
* conflict with the concurrent execution of the handler. A typical use of
* this method would be to clean thread local variables.
* Called instead of {@code postHandle} and {@code afterCompletion}, when
* the a handler is being executed concurrently. Implementations may use the
* provided request and response but should avoid modifying them in ways
* that would conflict with the concurrent execution of the handler. A
* typical use of this method would be to clean thread local variables.
*
* @param request the current request
* @param response the current response
* @param handler handler that started async execution, for type and/or instance examination
* @param handler handler (or {@link HandlerMethod}) that started async
* execution, for type and/or instance examination
* @throws Exception in case of errors
*/
void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler);
void afterConcurrentHandlingStarted(
HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception;
}

View File

@@ -20,6 +20,7 @@ import java.io.IOException;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.concurrent.Callable;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
@@ -44,12 +45,13 @@ import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.ConfigurableWebApplicationContext;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.request.async.WebAsyncUtils;
import org.springframework.web.context.request.async.CallableProcessingInterceptor;
import org.springframework.web.context.request.async.WebAsyncManager;
import org.springframework.web.context.request.async.WebAsyncManager.WebAsyncThreadInitializer;
import org.springframework.web.context.request.async.WebAsyncUtils;
import org.springframework.web.context.support.ServletRequestHandledEvent;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.context.support.XmlWebApplicationContext;
@@ -909,7 +911,7 @@ public abstract class FrameworkServlet extends HttpServletBean {
initContextHolders(request, localeContext, requestAttributes);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerAsyncThreadInitializer(this.getClass().getName(), createAsyncThreadInitializer(request));
asyncManager.registerCallableInterceptor(this.getClass().getName(), createRequestBindingInterceptor(request));
try {
doService(request, response);
@@ -992,13 +994,15 @@ public abstract class FrameworkServlet extends HttpServletBean {
}
}
private WebAsyncThreadInitializer createAsyncThreadInitializer(final HttpServletRequest request) {
private CallableProcessingInterceptor createRequestBindingInterceptor(final HttpServletRequest request) {
return new WebAsyncThreadInitializer() {
public void initialize() {
return new CallableProcessingInterceptor() {
public void preProcess(NativeWebRequest webRequest, Callable<?> task) {
initContextHolders(request, buildLocaleContext(request), new ServletRequestAttributes(request));
}
public void reset() {
public void postProcess(NativeWebRequest webRequest, Callable<?> task, Object concurrentResult) {
resetContextHolders(request, null, null);
}
};

View File

@@ -19,6 +19,8 @@ package org.springframework.web.servlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.method.HandlerMethod;
/**
* Workflow interface that allows for customized handler execution chains.
* Applications can register any number of existing or custom interceptors
@@ -36,8 +38,8 @@ import javax.servlet.http.HttpServletResponse;
* {@code postHandle} and {@code afterCompletion} callbacks. When concurrent
* handler execution completes, the request is dispatched back in order to
* proceed with rendering the model and all methods of this contract are invoked
* again. For further options and comments see
* {@code org.springframework.web.servlet.HandlerInterceptor}
* again. For further options and details see
* {@code org.springframework.web.servlet.AsyncHandlerInterceptor}
*
* <p>Typically an interceptor chain is defined per HandlerMapping bean,
* sharing its granularity. To be able to apply a certain interceptor chain
@@ -100,7 +102,8 @@ public interface HandlerInterceptor {
* getting applied in inverse order of the execution chain.
* @param request current HTTP request
* @param response current HTTP response
* @param handler chosen handler to execute, for type and/or instance examination
* @param handler handler (or {@link HandlerMethod}) that started async
* execution, for type and/or instance examination
* @param modelAndView the <code>ModelAndView</code> that the handler returned
* (can also be <code>null</code>)
* @throws Exception in case of errors
@@ -120,7 +123,8 @@ public interface HandlerInterceptor {
* the last to be invoked.
* @param request current HTTP request
* @param response current HTTP response
* @param handler chosen handler to execute, for type and/or instance examination
* @param handler handler (or {@link HandlerMethod}) that started async
* execution, for type and/or instance examination
* @param ex exception thrown on handler execution, if any
* @throws Exception in case of errors
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2006 the original author or authors.
* Copyright 2002-2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,7 +19,7 @@ package org.springframework.web.servlet.handler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.AsyncHandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
/**
@@ -29,7 +29,7 @@ import org.springframework.web.servlet.ModelAndView;
* @author Juergen Hoeller
* @since 05.12.2003
*/
public abstract class HandlerInterceptorAdapter implements HandlerInterceptor {
public abstract class HandlerInterceptorAdapter implements AsyncHandlerInterceptor {
/**
* This implementation always returns <code>true</code>.
@@ -55,4 +55,12 @@ public abstract class HandlerInterceptorAdapter implements HandlerInterceptor {
throws Exception {
}
/**
* This implementation is empty.
*/
public void afterConcurrentHandlingStarted(
HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
}
}