Add RequestBodyAdvice

RequestBodyAdvice is analogous to ResponseBodyAdvice (added in 4.1)
but for intercepting for reading the request with an
HttpMessageConverter for resolving an @RequestBody or an HttpEntity
method argument.

Issue: SPR-12501
This commit is contained in:
Rossen Stoyanchev 2015-03-16 16:36:07 -04:00
parent 073c176400
commit 0556ed4f16
12 changed files with 512 additions and 174 deletions

View File

@ -24,6 +24,7 @@ import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
@ -62,11 +63,27 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
protected final List<MediaType> allSupportedMediaTypes;
private final RequestResponseBodyAdviceChain advice;
public AbstractMessageConverterMethodArgumentResolver(List<HttpMessageConverter<?>> messageConverters) {
Assert.notEmpty(messageConverters, "'messageConverters' must not be empty");
this.messageConverters = messageConverters;
this.allSupportedMediaTypes = getAllSupportedMediaTypes(messageConverters);
/**
* Basic constructor with converters only.
*/
public AbstractMessageConverterMethodArgumentResolver(List<HttpMessageConverter<?>> converters) {
this(converters, null);
}
/**
* Constructor with converters and {@code Request~} and {@code ResponseBodyAdvice}.
* @since 4.2
*/
public AbstractMessageConverterMethodArgumentResolver(List<HttpMessageConverter<?>> converters,
List<Object> requestResponseBodyAdvice) {
Assert.notEmpty(converters, "'messageConverters' must not be empty");
this.messageConverters = converters;
this.allSupportedMediaTypes = getAllSupportedMediaTypes(converters);
this.advice = new RequestResponseBodyAdviceChain(requestResponseBodyAdvice);
}
@ -85,6 +102,15 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
}
/**
* Return the configured {@link RequestBodyAdvice} and
* {@link RequestBodyAdvice} where each instance may be wrapped as a
* {@link org.springframework.web.method.ControllerAdviceBean ControllerAdviceBean}.
*/
protected RequestResponseBodyAdviceChain getAdvice() {
return this.advice;
}
/**
* Create the method argument value of the expected parameter type by
* reading from the given request.
@ -108,7 +134,7 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
* from the given HttpInputMessage.
* @param <T> the expected type of the argument value to be created
* @param inputMessage the HTTP input message representing the current request
* @param methodParam the method parameter descriptor (may be {@code null})
* @param param the method parameter descriptor (may be {@code null})
* @param targetType the type of object to create, not necessarily the same as
* the method parameter type (e.g. for {@code HttpEntity<String>} method
* parameter the target type is String)
@ -118,7 +144,7 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
*/
@SuppressWarnings("unchecked")
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage,
MethodParameter methodParam, Type targetType) throws IOException, HttpMediaTypeNotSupportedException {
MethodParameter param, Type targetType) throws IOException, HttpMediaTypeNotSupportedException {
MediaType contentType;
try {
@ -131,32 +157,35 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
Class<?> contextClass = (methodParam != null ? methodParam.getContainingClass() : null);
Class<?> contextClass = (param != null ? param.getContainingClass() : null);
Class<T> targetClass = (targetType instanceof Class<?> ? (Class<T>) targetType : null);
if (targetClass == null) {
ResolvableType resolvableType = (methodParam != null ?
ResolvableType.forMethodParameter(methodParam) : ResolvableType.forType(targetType));
ResolvableType resolvableType = (param != null ?
ResolvableType.forMethodParameter(param) : ResolvableType.forType(targetType));
targetClass = (Class<T>) resolvableType.resolve();
}
for (HttpMessageConverter<?> converter : this.messageConverters) {
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
if (converter instanceof GenericHttpMessageConverter) {
GenericHttpMessageConverter<?> genericConverter = (GenericHttpMessageConverter<?>) converter;
if (genericConverter.canRead(targetType, contextClass, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Reading [" + targetType + "] as \"" +
contentType + "\" using [" + converter + "]");
logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
}
return genericConverter.read(targetType, contextClass, inputMessage);
inputMessage = getAdvice().beforeBodyRead(inputMessage, param, targetType, converterType);
T body = (T) genericConverter.read(targetType, contextClass, inputMessage);
return getAdvice().afterBodyRead(body, inputMessage, param, targetType, converterType);
}
}
else if (targetClass != null) {
if (converter.canRead(targetClass, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Reading [" + targetClass.getName() + "] as \"" +
contentType + "\" using [" + converter + "]");
logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
}
return ((HttpMessageConverter<T>) converter).read(targetClass, inputMessage);
inputMessage = getAdvice().beforeBodyRead(inputMessage, param, targetType, converterType);
T body = ((HttpMessageConverter<T>) converter).read(targetClass, inputMessage);
return getAdvice().afterBodyRead(body, inputMessage, param, targetType, converterType);
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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.
@ -22,6 +22,7 @@ import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@ -54,31 +55,25 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
private final ContentNegotiationManager contentNegotiationManager;
private final ResponseBodyAdviceChain adviceChain;
protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> messageConverters) {
this(messageConverters, null);
protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> converters) {
this(converters, null);
}
protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> messageConverters,
ContentNegotiationManager manager) {
this(messageConverters, manager, null);
protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> converters,
ContentNegotiationManager contentNegotiationManager) {
this(converters, contentNegotiationManager, null);
}
protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> messageConverters,
ContentNegotiationManager manager, List<Object> responseBodyAdvice) {
protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> converters,
ContentNegotiationManager manager, List<Object> requestResponseBodyAdvice) {
super(messageConverters);
super(converters, requestResponseBodyAdvice);
this.contentNegotiationManager = (manager != null ? manager : new ContentNegotiationManager());
this.adviceChain = new ResponseBodyAdviceChain(responseBodyAdvice);
}
protected ResponseBodyAdviceChain getAdviceChain() {
return this.adviceChain;
}
/**
* Creates a new {@link HttpOutputMessage} from the given {@link NativeWebRequest}.
* @param webRequest the web request to create an output message from
@ -155,13 +150,14 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
selectedMediaType = selectedMediaType.removeQualityValue();
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
if (messageConverter.canWrite(returnValueClass, selectedMediaType)) {
returnValue = this.adviceChain.invoke(returnValue, returnType, selectedMediaType,
(Class<HttpMessageConverter<?>>) messageConverter.getClass(), inputMessage, outputMessage);
returnValue = (T) getAdvice().beforeBodyWrite(returnValue, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
inputMessage, outputMessage);
if (returnValue != null) {
((HttpMessageConverter<T>) messageConverter).write(returnValue, selectedMediaType, outputMessage);
if (logger.isDebugEnabled()) {
logger.debug("Written [" + returnValue + "] as \"" + selectedMediaType + "\" using [" +
messageConverter + "]");
logger.debug("Written [" + returnValue + "] as \"" +
selectedMediaType + "\" using [" + messageConverter + "]");
}
}
return;

View File

@ -54,20 +54,47 @@ import org.springframework.web.method.support.ModelAndViewContainer;
*/
public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodProcessor {
public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> messageConverters) {
super(messageConverters);
/**
* Basic constructor with converters only. Suitable for resolving
* {@code HttpEntity}. For handling {@code ResponseEntity} consider also
* providing a {@code ContentNegotiationManager}.
*/
public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> converters) {
super(converters);
}
public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> messageConverters,
ContentNegotiationManager contentNegotiationManager) {
/**
* Basic constructor with converters and {@code ContentNegotiationManager}.
* Suitable for resolving {@code HttpEntity} and handling {@code ResponseEntity}
* without {@code Request~} or {@code ResponseBodyAdvice}.
*/
public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> converters,
ContentNegotiationManager manager) {
super(messageConverters, contentNegotiationManager);
super(converters, manager);
}
public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> messageConverters,
ContentNegotiationManager contentNegotiationManager, List<Object> responseBodyAdvice) {
/**
* Complete constructor for resolving {@code HttpEntity} method arguments.
* For handling {@code ResponseEntity} consider also providing a
* {@code ContentNegotiationManager}.
* @since 4.2
*/
public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> converters,
List<Object> requestResponseBodyAdvice) {
super(messageConverters, contentNegotiationManager, responseBodyAdvice);
super(converters, null, requestResponseBodyAdvice);
}
/**
* Complete constructor for resolving {@code HttpEntity} and handling
* {@code ResponseEntity}.
*/
public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> converters,
ContentNegotiationManager manager, List<Object> requestResponseBodyAdvice) {
super(converters, manager, requestResponseBodyAdvice);
}

