From 57c36dd39500654caeea124519257696d98f9f14 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 19 Sep 2012 09:25:50 -0400 Subject: [PATCH] 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. --- .../support/OpenSessionInViewFilter.java | 55 ++++-- .../support/OpenSessionInViewInterceptor.java | 52 ++++-- .../support/OpenSessionInViewFilter.java | 56 ++++-- .../support/OpenSessionInViewInterceptor.java | 53 ++++-- .../OpenEntityManagerInViewFilter.java | 54 ++++-- .../OpenEntityManagerInViewInterceptor.java | 55 ++++-- .../support/OpenSessionInViewTests.java | 8 +- .../support/OpenEntityManagerInViewTests.java | 17 +- .../request/async/AsyncWebRequest.java | 16 +- .../async/CallableInterceptorChain.java | 64 +++++++ .../async/CallableProcessingInterceptor.java | 67 +++++++ .../context/request/async/DeferredResult.java | 42 +++-- .../async/DeferredResultInterceptorChain.java | 71 ++++++++ .../DeferredResultProcessingInterceptor.java | 83 +++++++++ .../async/NoSupportAsyncWebRequest.java | 4 - .../async/StandardServletAsyncWebRequest.java | 5 - .../request/async/WebAsyncManager.java | 169 +++++++++--------- .../context/request/async/WebAsyncUtils.java | 9 +- .../request/async/DeferredResultTests.java | 2 +- .../request/async/WebAsyncManagerTests.java | 45 +++-- .../web/servlet/AsyncHandlerInterceptor.java | 31 ++-- .../web/servlet/FrameworkServlet.java | 18 +- .../web/servlet/HandlerInterceptor.java | 12 +- .../handler/HandlerInterceptorAdapter.java | 14 +- 24 files changed, 735 insertions(+), 267 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/web/context/request/async/CallableInterceptorChain.java create mode 100644 spring-web/src/main/java/org/springframework/web/context/request/async/CallableProcessingInterceptor.java create mode 100644 spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultInterceptorChain.java create mode 100644 spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultProcessingInterceptor.java diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate3/support/OpenSessionInViewFilter.java b/spring-orm/src/main/java/org/springframework/orm/hibernate3/support/OpenSessionInViewFilter.java index 9c9b619bc38..7cc99cf9d29 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate3/support/OpenSessionInViewFilter.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate3/support/OpenSessionInViewFilter.java @@ -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); + } } } diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate3/support/OpenSessionInViewInterceptor.java b/spring-orm/src/main/java/org/springframework/orm/hibernate3/support/OpenSessionInViewInterceptor.java index 76f40b29142..d259a55bea0 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate3/support/OpenSessionInViewInterceptor.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate3/support/OpenSessionInViewInterceptor.java @@ -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 Session 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()); + } + } } diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate4/support/OpenSessionInViewFilter.java b/spring-orm/src/main/java/org/springframework/orm/hibernate4/support/OpenSessionInViewFilter.java index a75aba87336..4393d31f673 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate4/support/OpenSessionInViewFilter.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate4/support/OpenSessionInViewFilter.java @@ -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); + } + } } diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate4/support/OpenSessionInViewInterceptor.java b/spring-orm/src/main/java/org/springframework/orm/hibernate4/support/OpenSessionInViewInterceptor.java index 05ae7f5fa20..58faf09215d 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate4/support/OpenSessionInViewInterceptor.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate4/support/OpenSessionInViewInterceptor.java @@ -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 Session 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()); + } } } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewFilter.java b/spring-orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewFilter.java index 7c9061cbeae..e4fe37f8b3b 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewFilter.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewFilter.java @@ -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); + } } } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewInterceptor.java b/spring-orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewInterceptor.java index fc9e39c54df..3930a1ae8cf 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewInterceptor.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewInterceptor.java @@ -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()); + } } } diff --git a/spring-orm/src/test/java/org/springframework/orm/hibernate3/support/OpenSessionInViewTests.java b/spring-orm/src/test/java/org/springframework/orm/hibernate3/support/OpenSessionInViewTests.java index 24566685cab..6eb9b8ae34d 100644 --- a/spring-orm/src/test/java/org/springframework/orm/hibernate3/support/OpenSessionInViewTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/hibernate3/support/OpenSessionInViewTests.java @@ -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() { @@ -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() { 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); diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewTests.java index ac211a13c57..7e3d1a9f9b2 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewTests.java @@ -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() { 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() { 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(); + } + } } diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/AsyncWebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/async/AsyncWebRequest.java index 07a4acb5743..9c9470b25a7 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/AsyncWebRequest.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/AsyncWebRequest.java @@ -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. */ diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/CallableInterceptorChain.java b/spring-web/src/main/java/org/springframework/web/context/request/async/CallableInterceptorChain.java new file mode 100644 index 00000000000..120d224040d --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/CallableInterceptorChain.java @@ -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 interceptors; + + private int interceptorIndex = -1; + + + public CallableInterceptorChain(Collection interceptors) { + this.interceptors = new ArrayList(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); + } + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/CallableProcessingInterceptor.java b/spring-web/src/main/java/org/springframework/web/context/request/async/CallableProcessingInterceptor.java new file mode 100644 index 00000000000..0b0c9accd20 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/CallableProcessingInterceptor.java @@ -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}. + *

