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
This commit is contained in:
Sébastien Deleuze 2020-11-26 12:15:23 +01:00
parent 1f701d9bad
commit 43faa439ab
13 changed files with 142 additions and 35 deletions

View File

@ -94,6 +94,7 @@ public class KotlinSerializationJsonMessageConverter extends AbstractJsonMessage
* Tries to find a serializer that can marshall or unmarshall instances of the given type * 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. * using kotlinx.serialization. If no serializer can be found, an exception is thrown.
* <p>Resolved serializers are cached and cached results are returned on successive calls. * <p>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 * @param type the type to find a serializer for
* @return a resolved serializer for the given type * @return a resolved serializer for the given type
* @throws RuntimeException if no serializer supporting the given type can be found * @throws RuntimeException if no serializer supporting the given type can be found

View File

@ -69,10 +69,38 @@ public class KotlinSerializationJsonDecoder extends AbstractDecoder<Object> {
this.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.
* <p>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 @Override
public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) { 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 @Override
@ -95,6 +123,7 @@ public class KotlinSerializationJsonDecoder extends AbstractDecoder<Object> {
* Tries to find a serializer that can marshall or unmarshall instances of the given type * 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. * using kotlinx.serialization. If no serializer can be found, an exception is thrown.
* <p>Resolved serializers are cached and cached results are returned on successive calls. * <p>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 * @param type the type to find a serializer for
* @return a resolved serializer for the given type * @return a resolved serializer for the given type
* @throws RuntimeException if no serializer supporting the given type can be found * @throws RuntimeException if no serializer supporting the given type can be found

View File

@ -71,8 +71,14 @@ public class KotlinSerializationJsonEncoder extends AbstractEncoder<Object> {
@Override @Override
public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) { public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) {
return (super.canEncode(elementType, mimeType) && !String.class.isAssignableFrom(elementType.toClass()) && try {
!ServerSentEvent.class.isAssignableFrom(elementType.toClass())); serializer(elementType.getType());
return (super.canEncode(elementType, mimeType) && !String.class.isAssignableFrom(elementType.toClass()) &&
!ServerSentEvent.class.isAssignableFrom(elementType.toClass()));
}
catch (Exception ex) {
return false;
}
} }
@Override @Override
@ -105,6 +111,7 @@ public class KotlinSerializationJsonEncoder extends AbstractEncoder<Object> {
* Tries to find a serializer that can marshall or unmarshall instances of the given type * 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. * using kotlinx.serialization. If no serializer can be found, an exception is thrown.
* <p>Resolved serializers are cached and cached results are returned on successive calls. * <p>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 * @param type the type to find a serializer for
* @return a resolved serializer for the given type * @return a resolved serializer for the given type
* @throws RuntimeException if no serializer supporting the given type can be found * @throws RuntimeException if no serializer supporting the given type can be found

View File

@ -312,6 +312,11 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
((ProtobufDecoder) codec).setMaxMessageSize(size); ((ProtobufDecoder) codec).setMaxMessageSize(size);
} }
} }
if (kotlinSerializationJsonPresent) {
if (codec instanceof KotlinSerializationJsonDecoder) {
((KotlinSerializationJsonDecoder) codec).setMaxInMemorySize(size);
}
}
if (jackson2Present) { if (jackson2Present) {
if (codec instanceof AbstractJackson2Decoder) { if (codec instanceof AbstractJackson2Decoder) {
((AbstractJackson2Decoder) codec).setMaxInMemorySize(size); ((AbstractJackson2Decoder) codec).setMaxInMemorySize(size);
@ -385,12 +390,12 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
return Collections.emptyList(); return Collections.emptyList();
} }
List<HttpMessageReader<?>> readers = new ArrayList<>(); List<HttpMessageReader<?>> readers = new ArrayList<>();
if (kotlinSerializationJsonPresent) {
addCodec(readers, new DecoderHttpMessageReader<>(getKotlinSerializationJsonDecoder()));
}
if (jackson2Present) { if (jackson2Present) {
addCodec(readers, new DecoderHttpMessageReader<>(getJackson2JsonDecoder())); addCodec(readers, new DecoderHttpMessageReader<>(getJackson2JsonDecoder()));
} }
else if (kotlinSerializationJsonPresent) {
addCodec(readers, new DecoderHttpMessageReader<>(getKotlinSerializationJsonDecoder()));
}
if (jackson2SmilePresent) { if (jackson2SmilePresent) {
addCodec(readers, new DecoderHttpMessageReader<>(this.jackson2SmileDecoder != null ? addCodec(readers, new DecoderHttpMessageReader<>(this.jackson2SmileDecoder != null ?
(Jackson2SmileDecoder) this.jackson2SmileDecoder : new Jackson2SmileDecoder())); (Jackson2SmileDecoder) this.jackson2SmileDecoder : new Jackson2SmileDecoder()));
@ -484,12 +489,12 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
*/ */
final List<HttpMessageWriter<?>> getBaseObjectWriters() { final List<HttpMessageWriter<?>> getBaseObjectWriters() {
List<HttpMessageWriter<?>> writers = new ArrayList<>(); List<HttpMessageWriter<?>> writers = new ArrayList<>();
if (kotlinSerializationJsonPresent) {
writers.add(new EncoderHttpMessageWriter<>(getKotlinSerializationJsonEncoder()));
}
if (jackson2Present) { if (jackson2Present) {
writers.add(new EncoderHttpMessageWriter<>(getJackson2JsonEncoder())); writers.add(new EncoderHttpMessageWriter<>(getJackson2JsonEncoder()));
} }
else if (kotlinSerializationJsonPresent) {
writers.add(new EncoderHttpMessageWriter<>(getKotlinSerializationJsonEncoder()));
}
if (jackson2SmilePresent) { if (jackson2SmilePresent) {
writers.add(new EncoderHttpMessageWriter<>(this.jackson2SmileEncoder != null ? writers.add(new EncoderHttpMessageWriter<>(this.jackson2SmileEncoder != null ?
(Jackson2SmileEncoder) this.jackson2SmileEncoder : new Jackson2SmileEncoder())); (Jackson2SmileEncoder) this.jackson2SmileEncoder : new Jackson2SmileEncoder()));

View File

@ -88,6 +88,28 @@ public class KotlinSerializationJsonHttpMessageConverter extends AbstractGeneric
} }
} }
@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 @Override
public final Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage) public final Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException { throws IOException, HttpMessageNotReadableException {
@ -151,6 +173,7 @@ public class KotlinSerializationJsonHttpMessageConverter extends AbstractGeneric
* Tries to find a serializer that can marshall or unmarshall instances of the given type * 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. * using kotlinx.serialization. If no serializer can be found, an exception is thrown.
* <p>Resolved serializers are cached and cached results are returned on successive calls. * <p>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 * @param type the type to find a serializer for
* @return a resolved serializer for the given type * @return a resolved serializer for the given type
* @throws RuntimeException if no serializer supporting the given type can be found * @throws RuntimeException if no serializer supporting the given type can be found

View File

@ -56,6 +56,8 @@ import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.http.codec.json.Jackson2SmileDecoder; import org.springframework.http.codec.json.Jackson2SmileDecoder;
import org.springframework.http.codec.json.Jackson2SmileEncoder; 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.multipart.MultipartHttpMessageWriter;
import org.springframework.http.codec.protobuf.ProtobufDecoder; import org.springframework.http.codec.protobuf.ProtobufDecoder;
import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter; import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter;
@ -81,7 +83,7 @@ public class ClientCodecConfigurerTests {
@Test @Test
public void defaultReaders() { public void defaultReaders() {
List<HttpMessageReader<?>> readers = this.configurer.getReaders(); List<HttpMessageReader<?>> 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(ByteArrayDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteBufferDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteBufferDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(DataBufferDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(DataBufferDecoder.class);
@ -91,6 +93,7 @@ public class ClientCodecConfigurerTests {
assertThat(getNextDecoder(readers).getClass()).isEqualTo(ProtobufDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ProtobufDecoder.class);
// SPR-16804 // SPR-16804
assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(FormHttpMessageReader.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(Jackson2JsonDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class);
@ -101,7 +104,7 @@ public class ClientCodecConfigurerTests {
@Test @Test
public void defaultWriters() { public void defaultWriters() {
List<HttpMessageWriter<?>> writers = this.configurer.getWriters(); List<HttpMessageWriter<?>> 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(ByteArrayEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteBufferEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteBufferEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(DataBufferEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(DataBufferEncoder.class);
@ -110,6 +113,7 @@ public class ClientCodecConfigurerTests {
assertStringEncoder(getNextEncoder(writers), true); assertStringEncoder(getNextEncoder(writers), true);
assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ProtobufHttpMessageWriter.class); assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ProtobufHttpMessageWriter.class);
assertThat(writers.get(this.index.getAndIncrement()).getClass()).isEqualTo(MultipartHttpMessageWriter.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(Jackson2JsonEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class);
@ -130,7 +134,7 @@ public class ClientCodecConfigurerTests {
int size = 99; int size = 99;
this.configurer.defaultCodecs().maxInMemorySize(size); this.configurer.defaultCodecs().maxInMemorySize(size);
List<HttpMessageReader<?>> readers = this.configurer.getReaders(); List<HttpMessageReader<?>> readers = this.configurer.getReaders();
assertThat(readers.size()).isEqualTo(13); assertThat(readers.size()).isEqualTo(14);
assertThat(((ByteArrayDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((ByteArrayDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
assertThat(((ByteBufferDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((ByteBufferDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
assertThat(((DataBufferDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((DataBufferDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
@ -140,6 +144,7 @@ public class ClientCodecConfigurerTests {
assertThat(((ProtobufDecoder) getNextDecoder(readers)).getMaxMessageSize()).isEqualTo(size); assertThat(((ProtobufDecoder) getNextDecoder(readers)).getMaxMessageSize()).isEqualTo(size);
assertThat(((FormHttpMessageReader) nextReader(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((FormHttpMessageReader) nextReader(readers)).getMaxInMemorySize()).isEqualTo(size);
assertThat(((KotlinSerializationJsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
assertThat(((Jackson2JsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((Jackson2JsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
assertThat(((Jackson2SmileDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((Jackson2SmileDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
assertThat(((Jaxb2XmlDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((Jaxb2XmlDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
@ -187,7 +192,7 @@ public class ClientCodecConfigurerTests {
writers = findCodec(this.configurer.getWriters(), MultipartHttpMessageWriter.class).getPartWriters(); writers = findCodec(this.configurer.getWriters(), MultipartHttpMessageWriter.class).getPartWriters();
assertThat(sseDecoder).isNotSameAs(jackson2Decoder); assertThat(sseDecoder).isNotSameAs(jackson2Decoder);
assertThat(writers).hasSize(11); assertThat(writers).hasSize(12);
} }
@Test // gh-24194 @Test // gh-24194
@ -197,7 +202,7 @@ public class ClientCodecConfigurerTests {
List<HttpMessageWriter<?>> writers = List<HttpMessageWriter<?>> writers =
findCodec(clone.getWriters(), MultipartHttpMessageWriter.class).getPartWriters(); findCodec(clone.getWriters(), MultipartHttpMessageWriter.class).getPartWriters();
assertThat(writers).hasSize(11); assertThat(writers).hasSize(12);
} }
@Test @Test
@ -211,7 +216,7 @@ public class ClientCodecConfigurerTests {
List<HttpMessageWriter<?>> writers = List<HttpMessageWriter<?>> writers =
findCodec(clone.getWriters(), MultipartHttpMessageWriter.class).getPartWriters(); findCodec(clone.getWriters(), MultipartHttpMessageWriter.class).getPartWriters();
assertThat(writers).hasSize(11); assertThat(writers).hasSize(12);
} }
private Decoder<?> getNextDecoder(List<HttpMessageReader<?>> readers) { private Decoder<?> getNextDecoder(List<HttpMessageReader<?>> readers) {

View File

@ -52,6 +52,8 @@ import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.http.codec.json.Jackson2SmileDecoder; import org.springframework.http.codec.json.Jackson2SmileDecoder;
import org.springframework.http.codec.json.Jackson2SmileEncoder; 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.ProtobufDecoder;
import org.springframework.http.codec.protobuf.ProtobufEncoder; import org.springframework.http.codec.protobuf.ProtobufEncoder;
import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter; import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter;
@ -79,7 +81,7 @@ class CodecConfigurerTests {
@Test @Test
void defaultReaders() { void defaultReaders() {
List<HttpMessageReader<?>> readers = this.configurer.getReaders(); List<HttpMessageReader<?>> 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(ByteArrayDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteBufferDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteBufferDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(DataBufferDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(DataBufferDecoder.class);
@ -88,6 +90,7 @@ class CodecConfigurerTests {
assertStringDecoder(getNextDecoder(readers), true); assertStringDecoder(getNextDecoder(readers), true);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(ProtobufDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ProtobufDecoder.class);
assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(FormHttpMessageReader.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(Jackson2JsonDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class);
@ -97,7 +100,7 @@ class CodecConfigurerTests {
@Test @Test
void defaultWriters() { void defaultWriters() {
List<HttpMessageWriter<?>> writers = this.configurer.getWriters(); List<HttpMessageWriter<?>> 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(ByteArrayEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteBufferEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteBufferEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(DataBufferEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(DataBufferEncoder.class);
@ -105,6 +108,7 @@ class CodecConfigurerTests {
assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ResourceHttpMessageWriter.class); assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ResourceHttpMessageWriter.class);
assertStringEncoder(getNextEncoder(writers), true); assertStringEncoder(getNextEncoder(writers), true);
assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ProtobufHttpMessageWriter.class); 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(Jackson2JsonEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class);
@ -133,7 +137,7 @@ class CodecConfigurerTests {
List<HttpMessageReader<?>> readers = this.configurer.getReaders(); List<HttpMessageReader<?>> readers = this.configurer.getReaders();
assertThat(readers.size()).isEqualTo(16); assertThat(readers.size()).isEqualTo(17);
assertThat(getNextDecoder(readers)).isSameAs(customDecoder1); assertThat(getNextDecoder(readers)).isSameAs(customDecoder1);
assertThat(readers.get(this.index.getAndIncrement())).isSameAs(customReader1); assertThat(readers.get(this.index.getAndIncrement())).isSameAs(customReader1);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteArrayDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteArrayDecoder.class);
@ -146,6 +150,7 @@ class CodecConfigurerTests {
assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(FormHttpMessageReader.class); assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(FormHttpMessageReader.class);
assertThat(getNextDecoder(readers)).isSameAs(customDecoder2); assertThat(getNextDecoder(readers)).isSameAs(customDecoder2);
assertThat(readers.get(this.index.getAndIncrement())).isSameAs(customReader2); 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(Jackson2JsonDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class);
@ -174,7 +179,7 @@ class CodecConfigurerTests {
List<HttpMessageWriter<?>> writers = this.configurer.getWriters(); List<HttpMessageWriter<?>> writers = this.configurer.getWriters();
assertThat(writers.size()).isEqualTo(15); assertThat(writers.size()).isEqualTo(16);
assertThat(getNextEncoder(writers)).isSameAs(customEncoder1); assertThat(getNextEncoder(writers)).isSameAs(customEncoder1);
assertThat(writers.get(this.index.getAndIncrement())).isSameAs(customWriter1); assertThat(writers.get(this.index.getAndIncrement())).isSameAs(customWriter1);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteArrayEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteArrayEncoder.class);
@ -186,6 +191,7 @@ class CodecConfigurerTests {
assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ProtobufHttpMessageWriter.class); assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ProtobufHttpMessageWriter.class);
assertThat(getNextEncoder(writers)).isSameAs(customEncoder2); assertThat(getNextEncoder(writers)).isSameAs(customEncoder2);
assertThat(writers.get(this.index.getAndIncrement())).isSameAs(customWriter2); 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(Jackson2JsonEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class);

View File

@ -56,6 +56,8 @@ import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.http.codec.json.Jackson2SmileDecoder; import org.springframework.http.codec.json.Jackson2SmileDecoder;
import org.springframework.http.codec.json.Jackson2SmileEncoder; 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.DefaultPartHttpMessageReader;
import org.springframework.http.codec.multipart.MultipartHttpMessageReader; import org.springframework.http.codec.multipart.MultipartHttpMessageReader;
import org.springframework.http.codec.multipart.PartHttpMessageWriter; import org.springframework.http.codec.multipart.PartHttpMessageWriter;
@ -83,7 +85,7 @@ public class ServerCodecConfigurerTests {
@Test @Test
public void defaultReaders() { public void defaultReaders() {
List<HttpMessageReader<?>> readers = this.configurer.getReaders(); List<HttpMessageReader<?>> 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(ByteArrayDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteBufferDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteBufferDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(DataBufferDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(DataBufferDecoder.class);
@ -94,6 +96,7 @@ public class ServerCodecConfigurerTests {
assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(FormHttpMessageReader.class); 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(DefaultPartHttpMessageReader.class);
assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(MultipartHttpMessageReader.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(Jackson2JsonDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class);
@ -103,7 +106,7 @@ public class ServerCodecConfigurerTests {
@Test @Test
public void defaultWriters() { public void defaultWriters() {
List<HttpMessageWriter<?>> writers = this.configurer.getWriters(); List<HttpMessageWriter<?>> 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(ByteArrayEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteBufferEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteBufferEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(DataBufferEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(DataBufferEncoder.class);
@ -112,6 +115,7 @@ public class ServerCodecConfigurerTests {
assertStringEncoder(getNextEncoder(writers), true); assertStringEncoder(getNextEncoder(writers), true);
assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ProtobufHttpMessageWriter.class); assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ProtobufHttpMessageWriter.class);
assertThat(writers.get(this.index.getAndIncrement()).getClass()).isEqualTo(PartHttpMessageWriter.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(Jackson2JsonEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class);
@ -152,6 +156,7 @@ public class ServerCodecConfigurerTests {
DefaultPartHttpMessageReader reader = (DefaultPartHttpMessageReader) multipartReader.getPartReader(); DefaultPartHttpMessageReader reader = (DefaultPartHttpMessageReader) multipartReader.getPartReader();
assertThat((reader).getMaxInMemorySize()).isEqualTo(size); assertThat((reader).getMaxInMemorySize()).isEqualTo(size);
assertThat(((KotlinSerializationJsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
assertThat(((Jackson2JsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((Jackson2JsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
assertThat(((Jackson2SmileDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((Jackson2SmileDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
assertThat(((Jaxb2XmlDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((Jaxb2XmlDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);

View File

@ -53,6 +53,11 @@ class KotlinSerializationJsonDecoderTests : AbstractDecoderTests<KotlinSerializa
MediaType("application", "json", StandardCharsets.US_ASCII))).isTrue() MediaType("application", "json", StandardCharsets.US_ASCII))).isTrue()
Assertions.assertThat(decoder.canDecode(ResolvableType.forClass(Pojo::class.java), Assertions.assertThat(decoder.canDecode(ResolvableType.forClass(Pojo::class.java),
MediaType("application", "json", StandardCharsets.ISO_8859_1))).isTrue() MediaType("application", "json", StandardCharsets.ISO_8859_1))).isTrue()
Assertions.assertThat(decoder.canDecode(ResolvableType.forClassWithGenerics(List::class.java, Int::class.java), MediaType.APPLICATION_JSON)).isTrue()
Assertions.assertThat(decoder.canDecode(ResolvableType.forClassWithGenerics(List::class.java, Pojo::class.java), MediaType.APPLICATION_JSON)).isTrue()
Assertions.assertThat(decoder.canDecode(ResolvableType.forClassWithGenerics(ArrayList::class.java, Int::class.java), MediaType.APPLICATION_JSON)).isTrue()
Assertions.assertThat(decoder.canDecode(ResolvableType.forClassWithGenerics(ArrayList::class.java, Int::class.java), MediaType.APPLICATION_PDF)).isFalse()
} }
@Test @Test

View File

@ -49,7 +49,11 @@ class KotlinSerializationJsonEncoderTests : AbstractEncoderTests<KotlinSerializa
MediaType("application", "json", StandardCharsets.UTF_8))).isTrue() MediaType("application", "json", StandardCharsets.UTF_8))).isTrue()
Assertions.assertThat(encoder.canEncode(ResolvableType.forClass(Pojo::class.java), Assertions.assertThat(encoder.canEncode(ResolvableType.forClass(Pojo::class.java),
MediaType("application", "json", StandardCharsets.US_ASCII))).isTrue() MediaType("application", "json", StandardCharsets.US_ASCII))).isTrue()
Assertions.assertThat(encoder.canEncode(ResolvableType.NONE, null)).isTrue()
Assertions.assertThat(encoder.canEncode(ResolvableType.forClassWithGenerics(List::class.java, Int::class.java), MediaType.APPLICATION_JSON)).isTrue()
Assertions.assertThat(encoder.canEncode(ResolvableType.forClassWithGenerics(List::class.java, KotlinSerializationJsonDecoderTests.Pojo::class.java), MediaType.APPLICATION_JSON)).isTrue()
Assertions.assertThat(encoder.canEncode(ResolvableType.forClassWithGenerics(ArrayList::class.java, Int::class.java), MediaType.APPLICATION_JSON)).isTrue()
Assertions.assertThat(encoder.canEncode(ResolvableType.forClassWithGenerics(ArrayList::class.java, Int::class.java), MediaType.APPLICATION_PDF)).isFalse()
} }
@Test @Test

View File

@ -24,6 +24,8 @@ import org.springframework.http.MediaType
import org.springframework.http.MockHttpInputMessage import org.springframework.http.MockHttpInputMessage
import org.springframework.http.MockHttpOutputMessage import org.springframework.http.MockHttpOutputMessage
import org.springframework.http.converter.HttpMessageNotReadableException import org.springframework.http.converter.HttpMessageNotReadableException
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import kotlin.reflect.javaType import kotlin.reflect.javaType
import kotlin.reflect.typeOf import kotlin.reflect.typeOf
@ -34,6 +36,7 @@ import kotlin.reflect.typeOf
* @author Andreas Ahlenstorf * @author Andreas Ahlenstorf
* @author Sebastien Deleuze * @author Sebastien Deleuze
*/ */
@Suppress("UsePropertyAccessSyntax")
class KotlinSerializationJsonHttpMessageConverterTests { class KotlinSerializationJsonHttpMessageConverterTests {
private val converter = KotlinSerializationJsonHttpMessageConverter() private val converter = KotlinSerializationJsonHttpMessageConverter()
@ -48,6 +51,11 @@ class KotlinSerializationJsonHttpMessageConverterTests {
assertThat(converter.canRead(Map::class.java, MediaType.APPLICATION_JSON)).isTrue() assertThat(converter.canRead(Map::class.java, MediaType.APPLICATION_JSON)).isTrue()
assertThat(converter.canRead(List::class.java, MediaType.APPLICATION_JSON)).isTrue() assertThat(converter.canRead(List::class.java, MediaType.APPLICATION_JSON)).isTrue()
assertThat(converter.canRead(Set::class.java, MediaType.APPLICATION_JSON)).isTrue() assertThat(converter.canRead(Set::class.java, MediaType.APPLICATION_JSON)).isTrue()
assertThat(converter.canRead(typeTokenOf<List<Int>>(), null, MediaType.APPLICATION_JSON)).isTrue()
assertThat(converter.canRead(typeTokenOf<List<SerializableBean>>(), null, MediaType.APPLICATION_JSON)).isTrue()
assertThat(converter.canRead(typeTokenOf<ArrayList<Int>>(), null, MediaType.APPLICATION_JSON)).isTrue()
assertThat(converter.canRead(typeTokenOf<List<Int>>(), null, MediaType.APPLICATION_PDF)).isFalse()
} }
@Test @Test
@ -60,6 +68,11 @@ class KotlinSerializationJsonHttpMessageConverterTests {
assertThat(converter.canWrite(Map::class.java, MediaType.APPLICATION_JSON)).isTrue() assertThat(converter.canWrite(Map::class.java, MediaType.APPLICATION_JSON)).isTrue()
assertThat(converter.canWrite(List::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(Set::class.java, MediaType.APPLICATION_JSON)).isTrue()
assertThat(converter.canWrite(typeTokenOf<List<Int>>(), null, MediaType.APPLICATION_JSON)).isTrue()
assertThat(converter.canWrite(typeTokenOf<List<SerializableBean>>(), null, MediaType.APPLICATION_JSON)).isTrue()
assertThat(converter.canWrite(typeTokenOf<ArrayList<Int>>(), null, MediaType.APPLICATION_JSON)).isTrue()
assertThat(converter.canWrite(typeTokenOf<List<Int>>(), null, MediaType.APPLICATION_PDF)).isFalse()
} }
@Test @Test
@ -296,4 +309,12 @@ class KotlinSerializationJsonHttpMessageConverterTests {
) )
data class NotSerializableBean(val string: String) data class NotSerializableBean(val string: String)
open class TypeBase<T>
inline fun <reified T> typeTokenOf(): Type {
val base = object : TypeBase<T>() {}
val superType = base::class.java.genericSuperclass!!
return (superType as ParameterizedType).actualTypeArguments.first()!!
}
} }

View File

@ -906,6 +906,9 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
} }
} }
if (kotlinSerializationJsonPresent) {
messageConverters.add(new KotlinSerializationJsonHttpMessageConverter());
}
if (jackson2Present) { if (jackson2Present) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json(); Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json();
if (this.applicationContext != null) { if (this.applicationContext != null) {
@ -919,9 +922,6 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
else if (jsonbPresent) { else if (jsonbPresent) {
messageConverters.add(new JsonbHttpMessageConverter()); messageConverters.add(new JsonbHttpMessageConverter());
} }
else if (kotlinSerializationJsonPresent) {
messageConverters.add(new KotlinSerializationJsonHttpMessageConverter());
}
if (jackson2SmilePresent) { if (jackson2SmilePresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.smile(); Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.smile();

View File

@ -389,17 +389,13 @@ project for more details.
=== Kotlin multiplatform serialization === Kotlin multiplatform serialization
As of Spring Framework 5.3, https://github.com/Kotlin/kotlinx.serialization[Kotlin multiplatform serialization] is 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. 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] 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
<<web#mvc-config-message-converters, configure message converters>> to remove `MappingJackson2HttpMessageConverter` and add
`KotlinSerializationJsonHttpMessageConverter`.
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 == Coroutines