View File

@ -0,0 +1,91 @@
/*
* Copyright 2002-2015 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 java.lang.reflect.Type;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
/**
* Allows customizing the request before its body is read and converted into an
* Object and also allows the resulting Object before it is passed into a
* controller method as an {code @RequestBody} or an {@code HttpEntity} method
* argument.
*
* <p>Implementations of this contract may be registered directly with the
* {@code RequestMappingHandlerAdapter} or more likely annotated with
* {@code @ControllerAdvice} in which case they are auto-detected.
*
* @author Rossen Stoyanchev
* @since 4.2
*/
public interface RequestBodyAdvice {
/**
* Invoked first to determine if this interceptor applies.
* @param methodParameter the method parameter
* @param targetType the target type, not necessarily the same as the method
* parameter type, e.g. for {@code HttpEntity<String>}.
* @param converterType the selected converter type
* @return whether this interceptor should be invoked or not
*/
boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType);
/**
* Invoked second (and last) if the body is empty.
* @param body set to {@code null} before the first advice is called
* @param inputMessage the request
* @param parameter the method parameter
* @param targetType the target type, not necessarily the same as the method
* parameter type, e.g. for {@code HttpEntity<String>}.
* @param converterType the selected converter type
* @return the value to use or {@code null} which may then raise an
* {@code HttpMessageNotReadableException} if the argument is required.
*/
Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType);
/**
* Invoked second before the request body is read and converted.
* @param inputMessage the request
* @param parameter the target method parameter
* @param targetType the target type, not necessarily the same as the method
* parameter type, e.g. for {@code HttpEntity<String>}.
* @param converterType the converter used to deserialize the body
* @return the input request or a new instance, never {@code null}
*/
HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType);
/**
* Invoked third (and last) after the request body is converted to an Object.
* @param body set to the converter Object before the 1st advice is called
* @param inputMessage the request
* @param parameter the target method parameter
* @param targetType the target type, not necessarily the same as the method
* parameter type, e.g. for {@code HttpEntity<String>}.
* @param converterType the converter used to deserialize the body
* @return the same body or a new instance
*/
Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType);
}

