Allow ResponseBodyAdvice to modify null return values
This change defers determination of whether to invoke a message converter in case of a null @ResponseBody value (or ResponseEntity with a null body) until after the invocation of the ResponseBodyAdvice chain. This allows a ResponseBodyAdvice to handle null values potentially turning them into non-null value.s Issue: SPR-12152
This commit is contained in:
@@ -75,6 +75,10 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
@@ -112,7 +116,7 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
|
||||
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
|
||||
throws IOException, HttpMediaTypeNotAcceptableException {
|
||||
|
||||
Class<?> returnValueClass = returnValue.getClass();
|
||||
Class<?> returnValueClass = getReturnValueType(returnValue, returnType);
|
||||
HttpServletRequest servletRequest = inputMessage.getServletRequest();
|
||||
List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(servletRequest);
|
||||
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(servletRequest, returnValueClass);
|
||||
@@ -150,10 +154,12 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
|
||||
if (messageConverter.canWrite(returnValueClass, selectedMediaType)) {
|
||||
returnValue = this.adviceChain.invoke(returnValue, returnType, selectedMediaType,
|
||||
(Class<HttpMessageConverter<?>>) messageConverter.getClass(), inputMessage, outputMessage);
|
||||
((HttpMessageConverter<T>) messageConverter).write(returnValue, selectedMediaType, outputMessage);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Written [" + returnValue + "] as \"" + selectedMediaType + "\" using [" +
|
||||
messageConverter + "]");
|
||||
if (returnValue != null) {
|
||||
((HttpMessageConverter<T>) messageConverter).write(returnValue, selectedMediaType, outputMessage);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Written [" + returnValue + "] as \"" + selectedMediaType + "\" using [" +
|
||||
messageConverter + "]");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -162,6 +168,16 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
|
||||
throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the type of the value to be written to the response. Typically this
|
||||
* is a simple check via getClass on the returnValue but if the returnValue is
|
||||
* null, then the returnType needs to be examined possibly including generic
|
||||
* type determination (e.g. {@code ResponseEntity<T>}).
|
||||
*/
|
||||
protected Class<?> getReturnValueType(Object returnValue, MethodParameter returnType) {
|
||||
return (returnValue != null ? returnValue.getClass() : returnType.getParameterType());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the media types that can be produced:
|
||||
* <ul>
|
||||
|
||||
@@ -22,6 +22,7 @@ import java.lang.reflect.Type;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.RequestEntity;
|
||||
@@ -134,13 +135,21 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
|
||||
}
|
||||
|
||||
Object body = responseEntity.getBody();
|
||||
if (body != null) {
|
||||
if (body != null || getAdviceChain().hasAdvice()) {
|
||||
writeWithMessageConverters(body, returnType, inputMessage, outputMessage);
|
||||
}
|
||||
else {
|
||||
// Flush headers to the HttpServletResponse
|
||||
outputMessage.getBody();
|
||||
}
|
||||
// Ensure headers are flushed even if no body was written
|
||||
outputMessage.getBody();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> getReturnValueType(Object returnValue, MethodParameter returnType) {
|
||||
if (returnValue != null) {
|
||||
return returnValue.getClass();
|
||||
}
|
||||
else {
|
||||
Type type = getHttpEntityType(returnType);
|
||||
return ResolvableType.forMethodParameter(returnType, type).resolve(Object.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter
|
||||
throws IOException, HttpMediaTypeNotAcceptableException {
|
||||
|
||||
mavContainer.setRequestHandled(true);
|
||||
if (returnValue != null) {
|
||||
if (returnValue != null || getAdviceChain().hasAdvice()) {
|
||||
writeWithMessageConverters(returnValue, returnType, webRequest);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,10 @@ 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;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -45,6 +47,10 @@ class ResponseBodyAdviceChain {
|
||||
}
|
||||
|
||||
|
||||
public boolean hasAdvice() {
|
||||
return !CollectionUtils.isEmpty(this.advice);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T invoke(T body, MethodParameter returnType,
|
||||
MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType,
|
||||
|
||||
@@ -20,6 +20,7 @@ import java.lang.reflect.Method;
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@@ -40,11 +41,13 @@ import org.springframework.mock.web.test.MockHttpServletResponse;
|
||||
import org.springframework.web.HttpMediaTypeNotAcceptableException;
|
||||
import org.springframework.web.HttpMediaTypeNotSupportedException;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.context.request.ServletWebRequest;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.BDDMockito.*;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.springframework.web.servlet.HandlerMapping.*;
|
||||
|
||||
/**
|
||||
@@ -219,6 +222,29 @@ public class HttpEntityMethodProcessorMockTests {
|
||||
verify(messageConverter).write(eq(body), eq(MediaType.TEXT_HTML), isA(HttpOutputMessage.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleReturnValueWithResponseBodyAdvice() throws Exception {
|
||||
ResponseEntity<String> returnValue = new ResponseEntity<>(HttpStatus.OK);
|
||||
|
||||
servletRequest.addHeader("Accept", "text/*");
|
||||
servletRequest.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, Collections.singleton(MediaType.TEXT_HTML));
|
||||
|
||||
ResponseBodyAdvice<String> advice = mock(ResponseBodyAdvice.class);
|
||||
given(advice.supports(any(), any())).willReturn(true);
|
||||
given(advice.beforeBodyWrite(any(), any(), any(), any(), any(), any())).willReturn("Foo");
|
||||
|
||||
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(
|
||||
Collections.singletonList(messageConverter), null, Collections.singletonList(advice));
|
||||
|
||||
reset(messageConverter);
|
||||
given(messageConverter.canWrite(String.class, MediaType.TEXT_HTML)).willReturn(true);
|
||||
|
||||
processor.handleReturnValue(returnValue, returnTypeResponseEntity, mavContainer, webRequest);
|
||||
|
||||
assertTrue(mavContainer.isRequestHandled());
|
||||
verify(messageConverter).write(eq("Foo"), eq(MediaType.TEXT_HTML), isA(HttpOutputMessage.class));
|
||||
}
|
||||
|
||||
@Test(expected = HttpMediaTypeNotAcceptableException.class)
|
||||
public void handleReturnValueNotAcceptable() throws Exception {
|
||||
String body = "Foo";
|
||||
|
||||
Reference in New Issue
Block a user