Skip to content

Commit 43faa43

Browse files
committed
Refine kotlinx.serialization support
This commit introduces the following changes: - Converters/codecs are now used based on generic type info. - On WebMvc and WebFlux, kotlinx.serialization is enabled along to Jackson because it only serializes Kotlin @serializable classes which is not enough for error or actuator endpoints in Boot as described on spring-projects/spring-boot#24238. TODO: leverage Kotlin/kotlinx.serialization#1164 when fixed. Closes gh-26147
1 parent 1f701d9 commit 43faa43

File tree

13 files changed

+142
-35
lines changed

13 files changed

+142
-35
lines changed

spring-messaging/src/main/java/org/springframework/messaging/converter/KotlinSerializationJsonMessageConverter.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ protected String toJson(Object payload, Type resolvedType) {
9494
* Tries to find a serializer that can marshall or unmarshall instances of the given type
9595
* using kotlinx.serialization. If no serializer can be found, an exception is thrown.
9696
* <p>Resolved serializers are cached and cached results are returned on successive calls.
97+
* TODO Avoid relying on throwing exception when https://github.com/Kotlin/kotlinx.serialization/pull/1164 is fixed
9798
* @param type the type to find a serializer for
9899
* @return a resolved serializer for the given type
99100
* @throws RuntimeException if no serializer supporting the given type can be found

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

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,38 @@ public KotlinSerializationJsonDecoder(Json json) {
6969
this.json = json;
7070
}
7171

72+
/**
73+
* Configure a limit on the number of bytes that can be buffered whenever
74+
* the input stream needs to be aggregated. This can be a result of
75+
* decoding to a single {@code DataBuffer},
76+
* {@link java.nio.ByteBuffer ByteBuffer}, {@code byte[]},
77+
* {@link org.springframework.core.io.Resource Resource}, {@code String}, etc.
78+
* It can also occur when splitting the input stream, e.g. delimited text,
79+
* in which case the limit applies to data buffered between delimiters.
80+
* <p>By default this is set to 256K.
81+
* @param byteCount the max number of bytes to buffer, or -1 for unlimited
82+
*/
83+
public void setMaxInMemorySize(int byteCount) {
84+
this.stringDecoder.setMaxInMemorySize(byteCount);
85+
}
86+
87+
/**
88+
* Return the {@link #setMaxInMemorySize configured} byte count limit.
89+
*/
90+
public int getMaxInMemorySize() {
91+
return this.stringDecoder.getMaxInMemorySize();
92+
}
93+
7294

7395
@Override
7496
public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) {
75-
return (super.canDecode(elementType, mimeType) && !CharSequence.class.isAssignableFrom(elementType.toClass()));
97+
try {
98+
serializer(elementType.getType());
99+
return (super.canDecode(elementType, mimeType) && !CharSequence.class.isAssignableFrom(elementType.toClass()));
100+
}
101+
catch (Exception ex) {
102+
return false;
103+
}
76104
}
77105

