From 2896c5d2ab23b6faf1004d3e9aef18d23d790c04 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 21 Mar 2017 21:03:29 -0400 Subject: [PATCH] Revise "streaming" MediaType support Push the knowledge of what media types represent "streaming" down to the Encoder level where knowledge is required (e.g. to encode a JSON array vs a stream of JSON elements). --- .../http/codec/DecoderHttpMessageReader.java | 6 +-- .../http/codec/EncoderHttpMessageWriter.java | 39 +++----------- ...erverHttpDecoder.java => HttpDecoder.java} | 5 +- ...erverHttpEncoder.java => HttpEncoder.java} | 20 ++++++-- .../ServerSentEventHttpMessageReader.java | 7 +++ .../ServerSentEventHttpMessageWriter.java | 11 +++- .../http/codec/json/Jackson2JsonDecoder.java | 6 +-- .../http/codec/json/Jackson2JsonEncoder.java | 51 ++++++++++++++----- 8 files changed, 86 insertions(+), 59 deletions(-) rename spring-web/src/main/java/org/springframework/http/codec/{ServerHttpDecoder.java => HttpDecoder.java} (91%) rename spring-web/src/main/java/org/springframework/http/codec/{ServerHttpEncoder.java => HttpEncoder.java} (75%) diff --git a/spring-web/src/main/java/org/springframework/http/codec/DecoderHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/DecoderHttpMessageReader.java index bc3ee2c8001..266a6dd46d4 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/DecoderHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/DecoderHttpMessageReader.java @@ -129,13 +129,13 @@ public class DecoderHttpMessageReader implements ServerHttpMessageReader { /** * Get additional hints for decoding for example based on the server request * or annotations from controller method parameters. By default, delegate to - * the decoder if it is an instance of {@link ServerHttpDecoder}. + * the decoder if it is an instance of {@link HttpDecoder}. */ protected Map getReadHints(ResolvableType streamType, ResolvableType elementType, ServerHttpRequest request, ServerHttpResponse response) { - if (this.decoder instanceof ServerHttpDecoder) { - ServerHttpDecoder httpDecoder = (ServerHttpDecoder) this.decoder; + if (this.decoder instanceof HttpDecoder) { + HttpDecoder httpDecoder = (HttpDecoder) this.decoder; return httpDecoder.getDecodeHints(streamType, elementType, request, response); } return Collections.emptyMap(); diff --git a/spring-web/src/main/java/org/springframework/http/codec/EncoderHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/EncoderHttpMessageWriter.java index ac964034029..c7397c475b5 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/EncoderHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/EncoderHttpMessageWriter.java @@ -16,7 +16,6 @@ package org.springframework.http.codec; -import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -49,22 +48,12 @@ import org.springframework.util.Assert; */ public class EncoderHttpMessageWriter implements ServerHttpMessageWriter { - /** - * Default list of media types that signify a "streaming" scenario such that - * there may be a time lag between items written and hence requires flushing. - */ - public static final List DEFAULT_STREAMING_MEDIA_TYPES = - Collections.singletonList(MediaType.APPLICATION_STREAM_JSON); - - private final Encoder encoder; private final List mediaTypes; private final MediaType defaultMediaType; - private final List streamingMediaTypes = new ArrayList<>(1); - /** * Create an instance wrapping the given {@link Encoder}. @@ -74,7 +63,6 @@ public class EncoderHttpMessageWriter implements ServerHttpMessageWriter { this.encoder = encoder; this.mediaTypes = MediaType.asMediaTypes(encoder.getEncodableMimeTypes()); this.defaultMediaType = initDefaultMediaType(this.mediaTypes); - this.streamingMediaTypes.addAll(DEFAULT_STREAMING_MEDIA_TYPES); } private static MediaType initDefaultMediaType(List mediaTypes) { @@ -94,23 +82,6 @@ public class EncoderHttpMessageWriter implements ServerHttpMessageWriter { return this.mediaTypes; } - /** - * Configure "streaming" media types for which flushing should be performed - * automatically vs at the end of the input stream. - *

By default this is set to {@link #DEFAULT_STREAMING_MEDIA_TYPES}. - * @param mediaTypes one or more media types to add to the list - */ - public void setStreamingMediaTypes(List mediaTypes) { - this.streamingMediaTypes.addAll(mediaTypes); - } - - /** - * Return the configured list of "streaming" media types. - */ - public List getStreamingMediaTypes() { - return Collections.unmodifiableList(this.streamingMediaTypes); - } - @Override public boolean canWrite(ResolvableType elementType, MediaType mediaType) { @@ -159,7 +130,9 @@ public class EncoderHttpMessageWriter implements ServerHttpMessageWriter { } private boolean isStreamingMediaType(MediaType contentType) { - return this.streamingMediaTypes.stream().anyMatch(contentType::isCompatibleWith); + return this.encoder instanceof HttpEncoder && + ((HttpEncoder) this.encoder).getStreamingMediaTypes().stream() + .anyMatch(contentType::isCompatibleWith); } @@ -180,13 +153,13 @@ public class EncoderHttpMessageWriter implements ServerHttpMessageWriter { /** * Get additional hints for encoding for example based on the server request * or annotations from controller method parameters. By default, delegate to - * the encoder if it is an instance of {@link ServerHttpEncoder}. + * the encoder if it is an instance of {@link HttpEncoder}. */ protected Map getWriteHints(ResolvableType streamType, ResolvableType elementType, MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response) { - if (this.encoder instanceof ServerHttpEncoder) { - ServerHttpEncoder httpEncoder = (ServerHttpEncoder) this.encoder; + if (this.encoder instanceof HttpEncoder) { + HttpEncoder httpEncoder = (HttpEncoder) this.encoder; return httpEncoder.getEncodeHints(streamType, elementType, mediaType, request, response); } return Collections.emptyMap(); diff --git a/spring-web/src/main/java/org/springframework/http/codec/ServerHttpDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/HttpDecoder.java similarity index 91% rename from spring-web/src/main/java/org/springframework/http/codec/ServerHttpDecoder.java rename to spring-web/src/main/java/org/springframework/http/codec/HttpDecoder.java index f5b5dddf1ff..146bd9ce94e 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/ServerHttpDecoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/HttpDecoder.java @@ -24,12 +24,13 @@ import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; /** - * {@code Decoder} extension for server-side decoding of the HTTP request body. + * Extension of {@code Decoder} exposing extra methods relevant in the context + * of HTTP applications. * * @author Rossen Stoyanchev * @since 5.0 */ -public interface ServerHttpDecoder extends Decoder { +public interface HttpDecoder extends Decoder { /** * Get decoding hints based on the server request or annotations on the diff --git a/spring-web/src/main/java/org/springframework/http/codec/ServerHttpEncoder.java b/spring-web/src/main/java/org/springframework/http/codec/HttpEncoder.java similarity index 75% rename from spring-web/src/main/java/org/springframework/http/codec/ServerHttpEncoder.java rename to spring-web/src/main/java/org/springframework/http/codec/HttpEncoder.java index 7b5fa512b62..98b3f94357a 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/ServerHttpEncoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/HttpEncoder.java @@ -16,6 +16,8 @@ package org.springframework.http.codec; +import java.util.Collections; +import java.util.List; import java.util.Map; import org.springframework.core.ResolvableType; @@ -26,12 +28,19 @@ import org.springframework.http.server.reactive.ServerHttpResponse; /** - * {@code Encoder} extension for server-side encoding of the HTTP response body. + * Extension of {@code Encoder} exposing extra methods relevant in the context + * of HTTP applications. * * @author Rossen Stoyanchev * @since 5.0 */ -public interface ServerHttpEncoder extends Encoder { +public interface HttpEncoder extends Encoder { + + /** + * Return "streaming" media types for which flushing should be performed + * automatically vs at the end of the input stream. + */ + List getStreamingMediaTypes(); /** * Get decoding hints based on the server request or annotations on the @@ -46,7 +55,10 @@ public interface ServerHttpEncoder extends Encoder { * @param response the current response * @return a Map with hints, possibly empty */ - Map getEncodeHints(ResolvableType actualType, ResolvableType elementType, - MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response); + default Map getEncodeHints(ResolvableType actualType, ResolvableType elementType, + MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response) { + + return Collections.emptyMap(); + } } diff --git a/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageReader.java index ceae7ced20c..4c845be9422 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageReader.java @@ -71,6 +71,13 @@ public class ServerSentEventHttpMessageReader implements HttpMessageReader getDecoder() { + return this.decoder; + } + @Override public List getReadableMediaTypes() { return Collections.singletonList(MediaType.TEXT_EVENT_STREAM); diff --git a/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageWriter.java index 429e4c264c1..0be5dc00fe0 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageWriter.java @@ -63,6 +63,13 @@ public class ServerSentEventHttpMessageWriter implements ServerHttpMessageWriter } + /** + * Return the configured {@code Encoder}. + */ + public Encoder getEncoder() { + return this.encoder; + } + @Override public List getWritableMediaTypes() { return WRITABLE_MEDIA_TYPES; @@ -154,8 +161,8 @@ public class ServerSentEventHttpMessageWriter implements ServerHttpMessageWriter private Map getEncodeHints(ResolvableType actualType, ResolvableType elementType, MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response) { - if (this.encoder instanceof ServerHttpEncoder) { - ServerHttpEncoder httpEncoder = (ServerHttpEncoder) this.encoder; + if (this.encoder instanceof HttpEncoder) { + HttpEncoder httpEncoder = (HttpEncoder) this.encoder; return httpEncoder.getEncodeHints(actualType, elementType, mediaType, request, response); } return Collections.emptyMap(); diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonDecoder.java index 3c7f8f1a514..0efc204c5e1 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonDecoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonDecoder.java @@ -33,7 +33,7 @@ import org.springframework.core.ResolvableType; import org.springframework.core.codec.CodecException; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; -import org.springframework.http.codec.ServerHttpDecoder; +import org.springframework.http.codec.HttpDecoder; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; @@ -48,7 +48,7 @@ import org.springframework.util.MimeType; * @since 5.0 * @see Jackson2JsonEncoder */ -public class Jackson2JsonDecoder extends Jackson2CodecSupport implements ServerHttpDecoder { +public class Jackson2JsonDecoder extends Jackson2CodecSupport implements HttpDecoder { private final JsonObjectDecoder fluxDecoder = new JsonObjectDecoder(true); @@ -118,7 +118,7 @@ public class Jackson2JsonDecoder extends Jackson2CodecSupport implements ServerH } - // ServerHttpDecoder... + // HttpDecoder... @Override public Map getDecodeHints(ResolvableType actualType, ResolvableType elementType, diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonEncoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonEncoder.java index 06fd3f30274..ad63326ceb0 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonEncoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonEncoder.java @@ -19,6 +19,8 @@ package org.springframework.http.codec.json; import java.io.IOException; import java.io.OutputStream; import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -40,14 +42,13 @@ import org.springframework.core.codec.CodecException; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.MediaType; -import org.springframework.http.codec.ServerHttpEncoder; +import org.springframework.http.codec.HttpEncoder; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; import org.springframework.util.MimeType; -import static org.springframework.http.MediaType.APPLICATION_STREAM_JSON; /** * Encode from an {@code Object} stream to a byte stream of JSON objects, @@ -58,27 +59,41 @@ import static org.springframework.http.MediaType.APPLICATION_STREAM_JSON; * @since 5.0 * @see Jackson2JsonDecoder */ -public class Jackson2JsonEncoder extends Jackson2CodecSupport implements ServerHttpEncoder { +public class Jackson2JsonEncoder extends Jackson2CodecSupport implements HttpEncoder { + + private final List streamingMediaTypes = new ArrayList<>(1); private final PrettyPrinter ssePrettyPrinter; + public Jackson2JsonEncoder() { this(Jackson2ObjectMapperBuilder.json().build()); } public Jackson2JsonEncoder(ObjectMapper mapper) { super(mapper); - DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter(); - prettyPrinter.indentObjectsWith(new DefaultIndenter(" ", "\ndata:")); - this.ssePrettyPrinter = prettyPrinter; + this.streamingMediaTypes.add(MediaType.APPLICATION_STREAM_JSON); + this.ssePrettyPrinter = initSsePrettyPrinter(); } + private static PrettyPrinter initSsePrettyPrinter() { + DefaultPrettyPrinter printer = new DefaultPrettyPrinter(); + printer.indentObjectsWith(new DefaultIndenter(" ", "\ndata:")); + return printer; + } - @Override - public boolean canEncode(ResolvableType elementType, MimeType mimeType) { - return this.mapper.canSerialize(elementType.getRawClass()) && - (mimeType == null || JSON_MIME_TYPES.stream().anyMatch(m -> m.isCompatibleWith(mimeType))); + + /** + * Configure "streaming" media types for which flushing should be performed + * automatically vs at the end of the stream. + *

By default this is set to {@link MediaType#APPLICATION_STREAM_JSON}. + * @param mediaTypes one or more media types to add to the list + * @see HttpEncoder#getStreamingMediaTypes() + */ + public void setStreamingMediaTypes(List mediaTypes) { + this.streamingMediaTypes.clear(); + this.streamingMediaTypes.addAll(mediaTypes); } @Override @@ -86,6 +101,13 @@ public class Jackson2JsonEncoder extends Jackson2CodecSupport implements ServerH return JSON_MIME_TYPES; } + + @Override + public boolean canEncode(ResolvableType elementType, MimeType mimeType) { + return this.mapper.canSerialize(elementType.getRawClass()) && + (mimeType == null || JSON_MIME_TYPES.stream().anyMatch(m -> m.isCompatibleWith(mimeType))); + } + @Override public Flux encode(Publisher inputStream, DataBufferFactory bufferFactory, ResolvableType elementType, MimeType mimeType, Map hints) { @@ -98,7 +120,7 @@ public class Jackson2JsonEncoder extends Jackson2CodecSupport implements ServerH return Flux.from(inputStream).map(value -> encodeValue(value, mimeType, bufferFactory, elementType, hints)); } - else if (APPLICATION_STREAM_JSON.isCompatibleWith(mimeType)) { + else if (MediaType.APPLICATION_STREAM_JSON.isCompatibleWith(mimeType)) { return Flux.from(inputStream).map(value -> { DataBuffer buffer = encodeValue(value, mimeType, bufferFactory, elementType, hints); buffer.write(new byte[]{'\n'}); @@ -147,7 +169,12 @@ public class Jackson2JsonEncoder extends Jackson2CodecSupport implements ServerH } - // ServerHttpEncoder... + // HttpEncoder... + + @Override + public List getStreamingMediaTypes() { + return Collections.unmodifiableList(this.streamingMediaTypes); + } @Override public Map getEncodeHints(ResolvableType actualType, ResolvableType elementType,