View File

@ -131,7 +131,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
private List<HttpMessageConverter<?>> messageConverters;
private List<Object> responseBodyAdvice = new ArrayList<Object>();
private List<Object> requestResponseBodyAdvice = new ArrayList<Object>();
private WebBindingInitializer webBindingInitializer;
@ -329,15 +329,24 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
}
/**
* Add one or more components to modify the response after the execution of a
* controller method annotated with {@code @ResponseBody}, or a method returning
* {@code ResponseEntity} and before the body is written to the response with
* the selected {@code HttpMessageConverter}.
* Add one or more {@code RequestBodyAdvice} instances to intercept the
* request before it is read and converted for {@code @RequestBody} and
* {@code HttpEntity} method arguments.
*/
public void setRequestBodyAdvice(List<RequestBodyAdvice> requestBodyAdvice) {
if (requestBodyAdvice != null) {
this.requestResponseBodyAdvice.addAll(requestBodyAdvice);
}
}
/**
* Add one or more {@code ResponseBodyAdvice} instances to intercept the
* response before {@code @ResponseBody} or {@code ResponseEntity} return
* values are written to the response body.
*/
public void setResponseBodyAdvice(List<ResponseBodyAdvice<?>> responseBodyAdvice) {
this.responseBodyAdvice.clear();
if (responseBodyAdvice != null) {
this.responseBodyAdvice.addAll(responseBodyAdvice);
this.requestResponseBodyAdvice.addAll(responseBodyAdvice);
}
}
@ -520,7 +529,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
List<ControllerAdviceBean> beans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
AnnotationAwareOrderComparator.sort(beans);
List<Object> responseBodyAdviceBeans = new ArrayList<Object>();
List<Object> requestResponseBodyAdviceBeans = new ArrayList<Object>();
for (ControllerAdviceBean bean : beans) {
Set<Method> attrMethods = HandlerMethodSelector.selectMethods(bean.getBeanType(), MODEL_ATTRIBUTE_METHODS);
@ -533,14 +542,18 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
this.initBinderAdviceCache.put(bean, binderMethods);
logger.info("Detected @InitBinder methods in " + bean);
}
if (RequestBodyAdvice.class.isAssignableFrom(bean.getBeanType())) {
requestResponseBodyAdviceBeans.add(bean);
logger.info("Detected RequestBodyAdvice bean in " + bean);
}
if (ResponseBodyAdvice.class.isAssignableFrom(bean.getBeanType())) {
responseBodyAdviceBeans.add(bean);
requestResponseBodyAdviceBeans.add(bean);
logger.info("Detected ResponseBodyAdvice bean in " + bean);
}
}
if (!responseBodyAdviceBeans.isEmpty()) {
this.responseBodyAdvice.addAll(0, responseBodyAdviceBeans);
if (!requestResponseBodyAdviceBeans.isEmpty()) {
this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans);
}
}
@ -559,8 +572,8 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
resolvers.add(new MatrixVariableMethodArgumentResolver());
resolvers.add(new MatrixVariableMapMethodArgumentResolver());
resolvers.add(new ServletModelAttributeMethodProcessor(false));
resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters()));
resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters()));
resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
resolvers.add(new RequestHeaderMapMethodArgumentResolver());
resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
@ -569,7 +582,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
// Type-based argument resolution
resolvers.add(new ServletRequestMethodArgumentResolver());
resolvers.add(new ServletResponseMethodArgumentResolver());
resolvers.add(new HttpEntityMethodProcessor(getMessageConverters()));
resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RedirectAttributesMethodArgumentResolver());
resolvers.add(new ModelMethodProcessor());
resolvers.add(new MapMethodProcessor());
@ -633,8 +646,8 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
handlers.add(new ViewMethodReturnValueHandler());
handlers.add(new ResponseBodyEmitterReturnValueHandler(getMessageConverters()));
handlers.add(new StreamingResponseBodyReturnValueHandler());
handlers.add(new HttpEntityMethodProcessor(
getMessageConverters(), this.contentNegotiationManager, this.responseBodyAdvice));
handlers.add(new HttpEntityMethodProcessor(getMessageConverters(),
this.contentNegotiationManager, this.requestResponseBodyAdvice));
handlers.add(new HttpHeadersReturnValueHandler());
handlers.add(new CallableMethodReturnValueHandler());
handlers.add(new DeferredResultMethodReturnValueHandler());
@ -643,8 +656,8 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
// Annotation-based return value types
handlers.add(new ModelAttributeMethodProcessor(false));
handlers.add(new RequestResponseBodyMethodProcessor(
getMessageConverters(), this.contentNegotiationManager, this.responseBodyAdvice));
handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(),
this.contentNegotiationManager, this.requestResponseBodyAdvice));
// Multi-purpose return value types
handlers.add(new ViewNameMethodReturnValueHandler());