78106
@Override
@@ -95,6 +123,7 @@ public Mono<Object> decodeToMono(Publisher<DataBuffer> inputStream, ResolvableTy
95123
* Tries to find a serializer that can marshall or unmarshall instances of the given type
96124
* using kotlinx.serialization. If no serializer can be found, an exception is thrown.
97125
* <p>Resolved serializers are cached and cached results are returned on successive calls.
126+
* TODO Avoid relying on throwing exception when https://github.com/Kotlin/kotlinx.serialization/pull/1164 is fixed
98127
* @param type the type to find a serializer for
99128
* @return a resolved serializer for the given type
100129
* @throws RuntimeException if no serializer supporting the given type can be found

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,14 @@ public KotlinSerializationJsonEncoder(Json json) {
7171

7272
@Override
7373
public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) {
74-
return (super.canEncode(elementType, mimeType) && !String.class.isAssignableFrom(elementType.toClass()) &&
75-
!ServerSentEvent.class.isAssignableFrom(elementType.toClass()));
74+
try {
75+
serializer(elementType.getType());
76+
return (super.canEncode(elementType, mimeType) && !String.class.isAssignableFrom(elementType.toClass()) &&
77+
!ServerSentEvent.class.isAssignableFrom(elementType.toClass()));
78+
}
79+
catch (Exception ex) {
80+
return false;
81+
}
7682
}
7783

7884
@Override
@@ -105,6 +111,7 @@ public DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory,
105111
* Tries to find a serializer that can marshall or unmarshall instances of the given type
106112
* using kotlinx.serialization. If no serializer can be found, an exception is thrown.
107113
* <p>Resolved serializers are cached and cached results are returned on successive calls.
114+
* TODO Avoid relying on throwing exception when https://github.com/Kotlin/kotlinx.serialization/pull/1164 is fixed
108115
* @param type the type to find a serializer for
109116
* @return a resolved serializer for the given type
110117
* @throws RuntimeException if no serializer supporting the given type can be found

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,11 @@ private void initCodec(@Nullable Object codec) {
312312
((ProtobufDecoder) codec).setMaxMessageSize(size);
313313
}
314314
}
315+
if (kotlinSerializationJsonPresent) {
316+
if (codec instanceof KotlinSerializationJsonDecoder) {
317+
((KotlinSerializationJsonDecoder) codec).setMaxInMemorySize(size);
318+
}
319+
}
315320
if (jackson2Present) {
316321
if (codec instanceof AbstractJackson2Decoder) {
317322
((AbstractJackson2Decoder) codec).setMaxInMemorySize(size);
@@ -385,12 +390,12 @@ final List<HttpMessageReader<?>> getObjectReaders() {
385390
return Collections.emptyList();
386391
}
387392
List<HttpMessageReader<?>> readers = new ArrayList<>();
393+
if (kotlinSerializationJsonPresent) {
394+
addCodec(readers, new DecoderHttpMessageReader<>(getKotlinSerializationJsonDecoder()));
395+
}
388396
if (jackson2Present) {
389397
addCodec(readers, new DecoderHttpMessageReader<>(getJackson2JsonDecoder()));
390398
}
391-
else if (kotlinSerializationJsonPresent) {
392-
addCodec(readers, new DecoderHttpMessageReader<>(getKotlinSerializationJsonDecoder()));
393-
}
394399
if (jackson2SmilePresent) {
395400
addCodec(readers, new DecoderHttpMessageReader<>(this.jackson2SmileDecoder != null ?
396401
(Jackson2SmileDecoder) this.jackson2SmileDecoder : new Jackson2SmileDecoder()));
@@ -484,12 +489,12 @@ final List<HttpMessageWriter<?>> getObjectWriters() {
484489
*/
485490
final List<HttpMessageWriter<?>> getBaseObjectWriters() {
486491
List<HttpMessageWriter<?>> writers = new ArrayList<>();
492+
if (kotlinSerializationJsonPresent) {
493+
writers.add(new EncoderHttpMessageWriter<>(getKotlinSerializationJsonEncoder()));
494+
}
487495
if (jackson2Present) {
488496
writers.add(new EncoderHttpMessageWriter<>(getJackson2JsonEncoder()));
489497
}
490-
else if (kotlinSerializationJsonPresent) {
491-
writers.add(new EncoderHttpMessageWriter<>(getKotlinSerializationJsonEncoder()));
492-
}
493498
if (jackson2SmilePresent) {
494499
writers.add(new EncoderHttpMessageWriter<>(this.jackson2SmileEncoder != null ?
495500
(Jackson2SmileEncoder) this.jackson2SmileEncoder : new Jackson2SmileEncoder()));

spring-web/src/main/java/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverter.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,28 @@ protected boolean supports(Class<?> clazz) {
8888
}
8989
}
9090

91+
@Override
92+
public boolean canRead(Type type, @Nullable Class<?> contextClass, @Nullable MediaType mediaType) {
93+
try {
94+
serializer(GenericTypeResolver.resolveType(type, contextClass));
95+
return canRead(mediaType);
96+
}
97+
catch (Exception ex) {
98+
return false;
99+
}
100+
}
101+
102+
@Override
103+
public boolean canWrite(@Nullable Type type, @Nullable Class<?> clazz, @Nullable MediaType mediaType) {
104+
try {
105+
serializer(GenericTypeResolver.resolveType(type, clazz));
106+
return canWrite(mediaType);
107+
}
108+
catch (Exception ex) {
109+
return false;
110+
}
111+
}
112+
91113
@Override
92114
public final Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage)
93115
throws IOException, HttpMessageNotReadableException {
@@ -151,6 +173,7 @@ private Charset getCharsetToUse(@Nullable MediaType contentType) {
151173
* Tries to find a serializer that can marshall or unmarshall instances of the given type
152174
* using kotlinx.serialization. If no serializer can be found, an exception is thrown.
153175
* <p>Resolved serializers are cached and cached results are returned on successive calls.
176+
* TODO Avoid relying on throwing exception when https://github.com/Kotlin/kotlinx.serialization/pull/1164 is fixed
154177
* @param type the type to find a serializer for
155178
* @return a resolved serializer for the given type
156179
* @throws RuntimeException if no serializer supporting the given type can be found

spring-web/src/test/java/org/springframework/http/codec/support/ClientCodecConfigurerTests.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@
5656
import org.springframework.http.codec.json.Jackson2JsonEncoder;
5757
import org.springframework.http.codec.json.Jackson2SmileDecoder;
5858
import org.springframework.http.codec.json.Jackson2SmileEncoder;
59+
import org.springframework.http.codec.json.KotlinSerializationJsonDecoder;
60+
import org.springframework.http.codec.json.KotlinSerializationJsonEncoder;
5961
import org.springframework.http.codec.multipart.MultipartHttpMessageWriter;
6062
import org.springframework.http.codec.protobuf.ProtobufDecoder;
6163
import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter;
@@ -81,7 +83,7 @@ public class ClientCodecConfigurerTests {
8183
@Test
8284
public void defaultReaders() {
8385
List<HttpMessageReader<?>> readers = this.configurer.getReaders();
84-
assertThat(readers.size()).isEqualTo(13);
86+
assertThat(readers.size()).isEqualTo(14);
8587
assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteArrayDecoder.class);
8688
assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteBufferDecoder.class);
8789
assertThat(getNextDecoder(readers).getClass()).isEqualTo(DataBufferDecoder.class);
@@ -91,6 +93,7 @@ public void defaultReaders() {
9193
assertThat(getNextDecoder(readers).getClass()).isEqualTo(ProtobufDecoder.class);
9294
// SPR-16804
9395
assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(FormHttpMessageReader.class);
96+
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationJsonDecoder.class);
9497
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2JsonDecoder.class);
9598
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class);
9699
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class);
@@ -101,7 +104,7 @@ public void defaultReaders() {
101104
@Test
102105
public void defaultWriters() {
103106
List<HttpMessageWriter<?>> writers = this.configurer.getWriters();
104-
assertThat(writers.size()).isEqualTo(12);
107+
assertThat(writers.size()).isEqualTo(13);
105108
assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteArrayEncoder.class);
106109
assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteBufferEncoder.class);
107110
assertThat(getNextEncoder(writers).getClass()).isEqualTo(DataBufferEncoder.class);
@@ -110,6 +113,7 @@ public void defaultWriters() {
110113
assertStringEncoder(getNextEncoder(writers), true);
111114
assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ProtobufHttpMessageWriter.class);
112115
assertThat(writers.get(this.index.getAndIncrement()).getClass()).isEqualTo(MultipartHttpMessageWriter.class);
116+
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationJsonEncoder.class);
113117
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2JsonEncoder.class);
114118
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class);
115119
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class);
@@ -130,7 +134,7 @@ public void maxInMemorySize() {
130134
int size = 99;
131135
this.configurer.defaultCodecs().maxInMemorySize(size);
132136
List<HttpMessageReader<?>> readers = this.configurer.getReaders();
133-
assertThat(readers.size()).isEqualTo(13);
137+
assertThat(readers.size()).isEqualTo(14);
134138
assertThat(((ByteArrayDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
135139
assertThat(((ByteBufferDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
136140
assertThat(((DataBufferDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
@@ -140,6 +144,7 @@ public void maxInMemorySize() {
140144
assertThat(((ProtobufDecoder) getNextDecoder(readers)).getMaxMessageSize()).isEqualTo(size);
141145
assertThat(((FormHttpMessageReader) nextReader(readers)).getMaxInMemorySize()).isEqualTo(size);
142146

147+
assertThat(((KotlinSerializationJsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
143148
assertThat(((Jackson2JsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
144149
assertThat(((Jackson2SmileDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
145150
assertThat(((Jaxb2XmlDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
@@ -187,7 +192,7 @@ public void clonedConfigurer() {
187192
writers = findCodec(this.configurer.getWriters(), MultipartHttpMessageWriter.class).getPartWriters();
188193

189194
assertThat(sseDecoder).isNotSameAs(jackson2Decoder);
190-
assertThat(writers).hasSize(11);
195+
assertThat(writers).hasSize(12);
191196
}
192197

193198
@Test // gh-24194
@@ -197,7 +202,7 @@ public void cloneShouldNotDropMultipartCodecs() {
197202
List<HttpMessageWriter<?>> writers =
198203
findCodec(clone.getWriters(), MultipartHttpMessageWriter.class).getPartWriters();
199204

200-
assertThat(writers).hasSize(11);
205+
assertThat(writers).hasSize(12);
201206
}
202207

203208
@Test
@@ -211,7 +216,7 @@ public void cloneShouldNotBeImpactedByChangesToOriginal() {
211216
List<HttpMessageWriter<?>> writers =
212217
findCodec(clone.getWriters(), MultipartHttpMessageWriter.class).getPartWriters();
213218

214-
assertThat(writers).hasSize(11);
219+
assertThat(writers).hasSize(12);
215220
}
216221

217222
private Decoder<?> getNextDecoder(List<HttpMessageReader<?>> readers) {

spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@
5252
import org.springframework.http.codec.json.Jackson2JsonEncoder;
5353
import org.springframework.http.codec.json.Jackson2SmileDecoder;
5454
import org.springframework.http.codec.json.Jackson2SmileEncoder;
55+
import org.springframework.http.codec.json.KotlinSerializationJsonDecoder;
56+
import org.springframework.http.codec.json.KotlinSerializationJsonEncoder;
5557
import org.springframework.http.codec.protobuf.ProtobufDecoder;
5658
import org.springframework.http.codec.protobuf.ProtobufEncoder;
5759
import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter;
@@ -79,7 +81,7 @@ class CodecConfigurerTests {
7981
@Test
8082
void defaultReaders() {
8183
List<HttpMessageReader<?>> readers = this.configurer.getReaders();
82-
assertThat(readers.size()).isEqualTo(12);
84+
assertThat(readers.size()).isEqualTo(13);
8385
assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteArrayDecoder.class);
8486
assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteBufferDecoder.class);
8587
assertThat(getNextDecoder(readers).getClass()).isEqualTo(DataBufferDecoder.class);
@@ -88,6 +90,7 @@ void defaultReaders() {
8890
assertStringDecoder(getNextDecoder(readers), true);
8991
assertThat(getNextDecoder(readers).getClass()).isEqualTo(ProtobufDecoder.class);
9092
assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(FormHttpMessageReader.class);
93+
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationJsonDecoder.class);
9194
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2JsonDecoder.class);
9295
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class);
9396
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class);
@@ -97,14 +100,15 @@ void defaultReaders() {
97100
@Test
98101
void defaultWriters() {
99102
List<HttpMessageWriter<?>> writers = this.configurer.getWriters();
100-
assertThat(writers.size()).isEqualTo(11);
103+
assertThat(writers.size()).isEqualTo(12);
101104
assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteArrayEncoder.class);
102105
assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteBufferEncoder.class);
103106
assertThat(getNextEncoder(writers).getClass()).isEqualTo(DataBufferEncoder.class);
104107
assertThat(getNextEncoder(writers).getClass()).isEqualTo(NettyByteBufEncoder.class);
105108
assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ResourceHttpMessageWriter.class);
106109
assertStringEncoder(getNextEncoder(writers), true);
107110
assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ProtobufHttpMessageWriter.class);
111+
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationJsonEncoder.class);
108112
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2JsonEncoder.class);
109113
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class);
110114
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class);
@@ -133,7 +137,7 @@ void defaultAndCustomReaders() {
133137

134138
List<HttpMessageReader<?>> readers = this.configurer.getReaders();
135139

136-
assertThat(readers.size()).isEqualTo(16);
140+
assertThat(readers.size()).isEqualTo(17);
137141
assertThat(getNextDecoder(readers)).isSameAs(customDecoder1);
138142
assertThat(readers.get(this.index.getAndIncrement())).isSameAs(customReader1);
139143
assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteArrayDecoder.class);
@@ -146,6 +150,7 @@ void defaultAndCustomReaders() {
146150
assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(FormHttpMessageReader.class);
147151
assertThat(getNextDecoder(readers)).isSameAs(customDecoder2);
148152
assertThat(readers.get(this.index.getAndIncrement())).isSameAs(customReader2);
153+
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationJsonDecoder.class);
149154
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2JsonDecoder.class);
150155
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class);
151156
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class);
@@ -174,7 +179,7 @@ void defaultAndCustomWriters() {
174179

175180
List<HttpMessageWriter<?>> writers = this.configurer.getWriters();
176181

177-
assertThat(writers.size()).isEqualTo(15);
182+
assertThat(writers.size()).isEqualTo(16);
178183
assertThat(getNextEncoder(writers)).isSameAs(customEncoder1);
179184
assertThat(writers.get(this.index.getAndIncrement())).isSameAs(customWriter1);
180185
assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteArrayEncoder.class);
@@ -186,6 +191,7 @@ void defaultAndCustomWriters() {
186191
assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ProtobufHttpMessageWriter.class);
187192
assertThat(getNextEncoder(writers)).isSameAs(customEncoder2);
188193
assertThat(writers.get(this.index.getAndIncrement())).isSameAs(customWriter2);
194+
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationJsonEncoder.class);
189195
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2JsonEncoder.class);
190196
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class);
191197
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class);

0 commit comments

Comments
 (0)