Add async options to MVC namespace and Java config

The MVC Java config method to implement is
WebMvcConfigurer.configureAsyncSupport(AsyncSupportConfigurer)

The MVC namespace element is:
<mvc:annotation-driven>
  <mvc:async-support default-timeout="2500" task-executor="myExecutor" />
</mvc:annotation-driven>

Issue: SPR-9694
This commit is contained in:
Rossen Stoyanchev 2012-08-17 17:05:16 -04:00
parent 4f55518290
commit 9c8c967caa
14 changed files with 552 additions and 257 deletions

View File

@ -174,6 +174,8 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
ManagedList<?> messageConverters = getMessageConverters(element, source, parserContext);
ManagedList<?> argumentResolvers = getArgumentResolvers(element, source, parserContext);
ManagedList<?> returnValueHandlers = getReturnValueHandlers(element, source, parserContext);
String asyncTimeout = getAsyncTimeout(element, source, parserContext);
RuntimeBeanReference asyncExecutor = getAsyncExecutor(element, source, parserContext);
RootBeanDefinition handlerAdapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class);
handlerAdapterDef.setSource(source);
@ -191,6 +193,12 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
if (returnValueHandlers != null) {
handlerAdapterDef.getPropertyValues().add("customReturnValueHandlers", returnValueHandlers);
}
if (asyncTimeout != null) {
handlerAdapterDef.getPropertyValues().add("asyncRequestTimeout", asyncTimeout);
}
if (asyncExecutor != null) {
handlerAdapterDef.getPropertyValues().add("taskExecutor", asyncExecutor);
}
String handlerAdapterName = parserContext.getReaderContext().registerWithGeneratedName(handlerAdapterDef);
RootBeanDefinition csInterceptorDef = new RootBeanDefinition(ConversionServiceExposingInterceptor.class);
@ -318,6 +326,21 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
}
}
private String getAsyncTimeout(Element element, Object source, ParserContext parserContext) {
Element asyncElement = DomUtils.getChildElementByTagName(element, "async-support");
return (asyncElement != null) ? asyncElement.getAttribute("default-timeout") : null;
}
private RuntimeBeanReference getAsyncExecutor(Element element, Object source, ParserContext parserContext) {
Element asyncElement = DomUtils.getChildElementByTagName(element, "async-support");
if (asyncElement != null) {
if (asyncElement.hasAttribute("task-executor")) {
return new RuntimeBeanReference(asyncElement.getAttribute("task-executor"));
}
}
return null;
}
private ManagedList<?> getArgumentResolvers(Element element, Object source, ParserContext parserContext) {
Element resolversElement = DomUtils.getChildElementByTagName(element, "argument-resolvers");
if (resolversElement != null) {

View File

@ -0,0 +1,75 @@
/*
* Copyright 2002-2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.config.annotation;
import java.util.concurrent.Callable;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.web.context.request.async.AsyncTask;
/**
* Helps with configuring a options for asynchronous request processing.
*
* @author Rossen Stoyanchev
* @since 3.2
*/
public class AsyncSupportConfigurer {
private AsyncTaskExecutor taskExecutor;
private Long timeout;
/**
* Set the default {@link AsyncTaskExecutor} to use when a controller method
* returns a {@link Callable}. Controller methods can override this default on
* a per-request basis by returning an {@link AsyncTask}.
*
* <p>By default a {@link SimpleAsyncTaskExecutor} instance is used and it's
* highly recommended to change that default in production since the simple
* executor does not re-use threads.
*
* @param taskExecutor the task executor instance to use by default
*/
public AsyncSupportConfigurer setTaskExecutor(AsyncTaskExecutor taskExecutor) {
this.taskExecutor = taskExecutor;
return this;
}
/**
* Specify the amount of time, in milliseconds, before asynchronous request
* handling times out. In Servlet 3, the timeout begins after the main request
* processing thread has exited and ends when the request is dispatched again
* for further processing of the concurrently produced result.
* <p>If this value is not set, the default timeout of the underlying
* implementation is used, e.g. 10 seconds on Tomcat with Servlet 3.
*
* @param timeout the timeout value in milliseconds
*/
public AsyncSupportConfigurer setDefaultTimeout(long timeout) {
this.timeout = timeout;
return this;
}
protected AsyncTaskExecutor getTaskExecutor() {
return this.taskExecutor;
}
protected Long getTimeout() {
return this.timeout;
}
}

View File

@ -60,6 +60,11 @@ public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
this.configurers.configureContentNegotiation(configurer);
}
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
this.configurers.configureAsyncSupport(configurer);
}
@Override
protected void addViewControllers(ViewControllerRegistry registry) {
this.configurers.addViewControllers(registry);

View File

@ -354,6 +354,17 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
adapter.setWebBindingInitializer(webBindingInitializer);
adapter.setCustomArgumentResolvers(argumentResolvers);
adapter.setCustomReturnValueHandlers(returnValueHandlers);
AsyncSupportConfigurer configurer = new AsyncSupportConfigurer();
configureAsyncSupport(configurer);
if (configurer.getTaskExecutor() != null) {
adapter.setTaskExecutor(configurer.getTaskExecutor());
}
if (configurer.getTimeout() != null) {
adapter.setAsyncRequestTimeout(configurer.getTimeout());
}
return adapter;
}
@ -516,6 +527,13 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
protected void addFormatters(FormatterRegistry registry) {
}
/**
* Override this method to configure asynchronous request processing options.
* @see AsyncSupportConfigurer
*/
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
}
/**
* Returns a {@link HttpRequestHandlerAdapter} for processing requests
* with {@link HttpRequestHandler}s.

View File

@ -74,6 +74,11 @@ public interface WebMvcConfigurer {
*/
void configureContentNegotiation(ContentNegotiationConfigurer configurer);
/**
* Configure asynchronous request handling options.
*/
void configureAsyncSupport(AsyncSupportConfigurer configurer);
/**
* Add resolvers to support custom controller method argument types.
* <p>This does not override the built-in support for resolving handler

View File

@ -64,6 +64,13 @@ public abstract class WebMvcConfigurerAdapter implements WebMvcConfigurer {
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
}
/**
* {@inheritDoc}
* <p>This implementation is empty.
*/
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
}
/**
* {@inheritDoc}
* <p>This implementation is empty.

View File

@ -55,6 +55,12 @@ class WebMvcConfigurerComposite implements WebMvcConfigurer {
}
}
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
for (WebMvcConfigurer delegate : this.delegates) {
delegate.configureAsyncSupport(configurer);
}
}
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
for (WebMvcConfigurer delegate : this.delegates) {
delegate.configureMessageConverters(converters);

View File

@ -24,6 +24,7 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.http.HttpServletRequest;
@ -62,6 +63,7 @@ import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
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.AsyncWebUtils;
import org.springframework.web.context.request.async.WebAsyncManager;
@ -400,26 +402,28 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
}
/**
* Set the AsyncTaskExecutor to use when a controller method returns a
* {@code Callable}.
* <p>The default instance type is a {@link SimpleAsyncTaskExecutor}.
* It's recommended to change that default in production as the simple
* executor does not re-use threads.
* Set the default {@link AsyncTaskExecutor} to use when a controller method
* return a {@link Callable}. Controller methods can override this default on
* a per-request basis by returning an {@link AsyncTask}.
* <p>By default a {@link SimpleAsyncTaskExecutor} instance is used.
* It's recommended to change that default in production as the simple executor
* does not re-use threads.
*/
public void setAsyncTaskExecutor(AsyncTaskExecutor taskExecutor) {
public void setTaskExecutor(AsyncTaskExecutor taskExecutor) {
this.taskExecutor = taskExecutor;
}
/**
* Set the timeout for asynchronous request processing in milliseconds.
* When the timeout begins depends on the underlying async technology.
* With the Servlet 3 async support the timeout begins after the main
* processing thread has exited and has been returned to the container pool.
* <p>If a value is not provided, the default timeout of the underlying
* async technology is used (10 seconds on Tomcat with Servlet 3 async).
* Specify the amount of time, in milliseconds, before concurrent handling
* should time out. In Servlet 3, the timeout begins after the main request
* processing thread has exited and ends when the request is dispatched again
* for further processing of the concurrently produced result.
* <p>If this value is not set, the default timeout of the underlying
* implementation is used, e.g. 10 seconds on Tomcat with Servlet 3.
* @param timeout the timeout value in milliseconds
*/
public void setAsyncRequestTimeout(long asyncRequestTimeout) {
this.asyncRequestTimeout = asyncRequestTimeout;
public void setAsyncRequestTimeout(long timeout) {
this.asyncRequestTimeout = timeout;
}
/**

View File

@ -90,6 +90,38 @@
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="async-support" minOccurs="0">
<xsd:annotation>
<xsd:documentation><![CDATA[
Configure options for asynchronous request processing.
]]></xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:attribute name="task-executor" type="xsd:string">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.core.task.AsyncTaskExecutor"><![CDATA[
The bean name of a default AsyncTaskExecutor to use when a controller method returns a {@link Callable}.
Controller methods can override this default on a per-request basis by returning an AsyncTask.
By default a SimpleAsyncTaskExecutor is used which does not re-use threads and is not recommended for production.
]]></xsd:documentation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="java:org.springframework.core.task.AsyncTaskExecutor" />
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="default-timeout" type="xsd:long">
<xsd:annotation>
<xsd:documentation><![CDATA[
Specify the amount of time, in milliseconds, before asynchronous request handling times out.
In Servlet 3, the timeout begins after the main request processing thread has exited and ends when the request is dispatched again for further processing of the concurrently produced result.
If this value is not set, the default timeout of the underlying implementation is used, e.g. 10 seconds on Tomcat with Servlet 3.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
</xsd:element>
</xsd:all>
<xsd:attribute name="conversion-service" type="xsd:string">
<xsd:annotation>

View File

@ -45,6 +45,7 @@ import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockRequestDispatcher;
import org.springframework.mock.web.MockServletContext;
import org.springframework.scheduling.concurrent.ConcurrentTaskExecutor;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
@ -451,7 +452,7 @@ public class MvcNamespaceTests {
}
@Test
public void testCustomContentNegotiationManager() throws Exception {
public void testContentNegotiationManager() throws Exception {
loadBeanDefinitions("mvc-config-content-negotiation-manager.xml", 12);
RequestMappingHandlerMapping mapping = appContext.getBean(RequestMappingHandlerMapping.class);
@ -462,6 +463,16 @@ public class MvcNamespaceTests {
assertEquals(Arrays.asList(MediaType.valueOf("application/rss+xml")), manager.resolveMediaTypes(webRequest));
}
@Test
public void testAsyncSupportOptions() throws Exception {
loadBeanDefinitions("mvc-config-async-support.xml", 13);
RequestMappingHandlerAdapter adapter = appContext.getBean(RequestMappingHandlerAdapter.class);
assertNotNull(adapter);
assertEquals(ConcurrentTaskExecutor.class, new DirectFieldAccessor(adapter).getPropertyValue("taskExecutor").getClass());
assertEquals(2500L, new DirectFieldAccessor(adapter).getPropertyValue("asyncRequestTimeout"));
}
private void loadBeanDefinitions(String fileName, int expectedBeanCount) {
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(appContext);

View File

@ -21,6 +21,7 @@ import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.verify;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
@ -54,14 +55,14 @@ import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolv
*/
public class DelegatingWebMvcConfigurationTests {
private DelegatingWebMvcConfiguration mvcConfiguration;
private DelegatingWebMvcConfiguration delegatingConfig;
private WebMvcConfigurer configurer;
private WebMvcConfigurer webMvcConfigurer;
@Before
public void setUp() {
configurer = EasyMock.createMock(WebMvcConfigurer.class);
mvcConfiguration = new DelegatingWebMvcConfiguration();
webMvcConfigurer = EasyMock.createMock(WebMvcConfigurer.class);
delegatingConfig = new DelegatingWebMvcConfiguration();
}
@Test
@ -71,18 +72,20 @@ public class DelegatingWebMvcConfigurationTests {
Capture<FormattingConversionService> conversionService = new Capture<FormattingConversionService>();
Capture<List<HandlerMethodArgumentResolver>> resolvers = new Capture<List<HandlerMethodArgumentResolver>>();
Capture<List<HandlerMethodReturnValueHandler>> handlers = new Capture<List<HandlerMethodReturnValueHandler>>();
Capture<AsyncSupportConfigurer> asyncConfigurer = new Capture<AsyncSupportConfigurer>();
configurer.configureMessageConverters(capture(converters));
configurer.configureContentNegotiation(capture(contentNegotiationConfigurer));
expect(configurer.getValidator()).andReturn(null);
expect(configurer.getMessageCodesResolver()).andReturn(null);
configurer.addFormatters(capture(conversionService));
configurer.addArgumentResolvers(capture(resolvers));
configurer.addReturnValueHandlers(capture(handlers));
replay(configurer);
webMvcConfigurer.configureMessageConverters(capture(converters));
webMvcConfigurer.configureContentNegotiation(capture(contentNegotiationConfigurer));
expect(webMvcConfigurer.getValidator()).andReturn(null);
expect(webMvcConfigurer.getMessageCodesResolver()).andReturn(null);
webMvcConfigurer.addFormatters(capture(conversionService));
webMvcConfigurer.addArgumentResolvers(capture(resolvers));
webMvcConfigurer.addReturnValueHandlers(capture(handlers));
webMvcConfigurer.configureAsyncSupport(capture(asyncConfigurer));
replay(webMvcConfigurer);
mvcConfiguration.setConfigurers(Arrays.asList(configurer));
RequestMappingHandlerAdapter adapter = mvcConfiguration.requestMappingHandlerAdapter();
delegatingConfig.setConfigurers(Arrays.asList(webMvcConfigurer));
RequestMappingHandlerAdapter adapter = delegatingConfig.requestMappingHandlerAdapter();
ConfigurableWebBindingInitializer initializer = (ConfigurableWebBindingInitializer) adapter.getWebBindingInitializer();
assertSame(conversionService.getValue(), initializer.getConversionService());
@ -91,8 +94,9 @@ public class DelegatingWebMvcConfigurationTests {
assertEquals(0, resolvers.getValue().size());
assertEquals(0, handlers.getValue().size());
assertEquals(converters.getValue(), adapter.getMessageConverters());
assertNotNull(asyncConfigurer);
verify(configurer);
verify(webMvcConfigurer);
}
@Test
@ -104,33 +108,33 @@ public class DelegatingWebMvcConfigurationTests {
converters.add(new StringHttpMessageConverter());
}
});
mvcConfiguration = new DelegatingWebMvcConfiguration();
mvcConfiguration.setConfigurers(configurers);
delegatingConfig = new DelegatingWebMvcConfiguration();
delegatingConfig.setConfigurers(configurers);
RequestMappingHandlerAdapter adapter = mvcConfiguration.requestMappingHandlerAdapter();
RequestMappingHandlerAdapter adapter = delegatingConfig.requestMappingHandlerAdapter();
assertEquals("Only one custom converter should be registered", 1, adapter.getMessageConverters().size());
}
@Test
public void getCustomValidator() {
expect(configurer.getValidator()).andReturn(new LocalValidatorFactoryBean());
replay(configurer);
expect(webMvcConfigurer.getValidator()).andReturn(new LocalValidatorFactoryBean());
replay(webMvcConfigurer);
mvcConfiguration.setConfigurers(Arrays.asList(configurer));
mvcConfiguration.mvcValidator();
delegatingConfig.setConfigurers(Arrays.asList(webMvcConfigurer));
delegatingConfig.mvcValidator();
verify(configurer);
verify(webMvcConfigurer);
}
@Test
public void getCustomMessageCodesResolver() {
expect(configurer.getMessageCodesResolver()).andReturn(new DefaultMessageCodesResolver());
replay(configurer);
expect(webMvcConfigurer.getMessageCodesResolver()).andReturn(new DefaultMessageCodesResolver());
replay(webMvcConfigurer);
mvcConfiguration.setConfigurers(Arrays.asList(configurer));
mvcConfiguration.getMessageCodesResolver();
delegatingConfig.setConfigurers(Arrays.asList(webMvcConfigurer));
delegatingConfig.getMessageCodesResolver();
verify(configurer);
verify(webMvcConfigurer);
}
@Test
@ -139,13 +143,13 @@ public class DelegatingWebMvcConfigurationTests {
Capture<List<HandlerExceptionResolver>> exceptionResolvers = new Capture<List<HandlerExceptionResolver>>();
Capture<ContentNegotiationConfigurer> contentNegotiationConfigurer = new Capture<ContentNegotiationConfigurer>();
configurer.configureMessageConverters(capture(converters));
configurer.configureContentNegotiation(capture(contentNegotiationConfigurer));
configurer.configureHandlerExceptionResolvers(capture(exceptionResolvers));
replay(configurer);
webMvcConfigurer.configureMessageConverters(capture(converters));
webMvcConfigurer.configureContentNegotiation(capture(contentNegotiationConfigurer));
webMvcConfigurer.configureHandlerExceptionResolvers(capture(exceptionResolvers));
replay(webMvcConfigurer);
mvcConfiguration.setConfigurers(Arrays.asList(configurer));
mvcConfiguration.handlerExceptionResolver();
delegatingConfig.setConfigurers(Arrays.asList(webMvcConfigurer));
delegatingConfig.handlerExceptionResolver();
assertEquals(3, exceptionResolvers.getValue().size());
assertTrue(exceptionResolvers.getValue().get(0) instanceof ExceptionHandlerExceptionResolver);
@ -153,7 +157,7 @@ public class DelegatingWebMvcConfigurationTests {
assertTrue(exceptionResolvers.getValue().get(2) instanceof DefaultHandlerExceptionResolver);
assertTrue(converters.getValue().size() > 0);
verify(configurer);
verify(webMvcConfigurer);
}
@Test
@ -165,10 +169,10 @@ public class DelegatingWebMvcConfigurationTests {
exceptionResolvers.add(new DefaultHandlerExceptionResolver());
}
});
mvcConfiguration.setConfigurers(configurers);
delegatingConfig.setConfigurers(configurers);
HandlerExceptionResolverComposite composite =
(HandlerExceptionResolverComposite) mvcConfiguration.handlerExceptionResolver();
(HandlerExceptionResolverComposite) delegatingConfig.handlerExceptionResolver();
assertEquals("Only one custom converter is expected", 1, composite.getExceptionResolvers().size());
}

