From 43faa439ab567c382e50ae670560024cebdf63d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Thu, 26 Nov 2020 12:15:23 +0100 Subject: [PATCH] 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 --- ...tlinSerializationJsonMessageConverter.java | 1 + .../json/KotlinSerializationJsonDecoder.java | 31 ++++++++++++++++++- .../json/KotlinSerializationJsonEncoder.java | 11 +++++-- .../http/codec/support/BaseDefaultCodecs.java | 17 ++++++---- ...SerializationJsonHttpMessageConverter.java | 23 ++++++++++++++ .../support/ClientCodecConfigurerTests.java | 17 ++++++---- .../codec/support/CodecConfigurerTests.java | 14 ++++++--- .../support/ServerCodecConfigurerTests.java | 9 ++++-- .../KotlinSerializationJsonDecoderTests.kt | 5 +++ .../KotlinSerializationJsonEncoderTests.kt | 6 +++- ...ializationJsonHttpMessageConverterTests.kt | 21 +++++++++++++ .../WebMvcConfigurationSupport.java | 6 ++-- src/docs/asciidoc/languages/kotlin.adoc | 16 ++++------ 13 files changed, 142 insertions(+), 35 deletions(-) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/KotlinSerializationJsonMessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/KotlinSerializationJsonMessageConverter.java index 213e424edfec..475a1f50008c 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/KotlinSerializationJsonMessageConverter.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/KotlinSerializationJsonMessageConverter.java @@ -94,6 +94,7 @@ protected String toJson(Object payload, Type resolvedType) { * Tries to find a serializer that can marshall or unmarshall instances of the given type * using kotlinx.serialization. If no serializer can be found, an exception is thrown. *

Resolved serializers are cached and cached results are returned on successive calls. + * TODO Avoid relying on throwing exception when https://github.com/Kotlin/kotlinx.serialization/pull/1164 is fixed * @param type the type to find a serializer for * @return a resolved serializer for the given type * @throws RuntimeException if no serializer supporting the given type can be found diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonDecoder.java index 0d71367f7dc1..58f582bb1e88 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonDecoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonDecoder.java @@ -69,10 +69,38 @@ public KotlinSerializationJsonDecoder(Json json) { this.json = json; } + /** + * Configure a limit on the number of bytes that can be buffered whenever + * the input stream needs to be aggregated. This can be a result of + * decoding to a single {@code DataBuffer}, + * {@link java.nio.ByteBuffer ByteBuffer}, {@code byte[]}, + * {@link org.springframework.core.io.Resource Resource}, {@code String}, etc. + * It can also occur when splitting the input stream, e.g. delimited text, + * in which case the limit applies to data buffered between delimiters. + *

By default this is set to 256K. + * @param byteCount the max number of bytes to buffer, or -1 for unlimited + */ + public void setMaxInMemorySize(int byteCount) { + this.stringDecoder.setMaxInMemorySize(byteCount); + } + + /** + * Return the {@link #setMaxInMemorySize configured} byte count limit. + */ + public int getMaxInMemorySize() { + return this.stringDecoder.getMaxInMemorySize(); + } + @Override public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) { - return (super.canDecode(elementType, mimeType) && !CharSequence.class.isAssignableFrom(elementType.toClass())); + try { + serializer(elementType.getType()); + return (super.canDecode(elementType, mimeType) && !CharSequence.class.isAssignableFrom(elementType.toClass())); + } + catch (Exception ex) { + return false; + } } @Override @@ -95,6 +123,7 @@ public Mono decodeToMono(Publisher inputStream, ResolvableTy * Tries to find a serializer that can marshall or unmarshall instances of the given type * using kotlinx.serialization. If no serializer can be found, an exception is thrown. *

Resolved serializers are cached and cached results are returned on successive calls. + * TODO Avoid relying on throwing exception when https://github.com/Kotlin/kotlinx.serialization/pull/1164 is fixed * @param type the type to find a serializer for * @return a resolved serializer for the given type * @throws RuntimeException if no serializer supporting the given type can be found diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonEncoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonEncoder.java index f2a619734c61..cd577d4c8280 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonEncoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonEncoder.java @@ -71,8 +71,14 @@ public KotlinSerializationJsonEncoder(Json json) { @Override public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) { - return (super.canEncode(elementType, mimeType) && !String.class.isAssignableFrom(elementType.toClass()) && - !ServerSentEvent.class.isAssignableFrom(elementType.toClass())); + try { + serializer(elementType.getType()); + return (super.canEncode(elementType, mimeType) && !String.class.isAssignableFrom(elementType.toClass()) && + !ServerSentEvent.class.isAssignableFrom(elementType.toClass())); + } + catch (Exception ex) { + return false; + } } @Override @@ -105,6 +111,7 @@ public DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory, * Tries to find a serializer that can marshall or unmarshall instances of the given type * using kotlinx.serialization. If no serializer can be found, an exception is thrown. *

Resolved serializers are cached and cached results are returned on successive calls. + * TODO Avoid relying on throwing exception when https://github.com/Kotlin/kotlinx.serialization/pull/1164 is fixed * @param type the type to find a serializer for * @return a resolved serializer for the given type * @throws RuntimeException if no serializer supporting the given type can be found diff --git a/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java b/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java index 0f40420298e2..00bc8887fae8 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java +++ b/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java @@ -312,6 +312,11 @@ private void initCodec(@Nullable Object codec) { ((ProtobufDecoder) codec).setMaxMessageSize(size); } } + if (kotlinSerializationJsonPresent) { + if (codec instanceof KotlinSerializationJsonDecoder) { + ((KotlinSerializationJsonDecoder) codec).setMaxInMemorySize(size); + } + } if (jackson2Present) { if (codec instanceof AbstractJackson2Decoder) { ((AbstractJackson2Decoder) codec).setMaxInMemorySize(size); @@ -385,12 +390,12 @@ final List> getObjectReaders() { return Collections.emptyList(); } List> readers = new ArrayList<>(); + if (kotlinSerializationJsonPresent) { + addCodec(readers, new DecoderHttpMessageReader<>(getKotlinSerializationJsonDecoder())); + } if (jackson2Present) { addCodec(readers, new DecoderHttpMessageReader<>(getJackson2JsonDecoder())); } - else if (kotlinSerializationJsonPresent) { - addCodec(readers, new DecoderHttpMessageReader<>(getKotlinSerializationJsonDecoder())); - } if (jackson2SmilePresent) { addCodec(readers, new DecoderHttpMessageReader<>(this.jackson2SmileDecoder != null ? (Jackson2SmileDecoder) this.jackson2SmileDecoder : new Jackson2SmileDecoder())); @@ -484,12 +489,12 @@ final List> getObjectWriters() { */ final List> getBaseObjectWriters() { List> writers = new ArrayList<>(); + if (kotlinSerializationJsonPresent) { + writers.add(new EncoderHttpMessageWriter<>(getKotlinSerializationJsonEncoder())); + } if (jackson2Present) { writers.add(new EncoderHttpMessageWriter<>(getJackson2JsonEncoder())); } - else if (kotlinSerializationJsonPresent) { - writers.add(new EncoderHttpMessageWriter<>(getKotlinSerializationJsonEncoder())); - } if (jackson2SmilePresent) { writers.add(new EncoderHttpMessageWriter<>(this.jackson2SmileEncoder != null ? (Jackson2SmileEncoder) this.jackson2SmileEncoder : new Jackson2SmileEncoder())); diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverter.java index 9190f21cd7ba..7fe280a770c0 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverter.java @@ -88,6 +88,28 @@ protected boolean supports(Class clazz) { } } + @Override + public boolean canRead(Type type, @Nullable Class contextClass, @Nullable MediaType mediaType) { + try { + serializer(GenericTypeResolver.resolveType(type, contextClass)); + return canRead(mediaType); + } + catch (Exception ex) { + return false; + } + } + + @Override + public boolean canWrite(@Nullable Type type, @Nullable Class clazz, @Nullable MediaType mediaType) { + try { + serializer(GenericTypeResolver.resolveType(type, clazz)); + return canWrite(mediaType); + } + catch (Exception ex) { + return false; + } + } + @Override public final Object read(Type type, @Nullable Class contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { @@ -151,6 +173,7 @@ private Charset getCharsetToUse(@Nullable MediaType contentType) { * Tries to find a serializer that can marshall or unmarshall instances of the given type * using kotlinx.serialization. If no serializer can be found, an exception is thrown. *

Resolved serializers are cached and cached results are returned on successive calls. + * TODO Avoid relying on throwing exception when https://github.com/Kotlin/kotlinx.serialization/pull/1164 is fixed * @param type the type to find a serializer for * @return a resolved serializer for the given type * @throws RuntimeException if no serializer supporting the given type can be found diff --git a/spring-web/src/test/java/org/springframework/http/codec/support/ClientCodecConfigurerTests.java b/spring-web/src/test/java/org/springframework/http/codec/support/ClientCodecConfigurerTests.java index 1c40633785a9..60bec60fb6b6 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/support/ClientCodecConfigurerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/support/ClientCodecConfigurerTests.java @@ -56,6 +56,8 @@ import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.http.codec.json.Jackson2SmileDecoder; import org.springframework.http.codec.json.Jackson2SmileEncoder; +import org.springframework.http.codec.json.KotlinSerializationJsonDecoder; +import org.springframework.http.codec.json.KotlinSerializationJsonEncoder; import org.springframework.http.codec.multipart.MultipartHttpMessageWriter; import org.springframework.http.codec.protobuf.ProtobufDecoder; import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter; @@ -81,7 +83,7 @@ public class ClientCodecConfigurerTests { @Test public void defaultReaders() { List> readers = this.configurer.getReaders(); - assertThat(readers.size()).isEqualTo(13); + assertThat(readers.size()).isEqualTo(14); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteArrayDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteBufferDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(DataBufferDecoder.class); @@ -91,6 +93,7 @@ public void defaultReaders() { assertThat(getNextDecoder(readers).getClass()).isEqualTo(ProtobufDecoder.class); // SPR-16804 assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(FormHttpMessageReader.class); + assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationJsonDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2JsonDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class); @@ -101,7 +104,7 @@ public void defaultReaders() { @Test public void defaultWriters() { List> writers = this.configurer.getWriters(); - assertThat(writers.size()).isEqualTo(12); + assertThat(writers.size()).isEqualTo(13); assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteArrayEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteBufferEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(DataBufferEncoder.class); @@ -110,6 +113,7 @@ public void defaultWriters() { assertStringEncoder(getNextEncoder(writers), true); assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ProtobufHttpMessageWriter.class); assertThat(writers.get(this.index.getAndIncrement()).getClass()).isEqualTo(MultipartHttpMessageWriter.class); + assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationJsonEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2JsonEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class); @@ -130,7 +134,7 @@ public void maxInMemorySize() { int size = 99; this.configurer.defaultCodecs().maxInMemorySize(size); List> readers = this.configurer.getReaders(); - assertThat(readers.size()).isEqualTo(13); + assertThat(readers.size()).isEqualTo(14); assertThat(((ByteArrayDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((ByteBufferDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((DataBufferDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); @@ -140,6 +144,7 @@ public void maxInMemorySize() { assertThat(((ProtobufDecoder) getNextDecoder(readers)).getMaxMessageSize()).isEqualTo(size); assertThat(((FormHttpMessageReader) nextReader(readers)).getMaxInMemorySize()).isEqualTo(size); + assertThat(((KotlinSerializationJsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((Jackson2JsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((Jackson2SmileDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((Jaxb2XmlDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); @@ -187,7 +192,7 @@ public void clonedConfigurer() { writers = findCodec(this.configurer.getWriters(), MultipartHttpMessageWriter.class).getPartWriters(); assertThat(sseDecoder).isNotSameAs(jackson2Decoder); - assertThat(writers).hasSize(11); + assertThat(writers).hasSize(12); } @Test // gh-24194 @@ -197,7 +202,7 @@ public void cloneShouldNotDropMultipartCodecs() { List> writers = findCodec(clone.getWriters(), MultipartHttpMessageWriter.class).getPartWriters(); - assertThat(writers).hasSize(11); + assertThat(writers).hasSize(12); } @Test @@ -211,7 +216,7 @@ public void cloneShouldNotBeImpactedByChangesToOriginal() { List> writers = findCodec(clone.getWriters(), MultipartHttpMessageWriter.class).getPartWriters(); - assertThat(writers).hasSize(11); + assertThat(writers).hasSize(12); } private Decoder getNextDecoder(List> readers) { diff --git a/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java b/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java index 26cb40fd2212..003646099fdd 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java @@ -52,6 +52,8 @@ import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.http.codec.json.Jackson2SmileDecoder; import org.springframework.http.codec.json.Jackson2SmileEncoder; +import org.springframework.http.codec.json.KotlinSerializationJsonDecoder; +import org.springframework.http.codec.json.KotlinSerializationJsonEncoder; import org.springframework.http.codec.protobuf.ProtobufDecoder; import org.springframework.http.codec.protobuf.ProtobufEncoder; import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter; @@ -79,7 +81,7 @@ class CodecConfigurerTests { @Test void defaultReaders() { List> readers = this.configurer.getReaders(); - assertThat(readers.size()).isEqualTo(12); + assertThat(readers.size()).isEqualTo(13); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteArrayDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteBufferDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(DataBufferDecoder.class); @@ -88,6 +90,7 @@ void defaultReaders() { assertStringDecoder(getNextDecoder(readers), true); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ProtobufDecoder.class); assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(FormHttpMessageReader.class); + assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationJsonDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2JsonDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class); @@ -97,7 +100,7 @@ void defaultReaders() { @Test void defaultWriters() { List> writers = this.configurer.getWriters(); - assertThat(writers.size()).isEqualTo(11); + assertThat(writers.size()).isEqualTo(12); assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteArrayEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteBufferEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(DataBufferEncoder.class); @@ -105,6 +108,7 @@ void defaultWriters() { assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ResourceHttpMessageWriter.class); assertStringEncoder(getNextEncoder(writers), true); assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ProtobufHttpMessageWriter.class); + assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationJsonEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2JsonEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class); @@ -133,7 +137,7 @@ void defaultAndCustomReaders() { List> readers = this.configurer.getReaders(); - assertThat(readers.size()).isEqualTo(16); + assertThat(readers.size()).isEqualTo(17); assertThat(getNextDecoder(readers)).isSameAs(customDecoder1); assertThat(readers.get(this.index.getAndIncrement())).isSameAs(customReader1); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteArrayDecoder.class); @@ -146,6 +150,7 @@ void defaultAndCustomReaders() { assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(FormHttpMessageReader.class); assertThat(getNextDecoder(readers)).isSameAs(customDecoder2); assertThat(readers.get(this.index.getAndIncrement())).isSameAs(customReader2); + assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationJsonDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2JsonDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class); @@ -174,7 +179,7 @@ void defaultAndCustomWriters() { List> writers = this.configurer.getWriters(); - assertThat(writers.size()).isEqualTo(15); + assertThat(writers.size()).isEqualTo(16); assertThat(getNextEncoder(writers)).isSameAs(customEncoder1); assertThat(writers.get(this.index.getAndIncrement())).isSameAs(customWriter1); assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteArrayEncoder.class); @@ -186,6 +191,7 @@ void defaultAndCustomWriters() { assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ProtobufHttpMessageWriter.class); assertThat(getNextEncoder(writers)).isSameAs(customEncoder2); assertThat(writers.get(this.index.getAndIncrement())).isSameAs(customWriter2); + assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationJsonEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2JsonEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class); diff --git a/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java b/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java index 3e24238068ad..0e458925b03c 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java @@ -56,6 +56,8 @@ import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.http.codec.json.Jackson2SmileDecoder; import org.springframework.http.codec.json.Jackson2SmileEncoder; +import org.springframework.http.codec.json.KotlinSerializationJsonDecoder; +import org.springframework.http.codec.json.KotlinSerializationJsonEncoder; import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader; import org.springframework.http.codec.multipart.MultipartHttpMessageReader; import org.springframework.http.codec.multipart.PartHttpMessageWriter; @@ -83,7 +85,7 @@ public class ServerCodecConfigurerTests { @Test public void defaultReaders() { List> readers = this.configurer.getReaders(); - assertThat(readers.size()).isEqualTo(14); + assertThat(readers.size()).isEqualTo(15); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteArrayDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteBufferDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(DataBufferDecoder.class); @@ -94,6 +96,7 @@ public void defaultReaders() { assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(FormHttpMessageReader.class); assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(DefaultPartHttpMessageReader.class); assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(MultipartHttpMessageReader.class); + assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationJsonDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2JsonDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class); @@ -103,7 +106,7 @@ public void defaultReaders() { @Test public void defaultWriters() { List> writers = this.configurer.getWriters(); - assertThat(writers.size()).isEqualTo(13); + assertThat(writers.size()).isEqualTo(14); assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteArrayEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteBufferEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(DataBufferEncoder.class); @@ -112,6 +115,7 @@ public void defaultWriters() { assertStringEncoder(getNextEncoder(writers), true); assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ProtobufHttpMessageWriter.class); assertThat(writers.get(this.index.getAndIncrement()).getClass()).isEqualTo(PartHttpMessageWriter.class); + assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationJsonEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2JsonEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class); @@ -152,6 +156,7 @@ public void maxInMemorySize() { DefaultPartHttpMessageReader reader = (DefaultPartHttpMessageReader) multipartReader.getPartReader(); assertThat((reader).getMaxInMemorySize()).isEqualTo(size); + assertThat(((KotlinSerializationJsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((Jackson2JsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((Jackson2SmileDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((Jaxb2XmlDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); diff --git a/spring-web/src/test/kotlin/org/springframework/http/codec/json/KotlinSerializationJsonDecoderTests.kt b/spring-web/src/test/kotlin/org/springframework/http/codec/json/KotlinSerializationJsonDecoderTests.kt index 6be5d1981cd6..2dedc390e33e 100644 --- a/spring-web/src/test/kotlin/org/springframework/http/codec/json/KotlinSerializationJsonDecoderTests.kt +++ b/spring-web/src/test/kotlin/org/springframework/http/codec/json/KotlinSerializationJsonDecoderTests.kt @@ -53,6 +53,11 @@ class KotlinSerializationJsonDecoderTests : AbstractDecoderTests>(), null, MediaType.APPLICATION_JSON)).isTrue() + assertThat(converter.canRead(typeTokenOf>(), null, MediaType.APPLICATION_JSON)).isTrue() + assertThat(converter.canRead(typeTokenOf>(), null, MediaType.APPLICATION_JSON)).isTrue() + assertThat(converter.canRead(typeTokenOf>(), null, MediaType.APPLICATION_PDF)).isFalse() } @Test @@ -60,6 +68,11 @@ class KotlinSerializationJsonHttpMessageConverterTests { assertThat(converter.canWrite(Map::class.java, MediaType.APPLICATION_JSON)).isTrue() assertThat(converter.canWrite(List::class.java, MediaType.APPLICATION_JSON)).isTrue() assertThat(converter.canWrite(Set::class.java, MediaType.APPLICATION_JSON)).isTrue() + + assertThat(converter.canWrite(typeTokenOf>(), null, MediaType.APPLICATION_JSON)).isTrue() + assertThat(converter.canWrite(typeTokenOf>(), null, MediaType.APPLICATION_JSON)).isTrue() + assertThat(converter.canWrite(typeTokenOf>(), null, MediaType.APPLICATION_JSON)).isTrue() + assertThat(converter.canWrite(typeTokenOf>(), null, MediaType.APPLICATION_PDF)).isFalse() } @Test @@ -296,4 +309,12 @@ class KotlinSerializationJsonHttpMessageConverterTests { ) data class NotSerializableBean(val string: String) + + open class TypeBase + + inline fun typeTokenOf(): Type { + val base = object : TypeBase() {} + val superType = base::class.java.genericSuperclass!! + return (superType as ParameterizedType).actualTypeArguments.first()!! + } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java index ae40c3dc9ffe..4995b3f28ff4 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java @@ -906,6 +906,9 @@ else if (jaxb2Present) { } } + if (kotlinSerializationJsonPresent) { + messageConverters.add(new KotlinSerializationJsonHttpMessageConverter()); + } if (jackson2Present) { Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json(); if (this.applicationContext != null) { @@ -919,9 +922,6 @@ else if (gsonPresent) { else if (jsonbPresent) { messageConverters.add(new JsonbHttpMessageConverter()); } - else if (kotlinSerializationJsonPresent) { - messageConverters.add(new KotlinSerializationJsonHttpMessageConverter()); - } if (jackson2SmilePresent) { Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.smile(); diff --git a/src/docs/asciidoc/languages/kotlin.adoc b/src/docs/asciidoc/languages/kotlin.adoc index 76355eabf2f2..f5cdfe5bcb26 100644 --- a/src/docs/asciidoc/languages/kotlin.adoc +++ b/src/docs/asciidoc/languages/kotlin.adoc @@ -389,17 +389,13 @@ project for more details. === Kotlin multiplatform serialization As of Spring Framework 5.3, https://github.com/Kotlin/kotlinx.serialization[Kotlin multiplatform serialization] is -supported in Spring MVC, Spring WebFlux and Spring Messaging. The builtin support currently only targets JSON format. - -To enable it, follow https://github.com/Kotlin/kotlinx.serialization#setup[those instructions] and make sure neither -Jackson, GSON or JSONB are in the classpath. - -NOTE: For a typical Spring Boot web application, that can be achieved by excluding `spring-boot-starter-json` dependency. - -In Spring MVC, if you need Jackson, GSON or JSONB for other purposes, you can keep them on the classpath and -<> to remove `MappingJackson2HttpMessageConverter` and add -`KotlinSerializationJsonHttpMessageConverter`. +supported in Spring MVC, Spring WebFlux and Spring Messaging (RSocket). The builtin support currently only targets JSON format. +To enable it, follow https://github.com/Kotlin/kotlinx.serialization#setup[those instructions] to add the related dependency and plugin. +With Spring MVC and WebFlux, both Kotlin serialization and Jackson will be configured by default if they are in the classpath since +Kotlin serialization is designed to serialize only Kotlin classes annotated with `@Serializable`. +With Spring Messaging (RSocket), make sure that neither Jackson, GSON or JSONB are in the classpath if you want automatic configuration, +if Jackson is needed configure `KotlinSerializationJsonMessageConverter` manually. == Coroutines