+ * A {@code CallableProcessingInterceptor} is invoked before and after the + * invocation of the {@code Callable} task in the asynchronous thread. + * + *

A {@code CallableProcessingInterceptor} may be registered as follows: + *

+ * CallableProcessingInterceptor interceptor = ... ;
+ * WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
+ * asyncManager.registerCallableInterceptor("key", interceptor);
+ * 
+ * + *

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; + +} diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResult.java b/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResult.java index b0675c8f491..b833db4159e 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResult.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResult.java @@ -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 { 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 { this.timeout = timeout; } - /** * Return the configured timeout value in milliseconds. */ @@ -88,8 +85,12 @@ public final class DeferredResult { } /** - * 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. + * + *

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 { } 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 { } /** - * 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. + *

+ * 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() { diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultInterceptorChain.java b/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultInterceptorChain.java new file mode 100644 index 00000000000..600fe0bbde1 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultInterceptorChain.java @@ -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 interceptors; + + + public DeferredResultInterceptorChain(Collection interceptors) { + this.interceptors = new ArrayList(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); + } + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultProcessingInterceptor.java b/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultProcessingInterceptor.java new file mode 100644 index 00000000000..2aa37bc2df4 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultProcessingInterceptor.java @@ -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). + * + *

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. + * + *

A {@code DeferredResultProcessingInterceptor} may be registered as follows: + *

+ * DeferredResultProcessingInterceptor interceptor = ... ;
+ * WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
+ * asyncManager.registerDeferredResultInterceptor("key", interceptor);
+ * 
+ * + *

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. + *

+ * 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; + +} diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/NoSupportAsyncWebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/async/NoSupportAsyncWebRequest.java index 7f626985c2a..956b6b406d6 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/NoSupportAsyncWebRequest.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/NoSupportAsyncWebRequest.java @@ -49,10 +49,6 @@ public class NoSupportAsyncWebRequest extends ServletWebRequest implements Async return false; } - public boolean isDispatched() { - return false; - } - // Not supported public void startAsync() { diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java index ba249e08318..4049c11a663 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java @@ -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. *

It is important to avoid use of request and response objects after async diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java index 5349e0bc88b..384199bdada 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java @@ -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 threadInitializers = new LinkedHashMap(); + private final Map callableInterceptors = + new LinkedHashMap(); + + private final Map deferredResultInterceptors = + new LinkedHashMap(); 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 initializers = - new ArrayList(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(); - - } - } diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncUtils.java b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncUtils.java index 2465040e451..4a2e8b7d82e 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncUtils.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncUtils.java @@ -64,10 +64,11 @@ public abstract class WebAsyncUtils { } /** - * 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 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} diff --git a/spring-web/src/test/java/org/springframework/web/context/request/async/DeferredResultTests.java b/spring-web/src/test/java/org/springframework/web/context/request/async/DeferredResultTests.java index a3df7a58f04..99918ce6dd6 100644 --- a/spring-web/src/test/java/org/springframework/web/context/request/async/DeferredResultTests.java +++ b/spring-web/src/test/java/org/springframework/web/context/request/async/DeferredResultTests.java @@ -100,7 +100,7 @@ public class DeferredResultTests { DeferredResult result = new DeferredResult(); assertFalse(result.isSetOrExpired()); - result.setExpired(); + result.getAndSetExpired(); assertTrue(result.isSetOrExpired()); assertFalse(result.setResult("hello")); } diff --git a/spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTests.java b/spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTests.java index f324273eb2f..91e0f079640 100644 --- a/spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTests.java +++ b/spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTests.java @@ -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 task = new Callable() { + 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() { - 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 deferredResult = new DeferredResult(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 deferredResult = new DeferredResult(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); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/AsyncHandlerInterceptor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/AsyncHandlerInterceptor.java index 372a8e5f455..597e6c73900 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/AsyncHandlerInterceptor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/AsyncHandlerInterceptor.java @@ -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. * *

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. * *

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; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java index 762a238ff22..58a73418522 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java @@ -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); } }; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerInterceptor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerInterceptor.java index b93a3862b9a..ebaafd92111 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerInterceptor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerInterceptor.java @@ -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} * *

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 ModelAndView that the handler returned * (can also be null) * @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 */ diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerInterceptorAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerInterceptorAdapter.java index 03b04274a2d..455977159cf 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerInterceptorAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerInterceptorAdapter.java @@ -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 true. @@ -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 { + } + }