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:
@@ -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)));
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user