Skip to content

Commit c430402

Browse files
committed
Improve support for Mono<ResponseEntity<?>>
If the body class is not resolvable from the return type and there is a body instance we now fall back on the class of the body instance. Issue: SPR-14877
1 parent 84d3808 commit c430402

File tree

6 files changed

+57
-30
lines changed

6 files changed

+57
-30
lines changed

spring-web-reactive/src/main/java/org/springframework/web/reactive/result/AbstractHandlerResultHandler.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.List;
2323
import java.util.Optional;
2424
import java.util.Set;
25+
import java.util.function.Supplier;
2526

2627
import org.springframework.core.MethodParameter;
2728
import org.springframework.core.Ordered;
@@ -101,13 +102,14 @@ public int getOrder() {
101102
* Select the best media type for the current request through a content
102103
* negotiation algorithm.
103104
* @param exchange the current request
104-
* @param producibleTypes the media types that can be produced for the current request
105+
* @param producibleTypesSupplier the media types that can be produced for the current request
105106
* @return the selected media type or {@code null}
106107
*/
107-
protected MediaType selectMediaType(ServerWebExchange exchange, List<MediaType> producibleTypes) {
108+
protected MediaType selectMediaType(ServerWebExchange exchange,
109+
Supplier<List<MediaType>> producibleTypesSupplier) {
108110

109111
List<MediaType> acceptableTypes = getAcceptableTypes(exchange);
110-
producibleTypes = getProducibleTypes(exchange, producibleTypes);
112+
List<MediaType> producibleTypes = getProducibleTypes(exchange, producibleTypesSupplier);
111113

112114
Set<MediaType> compatibleMediaTypes = new LinkedHashSet<>();
113115
for (MediaType acceptable : acceptableTypes) {
@@ -139,13 +141,15 @@ private List<MediaType> getAcceptableTypes(ServerWebExchange exchange) {
139141
}
140142

141143
@SuppressWarnings("unchecked")
142-
private List<MediaType> getProducibleTypes(ServerWebExchange exchange, List<MediaType> mediaTypes) {
144+
private List<MediaType> getProducibleTypes(ServerWebExchange exchange,
145+
Supplier<List<MediaType>> producibleTypesSupplier) {
146+
143147
Optional<Object> optional = exchange.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
144148
if (optional.isPresent()) {
145149
Set<MediaType> set = (Set<MediaType>) optional.get();
146150
return new ArrayList<>(set);
147151
}
148-
return mediaTypes;
152+
return producibleTypesSupplier.get();
149153
}
150154

151155
private MediaType selectMoreSpecificMediaType(MediaType acceptable, MediaType producible) {

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

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -93,50 +93,51 @@ public List<HttpMessageWriter<?>> getMessageWriters() {
9393
@SuppressWarnings("unchecked")
9494
protected Mono<Void> writeBody(Object body, MethodParameter bodyParameter, ServerWebExchange exchange) {
9595

96-
ResolvableType bodyType = ResolvableType.forMethodParameter(bodyParameter);
97-
ReactiveAdapter adapter = getAdapterRegistry().getAdapterFrom(bodyType.resolve(), body);
96+
ResolvableType valueType = ResolvableType.forMethodParameter(bodyParameter);
97+
Class<?> valueClass = valueType.resolve();
98+
ReactiveAdapter adapter = getAdapterRegistry().getAdapterFrom(valueClass, body);
9899

99100
Publisher<?> publisher;
100101
ResolvableType elementType;
101102
if (adapter != null) {
102103
publisher = adapter.toPublisher(body);
103104
elementType = adapter.getDescriptor().isNoValue() ?
104105
ResolvableType.forClass(Void.class) :
105-
bodyType.getGeneric(0);
106+
valueType.getGeneric(0);
106107
}
107108
else {
108109
publisher = Mono.justOrEmpty(body);
109-
elementType = bodyType;
110+
elementType = (valueClass == null && body != null ? ResolvableType.forInstance(body) : valueType);
110111
}
111112

112113
if (void.class == elementType.getRawClass() || Void.class == elementType.getRawClass()) {
113114
return Mono.from((Publisher<Void>) publisher)
114115
.doOnSubscribe(sub -> updateResponseStatus(bodyParameter, exchange));
115116
}
116117

117-
List<MediaType> producibleTypes = getProducibleMediaTypes(elementType);
118-
if (producibleTypes.isEmpty()) {
119-
return Mono.error(new IllegalStateException(
120-
"No converter for return value type: " + elementType));
121-
}
122-
123118
ServerHttpRequest request = exchange.getRequest();
124119
ServerHttpResponse response = exchange.getResponse();
125-
MediaType bestMediaType = selectMediaType(exchange, producibleTypes);
120+
MediaType bestMediaType = selectMediaType(exchange, () -> getProducibleMediaTypes(elementType));
126121
if (bestMediaType != null) {
127122
for (HttpMessageWriter<?> messageWriter : getMessageWriters()) {
128123
if (messageWriter.canWrite(elementType, bestMediaType)) {
129124
Mono<Void> bodyWriter = (messageWriter instanceof ServerHttpMessageWriter ?
130125
((ServerHttpMessageWriter<?>) messageWriter).write((Publisher) publisher,
131-
bodyType, elementType, bestMediaType, request, response, Collections.emptyMap()) :
126+
valueType, elementType, bestMediaType, request, response, Collections.emptyMap()) :
132127
messageWriter.write((Publisher) publisher, elementType,
133128
bestMediaType, response, Collections.emptyMap()));
134129
return bodyWriter.doOnSubscribe(sub -> updateResponseStatus(bodyParameter, exchange));
135130
}
136131
}
137132
}
133+
else {
134+
if (getProducibleMediaTypes(elementType).isEmpty()) {
135+
return Mono.error(new IllegalStateException(
136+
"No converter for return value type: " + elementType));
137+
}
138+
}
138139

139-
return Mono.error(new NotAcceptableStatusException(producibleTypes));
140+
return Mono.error(new NotAcceptableStatusException(getProducibleMediaTypes(elementType)));
140141
}
141142

142143
private List<MediaType> getProducibleMediaTypes(ResolvableType elementType) {

spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ private Mono<? extends Void> resolveAndRender(String viewName, Locale locale,
319319
views.addAll(getDefaultViews());
320320

321321
List<MediaType> producibleTypes = getProducibleMediaTypes(views);
322-
MediaType bestMediaType = selectMediaType(exchange, producibleTypes);
322+
MediaType bestMediaType = selectMediaType(exchange, () -> producibleTypes);
323323

324324
if (bestMediaType != null) {
325325
for (View view : views) {

spring-web-reactive/src/test/java/org/springframework/web/reactive/result/HandlerResultHandlerTests.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public void setUp() throws Exception {
7171
public void usesContentTypeResolver() throws Exception {
7272
TestResultHandler resultHandler = new TestResultHandler(new FixedContentTypeResolver(IMAGE_GIF));
7373
List<MediaType> mediaTypes = Arrays.asList(IMAGE_JPEG, IMAGE_GIF, IMAGE_PNG);
74-
MediaType actual = resultHandler.selectMediaType(this.exchange, mediaTypes);
74+
MediaType actual = resultHandler.selectMediaType(this.exchange, () -> mediaTypes);
7575

7676
assertEquals(IMAGE_GIF, actual);
7777
}
@@ -82,7 +82,7 @@ public void producibleMediaTypesRequestAttribute() throws Exception {
8282
this.exchange.getAttributes().put(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, producible);
8383

8484
List<MediaType> mediaTypes = Arrays.asList(IMAGE_JPEG, IMAGE_GIF, IMAGE_PNG);
85-
MediaType actual = resultHandler.selectMediaType(this.exchange, mediaTypes);
85+
MediaType actual = resultHandler.selectMediaType(this.exchange, () -> mediaTypes);
8686

8787
assertEquals(IMAGE_GIF, actual);
8888
}
@@ -92,7 +92,7 @@ public void sortsByQuality() throws Exception {
9292
this.request.setHeader("Accept", "text/plain; q=0.5, application/json");
9393

9494
List<MediaType> mediaTypes = Arrays.asList(TEXT_PLAIN, APPLICATION_JSON_UTF8);
95-
MediaType actual = this.resultHandler.selectMediaType(this.exchange, mediaTypes);
95+
MediaType actual = this.resultHandler.selectMediaType(this.exchange, () -> mediaTypes);
9696

9797
assertEquals(APPLICATION_JSON_UTF8, actual);
9898
}
@@ -102,15 +102,16 @@ public void charsetFromAcceptHeader() throws Exception {
102102
MediaType text8859 = MediaType.parseMediaType("text/plain;charset=ISO-8859-1");
103103
MediaType textUtf8 = MediaType.parseMediaType("text/plain;charset=UTF-8");
104104
this.request.getHeaders().setAccept(Collections.singletonList(text8859));
105-
MediaType actual = this.resultHandler.selectMediaType(this.exchange, Collections.singletonList(textUtf8));
105+
MediaType actual = this.resultHandler.selectMediaType(this.exchange,
106+
() -> Collections.singletonList(textUtf8));
106107

107108
assertEquals(text8859, actual);
108109
}
109110

110111
@Test // SPR-12894
111112
public void noConcreteMediaType() throws Exception {
112113
List<MediaType> producible = Collections.singletonList(ALL);
113-
MediaType actual = this.resultHandler.selectMediaType(this.exchange, producible);
114+
MediaType actual = this.resultHandler.selectMediaType(this.exchange, () -> producible);
114115

115116
assertEquals(APPLICATION_OCTET_STREAM, actual);
116117
}

spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageWriterResultHandlerTests.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,16 @@
4343
import org.springframework.core.codec.CharSequenceEncoder;
4444
import org.springframework.core.io.ClassPathResource;
4545
import org.springframework.core.io.Resource;
46-
import org.springframework.core.io.buffer.DataBuffer;
4746
import org.springframework.core.io.buffer.support.DataBufferTestUtils;
4847
import org.springframework.http.HttpMethod;
4948
import org.springframework.http.codec.EncoderHttpMessageWriter;
5049
import org.springframework.http.codec.HttpMessageWriter;
5150
import org.springframework.http.codec.ResourceHttpMessageWriter;
5251
import org.springframework.http.codec.json.Jackson2JsonEncoder;
5352
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
53+
import org.springframework.http.server.reactive.ServerHttpRequest;
5454
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
5555
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
56-
import org.springframework.http.server.reactive.ServerHttpRequest;
5756
import org.springframework.util.ObjectUtils;
5857
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
5958
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
@@ -211,7 +210,7 @@ private static class ParentClass {
211210
public ParentClass() {
212211
}
213212

214-
public ParentClass(String parentProperty) {
213+
ParentClass(String parentProperty) {
215214
this.parentProperty = parentProperty;
216215
}
217216

@@ -235,7 +234,7 @@ public Foo(String parentProperty) {
235234
@JsonTypeName("bar")
236235
private static class Bar extends ParentClass {
237236

238-
public Bar(String parentProperty) {
237+
Bar(String parentProperty) {
239238
super(parentProperty);
240239
}
241240
}
@@ -253,7 +252,7 @@ private static class SimpleBean implements Identifiable {
253252

254253
private String name;
255254

256-
public SimpleBean(Long id, String name) {
255+
SimpleBean(Long id, String name) {
257256
this.id = id;
258257
this.name = name;
259258
}

spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.time.temporal.ChronoUnit;
2424
import java.util.ArrayList;
2525
import java.util.Arrays;
26+
import java.util.Collections;
2627
import java.util.List;
2728
import java.util.concurrent.CompletableFuture;
2829

@@ -37,11 +38,11 @@
3738
import org.springframework.core.ResolvableType;
3839
import org.springframework.core.codec.ByteBufferEncoder;
3940
import org.springframework.core.codec.CharSequenceEncoder;
40-
import org.springframework.core.io.buffer.DataBuffer;
4141
import org.springframework.core.io.buffer.support.DataBufferTestUtils;
4242
import org.springframework.http.HttpHeaders;
4343
import org.springframework.http.HttpMethod;
4444
import org.springframework.http.HttpStatus;
45+
import org.springframework.http.MediaType;
4546
import org.springframework.http.ResponseEntity;
4647
import org.springframework.http.codec.EncoderHttpMessageWriter;
4748
import org.springframework.http.codec.HttpMessageWriter;
@@ -51,6 +52,7 @@
5152
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
5253
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
5354
import org.springframework.util.ObjectUtils;
55+
import org.springframework.web.reactive.HandlerMapping;
5456
import org.springframework.web.reactive.HandlerResult;
5557
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
5658
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
@@ -270,6 +272,23 @@ public void handleReturnValueChangedETagAndLastModified() throws Exception {
270272
assertConditionalResponse(HttpStatus.OK, "body", newEtag, oneMinAgo);
271273
}
272274

275+
@Test // SPR-14877
276+
public void handleMonoWithWildcardBodyType() throws Exception {
277+
278+
this.exchange.getAttributes().put(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE,
279+
Collections.singleton(MediaType.APPLICATION_JSON));
280+
281+
HandlerResult result = new HandlerResult(new TestController(), Mono.just(ok().body("body")),
282+
ResolvableMethod.onClass(TestController.class)
283+
.name("monoResponseEntityWildcard")
284+
.resolveReturnType());
285+
286+
this.resultHandler.handleResult(this.exchange, result).block(Duration.ofSeconds(5));
287+
288+
assertEquals(HttpStatus.OK, this.response.getStatusCode());
289+
assertResponseBody("\"body\"");
290+
}
291+
273292

274293
private void testHandle(Object returnValue, ResolvableType type) {
275294
HandlerResult result = handlerResult(returnValue, type);
@@ -333,6 +352,9 @@ private static class TestController {
333352
String string() { return null; }
334353

335354
Completable completable() { return null; }
355+
356+
Mono<ResponseEntity<?>> monoResponseEntityWildcard() { return null; }
357+
336358
}
337359

338360
}

0 commit comments

Comments
 (0)