View File

@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.Part;
@ -74,10 +75,24 @@ import org.springframework.web.util.WebUtils;
*/
public class RequestPartMethodArgumentResolver extends AbstractMessageConverterMethodArgumentResolver {
/**
* Basic constructor with converters only.
*/
public RequestPartMethodArgumentResolver(List<HttpMessageConverter<?>> messageConverters) {
super(messageConverters);
}
/**
* Constructor with converters and {@code Request~} and
* {@code ResponseBodyAdvice}.
*/
public RequestPartMethodArgumentResolver(List<HttpMessageConverter<?>> messageConverters,
List<Object> requestResponseBodyAdvice) {
super(messageConverters, requestResponseBodyAdvice);
}
/**
* Supports the following:

View File

@ -0,0 +1,176 @@
/*
* Copyright 2002-2015 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 java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.CollectionUtils;
import org.springframework.web.method.ControllerAdviceBean;
/**
* Invokes {@link RequestBodyAdvice} and {@link ResponseBodyAdvice} where each
* instance may be (and is most likely) wrapped with
* {@link org.springframework.web.method.ControllerAdviceBean ControllerAdviceBean}.
*
* @author Rossen Stoyanchev
* @since 4.2
*/
class RequestResponseBodyAdviceChain implements RequestBodyAdvice, ResponseBodyAdvice<Object> {
private final List<Object> requestBodyAdvice = new ArrayList<Object>(4);
private final List<Object> responseBodyAdvice = new ArrayList<Object>(4);
/**
* Create an instance from a list of objects that are either of type
* {@code ControllerAdviceBean} or {@code RequestBodyAdvice}.
*/
public RequestResponseBodyAdviceChain(List<Object> requestResponseBodyAdvice) {
initAdvice(requestResponseBodyAdvice);
}
private void initAdvice(List<Object> requestResponseBodyAdvice) {
if (requestResponseBodyAdvice == null) {
return;
}
for (Object advice : requestResponseBodyAdvice) {
Class<?> beanType = (advice instanceof ControllerAdviceBean ?
((ControllerAdviceBean) advice).getBeanType() : advice.getClass());
if (RequestBodyAdvice.class.isAssignableFrom(beanType)) {
this.requestBodyAdvice.add(advice);
}
else if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
this.responseBodyAdvice.add(advice);
}
}
}
private List<Object> getAdvice(Class<?> adviceType) {
if (RequestBodyAdvice.class.equals(adviceType)) {
return this.requestBodyAdvice;
}
else if (ResponseBodyAdvice.class.equals(adviceType)) {
return this.responseBodyAdvice;
}
else {
throw new IllegalArgumentException("Unexpected adviceType: " + adviceType);
}
}
@Override
public boolean supports(MethodParameter param, Type type, Class<? extends HttpMessageConverter<?>> converterType) {
throw new UnsupportedOperationException("Not implemented");
}
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
throw new UnsupportedOperationException("Not implemented");
}
@Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
for (RequestBodyAdvice advice : getMatchingAdvice(parameter, RequestBodyAdvice.class)) {
if (advice.supports(parameter, targetType, converterType)) {
body = advice.handleEmptyBody(body, inputMessage, parameter, targetType, converterType);
}
}
return body;
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage request, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
for (RequestBodyAdvice advice : getMatchingAdvice(parameter, RequestBodyAdvice.class)) {
if (advice.supports(parameter, targetType, converterType)) {
request = advice.beforeBodyRead(request, parameter, targetType, converterType);
}
}
return request;
}
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
for (RequestBodyAdvice advice : getMatchingAdvice(parameter, RequestBodyAdvice.class)) {
if (advice.supports(parameter, targetType, converterType)) {
body = advice.afterBodyRead(body, inputMessage, parameter, targetType, converterType);
}
}
return body;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType contentType,
Class<? extends HttpMessageConverter<?>> converterType,
ServerHttpRequest request, ServerHttpResponse response) {
return processBody(body, returnType, contentType, converterType, request, response);
}
@SuppressWarnings("unchecked")
private <T> Object processBody(Object body, MethodParameter returnType, MediaType contentType,
Class<? extends HttpMessageConverter<?>> converterType,
ServerHttpRequest request, ServerHttpResponse response) {
for (ResponseBodyAdvice<?> advice : getMatchingAdvice(returnType, ResponseBodyAdvice.class)) {
if (advice.supports(returnType, converterType)) {
body = ((ResponseBodyAdvice<T>) advice).beforeBodyWrite((T) body, returnType,
contentType, converterType, request, response);
}
}
return body;
}
@SuppressWarnings("unchecked")
private <A> List<A> getMatchingAdvice(MethodParameter parameter, Class<? extends A> adviceType) {
List<Object> availableAdvice = getAdvice(adviceType);
if (CollectionUtils.isEmpty(availableAdvice)) {
return Collections.emptyList();
}
List<A> result = new ArrayList<A>(availableAdvice.size());
for (Object advice : availableAdvice) {
if (advice instanceof ControllerAdviceBean) {
ControllerAdviceBean adviceBean = (ControllerAdviceBean) advice;
if (!adviceBean.isApplicableToBeanType(parameter.getContainingClass())) {
continue;
}
advice = adviceBean.resolveBean();
}
if (adviceType.isAssignableFrom(advice.getClass())) {
result.add((A) advice);
}
}
return result;
}
}

