Spring MVC supports reactive return values
This commit adds support for reactive library types to be returned directly from controller methods adapting them either to a ResponseBodyEmitter (streaming) or DeferredResult (non-streaming). The reactive libraries supported are the ones that can adapted to a Reactive Streams Publisher through the ReactiveAdapterRegistry. Issue: SPR-15365
This commit is contained in:
parent
ae1ed16cb8
commit
62c1e44db2
|
|
@ -914,6 +914,7 @@ project("spring-webmvc") {
|
||||||
exclude group: "org.springframework", module: "spring-web"
|
exclude group: "org.springframework", module: "spring-web"
|
||||||
}
|
}
|
||||||
optional('org.webjars:webjars-locator:0.32-1')
|
optional('org.webjars:webjars-locator:0.32-1')
|
||||||
|
optional("org.reactivestreams:reactive-streams")
|
||||||
testCompile("org.xmlunit:xmlunit-matchers:${xmlunitVersion}")
|
testCompile("org.xmlunit:xmlunit-matchers:${xmlunitVersion}")
|
||||||
testCompile("dom4j:dom4j:1.6.1") {
|
testCompile("dom4j:dom4j:1.6.1") {
|
||||||
exclude group: "xml-apis", module: "xml-apis"
|
exclude group: "xml-apis", module: "xml-apis"
|
||||||
|
|
@ -937,6 +938,10 @@ project("spring-webmvc") {
|
||||||
testCompile("joda-time:joda-time:${jodaVersion}")
|
testCompile("joda-time:joda-time:${jodaVersion}")
|
||||||
testCompile("org.slf4j:slf4j-jcl:${slf4jVersion}")
|
testCompile("org.slf4j:slf4j-jcl:${slf4jVersion}")
|
||||||
testCompile("org.mozilla:rhino:1.7.7.1")
|
testCompile("org.mozilla:rhino:1.7.7.1")
|
||||||
|
testCompile("io.projectreactor:reactor-core")
|
||||||
|
testCompile("io.reactivex:rxjava:${rxjavaVersion}")
|
||||||
|
testCompile("io.reactivex.rxjava2:rxjava:${rxjava2Version}")
|
||||||
|
testCompile("io.reactivex:rxjava-reactive-streams:${rxjavaAdapterVersion}")
|
||||||
testRuntime("org.jruby:jruby:9.1.8.0")
|
testRuntime("org.jruby:jruby:9.1.8.0")
|
||||||
testRuntime("org.python:jython-standalone:2.5.3")
|
testRuntime("org.python:jython-standalone:2.5.3")
|
||||||
testRuntime("org.jetbrains.kotlin:kotlin-compiler:${kotlinVersion}")
|
testRuntime("org.jetbrains.kotlin:kotlin-compiler:${kotlinVersion}")
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,16 @@ public class ReactiveAdapterRegistry {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the registry has any adapters which would be the case if any of
|
||||||
|
* Reactor, RxJava 2, or RxJava 1 (+ RxJava Reactive Streams bridge) are
|
||||||
|
* present on the classpath.
|
||||||
|
*/
|
||||||
|
public boolean hasAdapters() {
|
||||||
|
return !this.adapters.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a reactive type along with functions to adapt to and from a
|
* Register a reactive type along with functions to adapt to and from a
|
||||||
* Reactive Streams {@link Publisher}. The functions can assume their
|
* Reactive Streams {@link Publisher}. The functions can assume their
|
||||||
|
|
@ -113,6 +123,10 @@ public class ReactiveAdapterRegistry {
|
||||||
Object sourceToUse = (source instanceof Optional ? ((Optional<?>) source).orElse(null) : source);
|
Object sourceToUse = (source instanceof Optional ? ((Optional<?>) source).orElse(null) : source);
|
||||||
Class<?> clazz = (sourceToUse != null ? sourceToUse.getClass() : reactiveType);
|
Class<?> clazz = (sourceToUse != null ? sourceToUse.getClass() : reactiveType);
|
||||||
|
|
||||||
|
if (reactiveType == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return this.adapters.stream()
|
return this.adapters.stream()
|
||||||
.filter(adapter -> adapter.getReactiveType() == clazz)
|
.filter(adapter -> adapter.getReactiveType() == clazz)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,325 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2017 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.web.servlet.mvc.method.annotation;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
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;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.codec.ServerSentEvent;
|
||||||
|
import org.springframework.http.server.ServerHttpResponse;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.util.MimeType;
|
||||||
|
import org.springframework.web.HttpMediaTypeNotAcceptableException;
|
||||||
|
import org.springframework.web.accept.ContentNegotiationManager;
|
||||||
|
import org.springframework.web.context.request.NativeWebRequest;
|
||||||
|
import org.springframework.web.context.request.RequestAttributes;
|
||||||
|
import org.springframework.web.context.request.async.DeferredResult;
|
||||||
|
import org.springframework.web.context.request.async.WebAsyncUtils;
|
||||||
|
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||||
|
import org.springframework.web.servlet.HandlerMapping;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private helper class to assist with handling "reactive" return values types
|
||||||
|
* that can be adapted to a Reactive Streams {@link Publisher} through the
|
||||||
|
* {@link ReactiveAdapterRegistry}.
|
||||||
|
*
|
||||||
|
* <p>Such return values may be bridged to a {@link ResponseBodyEmitter} for
|
||||||
|
* streaming purposes at the presence of a streaming media type or based on the
|
||||||
|
* generic type.
|
||||||
|
*
|
||||||
|
* <p>For all other cases {@code Publisher} output is collected and bridged to
|
||||||
|
* {@link DeferredResult} for standard async request processing.
|
||||||
|
*
|
||||||
|
* @author Rossen Stoyanchev
|
||||||
|
* @since 5.0
|
||||||
|
*/
|
||||||
|
class ReactiveTypeHandler {
|
||||||
|
|
||||||
|
private static final MediaType JSON_TYPE = new MediaType("application", "*+json");
|
||||||
|
|
||||||
|
|
||||||
|
private final ReactiveAdapterRegistry reactiveRegistry;
|
||||||
|
|
||||||
|
private final ContentNegotiationManager contentNegotiationManager;
|
||||||
|
|
||||||
|
|
||||||
|
ReactiveTypeHandler(ReactiveAdapterRegistry registry, ContentNegotiationManager manager) {
|
||||||
|
Assert.notNull(registry, "ReactiveAdapterRegistry is required");
|
||||||
|
Assert.notNull(manager, "ContentNegotiationManager is required");
|
||||||
|
this.reactiveRegistry = registry;
|
||||||
|
this.contentNegotiationManager = manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the type can be adapted to a Reactive Streams {@link Publisher}.
|
||||||
|
*/
|
||||||
|
public boolean isReactiveType(Class<?> type) {
|
||||||
|
return this.reactiveRegistry.hasAdapters() && this.reactiveRegistry.getAdapter(type) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the given reactive return value and decide whether to adapt it
|
||||||
|
* to a {@link ResponseBodyEmitter} or a {@link DeferredResult}.
|
||||||
|
*
|
||||||
|
* @return an emitter for streaming or {@code null} if handled internally
|
||||||
|
* with a {@link DeferredResult}.
|
||||||
|
*/
|
||||||
|
public ResponseBodyEmitter handleValue(Object returnValue, MethodParameter returnType,
|
||||||
|
ModelAndViewContainer mav, NativeWebRequest request) throws Exception {
|
||||||
|
|
||||||
|
Assert.notNull(returnValue, "Expected return value");
|
||||||
|
ReactiveAdapter adapter = this.reactiveRegistry.getAdapter(returnValue.getClass());
|
||||||
|
Assert.state(adapter != null, "Unexpected return value: " + returnValue);
|
||||||
|
|
||||||
|
Class<?> elementType = returnType.nested().getNestedParameterType();
|
||||||
|
|
||||||
|
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) ||
|
||||||
|
ServerSentEvent.class.isAssignableFrom(elementType)) {
|
||||||
|
SseEmitter emitter = new SseEmitter();
|
||||||
|
new SseEmitterSubscriber(emitter).connect(adapter, returnValue);
|
||||||
|
return emitter;
|
||||||
|
}
|
||||||
|
if (mediaTypes.stream().anyMatch(MediaType.APPLICATION_STREAM_JSON::includes)) {
|
||||||
|
ResponseBodyEmitter emitter = getEmitter(MediaType.APPLICATION_STREAM_JSON);
|
||||||
|
new JsonEmitterSubscriber(emitter).connect(adapter, returnValue);
|
||||||
|
return emitter;
|
||||||
|
}
|
||||||
|
if (CharSequence.class.isAssignableFrom(elementType) && !jsonArrayOfStrings) {
|
||||||
|
ResponseBodyEmitter emitter = getEmitter(mediaType.orElse(MediaType.TEXT_PLAIN));
|
||||||
|
new TextEmitterSubscriber(emitter).connect(adapter, returnValue);
|
||||||
|
return emitter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not streaming...
|
||||||
|
DeferredResult<Object> result = new DeferredResult<>();
|
||||||
|
new DeferredResultSubscriber(result, jsonArrayOfStrings).connect(adapter, returnValue);
|
||||||
|
WebAsyncUtils.getAsyncManager(request).startDeferredResultProcessing(result, mav);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private Collection<MediaType> getMediaTypes(NativeWebRequest request)
|
||||||
|
throws HttpMediaTypeNotAcceptableException {
|
||||||
|
|
||||||
|
Collection<MediaType> mediaTypes = (Collection<MediaType>) request.getAttribute(
|
||||||
|
HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
|
||||||
|
|
||||||
|
return CollectionUtils.isEmpty(mediaTypes) ?
|
||||||
|
this.contentNegotiationManager.resolveMediaTypes(request) : mediaTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||||
|
private boolean isJsonArrayOfStrings(Class<?> elementType, Optional<MediaType> mediaType) {
|
||||||
|
return CharSequence.class.isAssignableFrom(elementType) && mediaType.filter(type ->
|
||||||
|
MediaType.APPLICATION_JSON.includes(type) || JSON_TYPE.includes(type)).isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResponseBodyEmitter getEmitter(MediaType mediaType) {
|
||||||
|
return new ResponseBodyEmitter() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void extendResponse(ServerHttpResponse outputMessage) {
|
||||||
|
outputMessage.getHeaders().setContentType(mediaType);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static abstract class AbstractEmitterSubscriber implements Subscriber<Object> {
|
||||||
|
|
||||||
|
private final ResponseBodyEmitter emitter;
|
||||||
|
|
||||||
|
private Subscription subscription;
|
||||||
|
|
||||||
|
|
||||||
|
protected AbstractEmitterSubscriber(ResponseBodyEmitter emitter) {
|
||||||
|
this.emitter = emitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void connect(ReactiveAdapter adapter, Object returnValue) {
|
||||||
|
Publisher<Object> publisher = adapter.toPublisher(returnValue);
|
||||||
|
publisher.subscribe(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected ResponseBodyEmitter getEmitter() {
|
||||||
|
return this.emitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSubscribe(Subscription subscription) {
|
||||||
|
this.subscription = subscription;
|
||||||
|
this.emitter.onTimeout(subscription::cancel);
|
||||||
|
subscription.request(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNext(Object element) {
|
||||||
|
try {
|
||||||
|
send(element);
|
||||||
|
this.subscription.request(1);
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
this.subscription.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void send(Object element) throws IOException;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(Throwable ex) {
|
||||||
|
this.emitter.completeWithError(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onComplete() {
|
||||||
|
this.emitter.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static class SseEmitterSubscriber extends AbstractEmitterSubscriber {
|
||||||
|
|
||||||
|
SseEmitterSubscriber(SseEmitter sseEmitter) {
|
||||||
|
super(sseEmitter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void send(Object element) throws IOException {
|
||||||
|
if (element instanceof ServerSentEvent) {
|
||||||
|
ServerSentEvent<?> event = (ServerSentEvent<?>) element;
|
||||||
|
((SseEmitter) getEmitter()).send(adapt(event));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
getEmitter().send(element, MediaType.APPLICATION_JSON);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SseEmitter.SseEventBuilder adapt(ServerSentEvent<?> event) {
|
||||||
|
SseEmitter.SseEventBuilder builder = SseEmitter.event();
|
||||||
|
event.id().ifPresent(builder::id);
|
||||||
|
event.comment().ifPresent(builder::comment);
|
||||||
|
event.data().ifPresent(builder::data);
|
||||||
|
event.retry().ifPresent(duration -> builder.reconnectTime(duration.toMillis()));
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static class JsonEmitterSubscriber extends AbstractEmitterSubscriber {
|
||||||
|
|
||||||
|
JsonEmitterSubscriber(ResponseBodyEmitter emitter) {
|
||||||
|
super(emitter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void send(Object element) throws IOException {
|
||||||
|
getEmitter().send(element, MediaType.APPLICATION_JSON);
|
||||||
|
getEmitter().send("\n", MediaType.TEXT_PLAIN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static class TextEmitterSubscriber extends AbstractEmitterSubscriber {
|
||||||
|
|
||||||
|
TextEmitterSubscriber(ResponseBodyEmitter emitter) {
|
||||||
|
super(emitter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void send(Object element) throws IOException {
|
||||||
|
getEmitter().send(element, MediaType.TEXT_PLAIN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static class DeferredResultSubscriber implements Subscriber<Object> {
|
||||||
|
|
||||||
|
private final DeferredResult<Object> result;
|
||||||
|
|
||||||
|
private final boolean jsonArrayOfStrings;
|
||||||
|
|
||||||
|
private final CollectedValuesList values = new CollectedValuesList();
|
||||||
|
|
||||||
|
|
||||||
|
DeferredResultSubscriber(DeferredResult<Object> result, boolean jsonArrayOfStrings) {
|
||||||
|
this.result = result;
|
||||||
|
this.jsonArrayOfStrings = jsonArrayOfStrings;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void connect(ReactiveAdapter adapter, Object returnValue) {
|
||||||
|
Publisher<Object> publisher = adapter.toPublisher(returnValue);
|
||||||
|
publisher.subscribe(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSubscribe(Subscription subscription) {
|
||||||
|
this.result.onTimeout(subscription::cancel);
|
||||||
|
subscription.request(Long.MAX_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNext(Object element) {
|
||||||
|
this.values.add(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(Throwable ex) {
|
||||||
|
this.result.setErrorResult(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onComplete() {
|
||||||
|
if (this.values.size() > 1) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
static class CollectedValuesList extends ArrayList<Object> {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -36,6 +36,7 @@ import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||||
import org.springframework.core.MethodIntrospector;
|
import org.springframework.core.MethodIntrospector;
|
||||||
import org.springframework.core.ParameterNameDiscoverer;
|
import org.springframework.core.ParameterNameDiscoverer;
|
||||||
|
import org.springframework.core.ReactiveAdapterRegistry;
|
||||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||||
import org.springframework.core.annotation.AnnotationUtils;
|
import org.springframework.core.annotation.AnnotationUtils;
|
||||||
import org.springframework.core.task.AsyncTaskExecutor;
|
import org.springframework.core.task.AsyncTaskExecutor;
|
||||||
|
|
@ -143,6 +144,8 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
|
||||||
|
|
||||||
private DeferredResultProcessingInterceptor[] deferredResultInterceptors = new DeferredResultProcessingInterceptor[0];
|
private DeferredResultProcessingInterceptor[] deferredResultInterceptors = new DeferredResultProcessingInterceptor[0];
|
||||||
|
|
||||||
|
private ReactiveAdapterRegistry reactiveRegistry = new ReactiveAdapterRegistry();
|
||||||
|
|
||||||
private boolean ignoreDefaultModelOnRedirect = false;
|
private boolean ignoreDefaultModelOnRedirect = false;
|
||||||
|
|
||||||
private int cacheSecondsForSessionAttributeHandlers = 0;
|
private int cacheSecondsForSessionAttributeHandlers = 0;
|
||||||
|
|
@ -408,6 +411,22 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
|
||||||
this.deferredResultInterceptors = interceptors.toArray(new DeferredResultProcessingInterceptor[interceptors.size()]);
|
this.deferredResultInterceptors = interceptors.toArray(new DeferredResultProcessingInterceptor[interceptors.size()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the registry for reactive library types to be supported as
|
||||||
|
* return values from controller methods.
|
||||||
|
*/
|
||||||
|
public void setReactiveRegistry(ReactiveAdapterRegistry reactiveRegistry) {
|
||||||
|
Assert.notNull(reactiveRegistry, "ReactiveAdapterRegistry is required");
|
||||||
|
this.reactiveRegistry = this.reactiveRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the configured reactive type registry of adapters.
|
||||||
|
*/
|
||||||
|
public ReactiveAdapterRegistry getReactiveAdapterRegistry() {
|
||||||
|
return this.reactiveRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* By default the content of the "default" model is used both during
|
* By default the content of the "default" model is used both during
|
||||||
* rendering and redirect scenarios. Alternatively a controller method
|
* rendering and redirect scenarios. Alternatively a controller method
|
||||||
|
|
@ -663,7 +682,8 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
|
||||||
handlers.add(new ModelAndViewMethodReturnValueHandler());
|
handlers.add(new ModelAndViewMethodReturnValueHandler());
|
||||||
handlers.add(new ModelMethodProcessor());
|
handlers.add(new ModelMethodProcessor());
|
||||||
handlers.add(new ViewMethodReturnValueHandler());
|
handlers.add(new ViewMethodReturnValueHandler());
|
||||||
handlers.add(new ResponseBodyEmitterReturnValueHandler(getMessageConverters()));
|
handlers.add(new ResponseBodyEmitterReturnValueHandler(getMessageConverters(),
|
||||||
|
this.reactiveRegistry, this.contentNegotiationManager));
|
||||||
handlers.add(new StreamingResponseBodyReturnValueHandler());
|
handlers.add(new StreamingResponseBodyReturnValueHandler());
|
||||||
handlers.add(new HttpEntityMethodProcessor(getMessageConverters(),
|
handlers.add(new HttpEntityMethodProcessor(getMessageConverters(),
|
||||||
this.contentNegotiationManager, this.requestResponseBodyAdvice));
|
this.contentNegotiationManager, this.requestResponseBodyAdvice));
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import org.apache.commons.logging.Log;
|
||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
|
||||||
import org.springframework.core.MethodParameter;
|
import org.springframework.core.MethodParameter;
|
||||||
|
import org.springframework.core.ReactiveAdapterRegistry;
|
||||||
import org.springframework.core.ResolvableType;
|
import org.springframework.core.ResolvableType;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
|
@ -35,6 +36,7 @@ import org.springframework.http.converter.HttpMessageConverter;
|
||||||
import org.springframework.http.server.ServerHttpResponse;
|
import org.springframework.http.server.ServerHttpResponse;
|
||||||
import org.springframework.http.server.ServletServerHttpResponse;
|
import org.springframework.http.server.ServletServerHttpResponse;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.web.accept.ContentNegotiationManager;
|
||||||
import org.springframework.web.context.request.NativeWebRequest;
|
import org.springframework.web.context.request.NativeWebRequest;
|
||||||
import org.springframework.web.context.request.async.DeferredResult;
|
import org.springframework.web.context.request.async.DeferredResult;
|
||||||
import org.springframework.web.context.request.async.WebAsyncUtils;
|
import org.springframework.web.context.request.async.WebAsyncUtils;
|
||||||
|
|
@ -57,10 +59,15 @@ public class ResponseBodyEmitterReturnValueHandler implements HandlerMethodRetur
|
||||||
|
|
||||||
private final List<HttpMessageConverter<?>> messageConverters;
|
private final List<HttpMessageConverter<?>> messageConverters;
|
||||||
|
|
||||||
|
private final ReactiveTypeHandler reactiveHandler;
|
||||||
|
|
||||||
|
|
||||||
|
public ResponseBodyEmitterReturnValueHandler(List<HttpMessageConverter<?>> messageConverters,
|
||||||
|
ReactiveAdapterRegistry reactiveRegistry, ContentNegotiationManager manager) {
|
||||||
|
|
||||||
public ResponseBodyEmitterReturnValueHandler(List<HttpMessageConverter<?>> messageConverters) {
|
|
||||||
Assert.notEmpty(messageConverters, "HttpMessageConverter List must not be empty");
|
Assert.notEmpty(messageConverters, "HttpMessageConverter List must not be empty");
|
||||||
this.messageConverters = messageConverters;
|
this.messageConverters = messageConverters;
|
||||||
|
this.reactiveHandler = new ReactiveTypeHandler(reactiveRegistry, manager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -71,11 +78,8 @@ public class ResponseBodyEmitterReturnValueHandler implements HandlerMethodRetur
|
||||||
ResolvableType.forMethodParameter(returnType).getGeneric(0).resolve() :
|
ResolvableType.forMethodParameter(returnType).getGeneric(0).resolve() :
|
||||||
returnType.getParameterType();
|
returnType.getParameterType();
|
||||||
|
|
||||||
return bodyType != null && supportsBodyType(bodyType);
|
return bodyType != null && (ResponseBodyEmitter.class.isAssignableFrom(bodyType) ||
|
||||||
}
|
this.reactiveHandler.isReactiveType(bodyType));
|
||||||
|
|
||||||
private boolean supportsBodyType(Class<?> bodyType) {
|
|
||||||
return ResponseBodyEmitter.class.isAssignableFrom(bodyType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -111,7 +115,11 @@ public class ResponseBodyEmitterReturnValueHandler implements HandlerMethodRetur
|
||||||
emitter = (ResponseBodyEmitter) returnValue;
|
emitter = (ResponseBodyEmitter) returnValue;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new IllegalStateException("Unexpected return value type: " + returnValue);
|
emitter = this.reactiveHandler.handleValue(returnValue, returnType, mavContainer, webRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emitter == null) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
emitter.extendResponse(outputMessage);
|
emitter.extendResponse(outputMessage);
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import java.io.IOException;
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
import org.springframework.core.MethodParameter;
|
import org.springframework.core.MethodParameter;
|
||||||
|
|
@ -270,7 +271,13 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
|
||||||
public ConcurrentResultMethodParameter(Object returnValue) {
|
public ConcurrentResultMethodParameter(Object returnValue) {
|
||||||
super(-1);
|
super(-1);
|
||||||
this.returnValue = returnValue;
|
this.returnValue = returnValue;
|
||||||
this.returnType = ResolvableType.forType(super.getGenericParameterType()).getGeneric(0);
|
|
||||||
|
ResolvableType candidateReturnType =
|
||||||
|
ResolvableType.forType(super.getGenericParameterType()).getGeneric(0);
|
||||||
|
|
||||||
|
this.returnType = returnValue instanceof ReactiveTypeHandler.CollectedValuesList ?
|
||||||
|
ResolvableType.forClassWithGenerics(List.class, candidateReturnType) :
|
||||||
|
candidateReturnType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ConcurrentResultMethodParameter(ConcurrentResultMethodParameter original) {
|
public ConcurrentResultMethodParameter(ConcurrentResultMethodParameter original) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,412 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2017 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.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;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import reactor.core.publisher.EmitterProcessor;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.core.publisher.MonoProcessor;
|
||||||
|
import rx.Single;
|
||||||
|
import rx.SingleEmitter;
|
||||||
|
|
||||||
|
import org.springframework.core.MethodParameter;
|
||||||
|
import org.springframework.core.ReactiveAdapterRegistry;
|
||||||
|
import org.springframework.core.ResolvableType;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.codec.ServerSentEvent;
|
||||||
|
import org.springframework.http.server.ServletServerHttpResponse;
|
||||||
|
import org.springframework.mock.web.test.MockHttpServletRequest;
|
||||||
|
import org.springframework.mock.web.test.MockHttpServletResponse;
|
||||||
|
import org.springframework.web.accept.ContentNegotiationManager;
|
||||||
|
import org.springframework.web.accept.ContentNegotiationManagerFactoryBean;
|
||||||
|
import org.springframework.web.context.request.NativeWebRequest;
|
||||||
|
import org.springframework.web.context.request.ServletWebRequest;
|
||||||
|
import org.springframework.web.context.request.async.AsyncWebRequest;
|
||||||
|
import org.springframework.web.context.request.async.StandardServletAsyncWebRequest;
|
||||||
|
import org.springframework.web.context.request.async.WebAsyncUtils;
|
||||||
|
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||||
|
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.assertTrue;
|
||||||
|
import static org.springframework.web.method.ResolvableMethod.on;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for {@link ReactiveTypeHandler}.
|
||||||
|
* @author Rossen Stoyanchev
|
||||||
|
*/
|
||||||
|
public class ReactiveTypeHandlerTests {
|
||||||
|
|
||||||
|
private ReactiveTypeHandler handler;
|
||||||
|
|
||||||
|
private MockHttpServletRequest servletRequest;
|
||||||
|
|
||||||
|
private MockHttpServletResponse servletResponse;
|
||||||
|
|
||||||
|
private NativeWebRequest webRequest;
|
||||||
|
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() throws Exception {
|
||||||
|
ContentNegotiationManagerFactoryBean factoryBean = new ContentNegotiationManagerFactoryBean();
|
||||||
|
factoryBean.afterPropertiesSet();
|
||||||
|
ContentNegotiationManager manager = factoryBean.getObject();
|
||||||
|
this.handler = new ReactiveTypeHandler(new ReactiveAdapterRegistry(), manager);
|
||||||
|
resetRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetRequest() {
|
||||||
|
this.servletRequest = new MockHttpServletRequest();
|
||||||
|
this.servletResponse = new MockHttpServletResponse();
|
||||||
|
this.webRequest = new ServletWebRequest(this.servletRequest, this.servletResponse);
|
||||||
|
|
||||||
|
AsyncWebRequest asyncWebRequest = new StandardServletAsyncWebRequest(this.servletRequest, this.servletResponse);
|
||||||
|
WebAsyncUtils.getAsyncManager(this.webRequest).setAsyncWebRequest(asyncWebRequest);
|
||||||
|
this.servletRequest.setAsyncSupported(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void supportsType() throws Exception {
|
||||||
|
assertTrue(this.handler.isReactiveType(Mono.class));
|
||||||
|
assertTrue(this.handler.isReactiveType(Single.class));
|
||||||
|
assertTrue(this.handler.isReactiveType(io.reactivex.Single.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void doesNotSupportType() throws Exception {
|
||||||
|
assertFalse(this.handler.isReactiveType(String.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void deferredResultSubscriberWithOneValue() throws Exception {
|
||||||
|
|
||||||
|
// Mono
|
||||||
|
MonoProcessor<String> mono = MonoProcessor.create();
|
||||||
|
testDeferredResultSubscriber(mono, Mono.class, () -> mono.onNext("foo"), "foo");
|
||||||
|
|
||||||
|
// Mono empty
|
||||||
|
MonoProcessor<String> monoEmpty = MonoProcessor.create();
|
||||||
|
testDeferredResultSubscriber(monoEmpty, Mono.class, monoEmpty::onComplete, null);
|
||||||
|
|
||||||
|
// RxJava 1 Single
|
||||||
|
AtomicReference<SingleEmitter<String>> ref = new AtomicReference<>();
|
||||||
|
Single<String> single = Single.fromEmitter(ref::set);
|
||||||
|
testDeferredResultSubscriber(single, Single.class, () -> ref.get().onSuccess("foo"), "foo");
|
||||||
|
|
||||||
|
// RxJava 2 Single
|
||||||
|
AtomicReference<io.reactivex.SingleEmitter<String>> ref2 = new AtomicReference<>();
|
||||||
|
io.reactivex.Single<String> single2 = io.reactivex.Single.create(ref2::set);
|
||||||
|
testDeferredResultSubscriber(single2, io.reactivex.Single.class, () -> ref2.get().onSuccess("foo"), "foo");
|
||||||
|
}
|
||||||
|
|
||||||
|
@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
|
||||||
|
public void deferredResultSubscriberWithMultipleValues() throws Exception {
|
||||||
|
|
||||||
|
// JSON must be preferred for Flux<String> -> List<String> or else we stream
|
||||||
|
this.servletRequest.addHeader("Accept", "application/json");
|
||||||
|
|
||||||
|
EmitterProcessor<String> emitter = EmitterProcessor.create();
|
||||||
|
testDeferredResultSubscriber(emitter, Flux.class, () -> {
|
||||||
|
emitter.connect();
|
||||||
|
emitter.onNext("foo");
|
||||||
|
emitter.onNext("bar");
|
||||||
|
emitter.onNext("baz");
|
||||||
|
emitter.onComplete();
|
||||||
|
}, Arrays.asList("foo", "bar", "baz"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void deferredResultSubscriberWithError() throws Exception {
|
||||||
|
|
||||||
|
IllegalStateException ex = new IllegalStateException();
|
||||||
|
|
||||||
|
// Mono
|
||||||
|
MonoProcessor<String> mono = MonoProcessor.create();
|
||||||
|
testDeferredResultSubscriber(mono, Mono.class, () -> mono.onError(ex), ex);
|
||||||
|
|
||||||
|
// RxJava 1 Single
|
||||||
|
AtomicReference<SingleEmitter<String>> ref = new AtomicReference<>();
|
||||||
|
Single<String> single = Single.fromEmitter(ref::set);
|
||||||
|
testDeferredResultSubscriber(single, Single.class, () -> ref.get().onError(ex), ex);
|
||||||
|
|
||||||
|
// RxJava 2 Single
|
||||||
|
AtomicReference<io.reactivex.SingleEmitter<String>> ref2 = new AtomicReference<>();
|
||||||
|
io.reactivex.Single<String> single2 = io.reactivex.Single.create(ref2::set);
|
||||||
|
testDeferredResultSubscriber(single2, io.reactivex.Single.class, () -> ref2.get().onError(ex), ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testJsonPreferred(String acceptHeaderValue, Object expected) throws Exception {
|
||||||
|
resetRequest();
|
||||||
|
this.servletRequest.addHeader("Accept", acceptHeaderValue);
|
||||||
|
MonoProcessor<String> mono = MonoProcessor.create();
|
||||||
|
testDeferredResultSubscriber(mono, Mono.class, mono::onComplete, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mediaTypes() throws Exception {
|
||||||
|
|
||||||
|
// Media type from request
|
||||||
|
this.servletRequest.addHeader("Accept", "text/event-stream");
|
||||||
|
testSseResponse(true);
|
||||||
|
|
||||||
|
// Media type from "produces" attribute
|
||||||
|
Set<MediaType> types = Collections.singleton(MediaType.TEXT_EVENT_STREAM);
|
||||||
|
this.servletRequest.setAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, types);
|
||||||
|
testSseResponse(true);
|
||||||
|
|
||||||
|
// No media type preferences
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testSseResponse(boolean expectSseEimtter) throws Exception {
|
||||||
|
ResponseBodyEmitter emitter = handleValue(Flux.empty(), Flux.class);
|
||||||
|
assertEquals(expectSseEimtter, emitter instanceof SseEmitter);
|
||||||
|
resetRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void writeServerSentEvents() throws Exception {
|
||||||
|
|
||||||
|
this.servletRequest.addHeader("Accept", "text/event-stream");
|
||||||
|
EmitterProcessor<String> processor = EmitterProcessor.create();
|
||||||
|
SseEmitter sseEmitter = (SseEmitter) handleValue(processor, Flux.class);
|
||||||
|
|
||||||
|
EmitterHandler emitterHandler = new EmitterHandler();
|
||||||
|
sseEmitter.initialize(emitterHandler);
|
||||||
|
|
||||||
|
processor.connect();
|
||||||
|
processor.onNext("foo");
|
||||||
|
processor.onNext("bar");
|
||||||
|
processor.onNext("baz");
|
||||||
|
processor.onComplete();
|
||||||
|
|
||||||
|
assertEquals("data:foo\n\ndata:bar\n\ndata:baz\n\n", emitterHandler.getOutput());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void writeSentEventsWithBuilder() throws Exception {
|
||||||
|
|
||||||
|
ResolvableType type = ResolvableType.forClassWithGenerics(ServerSentEvent.class, String.class);
|
||||||
|
|
||||||
|
EmitterProcessor<ServerSentEvent<?>> processor = EmitterProcessor.create();
|
||||||
|
SseEmitter sseEmitter = (SseEmitter) handleValue(processor, Flux.class, type);
|
||||||
|
|
||||||
|
EmitterHandler emitterHandler = new EmitterHandler();
|
||||||
|
sseEmitter.initialize(emitterHandler);
|
||||||
|
|
||||||
|
processor.connect();
|
||||||
|
processor.onNext(ServerSentEvent.builder("foo").id("1").build());
|
||||||
|
processor.onNext(ServerSentEvent.builder("bar").id("2").build());
|
||||||
|
processor.onNext(ServerSentEvent.builder("baz").id("3").build());
|
||||||
|
processor.onComplete();
|
||||||
|
|
||||||
|
assertEquals("id:1\ndata:foo\n\nid:2\ndata:bar\n\nid:3\ndata:baz\n\n",
|
||||||
|
emitterHandler.getOutput());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void writeStreamJson() throws Exception {
|
||||||
|
|
||||||
|
this.servletRequest.addHeader("Accept", "application/stream+json");
|
||||||
|
|
||||||
|
EmitterProcessor<String> processor = EmitterProcessor.create();
|
||||||
|
ResponseBodyEmitter emitter = handleValue(processor, Flux.class);
|
||||||
|
|
||||||
|
EmitterHandler emitterHandler = new EmitterHandler();
|
||||||
|
emitter.initialize(emitterHandler);
|
||||||
|
|
||||||
|
ServletServerHttpResponse message = new ServletServerHttpResponse(this.servletResponse);
|
||||||
|
emitter.extendResponse(message);
|
||||||
|
|
||||||
|
processor.connect();
|
||||||
|
processor.onNext("[\"foo\",\"bar\"]");
|
||||||
|
processor.onNext("[\"bar\",\"baz\"]");
|
||||||
|
processor.onComplete();
|
||||||
|
|
||||||
|
assertEquals("application/stream+json", message.getHeaders().getContentType().toString());
|
||||||
|
assertEquals("[\"foo\",\"bar\"]\n[\"bar\",\"baz\"]\n", emitterHandler.getOutput());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void writeText() throws Exception {
|
||||||
|
|
||||||
|
EmitterProcessor<String> processor = EmitterProcessor.create();
|
||||||
|
ResponseBodyEmitter emitter = handleValue(processor, Flux.class);
|
||||||
|
|
||||||
|
EmitterHandler emitterHandler = new EmitterHandler();
|
||||||
|
emitter.initialize(emitterHandler);
|
||||||
|
|
||||||
|
processor.connect();
|
||||||
|
processor.onNext("The quick");
|
||||||
|
processor.onNext(" brown fox jumps over ");
|
||||||
|
processor.onNext("the lazy dog");
|
||||||
|
processor.onComplete();
|
||||||
|
|
||||||
|
assertEquals("The quick brown fox jumps over the lazy dog", emitterHandler.getOutput());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void writeTextContentType() throws Exception {
|
||||||
|
|
||||||
|
// Any requested, concrete, "text" media type
|
||||||
|
this.servletRequest.addHeader("Accept", "*/*, text/*, text/markdown");
|
||||||
|
testEmitterContentType("text/markdown");
|
||||||
|
|
||||||
|
// Or any requested concrete media type
|
||||||
|
this.servletRequest.addHeader("Accept", "*/*, text/*, foo/bar");
|
||||||
|
testEmitterContentType("foo/bar");
|
||||||
|
|
||||||
|
// Or default to...
|
||||||
|
testEmitterContentType("text/plain");
|
||||||
|
|
||||||
|
// Or default to if not concrete..
|
||||||
|
this.servletRequest.addHeader("Accept", "text/*");
|
||||||
|
testEmitterContentType("text/plain");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testEmitterContentType(String expected) throws Exception {
|
||||||
|
ServletServerHttpResponse message = new ServletServerHttpResponse(this.servletResponse);
|
||||||
|
ResponseBodyEmitter emitter = handleValue(Flux.empty(), Flux.class);
|
||||||
|
emitter.extendResponse(message);
|
||||||
|
assertEquals(expected, message.getHeaders().getContentType().toString());
|
||||||
|
resetRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void testDeferredResultSubscriber(Object returnValue, Class<?> asyncType,
|
||||||
|
Runnable produceTask, Object expected) throws Exception {
|
||||||
|
|
||||||
|
ResponseBodyEmitter emitter = handleValue(returnValue, asyncType);
|
||||||
|
assertNull(emitter);
|
||||||
|
|
||||||
|
assertTrue(this.servletRequest.isAsyncStarted());
|
||||||
|
assertFalse(WebAsyncUtils.getAsyncManager(this.webRequest).hasConcurrentResult());
|
||||||
|
|
||||||
|
produceTask.run();
|
||||||
|
|
||||||
|
assertTrue(WebAsyncUtils.getAsyncManager(this.webRequest).hasConcurrentResult());
|
||||||
|
assertEquals(expected, WebAsyncUtils.getAsyncManager(this.webRequest).getConcurrentResult());
|
||||||
|
|
||||||
|
resetRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResponseBodyEmitter handleValue(Object returnValue, Class<?> asyncType) throws Exception {
|
||||||
|
return handleValue(returnValue, asyncType, ResolvableType.forClass(String.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResponseBodyEmitter handleValue(Object returnValue, Class<?> asyncType,
|
||||||
|
ResolvableType genericType) throws Exception {
|
||||||
|
|
||||||
|
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
|
||||||
|
MethodParameter returnType = on(TestController.class).resolveReturnType(asyncType, genericType);
|
||||||
|
return this.handler.handleValue(returnValue, returnType, mavContainer, this.webRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
static class TestController {
|
||||||
|
|
||||||
|
String handleString() { return null; }
|
||||||
|
|
||||||
|
Mono<String> handleMono() { return null; }
|
||||||
|
|
||||||
|
Single<String> handleSingle() { return null; }
|
||||||
|
|
||||||
|
io.reactivex.Single<String> handleSingleRxJava2() { return null; }
|
||||||
|
|
||||||
|
Flux<String> handleFlux() { return null; }
|
||||||
|
|
||||||
|
Flux<ServerSentEvent<String>> handleFluxSseEventBuilder() { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static class EmitterHandler implements ResponseBodyEmitter.Handler {
|
||||||
|
|
||||||
|
private final StringBuilder stringBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
|
||||||
|
public String getOutput() {
|
||||||
|
return this.stringBuilder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void send(Object data, MediaType mediaType) throws IOException {
|
||||||
|
this.stringBuilder.append(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void complete() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void completeWithError(Throwable failure) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTimeout(Runnable callback) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCompletion(Runnable callback) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -25,6 +25,7 @@ import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import org.springframework.core.MethodParameter;
|
import org.springframework.core.MethodParameter;
|
||||||
|
import org.springframework.core.ReactiveAdapterRegistry;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.http.converter.HttpMessageConverter;
|
import org.springframework.http.converter.HttpMessageConverter;
|
||||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||||
|
|
@ -32,6 +33,7 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConvert
|
||||||
import org.springframework.mock.web.test.MockAsyncContext;
|
import org.springframework.mock.web.test.MockAsyncContext;
|
||||||
import org.springframework.mock.web.test.MockHttpServletRequest;
|
import org.springframework.mock.web.test.MockHttpServletRequest;
|
||||||
import org.springframework.mock.web.test.MockHttpServletResponse;
|
import org.springframework.mock.web.test.MockHttpServletResponse;
|
||||||
|
import org.springframework.web.accept.ContentNegotiationManager;
|
||||||
import org.springframework.web.context.request.NativeWebRequest;
|
import org.springframework.web.context.request.NativeWebRequest;
|
||||||
import org.springframework.web.context.request.ServletWebRequest;
|
import org.springframework.web.context.request.ServletWebRequest;
|
||||||
import org.springframework.web.context.request.async.AsyncWebRequest;
|
import org.springframework.web.context.request.async.AsyncWebRequest;
|
||||||
|
|
@ -63,7 +65,9 @@ public class ResponseBodyEmitterReturnValueHandlerTests {
|
||||||
List<HttpMessageConverter<?>> converters = Arrays.asList(
|
List<HttpMessageConverter<?>> converters = Arrays.asList(
|
||||||
new StringHttpMessageConverter(), new MappingJackson2HttpMessageConverter());
|
new StringHttpMessageConverter(), new MappingJackson2HttpMessageConverter());
|
||||||
|
|
||||||
this.handler = new ResponseBodyEmitterReturnValueHandler(converters);
|
ReactiveAdapterRegistry registry = new ReactiveAdapterRegistry();
|
||||||
|
ContentNegotiationManager manager = new ContentNegotiationManager();
|
||||||
|
this.handler = new ResponseBodyEmitterReturnValueHandler(converters, registry, manager);
|
||||||
this.request = new MockHttpServletRequest();
|
this.request = new MockHttpServletRequest();
|
||||||
this.response = new MockHttpServletResponse();
|
this.response = new MockHttpServletResponse();
|
||||||
this.webRequest = new ServletWebRequest(this.request, this.response);
|
this.webRequest = new ServletWebRequest(this.request, this.response);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2016 the original author or authors.
|
* Copyright 2002-2017 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -19,22 +19,26 @@ package org.springframework.web.servlet.mvc.method.annotation;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.ArrayList;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
import org.springframework.core.MethodParameter;
|
import org.springframework.core.MethodParameter;
|
||||||
|
import org.springframework.core.ReactiveAdapterRegistry;
|
||||||
import org.springframework.core.annotation.AliasFor;
|
import org.springframework.core.annotation.AliasFor;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.http.converter.HttpMessageConverter;
|
import org.springframework.http.converter.HttpMessageConverter;
|
||||||
import org.springframework.http.converter.HttpMessageNotWritableException;
|
import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||||
|
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||||
import org.springframework.mock.web.test.MockHttpServletRequest;
|
import org.springframework.mock.web.test.MockHttpServletRequest;
|
||||||
import org.springframework.mock.web.test.MockHttpServletResponse;
|
import org.springframework.mock.web.test.MockHttpServletResponse;
|
||||||
|
import org.springframework.web.accept.ContentNegotiationManager;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.ResponseBody;
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
|
|
@ -48,8 +52,10 @@ import org.springframework.web.method.support.HandlerMethodReturnValueHandlerCom
|
||||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||||
import org.springframework.web.servlet.view.RedirectView;
|
import org.springframework.web.servlet.view.RedirectView;
|
||||||
|
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test fixture with {@link ServletInvocableHandlerMethod}.
|
* Test fixture with {@link ServletInvocableHandlerMethod}.
|
||||||
|
|
@ -60,9 +66,14 @@ import static org.mockito.Mockito.*;
|
||||||
*/
|
*/
|
||||||
public class ServletInvocableHandlerMethodTests {
|
public class ServletInvocableHandlerMethodTests {
|
||||||
|
|
||||||
private final HandlerMethodArgumentResolverComposite argumentResolvers = new HandlerMethodArgumentResolverComposite();
|
private final List<HttpMessageConverter<?>> converters =
|
||||||
|
Collections.singletonList(new StringHttpMessageConverter());
|
||||||
|
|
||||||
private final HandlerMethodReturnValueHandlerComposite returnValueHandlers = new HandlerMethodReturnValueHandlerComposite();
|
private final HandlerMethodArgumentResolverComposite argumentResolvers =
|
||||||
|
new HandlerMethodArgumentResolverComposite();
|
||||||
|
|
||||||
|
private final HandlerMethodReturnValueHandlerComposite returnValueHandlers =
|
||||||
|
new HandlerMethodReturnValueHandlerComposite();
|
||||||
|
|
||||||
private final ModelAndViewContainer mavContainer = new ModelAndViewContainer();
|
private final ModelAndViewContainer mavContainer = new ModelAndViewContainer();
|
||||||
|
|
||||||
|
|
@ -199,10 +210,8 @@ public class ServletInvocableHandlerMethodTests {
|
||||||
private void wrapConcurrentResult_ResponseBody(Object handler, Object result, Class<?> expectedReturnType)
|
private void wrapConcurrentResult_ResponseBody(Object handler, Object result, Class<?> expectedReturnType)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
|
|
||||||
List<HttpMessageConverter<?>> converters = new ArrayList<>();
|
|
||||||
converters.add(new StringHttpMessageConverter());
|
|
||||||
this.returnValueHandlers.addHandler(new ModelAndViewMethodReturnValueHandler());
|
this.returnValueHandlers.addHandler(new ModelAndViewMethodReturnValueHandler());
|
||||||
this.returnValueHandlers.addHandler(new RequestResponseBodyMethodProcessor(converters));
|
this.returnValueHandlers.addHandler(new RequestResponseBodyMethodProcessor(this.converters));
|
||||||
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(handler, "handle");
|
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(handler, "handle");
|
||||||
|
|
||||||
handlerMethod = handlerMethod.wrapConcurrentResult(result);
|
handlerMethod = handlerMethod.wrapConcurrentResult(result);
|
||||||
|
|
@ -213,9 +222,7 @@ public class ServletInvocableHandlerMethodTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void wrapConcurrentResult_ResponseEntity() throws Exception {
|
public void wrapConcurrentResult_ResponseEntity() throws Exception {
|
||||||
List<HttpMessageConverter<?>> converters = new ArrayList<>();
|
this.returnValueHandlers.addHandler(new HttpEntityMethodProcessor(this.converters));
|
||||||
converters.add(new StringHttpMessageConverter());
|
|
||||||
this.returnValueHandlers.addHandler(new HttpEntityMethodProcessor(converters));
|
|
||||||
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new ResponseEntityHandler(), "handleDeferred");
|
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new ResponseEntityHandler(), "handleDeferred");
|
||||||
handlerMethod = handlerMethod.wrapConcurrentResult(new ResponseEntity<>("bar", HttpStatus.OK));
|
handlerMethod = handlerMethod.wrapConcurrentResult(new ResponseEntity<>("bar", HttpStatus.OK));
|
||||||
handlerMethod.invokeAndHandle(this.webRequest, this.mavContainer);
|
handlerMethod.invokeAndHandle(this.webRequest, this.mavContainer);
|
||||||
|
|
@ -225,11 +232,7 @@ public class ServletInvocableHandlerMethodTests {
|
||||||
|
|
||||||
@Test // SPR-12287
|
@Test // SPR-12287
|
||||||
public void wrapConcurrentResult_ResponseEntityNullBody() throws Exception {
|
public void wrapConcurrentResult_ResponseEntityNullBody() throws Exception {
|
||||||
List<HttpMessageConverter<?>> converters = new ArrayList<>();
|
this.returnValueHandlers.addHandler(new HttpEntityMethodProcessor(this.converters));
|
||||||
converters.add(new StringHttpMessageConverter());
|
|
||||||
List<Object> advice = Collections.singletonList(mock(ResponseBodyAdvice.class));
|
|
||||||
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(converters, null, advice);
|
|
||||||
this.returnValueHandlers.addHandler(processor);
|
|
||||||
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new ResponseEntityHandler(), "handleDeferred");
|
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new ResponseEntityHandler(), "handleDeferred");
|
||||||
handlerMethod = handlerMethod.wrapConcurrentResult(new ResponseEntity<>(HttpStatus.OK));
|
handlerMethod = handlerMethod.wrapConcurrentResult(new ResponseEntity<>(HttpStatus.OK));
|
||||||
handlerMethod.invokeAndHandle(this.webRequest, this.mavContainer);
|
handlerMethod.invokeAndHandle(this.webRequest, this.mavContainer);
|
||||||
|
|
@ -240,11 +243,7 @@ public class ServletInvocableHandlerMethodTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void wrapConcurrentResult_ResponseEntityNullReturnValue() throws Exception {
|
public void wrapConcurrentResult_ResponseEntityNullReturnValue() throws Exception {
|
||||||
List<HttpMessageConverter<?>> converters = new ArrayList<>();
|
this.returnValueHandlers.addHandler(new HttpEntityMethodProcessor(this.converters));
|
||||||
converters.add(new StringHttpMessageConverter());
|
|
||||||
List<Object> advice = Collections.singletonList(mock(ResponseBodyAdvice.class));
|
|
||||||
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(converters, null, advice);
|
|
||||||
this.returnValueHandlers.addHandler(processor);
|
|
||||||
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new ResponseEntityHandler(), "handleDeferred");
|
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new ResponseEntityHandler(), "handleDeferred");
|
||||||
handlerMethod = handlerMethod.wrapConcurrentResult(null);
|
handlerMethod = handlerMethod.wrapConcurrentResult(null);
|
||||||
handlerMethod.invokeAndHandle(this.webRequest, this.mavContainer);
|
handlerMethod.invokeAndHandle(this.webRequest, this.mavContainer);
|
||||||
|
|
@ -255,10 +254,10 @@ public class ServletInvocableHandlerMethodTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void wrapConcurrentResult_ResponseBodyEmitter() throws Exception {
|
public void wrapConcurrentResult_ResponseBodyEmitter() throws Exception {
|
||||||
List<HttpMessageConverter<?>> converters = new ArrayList<>();
|
ReactiveAdapterRegistry registry = new ReactiveAdapterRegistry();
|
||||||
converters.add(new StringHttpMessageConverter());
|
ContentNegotiationManager manager = new ContentNegotiationManager();
|
||||||
this.returnValueHandlers.addHandler(new ResponseBodyEmitterReturnValueHandler(converters));
|
this.returnValueHandlers.addHandler(new ResponseBodyEmitterReturnValueHandler(this.converters, registry, manager));
|
||||||
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new AsyncHandler(), "handleWithEmitter");
|
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new StreamingHandler(), "handleEmitter");
|
||||||
handlerMethod = handlerMethod.wrapConcurrentResult(null);
|
handlerMethod = handlerMethod.wrapConcurrentResult(null);
|
||||||
handlerMethod.invokeAndHandle(this.webRequest, this.mavContainer);
|
handlerMethod.invokeAndHandle(this.webRequest, this.mavContainer);
|
||||||
|
|
||||||
|
|
@ -269,7 +268,7 @@ public class ServletInvocableHandlerMethodTests {
|
||||||
@Test
|
@Test
|
||||||
public void wrapConcurrentResult_StreamingResponseBody() throws Exception {
|
public void wrapConcurrentResult_StreamingResponseBody() throws Exception {
|
||||||
this.returnValueHandlers.addHandler(new StreamingResponseBodyReturnValueHandler());
|
this.returnValueHandlers.addHandler(new StreamingResponseBodyReturnValueHandler());
|
||||||
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new AsyncHandler(), "handleWithStreaming");
|
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new StreamingHandler(), "handleStreamBody");
|
||||||
handlerMethod = handlerMethod.wrapConcurrentResult(null);
|
handlerMethod = handlerMethod.wrapConcurrentResult(null);
|
||||||
handlerMethod.invokeAndHandle(this.webRequest, this.mavContainer);
|
handlerMethod.invokeAndHandle(this.webRequest, this.mavContainer);
|
||||||
|
|
||||||
|
|
@ -277,12 +276,27 @@ public class ServletInvocableHandlerMethodTests {
|
||||||
assertEquals("", this.response.getContentAsString());
|
assertEquals("", this.response.getContentAsString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void wrapConcurrentResult_CollectedValuesList() throws Exception {
|
||||||
|
List<HttpMessageConverter<?>> converters = Collections.singletonList(new MappingJackson2HttpMessageConverter());
|
||||||
|
this.request.addHeader("Accept", "application/json");
|
||||||
|
ReactiveTypeHandler.CollectedValuesList result = new ReactiveTypeHandler.CollectedValuesList();
|
||||||
|
result.add(Arrays.asList("foo1", "bar1"));
|
||||||
|
result.add(Arrays.asList("foo2", "bar2"));
|
||||||
|
|
||||||
|
ContentNegotiationManager manager = new ContentNegotiationManager();
|
||||||
|
this.returnValueHandlers.addHandler(new RequestResponseBodyMethodProcessor(converters, manager));
|
||||||
|
ServletInvocableHandlerMethod hm = getHandlerMethod(new MethodLevelResponseBodyHandler(), "handleFluxOfLists");
|
||||||
|
hm = hm.wrapConcurrentResult(result);
|
||||||
|
hm.invokeAndHandle(this.webRequest, this.mavContainer);
|
||||||
|
|
||||||
|
assertEquals(200, this.response.getStatus());
|
||||||
|
assertEquals("[[\"foo1\",\"bar1\"],[\"foo2\",\"bar2\"]]", this.response.getContentAsString());
|
||||||
|
}
|
||||||
|
|
||||||
@Test // SPR-12287 (16/Oct/14 comments)
|
@Test // SPR-12287 (16/Oct/14 comments)
|
||||||
public void responseEntityRawTypeWithNullBody() throws Exception {
|
public void responseEntityRawTypeWithNullBody() throws Exception {
|
||||||
List<HttpMessageConverter<?>> converters = Collections.singletonList(new StringHttpMessageConverter());
|
this.returnValueHandlers.addHandler(new HttpEntityMethodProcessor(this.converters));
|
||||||
List<Object> advice = Collections.singletonList(mock(ResponseBodyAdvice.class));
|
|
||||||
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(converters, null, advice);
|
|
||||||
this.returnValueHandlers.addHandler(processor);
|
|
||||||
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new ResponseEntityHandler(), "handleRawType");
|
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new ResponseEntityHandler(), "handleRawType");
|
||||||
handlerMethod.invokeAndHandle(this.webRequest, this.mavContainer);
|
handlerMethod.invokeAndHandle(this.webRequest, this.mavContainer);
|
||||||
|
|
||||||
|
|
@ -358,6 +372,12 @@ public class ServletInvocableHandlerMethodTests {
|
||||||
public DeferredResult<String> handle() {
|
public DeferredResult<String> handle() {
|
||||||
return new DeferredResult<>();
|
return new DeferredResult<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unusual but legal return type
|
||||||
|
// Properly test generic type handling of Flux values collected to a List
|
||||||
|
|
||||||
|
@ResponseBody
|
||||||
|
public Flux<List<String>> handleFluxOfLists() { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -413,15 +433,12 @@ public class ServletInvocableHandlerMethodTests {
|
||||||
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
private static class AsyncHandler {
|
private static class StreamingHandler {
|
||||||
|
|
||||||
public ResponseBodyEmitter handleWithEmitter() {
|
public ResponseBodyEmitter handleEmitter() { return null; }
|
||||||
return null;
|
|
||||||
}
|
public StreamingResponseBody handleStreamBody() { return null; }
|
||||||
|
|
||||||
public StreamingResponseBody handleWithStreaming() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue