From 51fc3b4aaf01a3d8f8f0f9ec98f5e5c1215cc096 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Sun, 18 May 2014 19:20:58 -0400 Subject: [PATCH] Refactor @JsonView support w/ ResponseBodyInterceptor The newly added support for ResponseBodyInterceptor is a good fit for the (also recently added) support for the Jackson @JsonView annotation. This change refactors the original implementation of @JsonView support for @ResponseBody and ResponseEntity controller methods this time implemented as an ResponseBodyInterceptor. Issue: SPR-7156 --- .../MethodParameterHttpMessageConverter.java | 62 --------- .../MappingJackson2HttpMessageConverter.java | 46 +------ ...ueHolder.java => MappingJacksonValue.java} | 4 +- ...pingJackson2HttpMessageConverterTests.java | 2 +- .../client/RestTemplateIntegrationTests.java | 6 +- .../AnnotationDrivenBeanDefinitionParser.java | 12 ++ .../WebMvcConfigurationSupport.java | 12 ++ ...stractMessageConverterMethodProcessor.java | 12 -- .../JsonViewResponseBodyInterceptor.java | 60 +++++++++ .../RequestMappingHandlerAdapter.java | 8 +- .../annotation/ResponseBodyInterceptor.java | 2 +- .../ResponseBodyInterceptorChain.java | 2 +- ...tationDrivenBeanDefinitionParserTests.java | 14 ++ .../WebMvcConfigurationSupportTests.java | 12 ++ .../RequestMappingHandlerAdapterTests.java | 47 ++++++- ...questResponseBodyMethodProcessorTests.java | 31 +++-- .../ResponseBodyInterceptorChainTests.java | 123 ++++++++++++++++++ 17 files changed, 321 insertions(+), 134 deletions(-) delete mode 100644 spring-web/src/main/java/org/springframework/http/converter/MethodParameterHttpMessageConverter.java rename spring-web/src/main/java/org/springframework/http/converter/json/{MappingJacksonValueHolder.java => MappingJacksonValue.java} (92%) create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewResponseBodyInterceptor.java diff --git a/spring-web/src/main/java/org/springframework/http/converter/MethodParameterHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/MethodParameterHttpMessageConverter.java deleted file mode 100644 index e141e48fedff..000000000000 --- a/spring-web/src/main/java/org/springframework/http/converter/MethodParameterHttpMessageConverter.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2002-2014 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.http.converter; - -import org.springframework.core.MethodParameter; -import org.springframework.http.HttpInputMessage; -import org.springframework.http.HttpOutputMessage; -import org.springframework.http.MediaType; - -import java.io.IOException; - -/** - * An HttpMessageConverter that supports converting the value returned from a - * method by incorporating {@link org.springframework.core.MethodParameter} - * information into the conversion. Such a converter can for example take into - * account information from method-level annotations. - * - * @author Rossen Stoyanchev - * @since 4.1 - */ -public interface MethodParameterHttpMessageConverter extends HttpMessageConverter { - - /** - * This method mirrors {@link HttpMessageConverter#canRead(Class, MediaType)} - * with an additional {@code MethodParameter}. - */ - boolean canRead(Class clazz, MediaType mediaType, MethodParameter parameter); - - /** - * This method mirrors {@link HttpMessageConverter#canWrite(Class, MediaType)} - * with an additional {@code MethodParameter}. - */ - boolean canWrite(Class clazz, MediaType mediaType, MethodParameter parameter); - - /** - * This method mirrors {@link HttpMessageConverter#read(Class, HttpInputMessage)} - * with an additional {@code MethodParameter}. - */ - T read(Class clazz, HttpInputMessage inputMessage, MethodParameter parameter) - throws IOException, HttpMessageNotReadableException; - - /** - * This method mirrors {@link HttpMessageConverter#write(Object, MediaType, HttpOutputMessage)} - * with an additional {@code MethodParameter}. - */ - void write(T t, MediaType contentType, HttpOutputMessage outputMessage, MethodParameter parameter) - throws IOException, HttpMessageNotWritableException; - -} diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java index 627d341b62c7..46234d2fefd2 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java @@ -21,7 +21,6 @@ import java.nio.charset.Charset; import java.util.concurrent.atomic.AtomicReference; -import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; @@ -30,7 +29,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; -import org.springframework.core.MethodParameter; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; @@ -38,7 +36,6 @@ import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; -import org.springframework.http.converter.MethodParameterHttpMessageConverter; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -60,7 +57,7 @@ * @since 3.1.2 */ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConverter - implements GenericHttpMessageConverter, MethodParameterHttpMessageConverter { + implements GenericHttpMessageConverter { public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); @@ -150,11 +147,6 @@ private void configurePrettyPrint() { } } - @Override - public boolean canRead(Class clazz, MediaType mediaType, MethodParameter parameter) { - return canRead(clazz, mediaType); - } - @Override public boolean canRead(Class clazz, MediaType mediaType) { return canRead(clazz, null, mediaType); @@ -205,11 +197,6 @@ public boolean canWrite(Class clazz, MediaType mediaType) { return false; } - @Override - public boolean canWrite(Class clazz, MediaType mediaType, MethodParameter parameter) { - return canWrite(clazz, mediaType); - } - @Override protected boolean supports(Class clazz) { // should not be called, since we override canRead/Write instead @@ -224,11 +211,6 @@ protected Object readInternal(Class clazz, HttpInputMessage inputMessage) return readJavaType(javaType, inputMessage); } - @Override - public Object read(Class clazz, HttpInputMessage inputMessage, MethodParameter parameter) throws IOException, HttpMessageNotReadableException { - return super.read(clazz, inputMessage); - } - @Override public Object read(Type type, Class contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { @@ -267,8 +249,8 @@ protected void writeInternal(Object object, HttpOutputMessage outputMessage) if (this.jsonPrefix != null) { jsonGenerator.writeRaw(this.jsonPrefix); } - if (object instanceof MappingJacksonValueHolder) { - MappingJacksonValueHolder valueHolder = (MappingJacksonValueHolder) object; + if (object instanceof MappingJacksonValue) { + MappingJacksonValue valueHolder = (MappingJacksonValue) object; object = valueHolder.getValue(); Class serializationView = valueHolder.getSerializationView(); this.objectMapper.writerWithView(serializationView).writeValue(jsonGenerator, object); @@ -282,20 +264,6 @@ protected void writeInternal(Object object, HttpOutputMessage outputMessage) } } - @Override - public void write(Object object, MediaType contentType, HttpOutputMessage outputMessage, MethodParameter parameter) - throws IOException, HttpMessageNotWritableException { - - JsonView annot = parameter.getMethodAnnotation(JsonView.class); - if (annot != null && annot.value().length != 0) { - MappingJacksonValueHolder serializationValue = new MappingJacksonValueHolder(object, annot.value()[0]); - super.write(serializationValue, contentType, outputMessage); - } - else { - super.write(object, contentType, outputMessage); - } - } - /** * Return the Jackson {@link JavaType} for the specified type and context class. *

The default implementation returns {@code typeFactory.constructType(type, contextClass)}, @@ -339,16 +307,16 @@ protected JsonEncoding getJsonEncoding(MediaType contentType) { @Override protected MediaType getDefaultContentType(Object object) throws IOException { - if (object instanceof MappingJacksonValueHolder) { - object = ((MappingJacksonValueHolder) object).getValue(); + if (object instanceof MappingJacksonValue) { + object = ((MappingJacksonValue) object).getValue(); } return super.getDefaultContentType(object); } @Override protected Long getContentLength(Object object, MediaType contentType) throws IOException { - if (object instanceof MappingJacksonValueHolder) { - object = ((MappingJacksonValueHolder) object).getValue(); + if (object instanceof MappingJacksonValue) { + object = ((MappingJacksonValue) object).getValue(); } return super.getContentLength(object, contentType); } diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonValueHolder.java b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonValue.java similarity index 92% rename from spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonValueHolder.java rename to spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonValue.java index a2331b883967..1e293e66b060 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonValueHolder.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonValue.java @@ -25,7 +25,7 @@ * * @see com.fasterxml.jackson.annotation.JsonView */ -public class MappingJacksonValueHolder { +public class MappingJacksonValue { private final Object value; @@ -37,7 +37,7 @@ public class MappingJacksonValueHolder { * @param value the Object to be serialized * @param serializationView the view to be applied */ - public MappingJacksonValueHolder(Object value, Class serializationView) { + public MappingJacksonValue(Object value, Class serializationView) { this.value = value; this.serializationView = serializationView; } diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java index 20096feb385b..b206fa0cfca7 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java @@ -245,7 +245,7 @@ public void jsonView() throws Exception { bean.setWithView1("with"); bean.setWithView2("with"); bean.setWithoutView("without"); - MappingJacksonValueHolder jsv = new MappingJacksonValueHolder(bean, MyJacksonView1.class); + MappingJacksonValue jsv = new MappingJacksonValue(bean, MyJacksonView1.class); this.converter.writeInternal(jsv, outputMessage); String result = outputMessage.getBodyAsString(Charset.forName("UTF-8")); diff --git a/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java b/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java index 996eff3b3993..1595cd6ed87e 100644 --- a/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java @@ -36,7 +36,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; -import org.springframework.http.converter.json.MappingJacksonValueHolder; +import org.springframework.http.converter.json.MappingJacksonValue; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -220,8 +220,8 @@ public void jsonPostForObjectWithJacksonView() throws URISyntaxException { HttpHeaders entityHeaders = new HttpHeaders(); entityHeaders.setContentType(new MediaType("application", "json", Charset.forName("UTF-8"))); MySampleBean bean = new MySampleBean("with", "with", "without"); - MappingJacksonValueHolder jsv = new MappingJacksonValueHolder(bean, MyJacksonView1.class); - HttpEntity entity = new HttpEntity(jsv); + MappingJacksonValue jsv = new MappingJacksonValue(bean, MyJacksonView1.class); + HttpEntity entity = new HttpEntity(jsv); String s = template.postForObject(baseUrl + "/jsonpost", entity, String.class, "post"); assertTrue(s.contains("\"with1\":\"with\"")); assertFalse(s.contains("\"with2\":\"with\"")); 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 26c653ce6b75..e821530ef166 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 @@ -19,6 +19,7 @@ import java.util.List; import java.util.Properties; +import org.springframework.web.servlet.mvc.method.annotation.JsonViewResponseBodyInterceptor; import org.w3c.dom.Element; import org.springframework.beans.factory.FactoryBean; @@ -196,6 +197,7 @@ else if (element.hasAttribute("enableMatrixVariables")) { handlerAdapterDef.getPropertyValues().add("contentNegotiationManager", contentNegotiationManager); handlerAdapterDef.getPropertyValues().add("webBindingInitializer", bindingDef); handlerAdapterDef.getPropertyValues().add("messageConverters", messageConverters); + addResponseBodyInterceptors(handlerAdapterDef); if (element.hasAttribute("ignore-default-model-on-redirect")) { Boolean ignoreDefaultModel = Boolean.valueOf(element.getAttribute("ignore-default-model-on-redirect")); @@ -247,6 +249,8 @@ else if (element.hasAttribute("ignoreDefaultModelOnRedirect")) { exceptionHandlerExceptionResolver.getPropertyValues().add("contentNegotiationManager", contentNegotiationManager); exceptionHandlerExceptionResolver.getPropertyValues().add("messageConverters", messageConverters); exceptionHandlerExceptionResolver.getPropertyValues().add("order", 0); + addResponseBodyInterceptors(exceptionHandlerExceptionResolver); + String methodExceptionResolverName = parserContext.getReaderContext().registerWithGeneratedName(exceptionHandlerExceptionResolver); @@ -280,6 +284,13 @@ else if (element.hasAttribute("ignoreDefaultModelOnRedirect")) { return null; } + protected void addResponseBodyInterceptors(RootBeanDefinition beanDef) { + if (jackson2Present) { + beanDef.getPropertyValues().add("responseBodyInterceptors", + new RootBeanDefinition(JsonViewResponseBodyInterceptor.class)); + } + } + private RuntimeBeanReference getConversionService(Element element, Object source, ParserContext parserContext) { RuntimeBeanReference conversionServiceRef; if (element.hasAttribute("conversion-service")) { @@ -493,6 +504,7 @@ private RootBeanDefinition createConverterDefinition(Class converterClass, Ob return beanDefinition; } + private ManagedList extractBeanSubElements(Element parentElement, ParserContext parserContext) { ManagedList list = new ManagedList(); list.setSource(parserContext.extractSource(parentElement)); 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 1bf7e423206c..bf185ad3f816 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 @@ -17,6 +17,7 @@ package org.springframework.web.servlet.config.annotation; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -74,8 +75,10 @@ import org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter; import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver; import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; +import org.springframework.web.servlet.mvc.method.annotation.JsonViewResponseBodyInterceptor; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyInterceptor; import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver; import org.springframework.web.servlet.resource.ResourceUrlProvider; import org.springframework.web.servlet.resource.ResourceUrlProviderExposingInterceptor; @@ -417,6 +420,11 @@ public RequestMappingHandlerAdapter requestMappingHandlerAdapter() { adapter.setCustomArgumentResolvers(argumentResolvers); adapter.setCustomReturnValueHandlers(returnValueHandlers); + if (jackson2Present) { + ResponseBodyInterceptor interceptor = new JsonViewResponseBodyInterceptor(); + adapter.setResponseBodyInterceptors(Arrays.asList(interceptor)); + } + AsyncSupportConfigurer configurer = new AsyncSupportConfigurer(); configureAsyncSupport(configurer); @@ -695,6 +703,10 @@ protected final void addDefaultHandlerExceptionResolvers(List messageConverter : this.messageConverters) { - if (messageConverter instanceof MethodParameterHttpMessageConverter) { - MethodParameterHttpMessageConverter c = (MethodParameterHttpMessageConverter) messageConverter; - if (c.canWrite(returnValueClass, selectedMediaType, returnType)) { - c.write(returnValue, selectedMediaType, outputMessage, returnType); - if (logger.isDebugEnabled()) { - logger.debug("Written [" + returnValue + "] as \"" + selectedMediaType + "\" using [" + - messageConverter + "]"); - } - return; - } - } if (messageConverter.canWrite(returnValueClass, selectedMediaType)) { returnValue = this.interceptorChain.invoke(returnValue, selectedMediaType, (Class>) messageConverter.getClass(), diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewResponseBodyInterceptor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewResponseBodyInterceptor.java new file mode 100644 index 000000000000..7713f12f03bb --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewResponseBodyInterceptor.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.mvc.method.annotation; + +import com.fasterxml.jackson.annotation.JsonView; +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.http.converter.json.MappingJacksonValue; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.util.Assert; + +/** + * A {@code ResponseBodyInterceptor} implementation that adds support for the + * Jackson {@code @JsonView} annotation on a Spring MVC {@code @RequestMapping} + * or {@code @ExceptionHandler} method. + * + * @author Rossen Stoyanchev + * @since 4.1 + */ +public class JsonViewResponseBodyInterceptor implements ResponseBodyInterceptor { + + + @Override + @SuppressWarnings("unchecked") + public T beforeBodyWrite(T body, MediaType contentType, Class> converterType, + MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) { + + if (!MappingJackson2HttpMessageConverter.class.equals(converterType)) { + return body; + } + + JsonView annotation = returnType.getMethodAnnotation(JsonView.class); + if (annotation == null) { + return body; + } + + Assert.isTrue(annotation.value().length != 0, + "Expected at least one serialization view class in JsonView annotation on " + returnType); + + return (T) new MappingJacksonValue(body, annotation.value()[0]); + } + +} 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 aaddbd21fc78..34cd288f327e 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 @@ -526,6 +526,8 @@ private void initControllerAdviceCache() { List beans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext()); Collections.sort(beans, new OrderComparator()); + List interceptorBeans = new ArrayList(); + for (ControllerAdviceBean bean : beans) { Set attrMethods = HandlerMethodSelector.selectMethods(bean.getBeanType(), MODEL_ATTRIBUTE_METHODS); if (!attrMethods.isEmpty()) { @@ -538,10 +540,14 @@ private void initControllerAdviceCache() { logger.info("Detected @InitBinder methods in " + bean); } if (ResponseBodyInterceptor.class.isAssignableFrom(bean.getBeanType())) { - this.responseBodyInterceptors.add(bean); + interceptorBeans.add(bean); logger.info("Detected ResponseBodyInterceptor implementation in " + bean); } } + + if (!interceptorBeans.isEmpty()) { + this.responseBodyInterceptors.addAll(0, interceptorBeans); + } } /** diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyInterceptor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyInterceptor.java index 55a266953b3b..a4463c244721 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyInterceptor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyInterceptor.java @@ -46,7 +46,7 @@ public interface ResponseBodyInterceptor { * * @return the body that was passed in or a modified, possibly new instance */ - T beforeBodyWrite(T body, MediaType contentType, Class> converterType, + T beforeBodyWrite(T body, MediaType contentType, Class> converterType, MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyInterceptorChain.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyInterceptorChain.java index f74dd75d5666..b47bd8d52b1e 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyInterceptorChain.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyInterceptorChain.java @@ -46,7 +46,7 @@ public ResponseBodyInterceptorChain(List interceptors) { } - public T invoke(T body, MediaType contentType, Class> converterType, + public T invoke(T body, MediaType contentType, Class> converterType, MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) { if (this.interceptors != null) { diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParserTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParserTests.java index c067334c8ad9..5ee33798fcc9 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParserTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParserTests.java @@ -38,9 +38,11 @@ import org.springframework.web.method.support.HandlerMethodReturnValueHandler; import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping; +import org.springframework.web.servlet.mvc.method.annotation.JsonViewResponseBodyInterceptor; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyInterceptor; import org.springframework.web.servlet.mvc.method.annotation.ServletWebArgumentResolverAdapter; import org.springframework.web.util.UrlPathHelper; @@ -93,6 +95,8 @@ public void testMessageConverters() { loadBeanDefinitions("mvc-config-message-converters.xml"); verifyMessageConverters(appContext.getBean(RequestMappingHandlerAdapter.class), true); verifyMessageConverters(appContext.getBean(ExceptionHandlerExceptionResolver.class), true); + verifyResponseBodyInterceptors(appContext.getBean(RequestMappingHandlerAdapter.class)); + verifyResponseBodyInterceptors(appContext.getBean(ExceptionHandlerExceptionResolver.class)); } @Test @@ -162,6 +166,16 @@ private void verifyMessageConverters(Object bean, boolean hasDefaultRegistration assertTrue(converters.get(1) instanceof ResourceHttpMessageConverter); } + @SuppressWarnings("unchecked") + private void verifyResponseBodyInterceptors(Object bean) { + assertNotNull(bean); + Object value = new DirectFieldAccessor(bean).getPropertyValue("responseBodyInterceptors"); + assertNotNull(value); + assertTrue(value instanceof List); + List converters = (List) value; + assertTrue(converters.get(0) instanceof JsonViewResponseBodyInterceptor); + } + } class TestWebArgumentResolver implements WebArgumentResolver { diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java index 961a5a550863..c71c12f8d785 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java @@ -23,6 +23,7 @@ import org.junit.Before; import org.junit.Test; +import org.springframework.beans.DirectFieldAccessor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; @@ -51,6 +52,7 @@ import org.springframework.web.servlet.handler.HandlerExceptionResolverComposite; import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver; import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; +import org.springframework.web.servlet.mvc.method.annotation.JsonViewResponseBodyInterceptor; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; @@ -157,6 +159,11 @@ public void requestMappingHandlerAdapter() throws Exception { Validator validator = initializer.getValidator(); assertNotNull(validator); assertTrue(validator instanceof LocalValidatorFactoryBean); + + DirectFieldAccessor fieldAccessor = new DirectFieldAccessor(adapter); + List interceptors = (List) fieldAccessor.getPropertyValue("responseBodyInterceptors"); + assertEquals(1, interceptors.size()); + assertEquals(JsonViewResponseBodyInterceptor.class, interceptors.get(0).getClass()); } @Test @@ -183,6 +190,11 @@ public void handlerExceptionResolver() throws Exception { ExceptionHandlerExceptionResolver eher = (ExceptionHandlerExceptionResolver) expectedResolvers.get(0); assertNotNull(eher.getApplicationContext()); + + DirectFieldAccessor fieldAccessor = new DirectFieldAccessor(eher); + List interceptors = (List) fieldAccessor.getPropertyValue("responseBodyInterceptors"); + assertEquals(1, interceptors.size()); + assertEquals(JsonViewResponseBodyInterceptor.class, interceptors.get(0).getClass()); } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java index 3ab95b344c36..9a8174bde4a5 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java @@ -23,6 +23,14 @@ import org.junit.BeforeClass; import org.junit.Test; +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.mock.web.test.MockHttpServletRequest; import org.springframework.mock.web.test.MockHttpServletResponse; import org.springframework.ui.Model; @@ -217,6 +225,21 @@ public void modelAttributePackageNameAdvice() throws Exception { assertEquals(null,mav.getModel().get("attr3")); } + // SPR-10859 + + @Test + public void responseBodyInterceptor() throws Exception { + this.webAppContext.registerSingleton("rba", ResponseCodeSuppressingAdvice.class); + this.webAppContext.refresh(); + + HandlerMethod handlerMethod = handlerMethod(new SimpleController(), "handleWithResponseEntity"); + this.handlerAdapter.afterPropertiesSet(); + this.handlerAdapter.handle(this.request, this.response, handlerMethod); + + assertEquals(200, this.response.getStatus()); + assertEquals("status=400, message=body", this.response.getContentAsString()); + } + private HandlerMethod handlerMethod(Object handler, String methodName, Class... paramTypes) throws Exception { Method method = handler.getClass().getDeclaredMethod(methodName, paramTypes); @@ -241,6 +264,10 @@ public void addAttributes(Model model) { public String handle() { return null; } + + public ResponseEntity handleWithResponseEntity() { + return new ResponseEntity("body", HttpStatus.BAD_REQUEST); + } } @@ -266,6 +293,7 @@ public String handle(Model model) { @ControllerAdvice private static class ModelAttributeAdvice { + @SuppressWarnings("unused") @ModelAttribute public void addAttributes(Model model) { model.addAttribute("attr1", "gAttr1"); @@ -274,9 +302,10 @@ public void addAttributes(Model model) { } - @ControllerAdvice({"org.springframework.web.servlet.mvc.method.annotation","java.lang"}) + @ControllerAdvice({"org.springframework.web.servlet.mvc.method.annotation", "java.lang"}) private static class ModelAttributePackageAdvice { + @SuppressWarnings("unused") @ModelAttribute public void addAttributes(Model model) { model.addAttribute("attr2", "gAttr2"); @@ -287,10 +316,26 @@ public void addAttributes(Model model) { @ControllerAdvice("java.lang") private static class ModelAttributeNotUsedPackageAdvice { + @SuppressWarnings("unused") @ModelAttribute public void addAttributes(Model model) { model.addAttribute("attr3", "gAttr3"); } } + @ControllerAdvice + private static class ResponseCodeSuppressingAdvice implements ResponseBodyInterceptor { + + @SuppressWarnings("unchecked") + @Override + public T beforeBodyWrite(T body, MediaType contentType, + Class> converterType, + MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) { + + int status = ((ServletServerHttpResponse) response).getServletResponse().getStatus(); + response.setStatusCode(HttpStatus.OK); + return (T) ("status=" + status + ", message=" + body); + } + } + } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java index 2c572e0ad999..939968df640c 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java @@ -19,6 +19,7 @@ import java.io.Serializable; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import com.fasterxml.jackson.annotation.JsonView; @@ -28,7 +29,9 @@ import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.target.SingletonTargetSource; import org.springframework.core.MethodParameter; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.http.converter.ByteArrayHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; @@ -287,36 +290,42 @@ public void supportsReturnTypeRestController() throws Exception { } @Test - public void handleResponseBodyJacksonView() throws Exception { + public void jacksonJsonViewWithResponseBody() throws Exception { Method method = JacksonViewController.class.getMethod("handleResponseBody"); HandlerMethod handlerMethod = new HandlerMethod(new JacksonViewController(), method); MethodParameter methodReturnType = handlerMethod.getReturnType(); List> converters = new ArrayList>(); converters.add(new MappingJackson2HttpMessageConverter()); - RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters); - processor.handleReturnValue(new JacksonViewController().handleResponseBody(), methodReturnType, mavContainer, webRequest); + RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor( + converters, null, Arrays.asList(new JsonViewResponseBodyInterceptor())); + + Object returnValue = new JacksonViewController().handleResponseBody(); + processor.handleReturnValue(returnValue, methodReturnType, this.mavContainer, this.webRequest); - String content = servletResponse.getContentAsString(); + String content = this.servletResponse.getContentAsString(); assertFalse(content.contains("\"withView1\":\"with\"")); assertTrue(content.contains("\"withView2\":\"with\"")); assertTrue(content.contains("\"withoutView\":\"without\"")); } @Test - public void handleResponseBodyJacksonViewAndModelAndView() throws Exception { - Method method = JacksonViewController.class.getMethod("handleResponseBodyWithModelAndView"); + public void jacksonJsonViewWithResponseEntity() throws Exception { + Method method = JacksonViewController.class.getMethod("handleResponseEntity"); HandlerMethod handlerMethod = new HandlerMethod(new JacksonViewController(), method); MethodParameter methodReturnType = handlerMethod.getReturnType(); List> converters = new ArrayList>(); converters.add(new MappingJackson2HttpMessageConverter()); - RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters); - processor.handleReturnValue(new JacksonViewController().handleResponseBody(), methodReturnType, mavContainer, webRequest); + HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor( + converters, null, Arrays.asList(new JsonViewResponseBodyInterceptor())); + + Object returnValue = new JacksonViewController().handleResponseEntity(); + processor.handleReturnValue(returnValue, methodReturnType, this.mavContainer, this.webRequest); - String content = servletResponse.getContentAsString(); + String content = this.servletResponse.getContentAsString(); assertFalse(content.contains("\"withView1\":\"with\"")); assertTrue(content.contains("\"withView2\":\"with\"")); assertTrue(content.contains("\"withoutView\":\"without\"")); @@ -465,14 +474,14 @@ public JacksonViewBean handleResponseBody() { @RequestMapping @JsonView(MyJacksonView2.class) - public ModelAndView handleResponseBodyWithModelAndView() { + public ResponseEntity handleResponseEntity() { JacksonViewBean bean = new JacksonViewBean(); bean.setWithView1("with"); bean.setWithView2("with"); bean.setWithoutView("without"); ModelAndView mav = new ModelAndView(new MappingJackson2JsonView()); mav.addObject("bean", bean); - return mav; + return new ResponseEntity(bean, HttpStatus.OK); } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyInterceptorChainTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyInterceptorChainTests.java index 90f6b0885fe7..5c031f196c28 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyInterceptorChainTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyInterceptorChainTests.java @@ -16,11 +16,134 @@ package org.springframework.web.servlet.mvc.method.annotation; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.http.server.ServletServerHttpResponse; +import org.springframework.mock.web.test.MockHttpServletRequest; +import org.springframework.mock.web.test.MockHttpServletResponse; +import org.springframework.stereotype.Controller; +import org.springframework.util.ClassUtils; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.method.ControllerAdviceBean; + +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.*; + /** + * Unit tests for + * {@link org.springframework.web.servlet.mvc.method.annotation.ResponseBodyInterceptorChain}. + * * @author Rossen Stoyanchev * @since 4.1 */ public class ResponseBodyInterceptorChainTests { + private String body; + + private MediaType contentType; + + private Class> converterType; + + private MethodParameter returnType; + + private ServerHttpRequest request; + + private ServerHttpResponse response; + + + @Before + public void setup() { + this.body = "body"; + this.contentType = MediaType.TEXT_PLAIN; + this.converterType = StringHttpMessageConverter.class; + this.returnType = new MethodParameter(ClassUtils.getMethod(this.getClass(), "handle"), -1); + this.request = new ServletServerHttpRequest(new MockHttpServletRequest()); + this.response = new ServletServerHttpResponse(new MockHttpServletResponse()); + } + + @Test + public void responseBodyInterceptor() { + + ResponseBodyInterceptor interceptor = Mockito.mock(ResponseBodyInterceptor.class); + ResponseBodyInterceptorChain chain = new ResponseBodyInterceptorChain(Arrays.asList(interceptor)); + + String expected = "body++"; + when(interceptor.beforeBodyWrite( + eq(this.body), eq(this.contentType), eq(this.converterType), eq(this.returnType), + same(this.request), same(this.response))).thenReturn(expected); + + String actual = chain.invoke(this.body, this.contentType, + this.converterType, this.returnType, this.request, this.response); + + assertEquals(expected, actual); + } + + @Test + public void controllerAdvice() { + + Object interceptor = new ControllerAdviceBean(new MyControllerAdvice()); + ResponseBodyInterceptorChain chain = new ResponseBodyInterceptorChain(Arrays.asList(interceptor)); + + String actual = chain.invoke(this.body, this.contentType, + this.converterType, this.returnType, this.request, this.response); + + assertEquals("body-MyControllerAdvice", actual); + } + + @Test + public void controllerAdviceNotApplicable() { + + Object interceptor = new ControllerAdviceBean(new TargetedControllerAdvice()); + ResponseBodyInterceptorChain chain = new ResponseBodyInterceptorChain(Arrays.asList(interceptor)); + + String actual = chain.invoke(this.body, this.contentType, + this.converterType, this.returnType, this.request, this.response); + + assertEquals(this.body, actual); + } + + + @ControllerAdvice + private static class MyControllerAdvice implements ResponseBodyInterceptor { + + @SuppressWarnings("unchecked") + @Override + public T beforeBodyWrite(T body, MediaType contentType, + Class> converterType, + MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) { + + return (T) (body + "-MyControllerAdvice"); + } + } + + @ControllerAdvice(annotations = Controller.class) + private static class TargetedControllerAdvice implements ResponseBodyInterceptor { + + @SuppressWarnings("unchecked") + @Override + public T beforeBodyWrite(T body, MediaType contentType, + Class> converterType, + MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) { + + return (T) (body + "-TargetedControllerAdvice"); + } + } + + @SuppressWarnings("unused") + @ResponseBody + public String handle() { + return ""; + } }