View File

@ -21,6 +21,7 @@ import java.io.InputStream;
import java.io.PushbackInputStream;
import java.lang.reflect.Type;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.springframework.core.Conventions;
@ -59,20 +60,48 @@ import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolv
*/
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> messageConverters) {
super(messageConverters);
/**
* Basic constructor with converters only. Suitable for resolving
* {@code @RequestBody}. For handling {@code @ResponseBody} consider also
* providing a {@code ContentNegotiationManager}.
*/
public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters) {
super(converters);
}
public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> messageConverters,
ContentNegotiationManager contentNegotiationManager) {
/**
* Basic constructor with converters and {@code ContentNegotiationManager}.
* Suitable for resolving {@code @RequestBody} and handling
* {@code @ResponseBody} without {@code Request~} or
* {@code ResponseBodyAdvice}.
*/
public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters,
ContentNegotiationManager manager) {
super(messageConverters, contentNegotiationManager);
super(converters, manager);
}
public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> messageConverters,
ContentNegotiationManager contentNegotiationManager, List<Object> responseBodyAdvice) {
/**
* Complete constructor for resolving {@code @RequestBody} method arguments.
* For handling {@code @ResponseBody} consider also providing a
* {@code ContentNegotiationManager}.
* @since 4.2
*/
public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters,
List<Object> requestResponseBodyAdvice) {
super(messageConverters, contentNegotiationManager, responseBodyAdvice);
super(converters, null, requestResponseBodyAdvice);
}
/**
* Complete constructor for resolving {@code @RequestBody} and handling
* {@code @ResponseBody}.
*/
public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters,
ContentNegotiationManager manager, List<Object> requestResponseBodyAdvice) {
super(converters, manager, requestResponseBodyAdvice);
}

