From c530745015d5c7031cfcb6f633f3f459ed97935d Mon Sep 17 00:00:00 2001 From: sdeleuze Date: Thu, 16 Nov 2017 14:42:57 +0100 Subject: [PATCH] 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 --- ...AbstractMessageReaderArgumentResolver.java | 32 +++++++++++++- .../AbstractMessageWriterResultHandler.java | 32 +++++++++++++- .../HttpEntityArgumentResolver.java | 2 +- .../ResponseEntityResultHandler.java | 7 +-- .../JacksonHintsIntegrationTests.java | 43 +++++++++++++++++++ 5 files changed, 108 insertions(+), 8 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java index 34d061e98c5..207190095ee 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java @@ -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 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 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 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))); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java index 230a6f219fb..211977e41ec 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java @@ -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 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 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()); } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolver.java index 7a3bb7b492d..6e53a85d2fa 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolver.java @@ -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())); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java index d2b00e6a851..b17771cf81c 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java @@ -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); }); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/JacksonHintsIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/JacksonHintsIntegrationTests.java index 85a45e8440c..247c635bc98 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/JacksonHintsIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/JacksonHintsIntegrationTests.java @@ -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> monoResponseEntity() { + return Mono.just(ResponseEntity.ok(new JacksonViewBean("with", "with", "without"))); + } + @GetMapping("/response/flux") @JsonView(MyJacksonView1.class) public Flux fluxResponse() { @@ -136,6 +169,16 @@ public class JacksonHintsIntegrationTests extends AbstractRequestMappingIntegrat return mono; } + @PostMapping("/request/entity/mono") + public Mono entityMonoRequest(@JsonView(MyJacksonView1.class) HttpEntity> entityMono) { + return entityMono.getBody(); + } + + @PostMapping("/request/entity/flux") + public Flux entityFluxRequest(@JsonView(MyJacksonView1.class) HttpEntity> entityFlux) { + return entityFlux.getBody(); + } + @PostMapping("/request/flux") public Flux fluxRequest(@JsonView(MyJacksonView1.class) @RequestBody Flux flux) { return flux;