View File

@ -0,0 +1,294 @@
/*
* Copyright 2002-2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.config.annotation;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import java.util.Arrays;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.TestBean;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.io.FileSystemResourceLoader;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockServletContext;
import org.springframework.scheduling.concurrent.ConcurrentTaskExecutor;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.DefaultMessageCodesResolver;
import org.springframework.validation.Errors;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator;
import org.springframework.web.accept.ContentNegotiationManager;
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.support.StaticWebApplicationContext;
import org.springframework.web.method.annotation.ModelAttributeMethodProcessor;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
import org.springframework.web.servlet.handler.ConversionServiceExposingInterceptor;
import org.springframework.web.servlet.handler.HandlerExceptionResolverComposite;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
/**
* A test fixture with a sub-class of {@link WebMvcConfigurationSupport} that
* implements the various {@link WebMvcConfigurer} extension points.
*
* @author Rossen Stoyanchev
*/
public class WebMvcConfigurationSupportExtensionTests {
private TestWebMvcConfigurationSupport webConfig;
private StaticWebApplicationContext webAppContext;
@Before
public void setUp() {
this.webAppContext = new StaticWebApplicationContext();
this.webAppContext.setServletContext(new MockServletContext(new FileSystemResourceLoader()));
this.webAppContext.registerSingleton("controller", TestController.class);
this.webConfig = new TestWebMvcConfigurationSupport();
this.webConfig.setApplicationContext(this.webAppContext);
this.webConfig.setServletContext(this.webAppContext.getServletContext());
}
@Test
public void handlerMappings() throws Exception {
RequestMappingHandlerMapping rmHandlerMapping = webConfig.requestMappingHandlerMapping();
rmHandlerMapping.setApplicationContext(webAppContext);
rmHandlerMapping.afterPropertiesSet();
HandlerExecutionChain chain = rmHandlerMapping.getHandler(new MockHttpServletRequest("GET", "/"));
assertNotNull(chain.getInterceptors());
assertEquals(2, chain.getInterceptors().length);
assertEquals(LocaleChangeInterceptor.class, chain.getInterceptors()[0].getClass());
assertEquals(ConversionServiceExposingInterceptor.class, chain.getInterceptors()[1].getClass());
AbstractHandlerMapping handlerMapping = (AbstractHandlerMapping) webConfig.viewControllerHandlerMapping();
handlerMapping.setApplicationContext(webAppContext);
assertNotNull(handlerMapping);
assertEquals(1, handlerMapping.getOrder());
HandlerExecutionChain handler = handlerMapping.getHandler(new MockHttpServletRequest("GET", "/path"));
assertNotNull(handler.getHandler());
handlerMapping = (AbstractHandlerMapping) webConfig.resourceHandlerMapping();
handlerMapping.setApplicationContext(webAppContext);
assertNotNull(handlerMapping);
assertEquals(Integer.MAX_VALUE-1, handlerMapping.getOrder());
handler = handlerMapping.getHandler(new MockHttpServletRequest("GET", "/resources/foo.gif"));
assertNotNull(handler.getHandler());
handlerMapping = (AbstractHandlerMapping) webConfig.defaultServletHandlerMapping();
handlerMapping.setApplicationContext(webAppContext);
assertNotNull(handlerMapping);
assertEquals(Integer.MAX_VALUE, handlerMapping.getOrder());
handler = handlerMapping.getHandler(new MockHttpServletRequest("GET", "/anyPath"));
assertNotNull(handler.getHandler());
}
@Test
public void requestMappingHandlerAdapter() throws Exception {
RequestMappingHandlerAdapter adapter = webConfig.requestMappingHandlerAdapter();
// ConversionService
String actual = webConfig.mvcConversionService().convert(new TestBean(), String.class);
assertEquals("converted", actual);
// Message converters
assertEquals(1, adapter.getMessageConverters().size());
// Custom argument resolvers and return value handlers
@SuppressWarnings("unchecked")
List<HandlerMethodArgumentResolver> argResolvers= (List<HandlerMethodArgumentResolver>)
new DirectFieldAccessor(adapter).getPropertyValue("customArgumentResolvers");
assertEquals(1, argResolvers.size());
@SuppressWarnings("unchecked")
List<HandlerMethodReturnValueHandler> handlers = (List<HandlerMethodReturnValueHandler>)
new DirectFieldAccessor(adapter).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(false, new DirectFieldAccessor(adapter).getPropertyValue("ignoreDefaultModelOnRedirect"));
}
@Test
public void webBindingInitializer() throws Exception {
RequestMappingHandlerAdapter adapter = webConfig.requestMappingHandlerAdapter();
ConfigurableWebBindingInitializer initializer = (ConfigurableWebBindingInitializer) adapter.getWebBindingInitializer();
assertNotNull(initializer);
BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(null, "");
initializer.getValidator().validate(null, bindingResult);
assertEquals("invalid", bindingResult.getAllErrors().get(0).getCode());
String[] codes = initializer.getMessageCodesResolver().resolveMessageCodes("invalid", null);
assertEquals("custom.invalid", codes[0]);
}
@Test
public void contentNegotiation() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo.json");
NativeWebRequest webRequest = new ServletWebRequest(request);
ContentNegotiationManager manager = webConfig.requestMappingHandlerMapping().getContentNegotiationManager();
assertEquals(Arrays.asList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(webRequest));
request.setRequestURI("/foo.xml");
assertEquals(Arrays.asList(MediaType.APPLICATION_XML), manager.resolveMediaTypes(webRequest));
request.setRequestURI("/foo.rss");
assertEquals(Arrays.asList(MediaType.valueOf("application/rss+xml")), manager.resolveMediaTypes(webRequest));
request.setRequestURI("/foo.atom");
assertEquals(Arrays.asList(MediaType.APPLICATION_ATOM_XML), manager.resolveMediaTypes(webRequest));
request.setRequestURI("/foo");
request.setParameter("f", "json");
assertEquals(Arrays.asList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(webRequest));
}
@Test
public void exceptionResolvers() throws Exception {
HandlerExceptionResolverComposite composite = (HandlerExceptionResolverComposite) webConfig.handlerExceptionResolver();
assertEquals(1, composite.getExceptionResolvers().size());
}
@Controller
private static class TestController {
@RequestMapping("/")
public void handle() {
}
}
/**
* Since WebMvcConfigurationSupport does not implement WebMvcConfigurer, the purpose
* of this test class is also to ensure the two are in sync with each other. Effectively
* that ensures that application config classes that use the combo {@code @EnableWebMvc}
* plus WebMvcConfigurer can switch to extending WebMvcConfigurationSupport directly for
* more advanced configuration needs.
*/
private class TestWebMvcConfigurationSupport extends WebMvcConfigurationSupport implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter<TestBean, String>() {
public String convert(TestBean source) {
return "converted";
}
});
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new MappingJackson2HttpMessageConverter());
}
@Override
public Validator getValidator() {
return new Validator() {
public void validate(Object target, Errors errors) {
errors.reject("invalid");
}
public boolean supports(Class<?> clazz) {
return true;
}
};
}
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.setFavorParameter(true).setParameterName("f");
}
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
configurer.setDefaultTimeout(2500).setTaskExecutor(new ConcurrentTaskExecutor());
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new ModelAttributeMethodProcessor(true));
}
@Override
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) {
returnValueHandlers.add(new ModelAttributeMethodProcessor(true));
}
@Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
exceptionResolvers.add(new SimpleMappingExceptionResolver());
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LocaleChangeInterceptor());
}
@SuppressWarnings("serial")
@Override
public MessageCodesResolver getMessageCodesResolver() {
return new DefaultMessageCodesResolver() {
@Override
public String[] resolveMessageCodes(String errorCode, String objectName) {
return new String[] { "custom." + errorCode };
}
};
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/path");
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**").addResourceLocations("src/test/java");
}
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable("default");
}
}
}

