Flush JSON stream after each element
Issue: SPR-15104
This commit is contained in:
@@ -31,6 +31,13 @@ import org.springframework.util.MimeType;
|
|||||||
*/
|
*/
|
||||||
public abstract class AbstractEncoder<T> implements Encoder<T> {
|
public abstract class AbstractEncoder<T> implements Encoder<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hint key to use with a {@link FlushingStrategy} value.
|
||||||
|
*/
|
||||||
|
public static final String FLUSHING_STRATEGY_HINT = AbstractEncoder.class.getName() + ".flushingStrategy";
|
||||||
|
|
||||||
|
public enum FlushingStrategy { AUTO, AFTER_EACH_ELEMENT }
|
||||||
|
|
||||||
private final List<MimeType> encodableMimeTypes;
|
private final List<MimeType> encodableMimeTypes;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.reactivestreams.Publisher;
|
import org.reactivestreams.Publisher;
|
||||||
|
import static org.springframework.core.codec.AbstractEncoder.FLUSHING_STRATEGY_HINT;
|
||||||
|
import static org.springframework.core.codec.AbstractEncoder.FlushingStrategy.AFTER_EACH_ELEMENT;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
@@ -102,7 +104,8 @@ public class EncoderHttpMessageWriter<T> implements HttpMessageWriter<T> {
|
|||||||
|
|
||||||
DataBufferFactory bufferFactory = outputMessage.bufferFactory();
|
DataBufferFactory bufferFactory = outputMessage.bufferFactory();
|
||||||
Flux<DataBuffer> body = this.encoder.encode(inputStream, bufferFactory, elementType, mediaType, hints);
|
Flux<DataBuffer> body = this.encoder.encode(inputStream, bufferFactory, elementType, mediaType, hints);
|
||||||
return outputMessage.writeWith(body);
|
return (hints.get(FLUSHING_STRATEGY_HINT) == AFTER_EACH_ELEMENT ?
|
||||||
|
outputMessage.writeAndFlushWith(body.map(Flux::just)) : outputMessage.writeWith(body));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -21,19 +21,24 @@ import java.util.HashMap;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonView;
|
import com.fasterxml.jackson.annotation.JsonView;
|
||||||
|
import org.reactivestreams.Publisher;
|
||||||
|
import static org.springframework.core.codec.AbstractEncoder.FLUSHING_STRATEGY_HINT;
|
||||||
|
import static org.springframework.core.codec.AbstractEncoder.FlushingStrategy.AFTER_EACH_ELEMENT;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import org.springframework.core.MethodParameter;
|
import org.springframework.core.MethodParameter;
|
||||||
import org.springframework.core.ResolvableType;
|
import org.springframework.core.ResolvableType;
|
||||||
|
import org.springframework.core.codec.AbstractEncoder;
|
||||||
import org.springframework.core.codec.Encoder;
|
import org.springframework.core.codec.Encoder;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ReactiveHttpOutputMessage;
|
||||||
import org.springframework.http.codec.json.AbstractJackson2Codec;
|
import org.springframework.http.codec.json.AbstractJackson2Codec;
|
||||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link ServerHttpMessageWriter} that resolves those annotation or request based Jackson 2 hints:
|
* Jackson {@link ServerHttpMessageWriter} that resolves {@code @JsonView} annotated handler
|
||||||
* <ul>
|
* method and deals with {@link AbstractEncoder#FLUSHING_STRATEGY_HINT}.
|
||||||
* <li>{@code @JsonView} annotated handler method</li>
|
|
||||||
* </ul>
|
|
||||||
*
|
*
|
||||||
* @author Sebastien Deleuze
|
* @author Sebastien Deleuze
|
||||||
* @since 5.0
|
* @since 5.0
|
||||||
@@ -72,4 +77,27 @@ public class Jackson2ServerHttpMessageWriter extends AbstractServerHttpMessageWr
|
|||||||
return hints;
|
return hints;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> write(Publisher<?> inputStream, ResolvableType elementType, MediaType mediaType,
|
||||||
|
ReactiveHttpOutputMessage outputMessage, Map<String, Object> hints) {
|
||||||
|
|
||||||
|
if ((mediaType != null) && mediaType.isCompatibleWith(MediaType.APPLICATION_STREAM_JSON)) {
|
||||||
|
Map<String, Object> hintsWithFlush = new HashMap<>(hints);
|
||||||
|
hintsWithFlush.put(FLUSHING_STRATEGY_HINT, AFTER_EACH_ELEMENT);
|
||||||
|
return super.write(inputStream, elementType, mediaType, outputMessage, hintsWithFlush);
|
||||||
|
}
|
||||||
|
return super.write(inputStream, elementType, mediaType, outputMessage, hints);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> write(Publisher<?> inputStream, ResolvableType streamType, ResolvableType elementType,
|
||||||
|
MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response, Map<String, Object> hints) {
|
||||||
|
|
||||||
|
if ((mediaType != null) && mediaType.isCompatibleWith(MediaType.APPLICATION_STREAM_JSON)) {
|
||||||
|
Map<String, Object> hintsWithFlush = new HashMap<>(hints);
|
||||||
|
hintsWithFlush.put(FLUSHING_STRATEGY_HINT, AFTER_EACH_ELEMENT);
|
||||||
|
return super.write(inputStream, streamType, elementType, mediaType, request, response, hintsWithFlush);
|
||||||
|
}
|
||||||
|
return super.write(inputStream, streamType, elementType, mediaType, request, response, hints);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import org.springframework.http.codec.EncoderHttpMessageWriter;
|
|||||||
import org.springframework.http.codec.FormHttpMessageReader;
|
import org.springframework.http.codec.FormHttpMessageReader;
|
||||||
import org.springframework.http.codec.HttpMessageReader;
|
import org.springframework.http.codec.HttpMessageReader;
|
||||||
import org.springframework.http.codec.HttpMessageWriter;
|
import org.springframework.http.codec.HttpMessageWriter;
|
||||||
|
import org.springframework.http.codec.Jackson2ServerHttpMessageWriter;
|
||||||
import org.springframework.http.codec.ResourceHttpMessageWriter;
|
import org.springframework.http.codec.ResourceHttpMessageWriter;
|
||||||
import org.springframework.http.codec.ServerSentEventHttpMessageWriter;
|
import org.springframework.http.codec.ServerSentEventHttpMessageWriter;
|
||||||
import org.springframework.http.codec.json.Jackson2JsonDecoder;
|
import org.springframework.http.codec.json.Jackson2JsonDecoder;
|
||||||
@@ -97,7 +98,7 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder {
|
|||||||
if (jackson2Present) {
|
if (jackson2Present) {
|
||||||
messageReader(new DecoderHttpMessageReader<>(new Jackson2JsonDecoder()));
|
messageReader(new DecoderHttpMessageReader<>(new Jackson2JsonDecoder()));
|
||||||
Jackson2JsonEncoder jsonEncoder = new Jackson2JsonEncoder();
|
Jackson2JsonEncoder jsonEncoder = new Jackson2JsonEncoder();
|
||||||
messageWriter(new EncoderHttpMessageWriter<>(jsonEncoder));
|
messageWriter(new Jackson2ServerHttpMessageWriter(jsonEncoder));
|
||||||
messageWriter(
|
messageWriter(
|
||||||
new ServerSentEventHttpMessageWriter(Collections.singletonList(jsonEncoder)));
|
new ServerSentEventHttpMessageWriter(Collections.singletonList(jsonEncoder)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ package org.springframework.web.reactive.result.method.annotation;
|
|||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Ignore;
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.test.StepVerifier;
|
import reactor.test.StepVerifier;
|
||||||
@@ -42,7 +41,6 @@ import static org.springframework.http.MediaType.APPLICATION_STREAM_JSON_VALUE;
|
|||||||
/**
|
/**
|
||||||
* @author Sebastien Deleuze
|
* @author Sebastien Deleuze
|
||||||
*/
|
*/
|
||||||
@Ignore
|
|
||||||
public class JsonStreamingIntegrationTests extends AbstractHttpHandlerIntegrationTests {
|
public class JsonStreamingIntegrationTests extends AbstractHttpHandlerIntegrationTests {
|
||||||
|
|
||||||
private AnnotationConfigApplicationContext wac;
|
private AnnotationConfigApplicationContext wac;
|
||||||
@@ -78,7 +76,8 @@ public class JsonStreamingIntegrationTests extends AbstractHttpHandlerIntegratio
|
|||||||
StepVerifier.create(result)
|
StepVerifier.create(result)
|
||||||
.expectNext(new Person("foo 0"))
|
.expectNext(new Person("foo 0"))
|
||||||
.expectNext(new Person("foo 1"))
|
.expectNext(new Person("foo 1"))
|
||||||
.verifyComplete();
|
.thenCancel()
|
||||||
|
.verify();
|
||||||
}
|
}
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -87,7 +86,7 @@ public class JsonStreamingIntegrationTests extends AbstractHttpHandlerIntegratio
|
|||||||
|
|
||||||
@RequestMapping(value = "/stream", produces = APPLICATION_STREAM_JSON_VALUE)
|
@RequestMapping(value = "/stream", produces = APPLICATION_STREAM_JSON_VALUE)
|
||||||
Flux<Person> person() {
|
Flux<Person> person() {
|
||||||
return Flux.interval(Duration.ofMillis(100)).map(l -> new Person("foo " + l)).take(2);
|
return Flux.interval(Duration.ofMillis(100)).map(l -> new Person("foo " + l));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user