Provide rich type information to ConversionService

When using the ConversionService to check and bridge to and from
reactive types we now generallly provide the full type information
available from method signatures. However that full type information
is not always necessary such as when we perform additional checks on
the generics of the reactive type (e.g. Mono<ResponseEntity>).

This allows us to switch to use DefaultFormattingConversionService
instead of GenericConversionService while also ensuring that the
CollectionToObjectConverter doesn't think it can convert List<?> to
any reactive type.

The ObjectToObjectConverter can also interfere because it is smart
enough to find the "from(Publisher<?>)" method on Flux and Mono.
To make up for that on the response side we now check if a type
is assignable to Publisher first in which case it is a simple cast.

In turn that means we don't need a PublisherToFluxConverter which can
be problematic in its own right because it can convert from Mono to
Flux which technically doesn't lose data but switches stream semantics.

Issue: #124, #128
This commit is contained in:
Rossen Stoyanchev 2016-07-03 21:36:18 -04:00
parent fb2e796048
commit 8c76581442
13 changed files with 177 additions and 155 deletions

View File

@ -1,51 +0,0 @@
/*
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.convert.support;
import java.util.LinkedHashSet;
import java.util.Set;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;
/**
* @author Sebastien Deleuze
*/
public class PublisherToFluxConverter implements GenericConverter {
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
Set<ConvertiblePair> pairs = new LinkedHashSet<>();
pairs.add(new ConvertiblePair(Publisher.class, Flux.class));
return pairs;
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return null;
}
else if (Publisher.class.isAssignableFrom(sourceType.getType())) {
return Flux.from((Publisher)source);
}
return null;
}
}

View File

