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
This commit is contained in:
@@ -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<T> extends HttpMessageConverter<T> {
|
||||
|
||||
/**
|
||||
* 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<? extends T> 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;
|
||||
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import java.lang.reflect.Type;
|
||||
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.JavaType;
|
||||
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.AbstractHttpMessageConverter;
|
||||
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 @@ import org.springframework.util.ClassUtils;
|
||||
* @since 3.1.2
|
||||
*/
|
||||
public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConverter<Object>
|
||||
implements GenericHttpMessageConverter<Object>, MethodParameterHttpMessageConverter<Object> {
|
||||
implements GenericHttpMessageConverter<Object> {
|
||||
|
||||
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
|
||||
|
||||
@@ -150,11 +147,6 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv
|
||||
}
|
||||
}
|
||||
|
||||
@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 class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv
|
||||
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 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv
|
||||
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 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv
|
||||
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 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv
|
||||
}
|
||||
}
|
||||
|
||||
@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.
|
||||
* <p>The default implementation returns {@code typeFactory.constructType(type, contextClass)},
|
||||
@@ -339,16 +307,16 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ package org.springframework.http.converter.json;
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
@@ -245,7 +245,7 @@ public class MappingJackson2HttpMessageConverterTests {
|
||||
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"));
|
||||
|
||||
@@ -36,7 +36,7 @@ import org.springframework.http.HttpStatus;
|
||||
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 class RestTemplateIntegrationTests extends AbstractJettyServerTestCase {
|
||||
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<MappingJacksonValueHolder> entity = new HttpEntity<MappingJacksonValueHolder>(jsv);
|
||||
MappingJacksonValue jsv = new MappingJacksonValue(bean, MyJacksonView1.class);
|
||||
HttpEntity<MappingJacksonValue> entity = new HttpEntity<MappingJacksonValue>(jsv);
|
||||
String s = template.postForObject(baseUrl + "/jsonpost", entity, String.class, "post");
|
||||
assertTrue(s.contains("\"with1\":\"with\""));
|
||||
assertFalse(s.contains("\"with2\":\"with\""));
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.springframework.web.servlet.config;
|
||||
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 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
|
||||
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 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
|
||||
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 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
|
||||
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 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
|
||||
return beanDefinition;
|
||||
}
|
||||
|
||||
|
||||
private ManagedList<BeanDefinitionHolder> extractBeanSubElements(Element parentElement, ParserContext parserContext) {
|
||||
ManagedList<BeanDefinitionHolder> list = new ManagedList<BeanDefinitionHolder>();
|
||||
list.setSource(parserContext.extractSource(parentElement));
|
||||
|
||||
@@ -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.HttpRequestHandlerAdapter;
|
||||
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 class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
|
||||
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 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
|
||||
exceptionHandlerExceptionResolver.setApplicationContext(this.applicationContext);
|
||||
exceptionHandlerExceptionResolver.setContentNegotiationManager(mvcContentNegotiationManager());
|
||||
exceptionHandlerExceptionResolver.setMessageConverters(getMessageConverters());
|
||||
if (jackson2Present) {
|
||||
ResponseBodyInterceptor interceptor = new JsonViewResponseBodyInterceptor();
|
||||
exceptionHandlerExceptionResolver.setResponseBodyInterceptors(Arrays.asList(interceptor));
|
||||
}
|
||||
exceptionHandlerExceptionResolver.afterPropertiesSet();
|
||||
|
||||
exceptionResolvers.add(exceptionHandlerExceptionResolver);
|
||||
|
||||
@@ -29,7 +29,6 @@ import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.MethodParameterHttpMessageConverter;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
@@ -148,17 +147,6 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
|
||||
if (selectedMediaType != null) {
|
||||
selectedMediaType = selectedMediaType.removeQualityValue();
|
||||
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
|
||||
if (messageConverter instanceof MethodParameterHttpMessageConverter) {
|
||||
MethodParameterHttpMessageConverter<T> c = (MethodParameterHttpMessageConverter<T>) 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<HttpMessageConverter<T>>) messageConverter.getClass(),
|
||||
|
||||
@@ -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> T beforeBodyWrite(T body, MediaType contentType, Class<? extends HttpMessageConverter<T>> 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]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -526,6 +526,8 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
|
||||
List<ControllerAdviceBean> beans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
|
||||
Collections.sort(beans, new OrderComparator());
|
||||
|
||||
List<Object> interceptorBeans = new ArrayList<Object>();
|
||||
|
||||
for (ControllerAdviceBean bean : beans) {
|
||||
Set<Method> attrMethods = HandlerMethodSelector.selectMethods(bean.getBeanType(), MODEL_ATTRIBUTE_METHODS);
|
||||
if (!attrMethods.isEmpty()) {
|
||||
@@ -538,10 +540,14 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -46,7 +46,7 @@ public interface ResponseBodyInterceptor {
|
||||
*
|
||||
* @return the body that was passed in or a modified, possibly new instance
|
||||
*/
|
||||
<T> T beforeBodyWrite(T body, MediaType contentType, Class<HttpMessageConverter<T>> converterType,
|
||||
<T> T beforeBodyWrite(T body, MediaType contentType, Class<? extends HttpMessageConverter<T>> converterType,
|
||||
MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response);
|
||||
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ class ResponseBodyInterceptorChain {
|
||||
}
|
||||
|
||||
|
||||
public <T> T invoke(T body, MediaType contentType, Class<HttpMessageConverter<T>> converterType,
|
||||
public <T> T invoke(T body, MediaType contentType, Class<? extends HttpMessageConverter<T>> converterType,
|
||||
MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) {
|
||||
|
||||
if (this.interceptors != null) {
|
||||
|
||||
@@ -38,9 +38,11 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
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 class AnnotationDrivenBeanDefinitionParserTests {
|
||||
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 @@ public class AnnotationDrivenBeanDefinitionParserTests {
|
||||
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<ResponseBodyInterceptor> converters = (List<ResponseBodyInterceptor>) value;
|
||||
assertTrue(converters.get(0) instanceof JsonViewResponseBodyInterceptor);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TestWebArgumentResolver implements WebArgumentResolver {
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.joda.time.DateTime;
|
||||
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.ConversionServiceExposingIntercep
|
||||
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 class WebMvcConfigurationSupportTests {
|
||||
Validator validator = initializer.getValidator();
|
||||
assertNotNull(validator);
|
||||
assertTrue(validator instanceof LocalValidatorFactoryBean);
|
||||
|
||||
DirectFieldAccessor fieldAccessor = new DirectFieldAccessor(adapter);
|
||||
List<Object> interceptors = (List<Object>) fieldAccessor.getPropertyValue("responseBodyInterceptors");
|
||||
assertEquals(1, interceptors.size());
|
||||
assertEquals(JsonViewResponseBodyInterceptor.class, interceptors.get(0).getClass());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -183,6 +190,11 @@ public class WebMvcConfigurationSupportTests {
|
||||
|
||||
ExceptionHandlerExceptionResolver eher = (ExceptionHandlerExceptionResolver) expectedResolvers.get(0);
|
||||
assertNotNull(eher.getApplicationContext());
|
||||
|
||||
DirectFieldAccessor fieldAccessor = new DirectFieldAccessor(eher);
|
||||
List<Object> interceptors = (List<Object>) fieldAccessor.getPropertyValue("responseBodyInterceptors");
|
||||
assertEquals(1, interceptors.size());
|
||||
assertEquals(JsonViewResponseBodyInterceptor.class, interceptors.get(0).getClass());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -23,6 +23,14 @@ import org.junit.Before;
|
||||
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 class RequestMappingHandlerAdapterTests {
|
||||
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 class RequestMappingHandlerAdapterTests {
|
||||
public String handle() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public ResponseEntity<String> handleWithResponseEntity() {
|
||||
return new ResponseEntity<String>("body", HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -266,6 +293,7 @@ public class RequestMappingHandlerAdapterTests {
|
||||
@ControllerAdvice
|
||||
private static class ModelAttributeAdvice {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@ModelAttribute
|
||||
public void addAttributes(Model model) {
|
||||
model.addAttribute("attr1", "gAttr1");
|
||||
@@ -274,9 +302,10 @@ public class RequestMappingHandlerAdapterTests {
|
||||
}
|
||||
|
||||
|
||||
@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 class RequestMappingHandlerAdapterTests {
|
||||
@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> T beforeBodyWrite(T body, MediaType contentType,
|
||||
Class<? extends HttpMessageConverter<T>> converterType,
|
||||
MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) {
|
||||
|
||||
int status = ((ServletServerHttpResponse) response).getServletResponse().getStatus();
|
||||
response.setStatusCode(HttpStatus.OK);
|
||||
return (T) ("status=" + status + ", message=" + body);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.springframework.web.servlet.mvc.method.annotation;
|
||||
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.junit.Test;
|
||||
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 class RequestResponseBodyMethodProcessorTests {
|
||||
}
|
||||
|
||||
@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<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
|
||||
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()));
|
||||
|
||||
String content = servletResponse.getContentAsString();
|
||||
Object returnValue = new JacksonViewController().handleResponseBody();
|
||||
processor.handleReturnValue(returnValue, methodReturnType, this.mavContainer, this.webRequest);
|
||||
|
||||
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<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
|
||||
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()));
|
||||
|
||||
String content = servletResponse.getContentAsString();
|
||||
Object returnValue = new JacksonViewController().handleResponseEntity();
|
||||
processor.handleReturnValue(returnValue, methodReturnType, this.mavContainer, this.webRequest);
|
||||
|
||||
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 class RequestResponseBodyMethodProcessorTests {
|
||||
|
||||
@RequestMapping
|
||||
@JsonView(MyJacksonView2.class)
|
||||
public ModelAndView handleResponseBodyWithModelAndView() {
|
||||
public ResponseEntity<JacksonViewBean> 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<JacksonViewBean>(bean, HttpStatus.OK);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<? extends HttpMessageConverter<String>> 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> T beforeBodyWrite(T body, MediaType contentType,
|
||||
Class<? extends HttpMessageConverter<T>> 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> T beforeBodyWrite(T body, MediaType contentType,
|
||||
Class<? extends HttpMessageConverter<T>> converterType,
|
||||
MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) {
|
||||
|
||||
return (T) (body + "-TargetedControllerAdvice");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@ResponseBody
|
||||
public String handle() {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user