From e14ba9dec329b0f2f3ea414e8119957abd8a2a4c Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 26 Oct 2012 21:29:54 -0400 Subject: [PATCH] Add config options for MVC async interceptors The MVC namespace and the MVC Java config now allow configuring CallableProcessingInterceptor and DeferredResultProcessingInterceptor instances. Issue: SPR-9914 --- .../request/async/WebAsyncManager.java | 10 ++++ .../AnnotationDrivenBeanDefinitionParser.java | 38 +++++++++++++++ .../annotation/AsyncSupportConfigurer.java | 47 +++++++++++++++++++ .../WebMvcConfigurationSupport.java | 2 + .../RequestMappingHandlerAdapter.java | 39 +++++++++++++++ .../web/servlet/config/spring-mvc-3.2.xsd | 40 ++++++++++++++++ .../web/servlet/config/MvcNamespaceTests.java | 34 ++++++++++++-- ...MvcConfigurationSupportExtensionTests.java | 36 ++++++++++---- .../config/mvc-config-async-support.xml | 9 +++- src/dist/changelog.txt | 1 + 10 files changed, 240 insertions(+), 16 deletions(-) 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 53f269171a..aa6de062c7 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 @@ -175,6 +175,11 @@ public final class WebAsyncManager { this.callableInterceptors.put(key, interceptor); } + public void registerAllCallableInterceptors(Map interceptors) { + Assert.notNull(interceptors); + this.callableInterceptors.putAll(interceptors); + } + /** * Register a {@link DeferredResultProcessingInterceptor} that will be * applied when concurrent request handling with a {@link DeferredResult} @@ -188,6 +193,11 @@ public final class WebAsyncManager { this.deferredResultInterceptors.put(key, interceptor); } + public void registerAllDeferredResultInterceptors(Map interceptors) { + Assert.notNull(interceptors); + this.deferredResultInterceptors.putAll(interceptors); + } + /** * Clear {@linkplain #getConcurrentResult() concurrentResult} and * {@linkplain #getConcurrentResultContext() concurrentResultContext}. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java index b86d5c5951..0d17c11063 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java @@ -174,6 +174,8 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { ManagedList returnValueHandlers = getReturnValueHandlers(element, source, parserContext); String asyncTimeout = getAsyncTimeout(element, source, parserContext); RuntimeBeanReference asyncExecutor = getAsyncExecutor(element, source, parserContext); + ManagedList callableInterceptors = getCallableInterceptors(element, source, parserContext); + ManagedList deferredResultInterceptors = getDeferredResultInterceptors(element, source, parserContext); RootBeanDefinition handlerAdapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class); handlerAdapterDef.setSource(source); @@ -197,6 +199,8 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { if (asyncExecutor != null) { handlerAdapterDef.getPropertyValues().add("taskExecutor", asyncExecutor); } + handlerAdapterDef.getPropertyValues().add("callableInterceptors", callableInterceptors); + handlerAdapterDef.getPropertyValues().add("deferredResultInterceptors", deferredResultInterceptors); String handlerAdapterName = parserContext.getReaderContext().registerWithGeneratedName(handlerAdapterDef); RootBeanDefinition csInterceptorDef = new RootBeanDefinition(ConversionServiceExposingInterceptor.class); @@ -337,6 +341,40 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { return null; } + private ManagedList getCallableInterceptors(Element element, Object source, ParserContext parserContext) { + ManagedList interceptors = new ManagedList(); + Element asyncElement = DomUtils.getChildElementByTagName(element, "async-support"); + if (asyncElement != null) { + Element interceptorsElement = DomUtils.getChildElementByTagName(asyncElement, "callable-interceptors"); + if (interceptorsElement != null) { + interceptors.setSource(source); + for (Element converter : DomUtils.getChildElementsByTagName(interceptorsElement, "bean")) { + BeanDefinitionHolder beanDef = parserContext.getDelegate().parseBeanDefinitionElement(converter); + beanDef = parserContext.getDelegate().decorateBeanDefinitionIfRequired(converter, beanDef); + interceptors.add(beanDef); + } + } + } + return interceptors; + } + + private ManagedList getDeferredResultInterceptors(Element element, Object source, ParserContext parserContext) { + ManagedList interceptors = new ManagedList(); + Element asyncElement = DomUtils.getChildElementByTagName(element, "async-support"); + if (asyncElement != null) { + Element interceptorsElement = DomUtils.getChildElementByTagName(asyncElement, "deferred-result-interceptors"); + if (interceptorsElement != null) { + interceptors.setSource(source); + for (Element converter : DomUtils.getChildElementsByTagName(interceptorsElement, "bean")) { + BeanDefinitionHolder beanDef = parserContext.getDelegate().parseBeanDefinitionElement(converter); + beanDef = parserContext.getDelegate().decorateBeanDefinitionIfRequired(converter, beanDef); + interceptors.add(beanDef); + } + } + } + return interceptors; + } + private ManagedList getArgumentResolvers(Element element, Object source, ParserContext parserContext) { Element resolversElement = DomUtils.getChildElementByTagName(element, "argument-resolvers"); if (resolversElement != null) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/AsyncSupportConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/AsyncSupportConfigurer.java index 5f42833fdc..ddc199f663 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/AsyncSupportConfigurer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/AsyncSupportConfigurer.java @@ -15,11 +15,18 @@ */ package org.springframework.web.servlet.config.annotation; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.concurrent.Callable; import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.util.Assert; import org.springframework.web.context.request.async.AsyncTask; +import org.springframework.web.context.request.async.CallableProcessingInterceptor; +import org.springframework.web.context.request.async.DeferredResult; +import org.springframework.web.context.request.async.DeferredResultProcessingInterceptor; /** * Helps with configuring a options for asynchronous request processing. @@ -33,6 +40,13 @@ public class AsyncSupportConfigurer { private Long timeout; + private final List callableInterceptors = + new ArrayList(); + + private final List deferredResultInterceptors = + new ArrayList(); + + /** * Set the default {@link AsyncTaskExecutor} to use when a controller method * returns a {@link Callable}. Controller methods can override this default on @@ -64,6 +78,31 @@ public class AsyncSupportConfigurer { return this; } + /** + * Configure lifecycle intercepters with callbacks around concurrent request + * execution that starts when a controller returns a + * {@link java.util.concurrent.Callable}. + * + * @param interceptors the interceptors to register + */ + public AsyncSupportConfigurer registerCallableInterceptors(CallableProcessingInterceptor... interceptors) { + Assert.notNull(interceptors, "Interceptors are required"); + this.callableInterceptors.addAll(Arrays.asList(interceptors)); + return this; + } + + /** + * Configure lifecycle intercepters with callbacks around concurrent request + * execution that starts when a controller returns a {@link DeferredResult}. + * + * @param interceptors the interceptors to register + */ + public AsyncSupportConfigurer registerDeferredResultInterceptors(DeferredResultProcessingInterceptor... interceptors) { + Assert.notNull(interceptors, "Interceptors are required"); + this.deferredResultInterceptors.addAll(Arrays.asList(interceptors)); + return this; + } + protected AsyncTaskExecutor getTaskExecutor() { return this.taskExecutor; } @@ -72,4 +111,12 @@ public class AsyncSupportConfigurer { return this.timeout; } + protected List getCallableInterceptors() { + return this.callableInterceptors; + } + + protected List getDeferredResultInterceptors() { + return this.deferredResultInterceptors; + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java index 85a64026cb..e961f63802 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java @@ -378,6 +378,8 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv if (configurer.getTimeout() != null) { adapter.setAsyncRequestTimeout(configurer.getTimeout()); } + adapter.setCallableInterceptors(configurer.getCallableInterceptors()); + adapter.setDeferredResultInterceptors(configurer.getDeferredResultInterceptors()); return adapter; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java index 197bf6f5ab..15ca91319b 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java @@ -48,6 +48,7 @@ import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.xml.SourceHttpMessageConverter; import org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter; import org.springframework.ui.ModelMap; +import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils.MethodFilter; import org.springframework.web.accept.ContentNegotiationManager; @@ -64,6 +65,8 @@ import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.context.request.WebRequest; import org.springframework.web.context.request.async.AsyncTask; import org.springframework.web.context.request.async.AsyncWebRequest; +import org.springframework.web.context.request.async.CallableProcessingInterceptor; +import org.springframework.web.context.request.async.DeferredResultProcessingInterceptor; import org.springframework.web.context.request.async.WebAsyncManager; import org.springframework.web.context.request.async.WebAsyncUtils; import org.springframework.web.method.ControllerAdviceBean; @@ -135,6 +138,12 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i private Long asyncRequestTimeout; + private final Map callableInterceptors = + new LinkedHashMap(); + + private final Map deferredResultInterceptors = + new LinkedHashMap(); + private boolean ignoreDefaultModelOnRedirect = false; private int cacheSecondsForSessionAttributeHandlers = 0; @@ -363,6 +372,34 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i this.asyncRequestTimeout = timeout; } + /** + * Configure {@code CallableProcessingInterceptor}'s to register on async requests. + * @param interceptors the interceptors to register + */ + public void setCallableInterceptors(List interceptors) { + Assert.notNull(interceptors); + for (int index = 0 ; index < interceptors.size(); index++) { + CallableProcessingInterceptor interceptor = interceptors.get(index); + this.callableInterceptors.put(getInterceptorKey(interceptor, index), interceptor); + } + } + + /** + * Configure {@code DeferredResultProcessingInterceptor}'s to register on async requests. + * @param interceptors the interceptors to register + */ + public void setDeferredResultInterceptors(List interceptors) { + Assert.notNull(interceptors); + for (int index = 0 ; index < interceptors.size(); index++) { + DeferredResultProcessingInterceptor interceptor = interceptors.get(index); + this.deferredResultInterceptors.put(getInterceptorKey(interceptor, index), interceptor); + } + } + + private String getInterceptorKey(Object interceptor, int index) { + return this.hashCode() + ":" + interceptor.getClass().getName() + "#" + index; + } + /** * By default the content of the "default" model is used both during * rendering and redirect scenarios. Alternatively a controller method @@ -703,6 +740,8 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i final WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); asyncManager.setTaskExecutor(this.taskExecutor); asyncManager.setAsyncWebRequest(asyncWebRequest); + asyncManager.registerAllCallableInterceptors(this.callableInterceptors); + asyncManager.registerAllDeferredResultInterceptors(this.deferredResultInterceptors); if (asyncManager.hasConcurrentResult()) { Object result = asyncManager.getConcurrentResult(); diff --git a/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc-3.2.xsd b/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc-3.2.xsd index f58bd9289b..e3a7b95407 100644 --- a/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc-3.2.xsd +++ b/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc-3.2.xsd @@ -97,6 +97,46 @@ ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + callableInterceptors = + (Map) fieldAccessor.getPropertyValue("callableInterceptors"); + assertEquals(1, callableInterceptors.size()); + + Map deferredResultInterceptors = + (Map) fieldAccessor.getPropertyValue("deferredResultInterceptors"); + assertEquals(1, deferredResultInterceptors.size()); } @@ -539,4 +559,8 @@ public class MvcNamespaceTests { } } + public static class TestCallableProcessingInterceptor extends CallableProcessingInterceptorAdapter { } + + public static class TestDeferredResultProcessingInterceptor extends DeferredResultProcessingInterceptorAdapter { } + } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java index db628be7a9..7547f26246 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java @@ -21,6 +21,7 @@ import static org.junit.Assert.assertNotNull; import java.util.Arrays; import java.util.List; +import java.util.Map; import org.junit.Before; import org.junit.Test; @@ -46,6 +47,10 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.request.async.CallableProcessingInterceptor; +import org.springframework.web.context.request.async.CallableProcessingInterceptorAdapter; +import org.springframework.web.context.request.async.DeferredResultProcessingInterceptor; +import org.springframework.web.context.request.async.DeferredResultProcessingInterceptorAdapter; import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.method.annotation.ModelAttributeMethodProcessor; import org.springframework.web.method.support.HandlerMethodArgumentResolver; @@ -117,6 +122,7 @@ public class WebMvcConfigurationSupportExtensionTests { assertNotNull(handler.getHandler()); } + @SuppressWarnings("unchecked") @Test public void requestMappingHandlerAdapter() throws Exception { RequestMappingHandlerAdapter adapter = webConfig.requestMappingHandlerAdapter(); @@ -128,22 +134,30 @@ public class WebMvcConfigurationSupportExtensionTests { // Message converters assertEquals(1, adapter.getMessageConverters().size()); + DirectFieldAccessor fieldAccessor = new DirectFieldAccessor(adapter); + // Custom argument resolvers and return value handlers - @SuppressWarnings("unchecked") - List argResolvers= (List) - new DirectFieldAccessor(adapter).getPropertyValue("customArgumentResolvers"); + List argResolvers = + (List) fieldAccessor.getPropertyValue("customArgumentResolvers"); assertEquals(1, argResolvers.size()); - @SuppressWarnings("unchecked") - List handlers = (List) - new DirectFieldAccessor(adapter).getPropertyValue("customReturnValueHandlers"); + List handlers = + (List) fieldAccessor.getPropertyValue("customReturnValueHandlers"); assertEquals(1, handlers.size()); // Async support options - assertEquals(ConcurrentTaskExecutor.class, new DirectFieldAccessor(adapter).getPropertyValue("taskExecutor").getClass()); - assertEquals(2500L, new DirectFieldAccessor(adapter).getPropertyValue("asyncRequestTimeout")); + assertEquals(ConcurrentTaskExecutor.class, fieldAccessor.getPropertyValue("taskExecutor").getClass()); + assertEquals(2500L, fieldAccessor.getPropertyValue("asyncRequestTimeout")); - assertEquals(false, new DirectFieldAccessor(adapter).getPropertyValue("ignoreDefaultModelOnRedirect")); + Map callableInterceptors = + (Map) fieldAccessor.getPropertyValue("callableInterceptors"); + assertEquals(1, callableInterceptors.size()); + + Map deferredResultInterceptors = + (Map) fieldAccessor.getPropertyValue("deferredResultInterceptors"); + assertEquals(1, deferredResultInterceptors.size()); + + assertEquals(false, fieldAccessor.getPropertyValue("ignoreDefaultModelOnRedirect")); } @Test @@ -240,7 +254,9 @@ public class WebMvcConfigurationSupportExtensionTests { @Override public void configureAsyncSupport(AsyncSupportConfigurer configurer) { - configurer.setDefaultTimeout(2500).setTaskExecutor(new ConcurrentTaskExecutor()); + configurer.setDefaultTimeout(2500).setTaskExecutor(new ConcurrentTaskExecutor()) + .registerCallableInterceptors(new CallableProcessingInterceptorAdapter() { }) + .registerDeferredResultInterceptors(new DeferredResultProcessingInterceptorAdapter() {}); } @Override diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-async-support.xml b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-async-support.xml index ed902a56c1..07dad7e38e 100644 --- a/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-async-support.xml +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-async-support.xml @@ -5,7 +5,14 @@ http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd"> - + + + + + + + + diff --git a/src/dist/changelog.txt b/src/dist/changelog.txt index 2999318b4e..6626dd644d 100644 --- a/src/dist/changelog.txt +++ b/src/dist/changelog.txt @@ -32,6 +32,7 @@ Changes in version 3.2 RC1 (2012-10-29) * added CallableProcessingInterceptor and DeferredResultProcessingInterceptor * added support for wildcard media types in AbstractView and ContentNegotiationViewResolver (SPR-9807) * the jackson message converters now include "application/*+json" in supported media types (SPR-7905) +* add options to configure MVC async interceptors in the MVC namespace and Java config (SPR-9914) Changes in version 3.2 M2 (2012-09-11) --------------------------------------