提交 a9b2a124 编写于 作者: R Rossen Stoyanchev

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
上级 a99ef6d9
...@@ -75,6 +75,10 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe ...@@ -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}. * Creates a new {@link HttpOutputMessage} from the given {@link NativeWebRequest}.
* @param webRequest the web request to create an output message from * @param webRequest the web request to create an output message from
...@@ -112,7 +116,7 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe ...@@ -112,7 +116,7 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException { throws IOException, HttpMediaTypeNotAcceptableException {
Class<?> returnValueClass = returnValue.getClass(); Class<?> returnValueClass = getReturnValueType(returnValue, returnType);
HttpServletRequest servletRequest = inputMessage.getServletRequest(); HttpServletRequest servletRequest = inputMessage.getServletRequest();
List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(servletRequest); List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(servletRequest);
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(servletRequest, returnValueClass); List<MediaType> producibleMediaTypes = getProducibleMediaTypes(servletRequest, returnValueClass);
...@@ -150,10 +154,12 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe ...@@ -150,10 +154,12 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
if (messageConverter.canWrite(returnValueClass, selectedMediaType)) { if (messageConverter.canWrite(returnValueClass, selectedMediaType)) {
returnValue = this.adviceChain.invoke(returnValue, returnType, selectedMediaType, returnValue = this.adviceChain.invoke(returnValue, returnType, selectedMediaType,
(Class<HttpMessageConverter<?>>) messageConverter.getClass(), inputMessage, outputMessage); (Class<HttpMessageConverter<?>>) messageConverter.getClass(), inputMessage, outputMessage);
((HttpMessageConverter<T>) messageConverter).write(returnValue, selectedMediaType, outputMessage); if (returnValue != null) {
if (logger.isDebugEnabled()) { ((HttpMessageConverter<T>) messageConverter).write(returnValue, selectedMediaType, outputMessage);
logger.debug("Written [" + returnValue + "] as \"" + selectedMediaType + "\" using [" + if (logger.isDebugEnabled()) {
messageConverter + "]"); logger.debug("Written [" + returnValue + "] as \"" + selectedMediaType + "\" using [" +
messageConverter + "]");
}
} }
return; return;
} }
...@@ -162,6 +168,16 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe ...@@ -162,6 +168,16 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes); 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: * Returns the media types that can be produced:
* <ul> * <ul>
......
...@@ -22,6 +22,7 @@ import java.lang.reflect.Type; ...@@ -22,6 +22,7 @@ import java.lang.reflect.Type;
import java.util.List; import java.util.List;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.RequestEntity; import org.springframework.http.RequestEntity;
...@@ -134,13 +135,21 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro ...@@ -134,13 +135,21 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
} }
Object body = responseEntity.getBody(); Object body = responseEntity.getBody();
if (body != null) { if (body != null || getAdviceChain().hasAdvice()) {
writeWithMessageConverters(body, returnType, inputMessage, outputMessage); writeWithMessageConverters(body, returnType, inputMessage, outputMessage);
} }
// 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 { else {
// Flush headers to the HttpServletResponse Type type = getHttpEntityType(returnType);
outputMessage.getBody(); return ResolvableType.forMethodParameter(returnType, type).resolve(Object.class);
} }
} }
} }
...@@ -198,7 +198,7 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter ...@@ -198,7 +198,7 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter
throws IOException, HttpMediaTypeNotAcceptableException { throws IOException, HttpMediaTypeNotAcceptableException {
mavContainer.setRequestHandled(true); mavContainer.setRequestHandled(true);
if (returnValue != null) { if (returnValue != null || getAdviceChain().hasAdvice()) {
writeWithMessageConverters(returnValue, returnType, webRequest); writeWithMessageConverters(returnValue, returnType, webRequest);
} }
} }
......
...@@ -23,8 +23,10 @@ import org.springframework.http.MediaType; ...@@ -23,8 +23,10 @@ import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.CollectionUtils;
import org.springframework.web.method.ControllerAdviceBean; import org.springframework.web.method.ControllerAdviceBean;
import java.util.Collections;
import java.util.List; import java.util.List;
/** /**
...@@ -45,6 +47,10 @@ class ResponseBodyAdviceChain { ...@@ -45,6 +47,10 @@ class ResponseBodyAdviceChain {
} }
public boolean hasAdvice() {
return !CollectionUtils.isEmpty(this.advice);
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public <T> T invoke(T body, MethodParameter returnType, public <T> T invoke(T body, MethodParameter returnType,
MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType,
......
...@@ -20,6 +20,7 @@ import java.lang.reflect.Method; ...@@ -20,6 +20,7 @@ import java.lang.reflect.Method;
import java.net.URI; import java.net.URI;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
...@@ -40,11 +41,13 @@ import org.springframework.mock.web.test.MockHttpServletResponse; ...@@ -40,11 +41,13 @@ import org.springframework.mock.web.test.MockHttpServletResponse;
import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.annotation.RequestMapping; 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.context.request.ServletWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.method.support.ModelAndViewContainer;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.mockito.BDDMockito.*; import static org.mockito.BDDMockito.*;
import static org.mockito.Matchers.eq;
import static org.springframework.web.servlet.HandlerMapping.*; import static org.springframework.web.servlet.HandlerMapping.*;
/** /**
...@@ -219,6 +222,29 @@ public class HttpEntityMethodProcessorMockTests { ...@@ -219,6 +222,29 @@ public class HttpEntityMethodProcessorMockTests {
verify(messageConverter).write(eq(body), eq(MediaType.TEXT_HTML), isA(HttpOutputMessage.class)); 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) @Test(expected = HttpMediaTypeNotAcceptableException.class)
public void handleReturnValueNotAcceptable() throws Exception { public void handleReturnValueNotAcceptable() throws Exception {
String body = "Foo"; String body = "Foo";
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册