View File

@ -1,78 +0,0 @@
/*
* Copyright 2002-2015 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 java.util.List;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.CollectionUtils;
import org.springframework.web.method.ControllerAdviceBean;
/**
* Invokes a a list of {@link ResponseBodyAdvice} beans.
*
* @author Rossen Stoyanchev
* @since 4.1
*/
class ResponseBodyAdviceChain {
private final List<Object> advice;
public ResponseBodyAdviceChain(List<Object> advice) {
this.advice = advice;
}
public boolean hasAdvice() {
return !CollectionUtils.isEmpty(this.advice);
}
@SuppressWarnings("unchecked")
public <T> T invoke(T body, MethodParameter returnType,
MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
if (this.advice != null) {
for (Object advice : this.advice) {
if (advice instanceof ControllerAdviceBean) {
ControllerAdviceBean adviceBean = (ControllerAdviceBean) advice;
if (!adviceBean.isApplicableToBeanType(returnType.getContainingClass())) {
continue;
}
advice = adviceBean.resolveBean();
}
if (advice instanceof ResponseBodyAdvice) {
ResponseBodyAdvice<T> typedAdvice = (ResponseBodyAdvice<T>) advice;
if (typedAdvice.supports(returnType, selectedConverterType)) {
body = typedAdvice.beforeBodyWrite(body, returnType,
selectedContentType, selectedConverterType, request, response);
}
}
else {
throw new IllegalStateException("Expected ResponseBodyAdvice: " + advice);
}
}
}
return body;
}
}

View File

