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:
Rossen Stoyanchev
2017-04-03 08:52:07 -04:00
parent ae1ed16cb8
commit 62c1e44db2
9 changed files with 860 additions and 48 deletions

View File

@@ -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) {
}
}
}

View File

@@ -25,6 +25,7 @@ import org.junit.Before;
import org.junit.Test;
import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
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.MockHttpServletRequest;
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.ServletWebRequest;
import org.springframework.web.context.request.async.AsyncWebRequest;
@@ -63,7 +65,9 @@ public class ResponseBodyEmitterReturnValueHandlerTests {
List<HttpMessageConverter<?>> converters = Arrays.asList(
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.response = new MockHttpServletResponse();
this.webRequest = new ServletWebRequest(this.request, this.response);

View File

@@ -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");
* 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.RetentionPolicy;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.junit.Test;
import reactor.core.publisher.Flux;
import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.annotation.AliasFor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotWritableException;
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.MockHttpServletResponse;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
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.servlet.view.RedirectView;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/**
* Test fixture with {@link ServletInvocableHandlerMethod}.
@@ -60,9 +66,14 @@ import static org.mockito.Mockito.*;
*/
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();
@@ -199,10 +210,8 @@ public class ServletInvocableHandlerMethodTests {
private void wrapConcurrentResult_ResponseBody(Object handler, Object result, Class<?> expectedReturnType)
throws Exception {
List<HttpMessageConverter<?>> converters = new ArrayList<>();
converters.add(new StringHttpMessageConverter());
this.returnValueHandlers.addHandler(new ModelAndViewMethodReturnValueHandler());
this.returnValueHandlers.addHandler(new RequestResponseBodyMethodProcessor(converters));
this.returnValueHandlers.addHandler(new RequestResponseBodyMethodProcessor(this.converters));
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(handler, "handle");
handlerMethod = handlerMethod.wrapConcurrentResult(result);
@@ -213,9 +222,7 @@ public class ServletInvocableHandlerMethodTests {
@Test
public void wrapConcurrentResult_ResponseEntity() throws Exception {
List<HttpMessageConverter<?>> converters = new ArrayList<>();
converters.add(new StringHttpMessageConverter());
this.returnValueHandlers.addHandler(new HttpEntityMethodProcessor(converters));
this.returnValueHandlers.addHandler(new HttpEntityMethodProcessor(this.converters));
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new ResponseEntityHandler(), "handleDeferred");
handlerMethod = handlerMethod.wrapConcurrentResult(new ResponseEntity<>("bar", HttpStatus.OK));
handlerMethod.invokeAndHandle(this.webRequest, this.mavContainer);
@@ -225,11 +232,7 @@ public class ServletInvocableHandlerMethodTests {
@Test // SPR-12287
public void wrapConcurrentResult_ResponseEntityNullBody() throws Exception {
List<HttpMessageConverter<?>> converters = new ArrayList<>();
converters.add(new StringHttpMessageConverter());
List<Object> advice = Collections.singletonList(mock(ResponseBodyAdvice.class));
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(converters, null, advice);
this.returnValueHandlers.addHandler(processor);
this.returnValueHandlers.addHandler(new HttpEntityMethodProcessor(this.converters));
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new ResponseEntityHandler(), "handleDeferred");
handlerMethod = handlerMethod.wrapConcurrentResult(new ResponseEntity<>(HttpStatus.OK));
handlerMethod.invokeAndHandle(this.webRequest, this.mavContainer);
@@ -240,11 +243,7 @@ public class ServletInvocableHandlerMethodTests {
@Test
public void wrapConcurrentResult_ResponseEntityNullReturnValue() throws Exception {
List<HttpMessageConverter<?>> converters = new ArrayList<>();
converters.add(new StringHttpMessageConverter());
List<Object> advice = Collections.singletonList(mock(ResponseBodyAdvice.class));
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(converters, null, advice);
this.returnValueHandlers.addHandler(processor);
this.returnValueHandlers.addHandler(new HttpEntityMethodProcessor(this.converters));
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new ResponseEntityHandler(), "handleDeferred");
handlerMethod = handlerMethod.wrapConcurrentResult(null);
handlerMethod.invokeAndHandle(this.webRequest, this.mavContainer);
@@ -255,10 +254,10 @@ public class ServletInvocableHandlerMethodTests {
@Test
public void wrapConcurrentResult_ResponseBodyEmitter() throws Exception {
List<HttpMessageConverter<?>> converters = new ArrayList<>();
converters.add(new StringHttpMessageConverter());
this.returnValueHandlers.addHandler(new ResponseBodyEmitterReturnValueHandler(converters));
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new AsyncHandler(), "handleWithEmitter");
ReactiveAdapterRegistry registry = new ReactiveAdapterRegistry();
ContentNegotiationManager manager = new ContentNegotiationManager();
this.returnValueHandlers.addHandler(new ResponseBodyEmitterReturnValueHandler(this.converters, registry, manager));
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new StreamingHandler(), "handleEmitter");
handlerMethod = handlerMethod.wrapConcurrentResult(null);
handlerMethod.invokeAndHandle(this.webRequest, this.mavContainer);
@@ -269,7 +268,7 @@ public class ServletInvocableHandlerMethodTests {
@Test
public void wrapConcurrentResult_StreamingResponseBody() throws Exception {
this.returnValueHandlers.addHandler(new StreamingResponseBodyReturnValueHandler());
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new AsyncHandler(), "handleWithStreaming");
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new StreamingHandler(), "handleStreamBody");
handlerMethod = handlerMethod.wrapConcurrentResult(null);
handlerMethod.invokeAndHandle(this.webRequest, this.mavContainer);
@@ -277,12 +276,27 @@ public class ServletInvocableHandlerMethodTests {
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)
public void responseEntityRawTypeWithNullBody() throws Exception {
List<HttpMessageConverter<?>> converters = Collections.singletonList(new StringHttpMessageConverter());
List<Object> advice = Collections.singletonList(mock(ResponseBodyAdvice.class));
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(converters, null, advice);
this.returnValueHandlers.addHandler(processor);
this.returnValueHandlers.addHandler(new HttpEntityMethodProcessor(this.converters));
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new ResponseEntityHandler(), "handleRawType");
handlerMethod.invokeAndHandle(this.webRequest, this.mavContainer);
@@ -358,6 +372,12 @@ public class ServletInvocableHandlerMethodTests {
public DeferredResult<String> handle() {
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")
private static class AsyncHandler {
private static class StreamingHandler {
public ResponseBodyEmitter handleWithEmitter() {
return null;
}
public ResponseBodyEmitter handleEmitter() { return null; }
public StreamingResponseBody handleStreamBody() { return null; }
public StreamingResponseBody handleWithStreaming() {
return null;
}
}
}