View File

@ -21,64 +21,44 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.TestBean;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.io.FileSystemResourceLoader;
import org.springframework.format.FormatterRegistry;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockServletContext;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.DefaultMessageCodesResolver;
import org.springframework.validation.Errors;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.accept.ContentNegotiationManager;
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.support.StaticWebApplicationContext;
import org.springframework.web.method.annotation.ModelAttributeMethodProcessor;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
import org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping;
import org.springframework.web.servlet.handler.ConversionServiceExposingInterceptor;
import org.springframework.web.servlet.handler.HandlerExceptionResolverComposite;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
/**
* A test fixture for {@link WebMvcConfigurationSupport}.
* A test fixture with an {@link WebMvcConfigurationSupport} instance.
*
* @author Rossen Stoyanchev
*/
public class WebMvcConfigurationSupportTests {
private TestWebMvcConfiguration mvcConfiguration;
private WebMvcConfigurationSupport mvcConfiguration;
@Before
public void setUp() {
mvcConfiguration = new TestWebMvcConfiguration();
mvcConfiguration = new WebMvcConfigurationSupport();
}
@Test
@ -157,8 +137,6 @@ public class WebMvcConfigurationSupportTests {
Validator validator = initializer.getValidator();
assertNotNull(validator);
assertTrue(validator instanceof LocalValidatorFactoryBean);
assertEquals(false, new DirectFieldAccessor(adapter).getPropertyValue("ignoreDefaultModelOnRedirect"));
}
@Test
@ -173,94 +151,6 @@ public class WebMvcConfigurationSupportTests {
assertEquals(expectedResolvers.size(), compositeResolver.getExceptionResolvers().size());
}
@Test
public void webMvcConfigurerExtensionHooks() throws Exception {
StaticWebApplicationContext appCxt = new StaticWebApplicationContext();
appCxt.setServletContext(new MockServletContext(new FileSystemResourceLoader()));
appCxt.registerSingleton("controller", TestController.class);
WebConfig webConfig = new WebConfig();
webConfig.setApplicationContext(appCxt);
webConfig.setServletContext(appCxt.getServletContext());
String actual = webConfig.mvcConversionService().convert(new TestBean(), String.class);
assertEquals("converted", actual);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo.json");
NativeWebRequest webRequest = new ServletWebRequest(request);
ContentNegotiationManager manager = webConfig.requestMappingHandlerMapping().getContentNegotiationManager();
assertEquals(Arrays.asList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(webRequest));
request.setRequestURI("/foo.xml");
assertEquals(Arrays.asList(MediaType.APPLICATION_XML), manager.resolveMediaTypes(webRequest));
request.setRequestURI("/foo.rss");
assertEquals(Arrays.asList(MediaType.valueOf("application/rss+xml")), manager.resolveMediaTypes(webRequest));
request.setRequestURI("/foo.atom");
assertEquals(Arrays.asList(MediaType.APPLICATION_ATOM_XML), manager.resolveMediaTypes(webRequest));
request.setRequestURI("/foo");
request.setParameter("f", "json");
assertEquals(Arrays.asList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(webRequest));
RequestMappingHandlerAdapter adapter = webConfig.requestMappingHandlerAdapter();
assertEquals(1, adapter.getMessageConverters().size());
ConfigurableWebBindingInitializer initializer = (ConfigurableWebBindingInitializer) adapter.getWebBindingInitializer();
assertNotNull(initializer);
BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(null, "");
initializer.getValidator().validate(null, bindingResult);
assertEquals("invalid", bindingResult.getAllErrors().get(0).getCode());
String[] codes = initializer.getMessageCodesResolver().resolveMessageCodes("invalid", null);
assertEquals("custom.invalid", codes[0]);
@SuppressWarnings("unchecked")
List<HandlerMethodArgumentResolver> argResolvers= (List<HandlerMethodArgumentResolver>)
new DirectFieldAccessor(adapter).getPropertyValue("customArgumentResolvers");
assertEquals(1, argResolvers.size());
@SuppressWarnings("unchecked")
List<HandlerMethodReturnValueHandler> handlers = (List<HandlerMethodReturnValueHandler>)
new DirectFieldAccessor(adapter).getPropertyValue("customReturnValueHandlers");
assertEquals(1, handlers.size());
HandlerExceptionResolverComposite composite = (HandlerExceptionResolverComposite) webConfig.handlerExceptionResolver();
assertEquals(1, composite.getExceptionResolvers().size());
RequestMappingHandlerMapping rmHandlerMapping = webConfig.requestMappingHandlerMapping();
rmHandlerMapping.setApplicationContext(appCxt);
rmHandlerMapping.afterPropertiesSet();
HandlerExecutionChain chain = rmHandlerMapping.getHandler(new MockHttpServletRequest("GET", "/"));
assertNotNull(chain.getInterceptors());
assertEquals(2, chain.getInterceptors().length);
assertEquals(LocaleChangeInterceptor.class, chain.getInterceptors()[0].getClass());
assertEquals(ConversionServiceExposingInterceptor.class, chain.getInterceptors()[1].getClass());
AbstractHandlerMapping handlerMapping = (AbstractHandlerMapping) webConfig.viewControllerHandlerMapping();
handlerMapping.setApplicationContext(appCxt);
assertNotNull(handlerMapping);
assertEquals(1, handlerMapping.getOrder());
HandlerExecutionChain handler = handlerMapping.getHandler(new MockHttpServletRequest("GET", "/path"));
assertNotNull(handler.getHandler());
handlerMapping = (AbstractHandlerMapping) webConfig.resourceHandlerMapping();
handlerMapping.setApplicationContext(appCxt);
assertNotNull(handlerMapping);
assertEquals(Integer.MAX_VALUE-1, handlerMapping.getOrder());
handler = handlerMapping.getHandler(new MockHttpServletRequest("GET", "/resources/foo.gif"));
assertNotNull(handler.getHandler());
handlerMapping = (AbstractHandlerMapping) webConfig.defaultServletHandlerMapping();
handlerMapping.setApplicationContext(appCxt);
assertNotNull(handlerMapping);
assertEquals(Integer.MAX_VALUE, handlerMapping.getOrder());
handler = handlerMapping.getHandler(new MockHttpServletRequest("GET", "/anyPath"));
assertNotNull(handler.getHandler());
}
@Controller
private static class TestController {
@ -270,96 +160,4 @@ public class WebMvcConfigurationSupportTests {
}
}
private static class TestWebMvcConfiguration extends WebMvcConfigurationSupport {
}
/**
* Since WebMvcConfigurationSupport does not implement WebMvcConfigurer, the purpose
* of this test class is also to ensure the two are in sync with each other. Effectively
* that ensures that application config classes that use the combo {@code @EnableWebMvc}
* plus WebMvcConfigurer can switch to extending WebMvcConfigurationSupport directly for
* more advanced configuration needs.
*/
private class WebConfig extends WebMvcConfigurationSupport implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter<TestBean, String>() {
public String convert(TestBean source) {
return "converted";
}
});
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new MappingJackson2HttpMessageConverter());
}
@Override
public Validator getValidator() {
return new Validator() {
public void validate(Object target, Errors errors) {
errors.reject("invalid");
}
public boolean supports(Class<?> clazz) {
return true;
}
};
}
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.setFavorParameter(true).setParameterName("f");
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new ModelAttributeMethodProcessor(true));
}
@Override
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) {
returnValueHandlers.add(new ModelAttributeMethodProcessor(true));
}
@Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
exceptionResolvers.add(new SimpleMappingExceptionResolver());
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LocaleChangeInterceptor());
}
@SuppressWarnings("serial")
@Override
public MessageCodesResolver getMessageCodesResolver() {
return new DefaultMessageCodesResolver() {
@Override
public String[] resolveMessageCodes(String errorCode, String objectName) {
return new String[] { "custom." + errorCode };
}
};
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/path");
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**").addResourceLocations("src/test/java");
}
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable("default");
}
}
}

View File

@ -0,0 +1,13 @@
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd">
<mvc:annotation-driven>
<mvc:async-support default-timeout="2500" task-executor="executor" />
</mvc:annotation-driven>
<bean id="executor" class="org.springframework.scheduling.concurrent.ConcurrentTaskExecutor" />
</beans>