Browse Source

Support handler methods returning void, Observable<Void>, etc.

pull/1111/head
Sebastien Deleuze 10 years ago
parent
commit
91c2b7afad
  1. 33
      spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java
  2. 71
      spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java
  3. 48
      spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java

33
spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java

@ -23,22 +23,35 @@ import reactor.Publishers;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.convert.ConversionService;
import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpRequest;
import org.springframework.http.server.ReactiveServerHttpResponse; import org.springframework.http.server.ReactiveServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.reactive.HandlerResultHandler; import org.springframework.web.reactive.HandlerResultHandler;
/** /**
* Supports {@link HandlerResult} with a {@code Publisher<Void>} value. * Supports {@link HandlerResult} with a {@code void} or {@code Publisher<Void>} value.
* An optional {link ConversionService} can be used to support types that can be converted to
* {@code Publisher<Void>}, like {@code Observable<Void>} or {@code CompletableFuture<Void>}.
* *
* @author Sebastien Deleuze * @author Sebastien Deleuze
*/ */
public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler { public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler {
private static final ResolvableType PUBLISHER_VOID = ResolvableType.forClassWithGenerics(Publisher.class, Void.class);
private int order = Ordered.LOWEST_PRECEDENCE; private int order = Ordered.LOWEST_PRECEDENCE;
private ConversionService conversionService;
public SimpleHandlerResultHandler() {
}
public SimpleHandlerResultHandler(ConversionService conversionService) {
Assert.notNull(conversionService, "'conversionService' is required.");
this.conversionService = conversionService;
}
public void setOrder(int order) { public void setOrder(int order) {
this.order = order; this.order = order;
@ -52,14 +65,24 @@ public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler
@Override @Override
public boolean supports(HandlerResult result) { public boolean supports(HandlerResult result) {
ResolvableType type = result.getValueType(); ResolvableType type = result.getValueType();
return type != null && PUBLISHER_VOID.isAssignableFrom(type); return type != null && Void.TYPE.equals(type.getRawClass()) ||
(Void.class.isAssignableFrom(type.getGeneric(0).getRawClass()) && isConvertibleToPublisher(type));
}
private boolean isConvertibleToPublisher(ResolvableType type) {
return Publisher.class.isAssignableFrom(type.getRawClass()) ||
((this.conversionService != null) && this.conversionService.canConvert(type.getRawClass(), Publisher.class));
} }
@Override @Override
public Publisher<Void> handleResult(ReactiveServerHttpRequest request, public Publisher<Void> handleResult(ReactiveServerHttpRequest request,
ReactiveServerHttpResponse response, HandlerResult result) { ReactiveServerHttpResponse response, HandlerResult result) {
Publisher<Void> completion = Publishers.completable((Publisher<?>)result.getValue()); Object value = result.getValue();
if (Void.TYPE.equals(result.getValueType().getRawClass())) {
return response.writeHeaders();
}
Publisher<Void> completion = (value instanceof Publisher ? (Publisher<Void>)value : this.conversionService.convert(value, Publisher.class));
return Publishers.concat(Publishers.from(Arrays.asList(completion, response.writeHeaders()))); return Publishers.concat(Publishers.from(Arrays.asList(completion, response.writeHeaders())));
} }
} }

71
spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java

@ -16,12 +16,20 @@
package org.springframework.web.reactive.handler; package org.springframework.web.reactive.handler;
import java.util.concurrent.CompletableFuture;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import org.junit.Test; import org.junit.Test;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import reactor.rx.Stream;
import rx.Observable;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter;
import org.springframework.core.convert.support.ReactiveStreamsToReactorConverter;
import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter;
import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.HandlerMethod;
import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResult;
@ -38,8 +46,44 @@ public class SimpleHandlerResultHandlerTests {
HandlerMethod hm = new HandlerMethod(controller, TestController.class.getMethod("voidReturnValue")); HandlerMethod hm = new HandlerMethod(controller, TestController.class.getMethod("voidReturnValue"));
ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType()); ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType());
assertTrue(resultHandler.supports(new HandlerResult(hm, null, type)));
hm = new HandlerMethod(controller, TestController.class.getMethod("publisherString"));
type = ResolvableType.forMethodParameter(hm.getReturnType());
assertFalse(resultHandler.supports(new HandlerResult(hm, null, type))); assertFalse(resultHandler.supports(new HandlerResult(hm, null, type)));
hm = new HandlerMethod(controller, TestController.class.getMethod("publisherVoid"));
type = ResolvableType.forMethodParameter(hm.getReturnType());
assertTrue(resultHandler.supports(new HandlerResult(hm, null, type)));
hm = new HandlerMethod(controller, TestController.class.getMethod("streamVoid"));
type = ResolvableType.forMethodParameter(hm.getReturnType());
// Reactor Stream is a Publisher
assertTrue(resultHandler.supports(new HandlerResult(hm, null, type)));
hm = new HandlerMethod(controller, TestController.class.getMethod("observableVoid"));
type = ResolvableType.forMethodParameter(hm.getReturnType());
assertFalse(resultHandler.supports(new HandlerResult(hm, null, type)));
hm = new HandlerMethod(controller, TestController.class.getMethod("completableFutureVoid"));
type = ResolvableType.forMethodParameter(hm.getReturnType());
assertFalse(resultHandler.supports(new HandlerResult(hm, null, type)));
}
@Test
public void supportsWithConversionService() throws NoSuchMethodException {
GenericConversionService conversionService = new GenericConversionService();
conversionService.addConverter(new ReactiveStreamsToCompletableFutureConverter());
conversionService.addConverter(new ReactiveStreamsToReactorConverter());
conversionService.addConverter(new ReactiveStreamsToRxJava1Converter());
SimpleHandlerResultHandler resultHandler = new SimpleHandlerResultHandler(conversionService);
TestController controller = new TestController();
HandlerMethod hm = new HandlerMethod(controller, TestController.class.getMethod("voidReturnValue"));
ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType());
assertTrue(resultHandler.supports(new HandlerResult(hm, null, type)));
hm = new HandlerMethod(controller, TestController.class.getMethod("publisherString")); hm = new HandlerMethod(controller, TestController.class.getMethod("publisherString"));
type = ResolvableType.forMethodParameter(hm.getReturnType()); type = ResolvableType.forMethodParameter(hm.getReturnType());
assertFalse(resultHandler.supports(new HandlerResult(hm, null, type))); assertFalse(resultHandler.supports(new HandlerResult(hm, null, type)));
@ -47,14 +91,25 @@ public class SimpleHandlerResultHandlerTests {
hm = new HandlerMethod(controller, TestController.class.getMethod("publisherVoid")); hm = new HandlerMethod(controller, TestController.class.getMethod("publisherVoid"));
type = ResolvableType.forMethodParameter(hm.getReturnType()); type = ResolvableType.forMethodParameter(hm.getReturnType());
assertTrue(resultHandler.supports(new HandlerResult(hm, null, type))); assertTrue(resultHandler.supports(new HandlerResult(hm, null, type)));
hm = new HandlerMethod(controller, TestController.class.getMethod("streamVoid"));
type = ResolvableType.forMethodParameter(hm.getReturnType());
assertTrue(resultHandler.supports(new HandlerResult(hm, null, type)));
hm = new HandlerMethod(controller, TestController.class.getMethod("observableVoid"));
type = ResolvableType.forMethodParameter(hm.getReturnType());
assertTrue(resultHandler.supports(new HandlerResult(hm, null, type)));
hm = new HandlerMethod(controller, TestController.class.getMethod("completableFutureVoid"));
type = ResolvableType.forMethodParameter(hm.getReturnType());
assertTrue(resultHandler.supports(new HandlerResult(hm, null, type)));
} }
@SuppressWarnings("unused") @SuppressWarnings("unused")
private static class TestController { private static class TestController {
public Publisher<String> voidReturnValue() { public void voidReturnValue() {
return null;
} }
public Publisher<String> publisherString() { public Publisher<String> publisherString() {
@ -64,6 +119,18 @@ public class SimpleHandlerResultHandlerTests {
public Publisher<Void> publisherVoid() { public Publisher<Void> publisherVoid() {
return null; return null;
} }
public Stream<Void> streamVoid() {
return null;
}
public Observable<Void> observableVoid() {
return null;
}
public CompletableFuture<Void> completableFutureVoid() {
return null;
}
} }
} }

48
spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java

@ -197,16 +197,18 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati
} }
@Test @Test
public void create() throws Exception { public void publisherCreate() throws Exception {
RestTemplate restTemplate = new RestTemplate(); create("http://localhost:" + this.port + "/publisher-create");
URI url = new URI("http://localhost:" + this.port + "/create"); }
RequestEntity<List<Person>> request = RequestEntity.post(url)
.contentType(MediaType.APPLICATION_JSON)
.body(Arrays.asList(new Person("Robert"), new Person("Marie")));
ResponseEntity<Void> response = restTemplate.exchange(request, Void.class);
assertEquals(HttpStatus.OK, response.getStatusCode()); @Test
assertEquals(2, this.wac.getBean(TestController.class).persons.size()); public void streamCreate() throws Exception {
create("http://localhost:" + this.port + "/stream-create");
}
@Test
public void observableCreate() throws Exception {
create("http://localhost:" + this.port + "/observable-create");
} }
@ -259,6 +261,18 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati
assertEquals("MARIE", results.get(1).getName()); assertEquals("MARIE", results.get(1).getName());
} }
private void create(String requestUrl) throws Exception {
RestTemplate restTemplate = new RestTemplate();
URI url = new URI(requestUrl);
RequestEntity<List<Person>> request = RequestEntity.post(url)
.contentType(MediaType.APPLICATION_JSON)
.body(Arrays.asList(new Person("Robert"), new Person("Marie")));
ResponseEntity<Void> response = restTemplate.exchange(request, Void.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertEquals(2, this.wac.getBean(TestController.class).persons.size());
}
@Configuration @Configuration
@SuppressWarnings("unused") @SuppressWarnings("unused")
@ -295,7 +309,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati
@Bean @Bean
public SimpleHandlerResultHandler simpleHandlerResultHandler() { public SimpleHandlerResultHandler simpleHandlerResultHandler() {
return new SimpleHandlerResultHandler(); return new SimpleHandlerResultHandler(conversionService());
} }
} }
@ -448,11 +462,21 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati
}); });
} }
@RequestMapping("/create") @RequestMapping("/publisher-create")
public Publisher<Void> create(@RequestBody Stream<Person> personStream) { public Publisher<Void> publisherCreate(@RequestBody Publisher<Person> personStream) {
return Streams.wrap(personStream).toList().onSuccess(persons::addAll).after();
}
@RequestMapping("/stream-create")
public Promise<Void> streamCreate(@RequestBody Stream<Person> personStream) {
return personStream.toList().onSuccess(persons::addAll).after(); return personStream.toList().onSuccess(persons::addAll).after();
} }
@RequestMapping("/observable-create")
public Observable<Void> observableCreate(@RequestBody Observable<Person> personStream) {
return personStream.toList().doOnNext(p -> persons.addAll(p)).flatMap(document -> Observable.empty());
}
//TODO add mixed and T request mappings tests //TODO add mixed and T request mappings tests
} }

Loading…
Cancel
Save