Skip to content

Commit 2896c5d

Browse files
committed
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).
1 parent 290e9be commit 2896c5d

File tree

8 files changed

+86
-59
lines changed

8 files changed

+86
-59
lines changed

spring-web/src/main/java/org/springframework/http/codec/DecoderHttpMessageReader.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,13 +129,13 @@ public Mono<T> readMono(ResolvableType actualType, ResolvableType elementType,
129129
/**
130130
* Get additional hints for decoding for example based on the server request
131131
* or annotations from controller method parameters. By default, delegate to
132-
* the decoder if it is an instance of {@link ServerHttpDecoder}.
132+
* the decoder if it is an instance of {@link HttpDecoder}.
133133
*/
134134
protected Map<String, Object> getReadHints(ResolvableType streamType,
135135
ResolvableType elementType, ServerHttpRequest request, ServerHttpResponse response) {
136136

137-
if (this.decoder instanceof ServerHttpDecoder) {
138-
ServerHttpDecoder<?> httpDecoder = (ServerHttpDecoder<?>) this.decoder;
137+
if (this.decoder instanceof HttpDecoder) {
138+
HttpDecoder<?> httpDecoder = (HttpDecoder<?>) this.decoder;
139139
return httpDecoder.getDecodeHints(streamType, elementType, request, response);
140140
}
141141
return Collections.emptyMap();

spring-web/src/main/java/org/springframework/http/codec/EncoderHttpMessageWriter.java

Lines changed: 6 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package org.springframework.http.codec;
1818

19-
import java.util.ArrayList;
2019
import java.util.Collections;
2120
import java.util.HashMap;
2221
import java.util.List;
@@ -49,22 +48,12 @@
4948
*/
5049
public class EncoderHttpMessageWriter<T> implements ServerHttpMessageWriter<T> {
5150

52-
/**
53-
* Default list of media types that signify a "streaming" scenario such that
54-
* there may be a time lag between items written and hence requires flushing.
55-
*/
56-
public static final List<MediaType> DEFAULT_STREAMING_MEDIA_TYPES =
57-
Collections.singletonList(MediaType.APPLICATION_STREAM_JSON);
58-
59-
6051
private final Encoder<T> encoder;
6152

6253
private final List<MediaType> mediaTypes;
6354

6455
private final MediaType defaultMediaType;
6556

66-
private final List<MediaType> streamingMediaTypes = new ArrayList<>(1);
67-
6857

6958
/**
7059
* Create an instance wrapping the given {@link Encoder}.
@@ -74,7 +63,6 @@ public EncoderHttpMessageWriter(Encoder<T> encoder) {
7463
this.encoder = encoder;
7564
this.mediaTypes = MediaType.asMediaTypes(encoder.getEncodableMimeTypes());
7665
this.defaultMediaType = initDefaultMediaType(this.mediaTypes);
77-
this.streamingMediaTypes.addAll(DEFAULT_STREAMING_MEDIA_TYPES);
7866
}
7967

8068
private static MediaType initDefaultMediaType(List<MediaType> mediaTypes) {
@@ -94,23 +82,6 @@ public List<MediaType> getWritableMediaTypes() {
9482
return this.mediaTypes;
9583
}
9684

97-
/**
98-
* Configure "streaming" media types for which flushing should be performed
99-
* automatically vs at the end of the input stream.
100-
* <p>By default this is set to {@link #DEFAULT_STREAMING_MEDIA_TYPES}.
101-
* @param mediaTypes one or more media types to add to the list
102-
*/
103-
public void setStreamingMediaTypes(List<MediaType> mediaTypes) {
104-
this.streamingMediaTypes.addAll(mediaTypes);
105-
}
106-
107-
/**
108-
* Return the configured list of "streaming" media types.
109-
*/
110-
public List<MediaType> getStreamingMediaTypes() {
111-
return Collections.unmodifiableList(this.streamingMediaTypes);
112-
}
113-
11485

11586
@Override
11687
public boolean canWrite(ResolvableType elementType, MediaType mediaType) {
@@ -159,7 +130,9 @@ private static MediaType addDefaultCharset(MediaType main, MediaType defaultType
159130
}
160131

161132
private boolean isStreamingMediaType(MediaType contentType) {
162-
return this.streamingMediaTypes.stream().anyMatch(contentType::isCompatibleWith);
133+
return this.encoder instanceof HttpEncoder &&
134+
((HttpEncoder<?>) this.encoder).getStreamingMediaTypes().stream()
135+
.anyMatch(contentType::isCompatibleWith);
163136
}
164137

165138

@@ -180,13 +153,13 @@ public Mono<Void> write(Publisher<? extends T> inputStream, ResolvableType actua
180153
/**
181154
* Get additional hints for encoding for example based on the server request
182155
* or annotations from controller method parameters. By default, delegate to
183-
* the encoder if it is an instance of {@link ServerHttpEncoder}.
156+
* the encoder if it is an instance of {@link HttpEncoder}.
184157
*/
185158
protected Map<String, Object> getWriteHints(ResolvableType streamType, ResolvableType elementType,
186159
MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response) {
187160

188-
if (this.encoder instanceof ServerHttpEncoder) {
189-
ServerHttpEncoder<?> httpEncoder = (ServerHttpEncoder<?>) this.encoder;
161+
if (this.encoder instanceof HttpEncoder) {
162+
HttpEncoder<?> httpEncoder = (HttpEncoder<?>) this.encoder;
190163
return httpEncoder.getEncodeHints(streamType, elementType, mediaType, request, response);
191164
}
192165
return Collections.emptyMap();

spring-web/src/main/java/org/springframework/http/codec/ServerHttpDecoder.java renamed to spring-web/src/main/java/org/springframework/http/codec/HttpDecoder.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,13 @@
2424
import org.springframework.http.server.reactive.ServerHttpResponse;
2525

2626
/**
27-
* {@code Decoder} extension for server-side decoding of the HTTP request body.
27+
* Extension of {@code Decoder} exposing extra methods relevant in the context
28+
* of HTTP applications.
2829
*
2930
* @author Rossen Stoyanchev
3031
* @since 5.0
3132
*/
32-
public interface ServerHttpDecoder<T> extends Decoder<T> {
33+
public interface HttpDecoder<T> extends Decoder<T> {
3334

3435
/**
3536
* Get decoding hints based on the server request or annotations on the

spring-web/src/main/java/org/springframework/http/codec/ServerHttpEncoder.java renamed to spring-web/src/main/java/org/springframework/http/codec/HttpEncoder.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.http.codec;
1818

19+
import java.util.Collections;
20+
import java.util.List;
1921
import java.util.Map;
2022

2123
import org.springframework.core.ResolvableType;
@@ -26,12 +28,19 @@
2628

2729

2830
/**
29-
* {@code Encoder} extension for server-side encoding of the HTTP response body.
31+
* Extension of {@code Encoder} exposing extra methods relevant in the context
32+
* of HTTP applications.
3033
*
3134
* @author Rossen Stoyanchev
3235
* @since 5.0
3336
*/
34-
public interface ServerHttpEncoder<T> extends Encoder<T> {
37+
public interface HttpEncoder<T> extends Encoder<T> {
38+
39+
/**
40+
* Return "streaming" media types for which flushing should be performed
41+
* automatically vs at the end of the input stream.
42+
*/
43+
List<MediaType> getStreamingMediaTypes();
3544

3645
/**
3746
* Get decoding hints based on the server request or annotations on the
@@ -46,7 +55,10 @@ public interface ServerHttpEncoder<T> extends Encoder<T> {
4655
* @param response the current response
4756
* @return a Map with hints, possibly empty
4857
*/
49-
Map<String, Object> getEncodeHints(ResolvableType actualType, ResolvableType elementType,
50-
MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response);
58+
default Map<String, Object> getEncodeHints(ResolvableType actualType, ResolvableType elementType,
59+
MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response) {
60+
61+
return Collections.emptyMap();
62+
}
5163

5264
}

spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageReader.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,13 @@ public ServerSentEventHttpMessageReader(Decoder<?> decoder) {
7171
}
7272

7373

74+
/**
75+
* Return the configured {@code Decoder}.
76+
*/
77+
public Decoder<?> getDecoder() {
78+
return this.decoder;
79+
}
80+
7481
@Override
7582
public List<MediaType> getReadableMediaTypes() {
7683
return Collections.singletonList(MediaType.TEXT_EVENT_STREAM);

spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageWriter.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@ public ServerSentEventHttpMessageWriter(Encoder<?> encoder) {
6363
}
6464

6565

66+
/**
67+
* Return the configured {@code Encoder}.
68+
*/
69+
public Encoder<?> getEncoder() {
70+
return this.encoder;
71+
}
72+
6673
@Override
6774
public List<MediaType> getWritableMediaTypes() {
6875
return WRITABLE_MEDIA_TYPES;
@@ -154,8 +161,8 @@ public Mono<Void> write(Publisher<?> input, ResolvableType actualType, Resolvabl
154161
private Map<String, Object> getEncodeHints(ResolvableType actualType, ResolvableType elementType,
155162
MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response) {
156163

157-
if (this.encoder instanceof ServerHttpEncoder) {
158-
ServerHttpEncoder<?> httpEncoder = (ServerHttpEncoder<?>) this.encoder;
164+
if (this.encoder instanceof HttpEncoder) {
165+
HttpEncoder<?> httpEncoder = (HttpEncoder<?>) this.encoder;
159166
return httpEncoder.getEncodeHints(actualType, elementType, mediaType, request, response);
160167
}
161168
return Collections.emptyMap();

spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonDecoder.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
import org.springframework.core.codec.CodecException;
3434
import org.springframework.core.io.buffer.DataBuffer;
3535
import org.springframework.core.io.buffer.DataBufferUtils;
36-
import org.springframework.http.codec.ServerHttpDecoder;
36+
import org.springframework.http.codec.HttpDecoder;
3737
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
3838
import org.springframework.http.server.reactive.ServerHttpRequest;
3939
import org.springframework.http.server.reactive.ServerHttpResponse;
@@ -48,7 +48,7 @@
4848
* @since 5.0
4949
* @see Jackson2JsonEncoder
5050
*/
51-
public class Jackson2JsonDecoder extends Jackson2CodecSupport implements ServerHttpDecoder<Object> {
51+
public class Jackson2JsonDecoder extends Jackson2CodecSupport implements HttpDecoder<Object> {
5252

5353
private final JsonObjectDecoder fluxDecoder = new JsonObjectDecoder(true);
5454

@@ -118,7 +118,7 @@ private Flux<Object> decodeInternal(JsonObjectDecoder objectDecoder, Publisher<D
118118
}
119119

120120

121-
// ServerHttpDecoder...
121+
// HttpDecoder...
122122

123123
@Override
124124
public Map<String, Object> getDecodeHints(ResolvableType actualType, ResolvableType elementType,

spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonEncoder.java

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import java.io.IOException;
2020
import java.io.OutputStream;
2121
import java.lang.annotation.Annotation;
22+
import java.util.ArrayList;
23+
import java.util.Collections;
2224
import java.util.List;
2325
import java.util.Map;
2426

@@ -40,14 +42,13 @@
4042
import org.springframework.core.io.buffer.DataBuffer;
4143
import org.springframework.core.io.buffer.DataBufferFactory;
4244
import org.springframework.http.MediaType;
43-
import org.springframework.http.codec.ServerHttpEncoder;
45+
import org.springframework.http.codec.HttpEncoder;
4446
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
4547
import org.springframework.http.server.reactive.ServerHttpRequest;
4648
import org.springframework.http.server.reactive.ServerHttpResponse;
4749
import org.springframework.util.Assert;
4850
import org.springframework.util.MimeType;
4951

50-
import static org.springframework.http.MediaType.APPLICATION_STREAM_JSON;
5152

5253
/**
5354
* Encode from an {@code Object} stream to a byte stream of JSON objects,
@@ -58,34 +59,55 @@
5859
* @since 5.0
5960
* @see Jackson2JsonDecoder
6061
*/
61-
public class Jackson2JsonEncoder extends Jackson2CodecSupport implements ServerHttpEncoder<Object> {
62+
public class Jackson2JsonEncoder extends Jackson2CodecSupport implements HttpEncoder<Object> {
63+
64+
private final List<MediaType> streamingMediaTypes = new ArrayList<>(1);
6265

6366
private final PrettyPrinter ssePrettyPrinter;
6467

6568

69+
6670
public Jackson2JsonEncoder() {
6771
this(Jackson2ObjectMapperBuilder.json().build());
6872
}
6973

7074
public Jackson2JsonEncoder(ObjectMapper mapper) {
7175
super(mapper);
72-
DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter();
73-
prettyPrinter.indentObjectsWith(new DefaultIndenter(" ", "\ndata:"));
74-
this.ssePrettyPrinter = prettyPrinter;
76+
this.streamingMediaTypes.add(MediaType.APPLICATION_STREAM_JSON);
77+
this.ssePrettyPrinter = initSsePrettyPrinter();
7578
}
7679

80+
private static PrettyPrinter initSsePrettyPrinter() {
81+
DefaultPrettyPrinter printer = new DefaultPrettyPrinter();
82+
printer.indentObjectsWith(new DefaultIndenter(" ", "\ndata:"));
83+
return printer;
84+
}
7785

78-
@Override
79-
public boolean canEncode(ResolvableType elementType, MimeType mimeType) {
80-
return this.mapper.canSerialize(elementType.getRawClass()) &&
81-
(mimeType == null || JSON_MIME_TYPES.stream().anyMatch(m -> m.isCompatibleWith(mimeType)));
86+
87+
/**
88+
* Configure "streaming" media types for which flushing should be performed
89+
* automatically vs at the end of the stream.
90+
* <p>By default this is set to {@link MediaType#APPLICATION_STREAM_JSON}.
91+
* @param mediaTypes one or more media types to add to the list
92+
* @see HttpEncoder#getStreamingMediaTypes()
93+
*/
94+
public void setStreamingMediaTypes(List<MediaType> mediaTypes) {
95+
this.streamingMediaTypes.clear();
96+
this.streamingMediaTypes.addAll(mediaTypes);
8297
}
8398

8499
@Override
85100
public List<MimeType> getEncodableMimeTypes() {
86101
return JSON_MIME_TYPES;
87102
}
88103

104+
105+
@Override
106+
public boolean canEncode(ResolvableType elementType, MimeType mimeType) {
107+
return this.mapper.canSerialize(elementType.getRawClass()) &&
108+
(mimeType == null || JSON_MIME_TYPES.stream().anyMatch(m -> m.isCompatibleWith(mimeType)));
109+
}
110+
89111
@Override
90112
public Flux<DataBuffer> encode(Publisher<?> inputStream, DataBufferFactory bufferFactory,
91113
ResolvableType elementType, MimeType mimeType, Map<String, Object> hints) {
@@ -98,7 +120,7 @@ public Flux<DataBuffer> encode(Publisher<?> inputStream, DataBufferFactory buffe
98120
return Flux.from(inputStream).map(value ->
99121
encodeValue(value, mimeType, bufferFactory, elementType, hints));
100122
}
101-
else if (APPLICATION_STREAM_JSON.isCompatibleWith(mimeType)) {
123+
else if (MediaType.APPLICATION_STREAM_JSON.isCompatibleWith(mimeType)) {
102124
return Flux.from(inputStream).map(value -> {
103125
DataBuffer buffer = encodeValue(value, mimeType, bufferFactory, elementType, hints);
104126
buffer.write(new byte[]{'\n'});
@@ -147,7 +169,12 @@ private DataBuffer encodeValue(Object value, MimeType mimeType, DataBufferFactor
147169
}
148170

149171

150-
// ServerHttpEncoder...
172+
// HttpEncoder...
173+
174+
@Override
175+
public List<MediaType> getStreamingMediaTypes() {
176+
return Collections.unmodifiableList(this.streamingMediaTypes);
177+
}
151178

152179
@Override
153180
public Map<String, Object> getEncodeHints(ResolvableType actualType, ResolvableType elementType,

0 commit comments

Comments
 (0)