@ -97,7 +97,7 @@ public class AnnotationDrivenBeanDefinitionParserTests {
loadBeanDefinitions("mvc-config-message-converters.xml");
verifyMessageConverters(appContext.getBean(RequestMappingHandlerAdapter.class), true);
verifyMessageConverters(appContext.getBean(ExceptionHandlerExceptionResolver.class), true);
verifyResponseBodyAdvice(appContext.getBean(RequestMappingHandlerAdapter.class));
verifyRequestResponseBodyAdvice(appContext.getBean(RequestMappingHandlerAdapter.class));
verifyResponseBodyAdvice(appContext.getBean(ExceptionHandlerExceptionResolver.class));
}
@ -182,6 +182,16 @@ public class AnnotationDrivenBeanDefinitionParserTests {
assertTrue(converters.get(0) instanceof JsonViewResponseBodyAdvice);
}
@SuppressWarnings("unchecked")
private void verifyRequestResponseBodyAdvice(Object bean) {
assertNotNull(bean);
Object value = new DirectFieldAccessor(bean).getPropertyValue("requestResponseBodyAdvice");
assertNotNull(value);
assertTrue(value instanceof List);
List<ResponseBodyAdvice> converters = (List<ResponseBodyAdvice>) value;
assertTrue(converters.get(0) instanceof JsonViewResponseBodyAdvice);
}
}
class TestWebArgumentResolver implements WebArgumentResolver {

View File

@ -188,7 +188,8 @@ public class WebMvcConfigurationSupportTests {
assertTrue(validator instanceof LocalValidatorFactoryBean);
DirectFieldAccessor fieldAccessor = new DirectFieldAccessor(adapter);
List<Object> interceptors = (List<Object>) fieldAccessor.getPropertyValue("responseBodyAdvice");
@SuppressWarnings("unchecked")
List<Object> interceptors = (List<Object>) fieldAccessor.getPropertyValue("requestResponseBodyAdvice");
assertEquals(1, interceptors.size());
assertEquals(JsonViewResponseBodyAdvice.class, interceptors.get(0).getClass());
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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.
@ -16,13 +16,18 @@
package org.springframework.web.servlet.mvc.method.annotation;
import static org.junit.Assert.*;
import static org.mockito.BDDMockito.*;
import java.util.Arrays;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
@ -38,17 +43,13 @@ import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.method.ControllerAdviceBean;
import static org.junit.Assert.*;
import static org.mockito.BDDMockito.*;
/**
* Unit tests for
* {@link ResponseBodyAdviceChain}.
* Unit tests for {@link RequestResponseBodyAdviceChain}.
*
* @author Rossen Stoyanchev
* @since 4.1
* @since 4.2
*/
public class ResponseBodyAdviceChainTests {
public class RequestResponseBodyAdviceChainTests {
private String body;
@ -56,10 +57,10 @@ public class ResponseBodyAdviceChainTests {
private Class<? extends HttpMessageConverter<?>> converterType;
private MethodParameter paramType;
private MethodParameter returnType;
private ServerHttpRequest request;
private ServerHttpResponse response;
@ -68,25 +69,53 @@ public class ResponseBodyAdviceChainTests {
this.body = "body";
this.contentType = MediaType.TEXT_PLAIN;
this.converterType = StringHttpMessageConverter.class;
this.returnType = new MethodParameter(ClassUtils.getMethod(this.getClass(), "handle"), -1);
this.paramType = new MethodParameter(ClassUtils.getMethod(this.getClass(), "handle", String.class), 0);
this.returnType = new MethodParameter(ClassUtils.getMethod(this.getClass(), "handle", String.class), -1);
this.request = new ServletServerHttpRequest(new MockHttpServletRequest());
this.response = new ServletServerHttpResponse(new MockHttpServletResponse());
}
@SuppressWarnings("unchecked")
@Test
public void requestBodyAdvice() {
RequestBodyAdvice requestAdvice = Mockito.mock(RequestBodyAdvice.class);
ResponseBodyAdvice<String> responseAdvice = Mockito.mock(ResponseBodyAdvice.class);
List<Object> advice = Arrays.asList(requestAdvice, responseAdvice);
RequestResponseBodyAdviceChain chain = new RequestResponseBodyAdviceChain(advice);
HttpInputMessage wrapped = new ServletServerHttpRequest(new MockHttpServletRequest());
given(requestAdvice.supports(this.paramType, String.class, this.converterType)).willReturn(true);
given(requestAdvice.beforeBodyRead(eq(this.request), eq(this.paramType), eq(String.class),
eq(this.converterType))).willReturn(wrapped);
assertSame(wrapped, chain.beforeBodyRead(this.request, this.paramType, String.class, this.converterType));
String modified = "body++";
given(requestAdvice.afterBodyRead(eq(this.body), eq(this.request), eq(this.paramType),
eq(String.class), eq(this.converterType))).willReturn(modified);
assertEquals(modified, chain.afterBodyRead(this.body, this.request, this.paramType,
String.class, this.converterType));
}
@SuppressWarnings("unchecked")
@Test
public void responseBodyAdvice() {
@SuppressWarnings("unchecked")
ResponseBodyAdvice<String> advice = Mockito.mock(ResponseBodyAdvice.class);
ResponseBodyAdviceChain chain = new ResponseBodyAdviceChain(Arrays.asList(advice));
RequestBodyAdvice requestAdvice = Mockito.mock(RequestBodyAdvice.class);
ResponseBodyAdvice<String> responseAdvice = Mockito.mock(ResponseBodyAdvice.class);
List<Object> advice = Arrays.asList(requestAdvice, responseAdvice);
RequestResponseBodyAdviceChain chain = new RequestResponseBodyAdviceChain(advice);
String expected = "body++";
given(advice.supports(this.returnType, this.converterType)).willReturn(true);
given(advice.beforeBodyWrite(eq(this.body), eq(this.returnType), eq(this.contentType),
given(responseAdvice.supports(this.returnType, this.converterType)).willReturn(true);
given(responseAdvice.beforeBodyWrite(eq(this.body), eq(this.returnType), eq(this.contentType),
eq(this.converterType), same(this.request), same(this.response))).willReturn(expected);
String actual = chain.invoke(this.body, this.returnType,
this.contentType, this.converterType, this.request, this.response);
String actual = (String) chain.beforeBodyWrite(this.body, this.returnType, this.contentType,
this.converterType, this.request, this.response);
assertEquals(expected, actual);
}
@ -95,10 +124,10 @@ public class ResponseBodyAdviceChainTests {
public void controllerAdvice() {
Object adviceBean = new ControllerAdviceBean(new MyControllerAdvice());
ResponseBodyAdviceChain chain = new ResponseBodyAdviceChain(Arrays.asList(adviceBean));
RequestResponseBodyAdviceChain chain = new RequestResponseBodyAdviceChain(Arrays.asList(adviceBean));
String actual = chain.invoke(this.body, this.returnType,
this.contentType, this.converterType, this.request, this.response);
String actual = (String) chain.beforeBodyWrite(this.body, this.returnType, this.contentType,
this.converterType, this.request, this.response);
assertEquals("body-MyControllerAdvice", actual);
}
@ -107,10 +136,10 @@ public class ResponseBodyAdviceChainTests {
public void controllerAdviceNotApplicable() {
Object adviceBean = new ControllerAdviceBean(new TargetedControllerAdvice());
ResponseBodyAdviceChain chain = new ResponseBodyAdviceChain(Arrays.asList(adviceBean));
RequestResponseBodyAdviceChain chain = new RequestResponseBodyAdviceChain(Arrays.asList(adviceBean));
String actual = chain.invoke(this.body, this.returnType,
this.contentType, this.converterType, this.request, this.response);
String actual = (String) chain.beforeBodyWrite(this.body, this.returnType, this.contentType,
this.converterType, this.request, this.response);
assertEquals(this.body, actual);
}
@ -152,10 +181,10 @@ public class ResponseBodyAdviceChainTests {
}
}
@SuppressWarnings("unused")
@ResponseBody
public String handle() {
public String handle(String body) {
return "";
}
}