提交 a32b5e61 编写于 作者: R Rossen Stoyanchev

Add support for Server-Sent Events

This commit adds ResponseBodyEmitter and SseEmitter (and also
ResponseEntity<ResponseBodyEmitter> and ResponseEntity<SseEmitter>) as
new return value types supported on @RequestMapping controller methods.

See Javadoc on respective types for more details.

Issue: SPR-12212
上级 ccb1c139
......@@ -213,6 +213,12 @@ import java.util.concurrent.Callable;
* <li>A {@link org.springframework.util.concurrent.ListenableFuture}
* which the application uses to produce a return value in a separate
* thread of its own choosing, as an alternative to returning a Callable.
* <li>A {@link org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter}
* can be used to write multiple objects to the response asynchronously;
* also supported as the body within {@code ResponseEntity}.</li>
* <li>An {@link org.springframework.web.servlet.mvc.method.annotation.SseEmitter}
* can be used to write Server-Sent Events to the response asynchronously;
* also supported as the body within {@code ResponseEntity}.</li>
* <li>{@code void} if the method handles the response itself (by
* writing the response content directly, declaring an argument of type
* {@link javax.servlet.ServletResponse} / {@link javax.servlet.http.HttpServletResponse}
......
......@@ -632,6 +632,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
handlers.add(new ModelAndViewMethodReturnValueHandler());
handlers.add(new ModelMethodProcessor());
handlers.add(new ViewMethodReturnValueHandler());
handlers.add(new ResponseBodyEmitterReturnValueHandler(getMessageConverters()));
handlers.add(new HttpEntityMethodProcessor(
getMessageConverters(), this.contentNegotiationManager, this.responseBodyAdvice));
handlers.add(new HttpHeadersReturnValueHandler());
......
/*
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.mvc.method.annotation;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.Assert;
/**
* A controller method return value type for asynchronous request processing
* where one or more objects are written to the response. While
* {@link org.springframework.web.context.request.async.DeferredResult DeferredResult}
* is used to produce a single result, a {@code ResponseBodyEmitter} can be used
* to send multiple objects where each object is written with a compatible
* {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverter}.
*
* <p>Supported as a return type on its own as well as within a
* {@link org.springframework.http.ResponseEntity ResponseEntity}.
*
* <pre>
* &#064;RequestMapping(value="/stream", method=RequestMethod.GET)
* public ResponseBodyEmitter handle() {
* ResponseBodyEmitter emitter = new ResponseBodyEmitter();
* // Pass the emitter to another component...
* return emitter;
* }
*
* // in another thread
* emitter.send(foo1);
*
* // and again
* emitter.send(foo2);
*
* // and done
* emitter.complete();
* </pre>
*
* <p><strong>Note:</strong> this class is not thread-safe. Callers must ensure
* that use from multiple threads is synchronized.
*
* @author Rossen Stoyanchev
* @since 4.2
*/
public class ResponseBodyEmitter {
private Handler handler;
/* Cache for objects sent before handler is set. */
private final Map<Object, MediaType> initHandlerCache = new LinkedHashMap<Object, MediaType>(10);
private volatile boolean complete;
private Throwable failure;
/**
* Invoked after the response is updated with the status code and headers,
* if the ResponseBodyEmitter is wrapped in a ResponseEntity, but before the
* response is committed, i.e. before the response body has been written to.
* <p>The default implementation is empty.
*/
protected void extendResponse(ServerHttpResponse outputMessage) {
}
void initialize(Handler handler) throws IOException {
synchronized (this) {
this.handler = handler;
for (Map.Entry<Object, MediaType> entry : this.initHandlerCache.entrySet()) {
try {
sendInternal(entry.getKey(), entry.getValue());
}
catch (Throwable ex) {
return;
}
}
if (this.complete) {
if (this.failure != null) {
this.handler.completeWithError(this.failure);
}
else {
this.handler.complete();
}
}
}
}
/**
* Write the given object to the response.
* <p>If any exception occurs a dispatch is made back to the app server where
* Spring MVC will pass the exception through its exception handling mechanism.
* @param object the object to write
* @throws IOException raised when an I/O error occurs
* @throws java.lang.IllegalStateException wraps any other errors
*/
public void send(Object object) throws IOException {
send(object, null);
}
/**
* Write the given object to the response also using a MediaType hint.
* <p>If any exception occurs a dispatch is made back to the app server where
* Spring MVC will pass the exception through its exception handling mechanism.
* @param object the object to write
* @param mediaType a MediaType hint for selecting an HttpMessageConverter
* @throws IOException raised when an I/O error occurs
* @throws java.lang.IllegalStateException wraps any other errors
*/
public void send(Object object, MediaType mediaType) throws IOException {
Assert.state(!this.complete, "ResponseBodyEmitter is already set complete.");
sendInternal(object, mediaType);
}
private void sendInternal(Object object, MediaType mediaType) throws IOException {
if (object == null) {
return;
}
if (handler == null) {
synchronized (this) {
if (handler == null) {
this.initHandlerCache.put(object, mediaType);
return;
}
}
}
try {
this.handler.send(object, mediaType);
}
catch(IOException ex){
this.handler.completeWithError(ex);
throw ex;
}
catch(Throwable ex){
this.handler.completeWithError(ex);
throw new IllegalStateException("Failed to send " + object, ex);
}
}
/**
* Complete request processing.
* <p>A dispatch is made into the app server where Spring MVC completes
* asynchronous request processing.
*/
public void complete() {
synchronized (this) {
this.complete = true;
if (handler != null) {
this.handler.complete();
}
}
}
/**
* Complete request processing with an error.
* <p>A dispatch is made into the app server where Spring MVC will pass the
* exception through its exception handling mechanism.
*/
public void completeWithError(Throwable ex) {
synchronized (this) {
this.complete = true;
this.failure = ex;
if (handler != null) {
this.handler.completeWithError(ex);
}
}
}
/**
* Handle sent objects and complete request processing.
*/
interface Handler {
void send(Object data, MediaType mediaType) throws IOException;
void complete();
void completeWithError(Throwable failure);
}
}
/*
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.mvc.method.annotation;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.async.DeferredResult;
import org.springframework.web.context.request.async.WebAsyncUtils;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* Supports return values of type {@link ResponseBodyEmitter} and also
* {@code ResponseEntity<ResponseBodyEmitter>}.
*
* @author Rossen Stoyanchev
* @since 4.2
*/
public class ResponseBodyEmitterReturnValueHandler implements HandlerMethodReturnValueHandler {
private static final Log logger = LogFactory.getLog(ResponseBodyEmitterReturnValueHandler.class);
private final List<HttpMessageConverter<?>> messageConverters;
public ResponseBodyEmitterReturnValueHandler(List<HttpMessageConverter<?>> messageConverters) {
Assert.notEmpty(messageConverters, "'messageConverters' must not be empty");
this.messageConverters = messageConverters;
}
@Override
public boolean supportsReturnType(MethodParameter returnType) {
if (ResponseBodyEmitter.class.isAssignableFrom(returnType.getParameterType())) {
return true;
}
else if (ResponseEntity.class.isAssignableFrom(returnType.getParameterType())) {
Type paramType = returnType.getGenericParameterType();
if (paramType instanceof ParameterizedType) {
ParameterizedType type = (ParameterizedType) paramType;
Type[] typeArguments = type.getActualTypeArguments();
if (typeArguments.length == 1) {
return ResponseBodyEmitter.class.isAssignableFrom((Class<?>) typeArguments[0]);
}
}
}
return false;
}
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
if (returnValue == null) {
mavContainer.setRequestHandled(true);
return;
}
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
ServerHttpResponse outputMessage = new ServletServerHttpResponse(response);
if (ResponseEntity.class.isAssignableFrom(returnValue.getClass())) {
ResponseEntity<?> responseEntity = (ResponseEntity<?>) returnValue;
outputMessage.setStatusCode(responseEntity.getStatusCode());
outputMessage.getHeaders().putAll(responseEntity.getHeaders());
returnValue = responseEntity.getBody();
if (returnValue == null) {
mavContainer.setRequestHandled(true);
return;
}
}
Assert.isInstanceOf(ResponseBodyEmitter.class, returnValue);
ResponseBodyEmitter emitter = (ResponseBodyEmitter) returnValue;
emitter.extendResponse(outputMessage);
// Commit the response and wrap to ignore further header changes
outputMessage.getBody();
outputMessage = new StreamingServletServerHttpResponse(outputMessage);
DeferredResult<?> deferredResult = new DeferredResult<Object>();
WebAsyncUtils.getAsyncManager(webRequest).startDeferredResultProcessing(deferredResult, mavContainer);
HttpMessageConvertingHandler handler = new HttpMessageConvertingHandler(outputMessage, deferredResult);
emitter.initialize(handler);
}
/**
* ResponseBodyEmitter.Handler that writes with HttpMessageConverter's.
*/
private class HttpMessageConvertingHandler implements ResponseBodyEmitter.Handler {
private final ServerHttpResponse outputMessage;
private final DeferredResult<?> deferredResult;
public HttpMessageConvertingHandler(ServerHttpResponse outputMessage, DeferredResult<?> deferredResult) {
this.outputMessage = outputMessage;
this.deferredResult = deferredResult;
}
@Override
public void send(Object data, MediaType mediaType) throws IOException {
sendInternal(data, mediaType);
}
@SuppressWarnings("unchecked")
private <T> void sendInternal(T data, MediaType mediaType) throws IOException {
for (HttpMessageConverter<?> converter : ResponseBodyEmitterReturnValueHandler.this.messageConverters) {
if (converter.canWrite(data.getClass(), mediaType)) {
((HttpMessageConverter<T>) converter).write(data, mediaType, this.outputMessage);
this.outputMessage.flush();
if (logger.isDebugEnabled()) {
logger.debug("Written [" + data + "] using [" + converter + "]");
}
return;
}
}
throw new IllegalArgumentException("No suitable converter for " + data);
}
@Override
public void complete() {
this.deferredResult.setResult(null);
}
@Override
public void completeWithError(Throwable failure) {
this.deferredResult.setErrorResult(failure);
}
}
/**
* Wrap to silently ignore header changes HttpMessageConverter's that would
* otherwise cause HttpHeaders to raise exceptions.
*/
private static class StreamingServletServerHttpResponse implements ServerHttpResponse {
private final ServerHttpResponse delegate;
private final HttpHeaders mutableHeaders = new HttpHeaders();
public StreamingServletServerHttpResponse(ServerHttpResponse delegate) {
this.delegate = delegate;
this.mutableHeaders.putAll(delegate.getHeaders());
}
@Override
public void setStatusCode(HttpStatus status) {
this.delegate.setStatusCode(status);
}
@Override
public HttpHeaders getHeaders() {
return this.mutableHeaders;
}
@Override
public OutputStream getBody() throws IOException {
return this.delegate.getBody();
}
@Override
public void flush() throws IOException {
this.delegate.flush();
}
@Override
public void close() {
this.delegate.close();
}
}
}
......@@ -25,6 +25,7 @@ import java.util.concurrent.Callable;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpStatus;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ResponseStatus;
......@@ -260,7 +261,16 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
@Override
public Class<?> getParameterType() {
return (this.returnValue != null ? this.returnValue.getClass() : this.returnType.getRawClass());
if (this.returnValue != null) {
return this.returnValue.getClass();
}
Class<?> parameterType = super.getParameterType();
if (ResponseBodyEmitter.class.isAssignableFrom(parameterType)) {
return parameterType;
}
Assert.isTrue(!ResolvableType.NONE.equals(this.returnType), "Expected one of" +
"Callable, DeferredResult, or ListenableFuture: " + super.getParameterType());
return this.returnType.getRawClass();
}
@Override
......
/*
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.mvc.method.annotation;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpResponse;
/**
* A specialization of
* {@link org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter
* ResponseBodyEmitter} for sending
* <a href="http://www.w3.org/TR/eventsource/">Server-Sent Events</a>.
*
* @author Rossen Stoyanchev
* @since 4.2
*/
public class SseEmitter extends ResponseBodyEmitter {
public static final MediaType TEXT_PLAIN = new MediaType("text", "plain", Charset.forName("UTF-8"));
@Override
protected void extendResponse(ServerHttpResponse outputMessage) {
super.extendResponse(outputMessage);
HttpHeaders headers = outputMessage.getHeaders();
if (headers.getContentType() == null) {
headers.setContentType(new MediaType("text", "event-stream"));
}
}
/**
* Send the object formatted as a single SSE "data" line. It's equivalent to:
* <pre>
*
* // static import of SseEmitter.*
*
* SseEmitter emitter = new SseEmitter();
* emitter.send(event().data(myObject));
* </pre>
* @param object the object to write
* @throws IOException raised when an I/O error occurs
* @throws java.lang.IllegalStateException wraps any other errors
*/
@Override
public void send(Object object) throws IOException {
send(object, null);
}
/**
* Send the object formatted as a single SSE "data" line. It's equivalent to:
* <pre>
*
* // static import of SseEmitter.*
*
* SseEmitter emitter = new SseEmitter();
* emitter.send(event().data(myObject, MediaType.APPLICATION_JSON));
* </pre>
* @param object the object to write
* @param mediaType a MediaType hint for selecting an HttpMessageConverter
* @throws IOException raised when an I/O error occurs
* @throws java.lang.IllegalStateException wraps any other errors
*/
@Override
public void send(Object object, MediaType mediaType) throws IOException {
if (object == null) {
return;
}
send(event().data(object, mediaType));
}
/**
* Send an SSE event prepared with the given builder. For example:
* <pre>
*
* // static import of SseEmitter
*
* SseEmitter emitter = new SseEmitter();
* emitter.send(event().name("update").id("1").data(myObject));
* </pre>
* @param builder a builder for an SSE formatted event.
* @throws IOException raised when an I/O error occurs
* @throws java.lang.IllegalStateException wraps any other errors
*/
public void send(SseEventBuilder builder) throws IOException {
Map<Object, MediaType> map = builder.build();
for (Map.Entry<Object, MediaType> entry : map.entrySet()) {
super.send(entry.getKey(), entry.getValue());
}
}
public static SseEventBuilder event() {
return new DefaultSseEventBuilder();
}
/**
* A builder for an SSE event.
*/
public interface SseEventBuilder {
/**
* Add an SSE "comment" line.
*/
SseEventBuilder comment(String comment);
/**
* Add an SSE "event" line.
*/
SseEventBuilder name(String eventName);
/**
* Add an SSE "id" line.
*/
SseEventBuilder id(String id);
/**
* Add an SSE "event" line.
*/
SseEventBuilder reconnectTime(long reconnectTimeMillis);
/**
* Add an SSE "data" line.
*/
SseEventBuilder data(Object object);
/**
* Add an SSE "data" line.
*/
SseEventBuilder data(Object object, MediaType mediaType);
/**
* Return a map with objects that represent the data to be written to
* the response as well as the required SSE text formatting that
* surrounds it.
*/
Map<Object, MediaType> build();
}
/**
* Default implementation of SseEventBuilder.
*/
private static class DefaultSseEventBuilder implements SseEventBuilder {
private final Map<Object, MediaType> map = new LinkedHashMap<Object, MediaType>(4);
private StringBuilder sb;
@Override
public SseEventBuilder comment(String comment) {
append(":").append(comment != null ? comment : "").append("\n");
return this;
}
@Override
public SseEventBuilder name(String name) {
append("name:").append(name != null ? name : "").append("\n");
return this;
}
@Override
public SseEventBuilder id(String id) {
append("id:").append(id != null ? id : "").append("\n");
return this;
}
@Override
public SseEventBuilder reconnectTime(long reconnectTimeMillis) {
append("retry:").append(String.valueOf(reconnectTimeMillis)).append("\n");
return this;
}
@Override
public SseEventBuilder data(Object object) {
return data(object, null);
}
@Override
public SseEventBuilder data(Object object, MediaType mediaType) {
append("data:");
saveAppendedText();
this.map.put(object, mediaType);
append("\n");
return this;
}
DefaultSseEventBuilder append(String text) {
if (this.sb == null) {
this.sb = new StringBuilder();
}
this.sb.append(text);
return this;
}
private void saveAppendedText() {
if (this.sb != null) {
this.map.put(this.sb.toString(), TEXT_PLAIN);
this.sb = null;
}
}
@Override
public Map<Object, MediaType> build() {
if (this.sb == null || this.sb.length() == 0 && this.map.isEmpty()) {
return Collections.<Object, MediaType>emptyMap();
}
append("\n");
saveAppendedText();
return this.map;
}
}
}
......@@ -79,7 +79,7 @@ public class ServletUriComponentsBuilder extends UriComponentsBuilder {
* {@code "/"} or {@code "*.do"}, the result will be the same as
* if calling {@link #fromContextPath(HttpServletRequest)}.
*/
public static ServletUriComponentsBuilder fromServletMapping(HttpServletRequest request) {
public static ServletUriComponentsBuilder fromServletMapping(HttpServletRequest request) {
ServletUriComponentsBuilder builder = fromContextPath(request);
if (StringUtils.hasText(new UrlPathHelper().getPathWithinServletMapping(request))) {
builder.path(request.getServletPath());
......
/*
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.mvc.method.annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.MethodParameter;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
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.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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.springframework.web.servlet.mvc.method.annotation.SseEmitter.event;
/**
* Unit tests for ResponseBodyEmitterReturnValueHandler.
* @author Rossen Stoyanchev
*/
public class ResponseBodyEmitterReturnValueHandlerTests {
private ResponseBodyEmitterReturnValueHandler handler;
private ModelAndViewContainer mavContainer;
private NativeWebRequest webRequest;
private MockHttpServletRequest request;
private MockHttpServletResponse response;
@Before
public void setUp() throws Exception {
List<HttpMessageConverter<?>> converters = Arrays.asList(
new StringHttpMessageConverter(), new MappingJackson2HttpMessageConverter());
this.handler = new ResponseBodyEmitterReturnValueHandler(converters);
this.mavContainer = new ModelAndViewContainer();
this.request = new MockHttpServletRequest();
this.response = new MockHttpServletResponse();
this.webRequest = new ServletWebRequest(this.request, this.response);
AsyncWebRequest asyncWebRequest = new StandardServletAsyncWebRequest(this.request, this.response);
WebAsyncUtils.getAsyncManager(this.webRequest).setAsyncWebRequest(asyncWebRequest);
this.request.setAsyncSupported(true);
}
@Test
public void supportsReturnType() throws Exception {
assertTrue(this.handler.supportsReturnType(returnType(TestController.class, "handle")));
assertTrue(this.handler.supportsReturnType(returnType(TestController.class, "handleSse")));
assertTrue(this.handler.supportsReturnType(returnType(TestController.class, "handleResponseEntity")));
assertFalse(this.handler.supportsReturnType(returnType(TestController.class, "handleResponseEntityString")));
}
@Test
public void responseBodyEmitter() throws Exception {
MethodParameter returnType = returnType(TestController.class, "handle");
ResponseBodyEmitter emitter = new ResponseBodyEmitter();
this.handler.handleReturnValue(emitter, returnType, this.mavContainer, this.webRequest);
assertTrue(this.request.isAsyncStarted());
assertEquals("", this.response.getContentAsString());
SimpleBean bean = new SimpleBean();
bean.setId(1L);
bean.setName("Joe");
emitter.send(bean);
emitter.send("\n");
bean.setId(2L);
bean.setName("John");
emitter.send(bean);
emitter.send("\n");
bean.setId(3L);
bean.setName("Jason");
emitter.send(bean);
assertEquals("{\"id\":1,\"name\":\"Joe\"}\n" +
"{\"id\":2,\"name\":\"John\"}\n" +
"{\"id\":3,\"name\":\"Jason\"}",
this.response.getContentAsString());
MockAsyncContext asyncContext = (MockAsyncContext) this.request.getAsyncContext();
assertNull(asyncContext.getDispatchedPath());
emitter.complete();
assertNotNull(asyncContext.getDispatchedPath());
}
@Test
public void sseEmitter() throws Exception {
MethodParameter returnType = returnType(TestController.class, "handleSse");
SseEmitter emitter = new SseEmitter();
this.handler.handleReturnValue(emitter, returnType, this.mavContainer, this.webRequest);
assertTrue(this.request.isAsyncStarted());
assertEquals(200, this.response.getStatus());
assertEquals("text/event-stream", this.response.getContentType());
SimpleBean bean1 = new SimpleBean();
bean1.setId(1L);
bean1.setName("Joe");
SimpleBean bean2 = new SimpleBean();
bean2.setId(2L);
bean2.setName("John");
emitter.send(event().comment("a test").name("update").id("1").reconnectTime(5000L).data(bean1).data(bean2));
assertEquals(":a test\n" +
"name:update\n" +
"id:1\n" +
"retry:5000\n" +
"data:{\"id\":1,\"name\":\"Joe\"}\n" +
"data:{\"id\":2,\"name\":\"John\"}\n" +
"\n",
this.response.getContentAsString());
}
@Test
public void responseEntitySse() throws Exception {
MethodParameter returnType = returnType(TestController.class, "handleResponseEntitySse");
ResponseEntity<SseEmitter> emitter = ResponseEntity.ok().header("foo", "bar").body(new SseEmitter());
this.handler.handleReturnValue(emitter, returnType, this.mavContainer, this.webRequest);
assertTrue(this.request.isAsyncStarted());
assertEquals(200, this.response.getStatus());
assertEquals("text/event-stream", this.response.getContentType());
assertEquals("bar", this.response.getHeader("foo"));
}
@Test
public void responseEntitySseNoContent() throws Exception {
MethodParameter returnType = returnType(TestController.class, "handleResponseEntitySse");
ResponseEntity<?> emitter = ResponseEntity.noContent().build();
this.handler.handleReturnValue(emitter, returnType, this.mavContainer, this.webRequest);
assertFalse(this.request.isAsyncStarted());
assertEquals(204, this.response.getStatus());
}
private MethodParameter returnType(Class<?> clazz, String methodName) throws NoSuchMethodException {
Method method = clazz.getDeclaredMethod(methodName);
return new MethodParameter(method, -1);
}
@SuppressWarnings("unused")
private static class TestController {
private ResponseBodyEmitter handle() {
return null;
}
private ResponseEntity<ResponseBodyEmitter> handleResponseEntity() {
return null;
}
private SseEmitter handleSse() {
return null;
}
private ResponseEntity<SseEmitter> handleResponseEntitySse() {
return null;
}
private ResponseEntity<String> handleResponseEntityString() {
return null;
}
}
private static class SimpleBean {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
/*
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.mvc.method.annotation;
import java.io.IOException;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.http.MediaType;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
/**
* Unit tests for {@link ResponseBodyEmitter}.
* @author Rossen Stoyanchev
*/
public class ResponseBodyEmitterTests {
private ResponseBodyEmitter emitter;
@Mock
private ResponseBodyEmitter.Handler handler;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
this.emitter = new ResponseBodyEmitter();
}
@Test
public void sendBeforeHandlerInitialized() throws Exception {
this.emitter.send("foo", MediaType.TEXT_PLAIN);
this.emitter.send("bar", MediaType.TEXT_PLAIN);
this.emitter.complete();
verifyNoMoreInteractions(this.handler);
this.emitter.initialize(this.handler);
verify(this.handler).send("foo", MediaType.TEXT_PLAIN);
verify(this.handler).send("bar", MediaType.TEXT_PLAIN);
verify(this.handler).complete();
verifyNoMoreInteractions(this.handler);
}
@Test
public void sendBeforeHandlerInitializedWithError() throws Exception {
IllegalStateException ex = new IllegalStateException();
this.emitter.send("foo", MediaType.TEXT_PLAIN);
this.emitter.send("bar", MediaType.TEXT_PLAIN);
this.emitter.completeWithError(ex);
verifyNoMoreInteractions(this.handler);
this.emitter.initialize(this.handler);
verify(this.handler).send("foo", MediaType.TEXT_PLAIN);
verify(this.handler).send("bar", MediaType.TEXT_PLAIN);
verify(this.handler).completeWithError(ex);
verifyNoMoreInteractions(this.handler);
}
@Test(expected = IllegalStateException.class)
public void sendFailsAfterComplete() throws Exception {
this.emitter.complete();
this.emitter.send("foo");
}
@Test
public void sendAfterHandlerInitialized() throws Exception {
this.emitter.initialize(this.handler);
verifyNoMoreInteractions(this.handler);
this.emitter.send("foo", MediaType.TEXT_PLAIN);
this.emitter.send("bar", MediaType.TEXT_PLAIN);
this.emitter.complete();
verify(this.handler).send("foo", MediaType.TEXT_PLAIN);
verify(this.handler).send("bar", MediaType.TEXT_PLAIN);
verify(this.handler).complete();
verifyNoMoreInteractions(this.handler);
}
@Test
public void sendAfterHandlerInitializedWithError() throws Exception {
this.emitter.initialize(this.handler);
verifyNoMoreInteractions(this.handler);
IllegalStateException ex = new IllegalStateException();
this.emitter.send("foo", MediaType.TEXT_PLAIN);
this.emitter.send("bar", MediaType.TEXT_PLAIN);
this.emitter.completeWithError(ex);
verify(this.handler).send("foo", MediaType.TEXT_PLAIN);
verify(this.handler).send("bar", MediaType.TEXT_PLAIN);
verify(this.handler).completeWithError(ex);
verifyNoMoreInteractions(this.handler);
}
@Test
public void sendWithError() throws Exception {
this.emitter.initialize(this.handler);
verifyNoMoreInteractions(this.handler);
IOException failure = new IOException();
doThrow(failure).when(this.handler).send("foo", MediaType.TEXT_PLAIN);
try {
this.emitter.send("foo", MediaType.TEXT_PLAIN);
fail("Expected exception");
}
catch (IOException ex) {
// expected
}
verify(this.handler).send("foo", MediaType.TEXT_PLAIN);
verify(this.handler).completeWithError(failure);
verifyNoMoreInteractions(this.handler);
}
}
/*
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.mvc.method.annotation;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.springframework.http.MediaType;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.springframework.web.servlet.mvc.method.annotation.SseEmitter.event;
/**
* Unit tests for {@link org.springframework.web.servlet.mvc.method.annotation.SseEmitter}.
* @author Rossen Stoyanchev
*/
public class SseEmitterTests {
private SseEmitter emitter;
private TestHandler handler;
@Before
public void setup() throws IOException {
this.handler = new TestHandler();
this.emitter = new SseEmitter();
this.emitter.initialize(this.handler);
}
@Test
public void send() throws Exception {
this.emitter.send("foo");
this.handler.assertSentObjectCount(3);
this.handler.assertObject(0, "data:", SseEmitter.TEXT_PLAIN);
this.handler.assertObject(1, "foo");
this.handler.assertObject(2, "\n\n", SseEmitter.TEXT_PLAIN);
}
@Test
public void sendWithMediaType() throws Exception {
this.emitter.send("foo", MediaType.TEXT_PLAIN);
this.handler.assertSentObjectCount(3);
this.handler.assertObject(0, "data:", SseEmitter.TEXT_PLAIN);
this.handler.assertObject(1, "foo", MediaType.TEXT_PLAIN);
this.handler.assertObject(2, "\n\n", SseEmitter.TEXT_PLAIN);
}
@Test
public void sendEventEmpty() throws Exception {
this.emitter.send(event());
this.handler.assertSentObjectCount(0);
}
@Test
public void sendEventWithDataLine() throws Exception {
this.emitter.send(event().data("foo"));
this.handler.assertSentObjectCount(3);
this.handler.assertObject(0, "data:", SseEmitter.TEXT_PLAIN);
this.handler.assertObject(1, "foo");
this.handler.assertObject(2, "\n\n", SseEmitter.TEXT_PLAIN);
}
@Test
public void sendEventWithTwoDataLines() throws Exception {
this.emitter.send(event().data("foo").data("bar"));
this.handler.assertSentObjectCount(5);
this.handler.assertObject(0, "data:", SseEmitter.TEXT_PLAIN);
this.handler.assertObject(1, "foo");
this.handler.assertObject(2, "\ndata:", SseEmitter.TEXT_PLAIN);
this.handler.assertObject(3, "bar");
this.handler.assertObject(4, "\n\n", SseEmitter.TEXT_PLAIN);
}
@Test
public void sendEventFull() throws Exception {
this.emitter.send(event().comment("blah").name("test").reconnectTime(5000L).id("1").data("foo"));
this.handler.assertSentObjectCount(3);
this.handler.assertObject(0, ":blah\nname:test\nretry:5000\nid:1\ndata:", SseEmitter.TEXT_PLAIN);
this.handler.assertObject(1, "foo");
this.handler.assertObject(2, "\n\n", SseEmitter.TEXT_PLAIN);
}
@Test
public void sendEventFullWithTwoDataLinesInTheMiddle() throws Exception {
this.emitter.send(event().comment("blah").data("foo").data("bar").name("test").reconnectTime(5000L).id("1"));
this.handler.assertSentObjectCount(5);
this.handler.assertObject(0, ":blah\ndata:", SseEmitter.TEXT_PLAIN);
this.handler.assertObject(1, "foo");
this.handler.assertObject(2, "\ndata:", SseEmitter.TEXT_PLAIN);
this.handler.assertObject(3, "bar");
this.handler.assertObject(4, "\nname:test\nretry:5000\nid:1\n\n", SseEmitter.TEXT_PLAIN);
}
private static class TestHandler implements ResponseBodyEmitter.Handler {
private List<Object> objects = new ArrayList<>();
private List<MediaType> mediaTypes = new ArrayList<>();
public void assertSentObjectCount(int size) {
assertEquals(size, this.objects.size());
}
public void assertObject(int index, Object object) {
assertObject(index, object, null);
}
public void assertObject(int index, Object object, MediaType mediaType) {
assertTrue(index <= this.objects.size());
assertEquals(object, this.objects.get(index));
assertEquals(mediaType, this.mediaTypes.get(index));
}
@Override
public void send(Object data, MediaType mediaType) throws IOException {
this.objects.add(data);
this.mediaTypes.add(mediaType);
}
@Override
public void complete() {
}
@Override
public void completeWithError(Throwable failure) {
}
}
}
......@@ -31063,6 +31063,10 @@ The following are the supported return types:
value from a thread of its own choosing.
* A `ListenableFuture<?>` can be returned when the application wants to produce the return
value from a thread of its own choosing.
* A `ResponseBodyEmitter` can be returned to write multiple objects to the response
asynchronously; also supported as the body within a `ResponseEntity`.
* An `SseEmitter` can be returned to write Server-Sent Events to the response
asynchronously; also supported as the body within a `ResponseEntity`.
* Any other return type is considered to be a single model attribute to be exposed to
the view, using the attribute name specified through `@ModelAttribute` at the method
level (or the default attribute name based on the return type class name). The model
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册