Consistent JSON array result for Flux<T> in Spring MVC
Issue: SPR-15456
This commit is contained in:
parent
cc102c2fcd
commit
d3b178a812
|
@ -27,6 +27,7 @@ import org.apache.commons.logging.LogFactory;
|
|||
import org.reactivestreams.Publisher;
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ReactiveAdapter;
|
||||
import org.springframework.core.ReactiveAdapterRegistry;
|
||||
|
@ -119,7 +120,6 @@ class ReactiveTypeHandler {
|
|||
|
||||
Collection<MediaType> mediaTypes = getMediaTypes(request);
|
||||
Optional<MediaType> mediaType = mediaTypes.stream().filter(MimeType::isConcrete).findFirst();
|
||||
boolean jsonArrayOfStrings = isJsonArrayOfStrings(elementType, mediaType);
|
||||
|
||||
if (adapter.isMultiValue()) {
|
||||
if (mediaTypes.stream().anyMatch(MediaType.TEXT_EVENT_STREAM::includes) ||
|
||||
|
@ -133,7 +133,7 @@ class ReactiveTypeHandler {
|
|||
new JsonEmitterSubscriber(emitter, this.taskExecutor).connect(adapter, returnValue);
|
||||
return emitter;
|
||||
}
|
||||
if (CharSequence.class.isAssignableFrom(elementType) && !jsonArrayOfStrings) {
|
||||
if (CharSequence.class.isAssignableFrom(elementType) && !isJsonStringArray(elementType, mediaType)) {
|
||||
ResponseBodyEmitter emitter = getEmitter(mediaType.orElse(MediaType.TEXT_PLAIN));
|
||||
new TextEmitterSubscriber(emitter, this.taskExecutor).connect(adapter, returnValue);
|
||||
return emitter;
|
||||
|
@ -142,7 +142,7 @@ class ReactiveTypeHandler {
|
|||
|
||||
// Not streaming...
|
||||
DeferredResult<Object> result = new DeferredResult<>();
|
||||
new DeferredResultSubscriber(result, jsonArrayOfStrings).connect(adapter, returnValue);
|
||||
new DeferredResultSubscriber(result, adapter).connect(adapter, returnValue);
|
||||
WebAsyncUtils.getAsyncManager(request).startDeferredResultProcessing(result, mav);
|
||||
|
||||
return null;
|
||||
|
@ -160,7 +160,7 @@ class ReactiveTypeHandler {
|
|||
}
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
private boolean isJsonArrayOfStrings(Class<?> elementType, Optional<MediaType> mediaType) {
|
||||
private boolean isJsonStringArray(Class<?> elementType, Optional<MediaType> mediaType) {
|
||||
return CharSequence.class.isAssignableFrom(elementType) && mediaType.filter(type ->
|
||||
MediaType.APPLICATION_JSON.includes(type) || JSON_TYPE.includes(type)).isPresent();
|
||||
}
|
||||
|
@ -387,14 +387,14 @@ class ReactiveTypeHandler {
|
|||
|
||||
private final DeferredResult<Object> result;
|
||||
|
||||
private final boolean jsonArrayOfStrings;
|
||||
private final boolean multiValueSource;
|
||||
|
||||
private final CollectedValuesList values = new CollectedValuesList();
|
||||
|
||||
|
||||
DeferredResultSubscriber(DeferredResult<Object> result, boolean jsonArrayOfStrings) {
|
||||
DeferredResultSubscriber(DeferredResult<Object> result, ReactiveAdapter adapter) {
|
||||
this.result = result;
|
||||
this.jsonArrayOfStrings = jsonArrayOfStrings;
|
||||
this.multiValueSource = adapter.isMultiValue();
|
||||
}
|
||||
|
||||
|
||||
|
@ -421,14 +421,14 @@ class ReactiveTypeHandler {
|
|||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
if (this.values.size() > 1) {
|
||||
if (this.values.size() > 1 || this.multiValueSource) {
|
||||
this.result.setResult(this.values);
|
||||
}
|
||||
else if (this.values.size() == 1) {
|
||||
this.result.setResult(this.values.get(0));
|
||||
}
|
||||
else {
|
||||
this.result.setResult(this.jsonArrayOfStrings ? this.values : null);
|
||||
this.result.setResult(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
package org.springframework.web.servlet.mvc.method.annotation;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
@ -53,6 +52,7 @@ import org.springframework.web.servlet.HandlerMapping;
|
|||
import static junit.framework.TestCase.assertNull;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.springframework.web.method.ResolvableMethod.on;
|
||||
|
||||
|
@ -127,15 +127,8 @@ public class ReactiveTypeHandlerTests {
|
|||
|
||||
@Test
|
||||
public void deferredResultSubscriberWithNoValues() throws Exception {
|
||||
|
||||
// Empty -> null
|
||||
MonoProcessor<String> monoEmpty = MonoProcessor.create();
|
||||
testDeferredResultSubscriber(monoEmpty, Mono.class, monoEmpty::onComplete, null);
|
||||
|
||||
// Empty -> List[0] when JSON is preferred
|
||||
this.servletRequest.addHeader("Accept", "application/json");
|
||||
MonoProcessor<String> monoEmpty2 = MonoProcessor.create();
|
||||
testDeferredResultSubscriber(monoEmpty2, Mono.class, monoEmpty2::onComplete, new ArrayList<>());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -177,23 +170,31 @@ public class ReactiveTypeHandlerTests {
|
|||
public void jsonArrayOfStrings() throws Exception {
|
||||
|
||||
// Empty -> null
|
||||
testJsonPreferred("text/plain", null);
|
||||
testJsonPreferred("text/plain, application/json", null);
|
||||
testJsonPreferred("text/markdown", null);
|
||||
testJsonPreferred("foo/bar", null);
|
||||
testJsonNotPreferred("text/plain");
|
||||
testJsonNotPreferred("text/plain, application/json");
|
||||
testJsonNotPreferred("text/markdown");
|
||||
testJsonNotPreferred("foo/bar");
|
||||
|
||||
// Empty -> List[0] when JSON is preferred
|
||||
testJsonPreferred("application/json", Collections.emptyList());
|
||||
testJsonPreferred("application/foo+json", Collections.emptyList());
|
||||
testJsonPreferred("application/json, text/plain", Collections.emptyList());
|
||||
testJsonPreferred("*/*, application/json, text/plain", Collections.emptyList());
|
||||
testJsonPreferred("application/json");
|
||||
testJsonPreferred("application/foo+json");
|
||||
testJsonPreferred("application/json, text/plain");
|
||||
testJsonPreferred("*/*, application/json, text/plain");
|
||||
}
|
||||
|
||||
private void testJsonPreferred(String acceptHeaderValue, Object expected) throws Exception {
|
||||
private void testJsonNotPreferred(String acceptHeaderValue) throws Exception {
|
||||
resetRequest();
|
||||
this.servletRequest.addHeader("Accept", acceptHeaderValue);
|
||||
MonoProcessor<String> mono = MonoProcessor.create();
|
||||
testDeferredResultSubscriber(mono, Mono.class, mono::onComplete, expected);
|
||||
EmitterProcessor<String> processor = EmitterProcessor.create();
|
||||
ResponseBodyEmitter emitter = handleValue(processor, Flux.class);
|
||||
assertNotNull(emitter);
|
||||
}
|
||||
|
||||
private void testJsonPreferred(String acceptHeaderValue) throws Exception {
|
||||
resetRequest();
|
||||
this.servletRequest.addHeader("Accept", acceptHeaderValue);
|
||||
EmitterProcessor<String> processor = EmitterProcessor.create();
|
||||
testDeferredResultSubscriber(processor, Flux.class, processor::onComplete, Collections.emptyList());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -212,8 +213,8 @@ public class ReactiveTypeHandlerTests {
|
|||
testSseResponse(false);
|
||||
|
||||
// Requested media types are sorted
|
||||
testJsonPreferred("text/plain;q=0.8, application/json;q=1.0", Collections.emptyList());
|
||||
testJsonPreferred("text/plain, application/json", null);
|
||||
testJsonPreferred("text/plain;q=0.8, application/json;q=1.0");
|
||||
testJsonNotPreferred("text/plain, application/json");
|
||||
}
|
||||
|
||||
private void testSseResponse(boolean expectSseEimtter) throws Exception {
|
||||
|
|
Loading…
Reference in New Issue