@ -28,25 +28,25 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.Encoder;
import org.springframework.core.codec.ByteBufferDecoder;
import org.springframework.core.codec.ByteBufferEncoder;
import org.springframework.core.convert.support.PublisherToFluxConverter;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.Encoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.core.codec.StringEncoder;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.support.MonoToCompletableFutureConverter;
import org.springframework.core.convert.support.ReactorToRxJava1Converter;
import org.springframework.format.Formatter;
import org.springframework.format.FormatterRegistry;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.MediaType;
import org.springframework.http.codec.SseEventEncoder;
import org.springframework.http.codec.json.JacksonJsonDecoder;
import org.springframework.http.codec.json.JacksonJsonEncoder;
import org.springframework.http.codec.xml.Jaxb2Decoder;
import org.springframework.http.codec.xml.Jaxb2Encoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.core.codec.StringEncoder;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterRegistry;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.core.convert.support.MonoToCompletableFutureConverter;
import org.springframework.core.convert.support.ReactorToRxJava1Converter;
import org.springframework.format.Formatter;
import org.springframework.http.MediaType;
import org.springframework.http.codec.SseEventEncoder;
import org.springframework.http.converter.reactive.CodecHttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.ResourceHttpMessageConverter;
@ -271,17 +271,13 @@ public class WebReactiveConfiguration implements ApplicationContextAware {
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
}
// TODO: switch to DefaultFormattingConversionService
@Bean
public GenericConversionService mvcConversionService() {
GenericConversionService service = new GenericConversionService();
public FormattingConversionService mvcConversionService() {
FormattingConversionService service = new DefaultFormattingConversionService();
addFormatters(service);
return service;
}
// TODO: switch to FormatterRegistry
/**
* Override to add custom {@link Converter}s and {@link Formatter}s.
* <p>By default this method method registers:
@ -290,9 +286,8 @@ public class WebReactiveConfiguration implements ApplicationContextAware {
* <li>{@link ReactorToRxJava1Converter}
* </ul>
*/
protected void addFormatters(ConverterRegistry registry) {
protected void addFormatters(FormatterRegistry registry) {
registry.addConverter(new MonoToCompletableFutureConverter());
registry.addConverter(new PublisherToFluxConverter());
if (DependencyUtils.hasRxJava1()) {
registry.addConverter(new ReactorToRxJava1Converter());
}

View File

@ -18,12 +18,14 @@ package org.springframework.web.reactive.result;
import java.util.Optional;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.util.Assert;
import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.reactive.HandlerResultHandler;
@ -42,6 +44,11 @@ import org.springframework.web.server.ServerWebExchange;
*/
public class SimpleResultHandler implements Ordered, HandlerResultHandler {
protected static final TypeDescriptor MONO_TYPE = TypeDescriptor.valueOf(Mono.class);
protected static final TypeDescriptor FLUX_TYPE = TypeDescriptor.valueOf(Flux.class);
private ConversionService conversionService;
private int order = Ordered.LOWEST_PRECEDENCE;
@ -83,14 +90,19 @@ public class SimpleResultHandler implements Ordered, HandlerResultHandler {
if (Void.TYPE.equals(type.getRawClass())) {
return true;
}
if (getConversionService().canConvert(type.getRawClass(), Mono.class) ||
getConversionService().canConvert(type.getRawClass(), Flux.class)) {
TypeDescriptor source = new TypeDescriptor(result.getReturnTypeSource());
if (Publisher.class.isAssignableFrom(type.getRawClass()) ||
canConvert(source, MONO_TYPE) || canConvert(source, FLUX_TYPE)) {
Class<?> clazz = result.getReturnType().getGeneric(0).getRawClass();
return Void.class.equals(clazz);
}
return false;
}
private boolean canConvert(TypeDescriptor source, TypeDescriptor target) {
return getConversionService().canConvert(source, target);
}
@SuppressWarnings("unchecked")
@Override
public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
@ -98,19 +110,14 @@ public class SimpleResultHandler implements Ordered, HandlerResultHandler {
if (!optional.isPresent()) {
return Mono.empty();
}
Object returnValue = optional.get();
if (returnValue instanceof Mono) {
return (Mono<Void>) returnValue;
}
ResolvableType returnType = result.getReturnType();
if (getConversionService().canConvert(returnType.getRawClass(), Mono.class)) {
return this.conversionService.convert(returnValue, Mono.class);
}
else {
return this.conversionService.convert(returnValue, Flux.class).single();
Object value = optional.get();
if (Publisher.class.isAssignableFrom(result.getReturnType().getRawClass())) {
return Mono.from((Publisher<?>) value).then();
}
TypeDescriptor source = new TypeDescriptor(result.getReturnTypeSource());
return canConvert(source, MONO_TYPE) ?
((Mono<Void>) getConversionService().convert(value, source, MONO_TYPE)) :
((Flux<Void>) getConversionService().convert(value, source, FLUX_TYPE)).single();
}
}

View File

@ -22,8 +22,10 @@ import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.http.MediaType;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.server.reactive.ServerHttpResponse;
@ -41,6 +43,11 @@ import org.springframework.web.server.ServerWebExchange;
*/
public abstract class AbstractMessageConverterResultHandler extends ContentNegotiatingResultHandlerSupport {
protected static final TypeDescriptor MONO_TYPE = TypeDescriptor.valueOf(Mono.class);
protected static final TypeDescriptor FLUX_TYPE = TypeDescriptor.valueOf(Flux.class);
private final List<HttpMessageConverter<?>> messageConverters;
@ -70,28 +77,34 @@ public abstract class AbstractMessageConverterResultHandler extends ContentNegot
@SuppressWarnings("unchecked")
protected Mono<Void> writeBody(ServerWebExchange exchange, Object body, ResolvableType bodyType) {
protected Mono<Void> writeBody(ServerWebExchange exchange, Object body,
ResolvableType bodyType, MethodParameter bodyTypeParameter) {
boolean convertToFlux = getConversionService().canConvert(bodyType.getRawClass(), Flux.class);
boolean convertToMono = getConversionService().canConvert(bodyType.getRawClass(), Mono.class);
Publisher<?> publisher = null;
ResolvableType elementType;
ResolvableType elementType = convertToFlux || convertToMono ? bodyType.getGeneric(0) : bodyType;
Publisher<?> publisher;
if (body == null) {
publisher = Mono.empty();
}
else if (convertToMono) {
publisher = getConversionService().convert(body, Mono.class);
}
else if (convertToFlux) {
publisher = getConversionService().convert(body, Flux.class);
if (Publisher.class.isAssignableFrom(bodyType.getRawClass())) {
publisher = (Publisher<?>) body;
}
else {
publisher = Mono.just(body);
TypeDescriptor descriptor = new TypeDescriptor(bodyTypeParameter);
if (getConversionService().canConvert(descriptor, MONO_TYPE)) {
publisher = (Publisher<?>) getConversionService().convert(body, descriptor, MONO_TYPE);
}
else if (getConversionService().canConvert(descriptor, FLUX_TYPE)) {
publisher = (Publisher<?>) getConversionService().convert(body, descriptor, FLUX_TYPE);
}
}
if (Void.class.equals(elementType.getRawClass())) {
if (publisher != null) {
elementType = bodyType.getGeneric(0);
}
else {
elementType = bodyType;
publisher = Mono.justOrEmpty(body);
}
if (void.class == elementType.getRawClass() || Void.class == elementType.getRawClass()) {
return Mono.from((Publisher<Void>) publisher);
}

View File

@ -29,6 +29,7 @@ import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.http.MediaType;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.server.reactive.ServerHttpRequest;
@ -57,6 +58,11 @@ import org.springframework.web.server.UnsupportedMediaTypeStatusException;
*/
public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolver {
private static final TypeDescriptor MONO_TYPE = TypeDescriptor.valueOf(Mono.class);
private static final TypeDescriptor FLUX_TYPE = TypeDescriptor.valueOf(Flux.class);
private final List<HttpMessageConverter<?>> messageConverters;
private final ConversionService conversionService;
@ -123,8 +129,9 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve
ResolvableType type = ResolvableType.forMethodParameter(parameter);
boolean convertFromMono = getConversionService().canConvert(Mono.class, type.getRawClass());
boolean convertFromFlux = getConversionService().canConvert(Flux.class, type.getRawClass());
TypeDescriptor typeDescriptor = new TypeDescriptor(parameter);
boolean convertFromMono = getConversionService().canConvert(MONO_TYPE, typeDescriptor);
boolean convertFromFlux = getConversionService().canConvert(FLUX_TYPE, typeDescriptor);
ResolvableType elementType = convertFromMono || convertFromFlux ? type.getGeneric(0) : type;
@ -141,7 +148,7 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve
if (this.validator != null) {
flux = flux.map(applyValidationIfApplicable(parameter));
}
return Mono.just(this.conversionService.convert(flux, type.getRawClass()));
return Mono.just(getConversionService().convert(flux, FLUX_TYPE, typeDescriptor));
}
else {
Mono<?> mono = converter.readOne(elementType, request);
@ -151,7 +158,7 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve
if (!convertFromMono) {
return mono.map(value-> value); // TODO: MonoToObjectConverter
}
return Mono.just(this.conversionService.convert(mono, type.getRawClass()));
return Mono.just(getConversionService().convert(mono, MONO_TYPE, typeDescriptor));
}
}
}

View File

@ -114,7 +114,8 @@ public class ResponseBodyResultHandler extends AbstractMessageConverterResultHan
public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
Object body = result.getReturnValue().orElse(null);
ResolvableType bodyType = result.getReturnType();
return writeBody(exchange, body, bodyType);
MethodParameter bodyTypeParameter = result.getReturnTypeSource();
return writeBody(exchange, body, bodyType, bodyTypeParameter);
}
}

View File

@ -20,6 +20,7 @@ import java.util.Optional;
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.HttpEntity;
@ -85,7 +86,6 @@ public class ResponseEntityResultHandler extends AbstractMessageConverterResultH
else if (getConversionService().canConvert(returnType.getRawClass(), Mono.class)) {
ResolvableType genericType = result.getReturnType().getGeneric(0);
return isSupportedType(genericType);
}
return false;
}
@ -100,17 +100,25 @@ public class ResponseEntityResultHandler extends AbstractMessageConverterResultH
public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
ResolvableType returnType = result.getReturnType();
Mono<?> returnValueMono;
ResolvableType bodyType;
ResolvableType bodyType;
MethodParameter bodyTypeParameter;
Mono<?> returnValueMono;
Optional<Object> optional = result.getReturnValue();
if (optional.isPresent() && getConversionService().canConvert(returnType.getRawClass(), Mono.class)) {
returnValueMono = getConversionService().convert(optional.get(), Mono.class);
bodyType = returnType.getGeneric(0).getGeneric(0);
bodyType = returnType.getGeneric(0, 0);
bodyTypeParameter = new MethodParameter(result.getReturnTypeSource());
bodyTypeParameter.increaseNestingLevel();
bodyTypeParameter.increaseNestingLevel();
}
else {
returnValueMono = Mono.justOrEmpty(optional);
bodyType = returnType.getGeneric(0);
bodyTypeParameter = new MethodParameter(result.getReturnTypeSource());
bodyTypeParameter.increaseNestingLevel();
}
return returnValueMono.then(returnValue -> {
@ -132,7 +140,7 @@ public class ResponseEntityResultHandler extends AbstractMessageConverterResultH
.forEach(entry -> responseHeaders.put(entry.getKey(), entry.getValue()));
}
return writeBody(exchange, httpEntity.getBody(), bodyType);
return writeBody(exchange, httpEntity.getBody(), bodyType, bodyTypeParameter);
});
}

View File

@ -16,6 +16,7 @@
package org.springframework.web.reactive.result;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.junit.Before;
@ -26,10 +27,10 @@ import rx.Observable;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.core.convert.support.MonoToCompletableFutureConverter;
import org.springframework.core.convert.support.PublisherToFluxConverter;
import org.springframework.core.convert.support.ReactorToRxJava1Converter;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.web.reactive.HandlerResult;
import static org.junit.Assert.assertEquals;
@ -46,16 +47,15 @@ public class SimpleResultHandlerTests {
@Before
public void setUp() throws Exception {
GenericConversionService conversionService = new GenericConversionService();
conversionService.addConverter(new MonoToCompletableFutureConverter());
conversionService.addConverter(new PublisherToFluxConverter());
conversionService.addConverter(new ReactorToRxJava1Converter());
this.resultHandler = new SimpleResultHandler(conversionService);
FormattingConversionService service = new DefaultFormattingConversionService();
service.addConverter(new MonoToCompletableFutureConverter());
service.addConverter(new ReactorToRxJava1Converter());
this.resultHandler = new SimpleResultHandler(service);
}
@Test
public void supportsWithConversionService() throws NoSuchMethodException {
public void supports() throws NoSuchMethodException {
testSupports(ResolvableType.forClass(void.class), true);
testSupports(ResolvableType.forClassWithGenerics(Publisher.class, Void.class), true);
testSupports(ResolvableType.forClassWithGenerics(Flux.class, Void.class), true);
@ -66,6 +66,11 @@ public class SimpleResultHandlerTests {
testSupports(ResolvableType.forClassWithGenerics(Publisher.class, String.class), false);
}
@Test
public void supportsUsesGenericTypeInformation() throws Exception {
testSupports(ResolvableType.forClassWithGenerics(List.class, Void.class), false);
}
private void testSupports(ResolvableType type, boolean result) {
MethodParameter param = ResolvableMethod.on(TestController.class).returning(type).resolveReturnType();
HandlerResult handlerResult = new HandlerResult(new TestController(), null, param);
@ -90,6 +95,8 @@ public class SimpleResultHandlerTests {
public Publisher<Void> publisher() { return null; }
public List<Void> list() { return null; }
}
}

View File

@ -36,12 +36,12 @@ import reactor.core.publisher.Mono;
import reactor.core.test.TestSubscriber;
import rx.Observable;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.ByteBufferEncoder;
import org.springframework.core.codec.StringEncoder;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.core.convert.support.MonoToCompletableFutureConverter;
import org.springframework.core.convert.support.PublisherToFluxConverter;
import org.springframework.core.convert.support.ReactorToRxJava1Converter;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
@ -58,6 +58,7 @@ import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.ObjectUtils;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
import org.springframework.web.reactive.result.ResolvableMethod;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.session.MockWebSessionManager;
@ -92,8 +93,8 @@ public class MessageConverterResultHandlerTests {
@Test // SPR-12894
public void useDefaultContentType() throws Exception {
Resource body = new ClassPathResource("logo.png", getClass());
ResolvableType bodyType = ResolvableType.forType(Resource.class);
this.resultHandler.writeBody(this.exchange, body, bodyType).block(Duration.ofSeconds(5));
ResolvableType type = ResolvableType.forType(Resource.class);
this.resultHandler.writeBody(this.exchange, body, type, returnType(type)).block(Duration.ofSeconds(5));
assertEquals("image/x-png", this.response.getHeaders().getFirst("Content-Type"));
}
@ -104,22 +105,22 @@ public class MessageConverterResultHandlerTests {
Collections.singleton(APPLICATION_JSON));
String body = "foo";
ResolvableType bodyType = ResolvableType.forType(String.class);
this.resultHandler.writeBody(this.exchange, body, bodyType).block(Duration.ofSeconds(5));
ResolvableType type = ResolvableType.forType(String.class);
this.resultHandler.writeBody(this.exchange, body, type, returnType(type)).block(Duration.ofSeconds(5));
assertEquals(APPLICATION_JSON_UTF8, this.response.getHeaders().getContentType());
}
@Test
public void voidReturnType() throws Exception {
testVoidReturnType(null, ResolvableType.forType(Void.class));
testVoidReturnType(null, ResolvableType.forType(void.class));
testVoidReturnType(Mono.empty(), ResolvableType.forClassWithGenerics(Mono.class, Void.class));
testVoidReturnType(Flux.empty(), ResolvableType.forClassWithGenerics(Flux.class, Void.class));
testVoidReturnType(Observable.empty(), ResolvableType.forClassWithGenerics(Observable.class, Void.class));
}
private void testVoidReturnType(Object body, ResolvableType bodyType) {
this.resultHandler.writeBody(this.exchange, body, bodyType).block(Duration.ofSeconds(5));
private void testVoidReturnType(Object body, ResolvableType type) {
this.resultHandler.writeBody(this.exchange, body, type, returnType(type)).block(Duration.ofSeconds(5));
assertNull(this.response.getHeaders().get("Content-Type"));
assertNull(this.response.getBody());
@ -128,10 +129,10 @@ public class MessageConverterResultHandlerTests {
@Test // SPR-13135
public void unsupportedReturnType() throws Exception {
ByteArrayOutputStream body = new ByteArrayOutputStream();
ResolvableType bodyType = ResolvableType.forType(OutputStream.class);
ResolvableType type = ResolvableType.forType(OutputStream.class);
HttpMessageConverter<?> converter = new CodecHttpMessageConverter<>(new ByteBufferEncoder());
Mono<Void> mono = createResultHandler(converter).writeBody(this.exchange, body, bodyType);
Mono<Void> mono = createResultHandler(converter).writeBody(this.exchange, body, type, returnType(type));
TestSubscriber.subscribe(mono).assertError(IllegalStateException.class);
}
@ -139,8 +140,8 @@ public class MessageConverterResultHandlerTests {
@Test // SPR-12811
public void jacksonTypeOfListElement() throws Exception {
List<ParentClass> body = Arrays.asList(new Foo("foo"), new Bar("bar"));
ResolvableType bodyType = ResolvableType.forClassWithGenerics(List.class, ParentClass.class);
this.resultHandler.writeBody(this.exchange, body, bodyType).block(Duration.ofSeconds(5));
ResolvableType type = ResolvableType.forClassWithGenerics(List.class, ParentClass.class);
this.resultHandler.writeBody(this.exchange, body, type, returnType(type)).block(Duration.ofSeconds(5));
assertEquals(APPLICATION_JSON_UTF8, this.response.getHeaders().getContentType());
assertResponseBody("[{\"type\":\"foo\",\"parentProperty\":\"foo\"}," +
@ -151,8 +152,8 @@ public class MessageConverterResultHandlerTests {
@Ignore
public void jacksonTypeWithSubType() throws Exception {
SimpleBean body = new SimpleBean(123L, "foo");
ResolvableType bodyType = ResolvableType.forClass(Identifiable.class);
this.resultHandler.writeBody(this.exchange, body, bodyType).block(Duration.ofSeconds(5));
ResolvableType type = ResolvableType.forClass(Identifiable.class);
this.resultHandler.writeBody(this.exchange, body, type, returnType(type)).block(Duration.ofSeconds(5));
assertEquals(APPLICATION_JSON_UTF8, this.response.getHeaders().getContentType());
assertResponseBody("{\"id\":123,\"name\":\"foo\"}");
@ -162,14 +163,18 @@ public class MessageConverterResultHandlerTests {
@Ignore
public void jacksonTypeWithSubTypeOfListElement() throws Exception {
List<SimpleBean> body = Arrays.asList(new SimpleBean(123L, "foo"), new SimpleBean(456L, "bar"));
ResolvableType bodyType = ResolvableType.forClassWithGenerics(List.class, Identifiable.class);
this.resultHandler.writeBody(this.exchange, body, bodyType).block(Duration.ofSeconds(5));
ResolvableType type = ResolvableType.forClassWithGenerics(List.class, Identifiable.class);
this.resultHandler.writeBody(this.exchange, body, type, returnType(type)).block(Duration.ofSeconds(5));
assertEquals(APPLICATION_JSON_UTF8, this.response.getHeaders().getContentType());
assertResponseBody("[{\"id\":123,\"name\":\"foo\"},{\"id\":456,\"name\":\"bar\"}]");
}
private MethodParameter returnType(ResolvableType bodyType) {
return ResolvableMethod.on(TestController.class).returning(bodyType).resolveReturnType();
}
private AbstractMessageConverterResultHandler createResultHandler(HttpMessageConverter<?>... converters) {
List<HttpMessageConverter<?>> converterList;
if (ObjectUtils.isEmpty(converters)) {
@ -186,7 +191,6 @@ public class MessageConverterResultHandlerTests {
GenericConversionService service = new GenericConversionService();
service.addConverter(new MonoToCompletableFutureConverter());
service.addConverter(new PublisherToFluxConverter());
service.addConverter(new ReactorToRxJava1Converter());
RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder().build();
@ -267,4 +271,29 @@ public class MessageConverterResultHandlerTests {
}
}
@SuppressWarnings("unused")
private static class TestController {
Resource resource() { return null; }
String string() { return null; }
void voidReturn() { }
Mono<Void> monoVoid() { return null; }
Flux<Void> fluxVoid() { return null; }
Observable<Void> observableVoid() { return null; }
OutputStream outputStream() { return null; }
List<ParentClass> listParentClass() { return null; }
Identifiable identifiable() { return null; }
List<Identifiable> listIdentifiable() { return null; }
}
}

View File

@ -42,16 +42,16 @@ import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.SynthesizingMethodParameter;
import org.springframework.core.codec.Decoder;
import org.springframework.core.convert.support.PublisherToFluxConverter;
import org.springframework.http.codec.json.JacksonJsonDecoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.core.convert.support.GenericConversionService;
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.MediaType;
import org.springframework.http.codec.json.JacksonJsonDecoder;
import org.springframework.http.converter.reactive.CodecHttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.server.reactive.MockServerHttpRequest;
@ -202,23 +202,29 @@ public class RequestBodyArgumentResolverTests {
@SuppressWarnings("unchecked")
private <T> T resolveValue(String paramName, Class<T> valueType, String body) {
this.request.getHeaders().setContentType(MediaType.APPLICATION_JSON);
this.request.writeWith(Flux.just(dataBuffer(body)));
Mono<Object> result = this.resolver.resolveArgument(parameter(paramName), this.model, this.exchange);
Object value = result.block(Duration.ofSeconds(5));
assertNotNull(value);
assertTrue("Actual type: " + value.getClass(), valueType.isAssignableFrom(value.getClass()));
return (T) value;
}
@SuppressWarnings("Convert2MethodRef")
private RequestBodyArgumentResolver resolver(Decoder<?>... decoders) {
List<HttpMessageConverter<?>> converters = new ArrayList<>();
Arrays.asList(decoders).forEach(decoder -> converters.add(new CodecHttpMessageConverter<>(decoder)));
GenericConversionService service = new GenericConversionService();
FormattingConversionService service = new DefaultFormattingConversionService();
service.addConverter(new MonoToCompletableFutureConverter());
service.addConverter(new PublisherToFluxConverter());
service.addConverter(new ReactorToRxJava1Converter());
return new RequestBodyArgumentResolver(converters, service, new TestBeanValidator());
}

View File

@ -28,10 +28,10 @@ import reactor.core.publisher.Mono;
import org.springframework.core.codec.ByteBufferEncoder;
import org.springframework.core.codec.StringEncoder;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.core.convert.support.MonoToCompletableFutureConverter;
import org.springframework.core.convert.support.PublisherToFluxConverter;
import org.springframework.core.convert.support.ReactorToRxJava1Converter;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.codec.json.JacksonJsonEncoder;
@ -99,9 +99,8 @@ public class ResponseBodyResultHandlerTests {
else {
converterList = Arrays.asList(converters);
}
GenericConversionService service = new GenericConversionService();
FormattingConversionService service = new DefaultFormattingConversionService();
service.addConverter(new MonoToCompletableFutureConverter());
service.addConverter(new PublisherToFluxConverter());
service.addConverter(new ReactorToRxJava1Converter());
RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder().build();

View File

@ -33,11 +33,11 @@ import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.ByteBufferEncoder;
import org.springframework.core.codec.StringEncoder;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.core.convert.support.MonoToCompletableFutureConverter;
import org.springframework.core.convert.support.PublisherToFluxConverter;
import org.springframework.core.convert.support.ReactorToRxJava1Converter;
import org.springframework.core.io.buffer.support.DataBufferTestUtils;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@ -102,9 +102,8 @@ public class ResponseEntityResultHandlerTests {
else {
converterList = Arrays.asList(converters);
}
GenericConversionService service = new GenericConversionService();
FormattingConversionService service = new DefaultFormattingConversionService();
service.addConverter(new MonoToCompletableFutureConverter());
service.addConverter(new PublisherToFluxConverter());
service.addConverter(new ReactorToRxJava1Converter());

View File

@ -38,13 +38,13 @@ import rx.Single;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType;
import org.springframework.core.convert.support.ConfigurableConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.convert.support.PublisherToFluxConverter;
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.core.io.buffer.support.DataBufferTestUtils;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.MockServerHttpRequest;
@ -264,10 +264,12 @@ public class ViewResolutionResultHandlerTests {
}
private ViewResolutionResultHandler createResultHandler(List<View> defaultViews, ViewResolver... resolvers) {
ConfigurableConversionService service = new DefaultConversionService();
FormattingConversionService service = new DefaultFormattingConversionService();
service.addConverter(new MonoToCompletableFutureConverter());
service.addConverter(new ReactorToRxJava1Converter());
service.addConverter(new PublisherToFluxConverter());
List<ViewResolver> resolverList = Arrays.asList(resolvers);
ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolverList, service);
handler.setDefaultViews(defaultViews);
return handler;