Fix JsonView + HttpEntity Reactive handling

This commit adds
AbstractMessageReaderArgumentResolver#readBody and
AbstractMessageWriterResultHandler#writeBody variants
which allow to pass the actual MethodParameter in order
to perform proper annotation-based hint resolution with
nested generics, for example with HttpEntity.

Issue: SPR-16098
This commit is contained in:
sdeleuze
2017-11-16 14:42:57 +01:00
parent e4d405271a
commit c530745015
5 changed files with 108 additions and 8 deletions

View File

@@ -62,6 +62,7 @@ import org.springframework.web.server.UnsupportedMediaTypeStatusException;
* failure results in an {@link ServerWebInputException}.
*
* @author Rossen Stoyanchev
* @author Sebastien Deleuze
* @since 5.0
*/
public abstract class AbstractMessageReaderArgumentResolver extends HandlerMethodArgumentResolverSupport {
@@ -109,10 +110,37 @@ public abstract class AbstractMessageReaderArgumentResolver extends HandlerMetho
}
/**
* Read the body from a method argument with {@link HttpMessageReader}.
* @param bodyParameter the {@link MethodParameter} to read
* @param isBodyRequired true if the body is required
* @param bindingContext the binding context to use
* @param exchange the current exchange
* @return the body
* @see #readBody(MethodParameter, MethodParameter, boolean, BindingContext, ServerWebExchange)
*/
protected Mono<Object> readBody(MethodParameter bodyParameter, boolean isBodyRequired,
BindingContext bindingContext, ServerWebExchange exchange) {
return this.readBody(bodyParameter, null, isBodyRequired, bindingContext, exchange);
}
/**
* Read the body from a method argument with {@link HttpMessageReader}.
* @param bodyParameter the {@link MethodParameter} to read
* @param actualParameter the actual {@link MethodParameter} to read; could be different
* from {@code bodyParameter} when processing {@code HttpEntity} for example
* @param isBodyRequired true if the body is required
* @param bindingContext the binding context to use
* @param exchange the current exchange
* @return the body
* @since 5.0.2
*/
protected Mono<Object> readBody(MethodParameter bodyParameter, @Nullable MethodParameter actualParameter,
boolean isBodyRequired, BindingContext bindingContext, ServerWebExchange exchange) {
ResolvableType bodyType = ResolvableType.forMethodParameter(bodyParameter);
ResolvableType actualType = (actualParameter == null ?
bodyType : ResolvableType.forMethodParameter(actualParameter));
Class<?> resolvedType = bodyType.resolve();
ReactiveAdapter adapter = (resolvedType != null ? getAdapterRegistry().getAdapter(resolvedType) : null);
ResolvableType elementType = (adapter != null ? bodyType.getGeneric() : bodyType);
@@ -127,7 +155,7 @@ public abstract class AbstractMessageReaderArgumentResolver extends HandlerMetho
if (reader.canRead(elementType, mediaType)) {
Map<String, Object> readHints = Collections.emptyMap();
if (adapter != null && adapter.isMultiValue()) {
Flux<?> flux = reader.read(bodyType, elementType, request, response, readHints);
Flux<?> flux = reader.read(actualType, elementType, request, response, readHints);
flux = flux.onErrorResume(ex -> Flux.error(handleReadError(bodyParameter, ex)));
if (isBodyRequired || !adapter.supportsEmpty()) {
flux = flux.switchIfEmpty(Flux.error(handleMissingBody(bodyParameter)));
@@ -141,7 +169,7 @@ public abstract class AbstractMessageReaderArgumentResolver extends HandlerMetho
}
else {
// Single-value (with or without reactive type wrapper)
Mono<?> mono = reader.readMono(bodyType, elementType, request, response, readHints);
Mono<?> mono = reader.readMono(actualType, elementType, request, response, readHints);
mono = mono.onErrorResume(ex -> Mono.error(handleReadError(bodyParameter, ex)));
if (isBodyRequired || (adapter != null && !adapter.supportsEmpty())) {
mono = mono.switchIfEmpty(Mono.error(handleMissingBody(bodyParameter)));

View File

@@ -43,6 +43,7 @@ import org.springframework.web.server.ServerWebExchange;
* to the response with {@link HttpMessageWriter}.
*
* @author Rossen Stoyanchev
* @author Sebastien Deleuze
* @since 5.0
*/
public abstract class AbstractMessageWriterResultHandler extends HandlerResultHandlerSupport {
@@ -86,9 +87,36 @@ public abstract class AbstractMessageWriterResultHandler extends HandlerResultHa
}
@SuppressWarnings("unchecked")
/**
* Write a given body to the response with {@link HttpMessageWriter}.
* @param body the object to write
* @param bodyParameter the {@link MethodParameter} of the body to write
* @param exchange the current exchange
* @return indicates completion or error
* @see #writeBody(Object, MethodParameter, MethodParameter, ServerWebExchange)
*/
protected Mono<Void> writeBody(@Nullable Object body, MethodParameter bodyParameter, ServerWebExchange exchange) {
return this.writeBody(body, bodyParameter, null, exchange);
}
/**
* Write a given body to the response with {@link HttpMessageWriter}.
* @param body the object to write
* @param bodyParameter the {@link MethodParameter} of the body to write
* @param actualParameter the actual return type of the method that returned the
* value; could be different from {@code bodyParameter} when processing {@code HttpEntity}
* for example
* @param exchange the current exchange
* @return indicates completion or error
* @since 5.0.2
*/
@SuppressWarnings("unchecked")
protected Mono<Void> writeBody(@Nullable Object body, MethodParameter bodyParameter,
@Nullable MethodParameter actualParameter, ServerWebExchange exchange) {
ResolvableType bodyType = ResolvableType.forMethodParameter(bodyParameter);
ResolvableType actualType = (actualParameter == null ?
bodyType : ResolvableType.forMethodParameter(actualParameter));
Class<?> bodyClass = bodyType.resolve();
ReactiveAdapter adapter = getAdapterRegistry().getAdapter(bodyClass, body);
@@ -115,7 +143,7 @@ public abstract class AbstractMessageWriterResultHandler extends HandlerResultHa
if (bestMediaType != null) {
for (HttpMessageWriter<?> writer : getMessageWriters()) {
if (writer.canWrite(elementType, bestMediaType)) {
return writer.write((Publisher) publisher, bodyType, elementType,
return writer.write((Publisher) publisher, actualType, elementType,
bestMediaType, request, response, Collections.emptyMap());
}
}

View File

@@ -58,7 +58,7 @@ public class HttpEntityArgumentResolver extends AbstractMessageReaderArgumentRes
MethodParameter parameter, BindingContext bindingContext, ServerWebExchange exchange) {
Class<?> entityType = parameter.getParameterType();
return readBody(parameter.nested(), false, bindingContext, exchange)
return readBody(parameter.nested(), parameter, false, bindingContext, exchange)
.map(body -> createEntity(body, entityType, exchange.getRequest()))
.defaultIfEmpty(createEntity(null, entityType, exchange.getRequest()));
}

View File

@@ -114,15 +114,16 @@ public class ResponseEntityResultHandler extends AbstractMessageWriterResultHand
Mono<?> returnValueMono;
MethodParameter bodyParameter;
ReactiveAdapter adapter = getAdapter(result);
MethodParameter actualParameter = result.getReturnTypeSource();
if (adapter != null) {
Assert.isTrue(!adapter.isMultiValue(), "Only a single ResponseEntity supported");
returnValueMono = Mono.from(adapter.toPublisher(result.getReturnValue()));
bodyParameter = result.getReturnTypeSource().nested().nested();
bodyParameter = actualParameter.nested().nested();
}
else {
returnValueMono = Mono.justOrEmpty(result.getReturnValue());
bodyParameter = result.getReturnTypeSource().nested();
bodyParameter = actualParameter.nested();
}
return returnValueMono.flatMap(returnValue -> {
@@ -169,7 +170,7 @@ public class ResponseEntityResultHandler extends AbstractMessageWriterResultHand
return exchange.getResponse().setComplete();
}
return writeBody(httpEntity.getBody(), bodyParameter, exchange);
return writeBody(httpEntity.getBody(), bodyParameter, actualParameter, exchange);
});
}

View File

@@ -28,7 +28,9 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpEntity;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -63,6 +65,12 @@ public class JacksonHintsIntegrationTests extends AbstractRequestMappingIntegrat
assertEquals(expected, performGet("/response/mono", MediaType.APPLICATION_JSON_UTF8, String.class).getBody());
}
@Test // SPR-16098
public void jsonViewWithMonoResponseEntity() throws Exception {
String expected = "{\"withView1\":\"with\"}";
assertEquals(expected, performGet("/response/entity", MediaType.APPLICATION_JSON_UTF8, String.class).getBody());
}
@Test
public void jsonViewWithFluxResponse() throws Exception {
String expected = "[{\"withView1\":\"with\"},{\"withView1\":\"with\"}]";
@@ -83,6 +91,25 @@ public class JacksonHintsIntegrationTests extends AbstractRequestMappingIntegrat
new JacksonViewBean("with", "with", "without"), MediaType.APPLICATION_JSON_UTF8, String.class).getBody());
}
@Test // SPR-16098
public void jsonViewWithEntityMonoRequest() throws Exception {
String expected = "{\"withView1\":\"with\",\"withView2\":null,\"withoutView\":null}";
assertEquals(expected, performPost("/request/entity/mono", MediaType.APPLICATION_JSON,
new JacksonViewBean("with", "with", "without"),
MediaType.APPLICATION_JSON_UTF8, String.class).getBody());
}
@Test // SPR-16098
public void jsonViewWithEntityFluxRequest() throws Exception {
String expected = "[" +
"{\"withView1\":\"with\",\"withView2\":null,\"withoutView\":null}," +
"{\"withView1\":\"with\",\"withView2\":null,\"withoutView\":null}]";
assertEquals(expected, performPost("/request/entity/flux", MediaType.APPLICATION_JSON,
Arrays.asList(new JacksonViewBean("with", "with", "without"),
new JacksonViewBean("with", "with", "without")),
MediaType.APPLICATION_JSON_UTF8, String.class).getBody());
}
@Test
public void jsonViewWithFluxRequest() throws Exception {
String expected = "[" +
@@ -120,6 +147,12 @@ public class JacksonHintsIntegrationTests extends AbstractRequestMappingIntegrat
return Mono.just(new JacksonViewBean("with", "with", "without"));
}
@GetMapping("/response/entity")
@JsonView(MyJacksonView1.class)
public Mono<ResponseEntity<JacksonViewBean>> monoResponseEntity() {
return Mono.just(ResponseEntity.ok(new JacksonViewBean("with", "with", "without")));
}
@GetMapping("/response/flux")
@JsonView(MyJacksonView1.class)
public Flux<JacksonViewBean> fluxResponse() {
@@ -136,6 +169,16 @@ public class JacksonHintsIntegrationTests extends AbstractRequestMappingIntegrat
return mono;
}
@PostMapping("/request/entity/mono")
public Mono<JacksonViewBean> entityMonoRequest(@JsonView(MyJacksonView1.class) HttpEntity<Mono<JacksonViewBean>> entityMono) {
return entityMono.getBody();
}
@PostMapping("/request/entity/flux")
public Flux<JacksonViewBean> entityFluxRequest(@JsonView(MyJacksonView1.class) HttpEntity<Flux<JacksonViewBean>> entityFlux) {
return entityFlux.getBody();
}
@PostMapping("/request/flux")
public Flux<JacksonViewBean> fluxRequest(@JsonView(MyJacksonView1.class) @RequestBody Flux<JacksonViewBean> flux) {
return flux;