diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterArgumentResolver.java index 44f462d6587..5f43652c36e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterArgumentResolver.java @@ -105,7 +105,8 @@ public abstract class AbstractMessageConverterArgumentResolver { } - protected Mono readBody(MethodParameter bodyParameter, ServerWebExchange exchange) { + protected Mono readBody(MethodParameter bodyParameter, boolean isBodyRequired, + ServerWebExchange exchange) { TypeDescriptor typeDescriptor = new TypeDescriptor(bodyParameter); boolean convertFromMono = getConversionService().canConvert(MONO_TYPE, typeDescriptor); @@ -125,14 +126,22 @@ public abstract class AbstractMessageConverterArgumentResolver { for (HttpMessageConverter converter : getMessageConverters()) { if (converter.canRead(elementType, mediaType)) { if (convertFromFlux) { - Flux flux = converter.read(elementType, request); + Flux flux = converter.read(elementType, request) + .onErrorResumeWith(ex -> Flux.error(getReadError(ex, bodyParameter))); + if (checkRequired(bodyParameter, isBodyRequired)) { + flux = flux.switchIfEmpty(Flux.error(getRequiredBodyError(bodyParameter))); + } if (this.validator != null) { flux = flux.map(applyValidationIfApplicable(bodyParameter)); } return Mono.just(getConversionService().convert(flux, FLUX_TYPE, typeDescriptor)); } else { - Mono mono = converter.readMono(elementType, request); + Mono mono = converter.readMono(elementType, request) + .otherwise(ex -> Mono.error(getReadError(ex, bodyParameter))); + if (checkRequired(bodyParameter, isBodyRequired)) { + mono = mono.otherwiseIfEmpty(Mono.error(getRequiredBodyError(bodyParameter))); + } if (this.validator != null) { mono = mono.map(applyValidationIfApplicable(bodyParameter)); } @@ -149,6 +158,22 @@ public abstract class AbstractMessageConverterArgumentResolver { return Mono.error(new UnsupportedMediaTypeStatusException(mediaType, this.supportedMediaTypes)); } + protected boolean checkRequired(MethodParameter bodyParameter, boolean isBodyRequired) { + if ("rx.Single".equals(bodyParameter.getNestedParameterType().getName())) { + return true; + } + return isBodyRequired; + } + + protected ServerWebInputException getReadError(Throwable ex, MethodParameter parameter) { + return new ServerWebInputException("Failed to read HTTP message", parameter, ex); + } + + protected ServerWebInputException getRequiredBodyError(MethodParameter parameter) { + return new ServerWebInputException("Required request body is missing: " + + parameter.getMethod().toGenericString()); + } + protected Function applyValidationIfApplicable(MethodParameter methodParam) { Annotation[] annotations = methodParam.getParameterAnnotations(); for (Annotation ann : annotations) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolver.java index b2bb395e1d1..0cd9bcf6b2d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolver.java @@ -91,17 +91,22 @@ public class HttpEntityArgumentResolver extends AbstractMessageConverterArgument bodyParameter.increaseNestingLevel(); } - return readBody(bodyParameter, exchange) - .map(body -> { - ServerHttpRequest request = exchange.getRequest(); - HttpHeaders headers = request.getHeaders(); - if (RequestEntity.class == entityType.getRawClass()) { - return new RequestEntity<>(body, headers, request.getMethod(), request.getURI()); - } - else { - return new HttpEntity<>(body, headers); - } - }); + return readBody(bodyParameter, false, exchange) + .map(body -> createHttpEntity(body, entityType, exchange)) + .defaultIfEmpty(createHttpEntity(null, entityType, exchange)); + } + + private Object createHttpEntity(Object body, ResolvableType entityType, + ServerWebExchange exchange) { + + ServerHttpRequest request = exchange.getRequest(); + HttpHeaders headers = request.getHeaders(); + if (RequestEntity.class == entityType.getRawClass()) { + return new RequestEntity<>(body, headers, request.getMethod(), request.getURI()); + } + else { + return new HttpEntity<>(body, headers); + } } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java index 8d8e7f01acd..fc12bbeb26a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java @@ -21,7 +21,6 @@ import java.util.List; import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; -import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.ui.ModelMap; @@ -79,7 +78,8 @@ public class RequestBodyArgumentResolver extends AbstractMessageConverterArgumen @Override public Mono resolveArgument(MethodParameter param, ModelMap model, ServerWebExchange exchange) { - return readBody(param, exchange); + boolean isRequired = param.getParameterAnnotation(RequestBody.class).required(); + return readBody(param, isRequired, exchange); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index affa9c3f75b..65c721844e4 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -49,6 +49,7 @@ import org.springframework.web.reactive.result.method.annotation.ResponseBodyRes import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; import org.springframework.web.server.WebExceptionHandler; import org.springframework.web.server.WebHandler; import org.springframework.web.server.adapter.DefaultServerWebExchange; @@ -163,7 +164,8 @@ public class DispatcherHandlerErrorTests { Mono publisher = this.dispatcherHandler.handle(this.exchange); TestSubscriber.subscribe(publisher) - .assertErrorWith(ex -> assertSame(EXCEPTION, ex)); + .assertError(ServerWebInputException.class) + .assertErrorWith(ex -> assertSame(EXCEPTION, ex.getCause())); } @Test diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ResolvableMethod.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ResolvableMethod.java index bd2ede31512..1070c488dc8 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ResolvableMethod.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ResolvableMethod.java @@ -171,8 +171,8 @@ public class ResolvableMethod { matches.add(param); } - Assert.isTrue(!matches.isEmpty(), "No matching method argument: " + this); - Assert.isTrue(matches.size() == 1, "Multiple matching method arguments: " + matches); + Assert.isTrue(!matches.isEmpty(), "No matching arg on " + method.toString()); + Assert.isTrue(matches.size() == 1, "Multiple matching args: " + matches + " on " + method.toString()); return matches.get(0); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolverTests.java index f42dce42eec..0df938c38b7 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolverTests.java @@ -25,6 +25,8 @@ import java.util.concurrent.CompletableFuture; import org.junit.Before; import org.junit.Test; +import reactor.core.converter.RxJava1ObservableConverter; +import reactor.core.converter.RxJava1SingleConverter; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.test.TestSubscriber; @@ -51,12 +53,14 @@ import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.ui.ExtendedModelMap; import org.springframework.web.reactive.result.ResolvableMethod; import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.MockWebSessionManager; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.springframework.core.ResolvableType.forClassWithGenerics; @@ -106,6 +110,68 @@ public class HttpEntityArgumentResolverTests { assertFalse(this.resolver.supportsParameter(this.testMethod.resolveParam(type))); } + @Test + public void emptyBodyWithString() throws Exception { + ResolvableType type = httpEntity(String.class); + HttpEntity entity = resolveValueWithEmptyBody(type); + + assertNull(entity.getBody()); + } + + @Test + public void emptyBodyWithMono() throws Exception { + ResolvableType type = httpEntity(forClassWithGenerics(Mono.class, String.class)); + HttpEntity> entity = resolveValueWithEmptyBody(type); + + TestSubscriber.subscribe(entity.getBody()) + .assertNoError() + .assertComplete() + .assertNoValues(); + } + + @Test + public void emptyBodyWithFlux() throws Exception { + ResolvableType type = httpEntity(forClassWithGenerics(Flux.class, String.class)); + HttpEntity> entity = resolveValueWithEmptyBody(type); + + TestSubscriber.subscribe(entity.getBody()) + .assertNoError() + .assertComplete() + .assertNoValues(); + } + + @Test + public void emptyBodyWithSingle() throws Exception { + ResolvableType type = httpEntity(forClassWithGenerics(Single.class, String.class)); + HttpEntity> entity = resolveValueWithEmptyBody(type); + + TestSubscriber.subscribe(RxJava1SingleConverter.from(entity.getBody())) + .assertNoValues() + .assertError(ServerWebInputException.class); + } + + @Test + public void emptyBodyWithObservable() throws Exception { + ResolvableType type = httpEntity(forClassWithGenerics(Observable.class, String.class)); + HttpEntity> entity = resolveValueWithEmptyBody(type); + + TestSubscriber.subscribe(RxJava1ObservableConverter.from(entity.getBody())) + .assertNoError() + .assertComplete() + .assertNoValues(); + } + + @Test + public void emptyBodyWithCompletableFuture() throws Exception { + ResolvableType type = httpEntity(forClassWithGenerics(CompletableFuture.class, String.class)); + HttpEntity> entity = resolveValueWithEmptyBody(type); + + entity.getBody().whenComplete((body, ex) -> { + assertNull(body); + assertNull(ex); + }); + } + @Test public void httpEntityWithStringBody() throws Exception { String body = "line1"; @@ -211,6 +277,17 @@ public class HttpEntityArgumentResolverTests { return (T) value; } + @SuppressWarnings("unchecked") + private HttpEntity resolveValueWithEmptyBody(ResolvableType type) { + this.request.writeWith(Flux.empty()); + MethodParameter param = this.testMethod.resolveParam(type); + Mono result = this.resolver.resolveArgument(param, new ExtendedModelMap(), this.exchange); + HttpEntity httpEntity = (HttpEntity) result.block(Duration.ofSeconds(5)); + + assertEquals(this.request.getHeaders(), httpEntity.getHeaders()); + return (HttpEntity) httpEntity; + } + private DataBuffer dataBuffer(String body) { byte[] bytes = body.getBytes(Charset.forName("UTF-8")); ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); @@ -224,10 +301,10 @@ public class HttpEntityArgumentResolverTests { Mono monoString, HttpEntity httpEntity, HttpEntity> monoBody, - HttpEntity> singleBody, - HttpEntity> completableFutureBody, HttpEntity> fluxBody, + HttpEntity> singleBody, HttpEntity> observableBody, + HttpEntity> completableFutureBody, RequestEntity requestEntity) {} } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterArgumentResolverTests.java index ea6d7cd81cb..18b6c582a8b 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterArgumentResolverTests.java @@ -88,7 +88,7 @@ public class MessageConverterArgumentResolverTests { @Before public void setUp() throws Exception { - this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); + this.request = new MockServerHttpRequest(HttpMethod.POST, new URI("/path")); MockServerHttpResponse response = new MockServerHttpResponse(); this.exchange = new DefaultServerWebExchange(this.request, response, new MockWebSessionManager()); } @@ -100,20 +100,23 @@ public class MessageConverterArgumentResolverTests { this.request.writeWith(Flux.just(dataBuffer(body))); ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class); MethodParameter param = this.testMethod.resolveParam(type); - Mono result = this.resolver.readBody(param, this.exchange); + Mono result = this.resolver.readBody(param, true, this.exchange); TestSubscriber.subscribe(result) .assertError(UnsupportedMediaTypeStatusException.class); } - @Test // SPR-9942 - public void noContent() throws Exception { + // More extensive "empty body" tests in RequestBody- and HttpEntityArgumentResolverTests + + @Test @SuppressWarnings("unchecked") // SPR-9942 + public void emptyBody() throws Exception { this.request.writeWith(Flux.empty()); + this.request.getHeaders().setContentType(MediaType.APPLICATION_JSON); ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class); MethodParameter param = this.testMethod.resolveParam(type); - Mono result = this.resolver.readBody(param, this.exchange); + Mono result = (Mono) this.resolver.readBody(param, true, this.exchange).block(); - TestSubscriber.subscribe(result).assertError(UnsupportedMediaTypeStatusException.class); + TestSubscriber.subscribe(result).assertError(ServerWebInputException.class); } @Test @@ -262,7 +265,7 @@ public class MessageConverterArgumentResolverTests { this.request.getHeaders().setContentType(MediaType.APPLICATION_JSON); this.request.writeWith(Flux.just(dataBuffer(body))); - Mono result = this.resolver.readBody(param, this.exchange); + Mono result = this.resolver.readBody(param, true, this.exchange); Object value = result.block(Duration.ofSeconds(5)); assertNotNull(value); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java index a694d9c98e6..3300637bb1f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java @@ -15,26 +15,53 @@ */ package org.springframework.web.reactive.result.method.annotation; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; +import org.junit.Before; import org.junit.Test; +import reactor.core.converter.RxJava1ObservableConverter; +import reactor.core.converter.RxJava1SingleConverter; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; +import rx.Observable; +import rx.Single; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.codec.StringDecoder; import org.springframework.core.convert.support.MonoToCompletableFutureConverter; import org.springframework.core.convert.support.ReactorToRxJava1Converter; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.format.support.FormattingConversionService; +import org.springframework.http.HttpMethod; import org.springframework.http.converter.reactive.CodecHttpMessageConverter; import org.springframework.http.converter.reactive.HttpMessageConverter; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.ui.ExtendedModelMap; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.reactive.result.ResolvableMethod; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.springframework.core.ResolvableType.forClass; import static org.springframework.core.ResolvableType.forClassWithGenerics; /** @@ -46,21 +73,130 @@ import static org.springframework.core.ResolvableType.forClassWithGenerics; */ public class RequestBodyArgumentResolverTests { + private RequestBodyArgumentResolver resolver = resolver(); + + private ServerWebExchange exchange; + + private MockServerHttpRequest request; + + private ResolvableMethod testMethod = ResolvableMethod.on(this.getClass()).name("handle"); + + + @Before + public void setUp() throws Exception { + this.request = new MockServerHttpRequest(HttpMethod.POST, new URI("/path")); + MockServerHttpResponse response = new MockServerHttpResponse(); + this.exchange = new DefaultServerWebExchange(this.request, response, new MockWebSessionManager()); + } + @Test public void supports() throws Exception { + ResolvableType type = forClassWithGenerics(Mono.class, String.class); + MethodParameter param = this.testMethod.resolveParam(type, requestBody(true)); + assertTrue(this.resolver.supportsParameter(param)); + + MethodParameter parameter = this.testMethod.resolveParam(p -> !p.hasParameterAnnotations()); + assertFalse(this.resolver.supportsParameter(parameter)); + } - ResolvableMethod testMethod = ResolvableMethod.on(getClass()).name("handle"); - RequestBodyArgumentResolver resolver = resolver(); + @Test + public void stringBody() throws Exception { + String body = "line1"; + ResolvableType type = forClass(String.class); + MethodParameter param = this.testMethod.resolveParam(type, requestBody(true)); + String value = resolveValue(param, body); + assertEquals(body, value); + } + + @Test(expected = ServerWebInputException.class) + public void emptyBodyWithString() throws Exception { + resolveValueWithEmptyBody(forClass(String.class), true); + } + + @Test + public void emptyBodyWithStringNotRequired() throws Exception { + ResolvableType type = forClass(String.class); + String body = resolveValueWithEmptyBody(type, false); + + assertNull(body); + } + + @Test + public void emptyBodyWithMono() throws Exception { ResolvableType type = forClassWithGenerics(Mono.class, String.class); - MethodParameter param = testMethod.resolveParam(type); - assertTrue(resolver.supportsParameter(param)); - MethodParameter parameter = testMethod.resolveParam(p -> !p.hasParameterAnnotations()); - assertFalse(resolver.supportsParameter(parameter)); + TestSubscriber.subscribe(resolveValueWithEmptyBody(type, true)) + .assertNoValues() + .assertError(ServerWebInputException.class); + + TestSubscriber.subscribe(resolveValueWithEmptyBody(type, false)) + .assertNoValues() + .assertComplete(); + } + + @Test + public void emptyBodyWithFlux() throws Exception { + ResolvableType type = forClassWithGenerics(Flux.class, String.class); + + TestSubscriber.subscribe(resolveValueWithEmptyBody(type, true)) + .assertNoValues() + .assertError(ServerWebInputException.class); + + TestSubscriber.subscribe(resolveValueWithEmptyBody(type, false)) + .assertNoValues() + .assertComplete(); + } + + @Test + public void emptyBodyWithSingle() throws Exception { + ResolvableType type = forClassWithGenerics(Single.class, String.class); + + Single single = resolveValueWithEmptyBody(type, true); + TestSubscriber.subscribe(RxJava1SingleConverter.from(single)) + .assertNoValues() + .assertError(ServerWebInputException.class); + + single = resolveValueWithEmptyBody(type, false); + TestSubscriber.subscribe(RxJava1SingleConverter.from(single)) + .assertNoValues() + .assertError(ServerWebInputException.class); + } + + @Test + public void emptyBodyWithObservable() throws Exception { + ResolvableType type = forClassWithGenerics(Observable.class, String.class); + + Observable observable = resolveValueWithEmptyBody(type, true); + TestSubscriber.subscribe(RxJava1ObservableConverter.from(observable)) + .assertNoValues() + .assertError(ServerWebInputException.class); + + observable = resolveValueWithEmptyBody(type, false); + TestSubscriber.subscribe(RxJava1ObservableConverter.from(observable)) + .assertNoValues() + .assertComplete(); + } + + @Test + public void emptyBodyWithCompletableFuture() throws Exception { + ResolvableType type = forClassWithGenerics(CompletableFuture.class, String.class); + + CompletableFuture future = resolveValueWithEmptyBody(type, true); + future.whenComplete((text, ex) -> { + assertNull(text); + assertNotNull(ex); + }); + + future = resolveValueWithEmptyBody(type, false); + future.whenComplete((text, ex) -> { + assertNotNull(text); + assertNull(ex); + }); } + private RequestBodyArgumentResolver resolver() { List> converters = new ArrayList<>(); converters.add(new CodecHttpMessageConverter<>(new StringDecoder())); @@ -72,8 +208,62 @@ public class RequestBodyArgumentResolverTests { return new RequestBodyArgumentResolver(converters, service); } + private T resolveValue(MethodParameter param, String body) { + this.request.writeWith(Flux.just(dataBuffer(body))); + Mono result = this.resolver.readBody(param, true, this.exchange); + Object value = result.block(Duration.ofSeconds(5)); + + assertNotNull(value); + assertTrue("Unexpected return value type: " + value, + param.getParameterType().isAssignableFrom(value.getClass())); + + //noinspection unchecked + return (T) value; + } + + private T resolveValueWithEmptyBody(ResolvableType type, boolean required) { + this.request.writeWith(Flux.empty()); + MethodParameter param = this.testMethod.resolveParam(type, requestBody(required)); + Mono result = this.resolver.resolveArgument(param, new ExtendedModelMap(), this.exchange); + Object value = result.block(Duration.ofSeconds(5)); + + if (value != null) { + assertTrue("Unexpected return value type: " + value, + param.getParameterType().isAssignableFrom(value.getClass())); + } + + //noinspection unchecked + return (T) value; + } + + private Predicate requestBody(boolean required) { + return p -> { + RequestBody annotation = p.getParameterAnnotation(RequestBody.class); + return annotation != null && annotation.required() == required; + }; + } + + private DataBuffer dataBuffer(String body) { + byte[] bytes = body.getBytes(Charset.forName("UTF-8")); + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); + return new DefaultDataBufferFactory().wrap(byteBuffer); + } + @SuppressWarnings("unused") - void handle(@RequestBody Mono monoString, String paramWithoutAnnotation) {} + void handle( + @RequestBody String string, + @RequestBody Mono mono, + @RequestBody Flux flux, + @RequestBody Single single, + @RequestBody Observable obs, + @RequestBody CompletableFuture future, + @RequestBody(required = false) String stringNotRequired, + @RequestBody(required = false) Mono monoNotRequired, + @RequestBody(required = false) Flux fluxNotRequired, + @RequestBody(required = false) Single singleNotRequired, + @RequestBody(required = false) Observable obsNotRequired, + @RequestBody(required = false) CompletableFuture futureNotRequired, + String notAnnotated) {} }