Browse Source

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
pull/1596/merge
sdeleuze 8 years ago
parent
commit
c530745015
  1. 32
      spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java
  2. 32
      spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java
  3. 2
      spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolver.java
  4. 7
      spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java
  5. 43
      spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/JacksonHintsIntegrationTests.java

32
spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java

@ -62,6 +62,7 @@ import org.springframework.web.server.UnsupportedMediaTypeStatusException; @@ -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 @@ -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 @@ -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 @@ -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)));

32
spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java

@ -43,6 +43,7 @@ import org.springframework.web.server.ServerWebExchange; @@ -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 @@ -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 @@ -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());
}
}

2
spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolver.java

@ -58,7 +58,7 @@ public class HttpEntityArgumentResolver extends AbstractMessageReaderArgumentRes @@ -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()));
}

7
spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java

@ -114,15 +114,16 @@ public class ResponseEntityResultHandler extends AbstractMessageWriterResultHand @@ -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 @@ -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);
});
}

43
spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/JacksonHintsIntegrationTests.java

@ -28,7 +28,9 @@ import org.springframework.context.ApplicationContext; @@ -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 @@ -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 @@ -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 @@ -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 @@ -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;

Loading…
Cancel
Save