Add kotlinx.serialization JSON support to Spring Messaging

Closes gh-25883
This commit is contained in:
Sébastien Deleuze 2020-10-26 20:06:07 +01:00
parent f329748657
commit 3f01af6f7c
5 changed files with 333 additions and 1 deletions

View File

@ -1,6 +1,7 @@
description = "Spring Messaging"
apply plugin: "kotlin"
apply plugin: "kotlinx-serialization"
dependencies {
compile(project(":spring-beans"))
@ -17,6 +18,7 @@ dependencies {
optional("javax.xml.bind:jaxb-api")
optional("com.google.protobuf:protobuf-java-util")
optional("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
optional("org.jetbrains.kotlinx:kotlinx-serialization-json")
testCompile(project(":kotlin-coroutines"))
testCompile(testFixtures(project(":spring-core")))
testCompile("javax.inject:javax.inject-tck")

View File

@ -0,0 +1,109 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.messaging.converter;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Type;
import java.util.Map;
import kotlinx.serialization.KSerializer;
import kotlinx.serialization.SerializersKt;
import kotlinx.serialization.json.Json;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.FileCopyUtils;
/**
* Implementation of {@link MessageConverter} that can read and write JSON
* using <a href="https://github.com/Kotlin/kotlinx.serialization">kotlinx.serialization</a>.
*
* <p>This converter can be used to bind {@code @Serializable} Kotlin classes.
*
* @author Sebastien Deleuze
* @since 5.3
*/
public class KotlinSerializationJsonMessageConverter extends AbstractJsonMessageConverter {
private static final Map<Type, KSerializer<Object>> serializerCache = new ConcurrentReferenceHashMap<>();
private final Json json;
/**
* Construct a new {@code KotlinSerializationJsonMessageConverter} with default configuration.
*/
public KotlinSerializationJsonMessageConverter() {
this(Json.Default);
}
/**
* Construct a new {@code KotlinSerializationJsonMessageConverter} with the given delegate.
* @param json the Json instance to use
*/
public KotlinSerializationJsonMessageConverter(Json json) {
this.json = json;
}
@Override
protected Object fromJson(Reader reader, Type resolvedType) {
try {
return fromJson(FileCopyUtils.copyToString(reader), resolvedType);
}
catch (IOException ex) {
throw new MessageConversionException("Could not read JSON: " + ex.getMessage(), ex);
}
}
@Override
protected Object fromJson(String payload, Type resolvedType) {
return this.json.decodeFromString(serializer(resolvedType), payload);
}
@Override
protected void toJson(Object payload, Type resolvedType, Writer writer) {
try {
writer.write(toJson(payload, resolvedType).toCharArray());
}
catch (IOException ex) {
throw new MessageConversionException("Could not write JSON: " + ex.getMessage(), ex);
}
}
@Override
protected String toJson(Object payload, Type resolvedType) {
return this.json.encodeToString(serializer(resolvedType), payload);
}
/**
* 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.
* <p>Resolved serializers are cached and cached results are returned on successive calls.
* @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
*/
private KSerializer<Object> serializer(Type type) {
KSerializer<Object> serializer = serializerCache.get(type);
if (serializer == null) {
serializer = SerializersKt.serializer(type);
serializerCache.put(type, serializer);
}
return serializer;
}
}

View File

@ -36,6 +36,7 @@ import org.springframework.messaging.converter.CompositeMessageConverter;
import org.springframework.messaging.converter.DefaultContentTypeResolver;
import org.springframework.messaging.converter.GsonMessageConverter;
import org.springframework.messaging.converter.JsonbMessageConverter;
import org.springframework.messaging.converter.KotlinSerializationJsonMessageConverter;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.converter.StringMessageConverter;
@ -101,6 +102,8 @@ public abstract class AbstractMessageBrokerConfiguration implements ApplicationC
private static final boolean jsonbPresent;
private static final boolean kotlinSerializationJsonPresent;
static {
ClassLoader classLoader = AbstractMessageBrokerConfiguration.class.getClassLoader();
@ -108,6 +111,7 @@ public abstract class AbstractMessageBrokerConfiguration implements ApplicationC
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", classLoader);
kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader);
}
@ -411,6 +415,9 @@ public abstract class AbstractMessageBrokerConfiguration implements ApplicationC
else if (jsonbPresent) {
converters.add(new JsonbMessageConverter());
}
else if (kotlinSerializationJsonPresent) {
converters.add(new KotlinSerializationJsonMessageConverter());
}
}
return new CompositeMessageConverter(converters);
}

View File

@ -0,0 +1,214 @@
/*
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.messaging.converter
import kotlinx.serialization.Serializable
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Test
import org.springframework.core.MethodParameter
import org.springframework.messaging.support.MessageBuilder
import java.nio.charset.StandardCharsets
import kotlin.reflect.typeOf
@Suppress("UsePropertyAccessSyntax")
class KotlinSerializationJsonMessageConverterTests {
private val converter = KotlinSerializationJsonMessageConverter()
@Test
fun readObject() {
val payload = """
{
"bytes": [
1,
2
],
"array": [
"Foo",
"Bar"
],
"number": 42,
"string": "Foo",
"bool": true,
"fraction": 42
}
""".trimIndent()
val message = MessageBuilder.withPayload(payload.toByteArray(StandardCharsets.UTF_8)).build()
val result = converter.fromMessage(message, SerializableBean::class.java) as SerializableBean
Assertions.assertThat(result.bytes).containsExactly(0x1, 0x2)
Assertions.assertThat(result.array).containsExactly("Foo", "Bar")
Assertions.assertThat(result.number).isEqualTo(42)
Assertions.assertThat(result.string).isEqualTo("Foo")
Assertions.assertThat(result.bool).isTrue()
Assertions.assertThat(result.fraction).isEqualTo(42.0f)
}
@Test
@Suppress("UNCHECKED_CAST")
fun readArrayOfObjects() {
val payload = """
[
{
"bytes": [
1,
2
],
"array": [
"Foo",
"Bar"
],
"number": 42,
"string": "Foo",
"bool": true,
"fraction": 42
}
]
""".trimIndent()
val message = MessageBuilder.withPayload(payload.toByteArray(StandardCharsets.UTF_8)).build()
val result = converter.fromMessage(message, Array<SerializableBean>::class.java) as Array<SerializableBean>
Assertions.assertThat(result).hasSize(1)
Assertions.assertThat(result[0].bytes).containsExactly(0x1, 0x2)
Assertions.assertThat(result[0].array).containsExactly("Foo", "Bar")
Assertions.assertThat(result[0].number).isEqualTo(42)
Assertions.assertThat(result[0].string).isEqualTo("Foo")
Assertions.assertThat(result[0].bool).isTrue()
Assertions.assertThat(result[0].fraction).isEqualTo(42.0f)
}
@Test
@Suppress("UNCHECKED_CAST")
@ExperimentalStdlibApi
fun readGenericCollection() {
val payload = """
[
{
"bytes": [
1,
2
],
"array": [
"Foo",
"Bar"
],
"number": 42,
"string": "Foo",
"bool": true,
"fraction": 42
}
]
""".trimIndent()
val method = javaClass.getDeclaredMethod("handleList", List::class.java)
val param = MethodParameter(method, 0)
val message = MessageBuilder.withPayload(payload.toByteArray(StandardCharsets.UTF_8)).build()
val result = converter.fromMessage(message, typeOf<List<SerializableBean>>()::class.java, param) as List<SerializableBean>
Assertions.assertThat(result).hasSize(1)
Assertions.assertThat(result[0].bytes).containsExactly(0x1, 0x2)
Assertions.assertThat(result[0].array).containsExactly("Foo", "Bar")
Assertions.assertThat(result[0].number).isEqualTo(42)
Assertions.assertThat(result[0].string).isEqualTo("Foo")
Assertions.assertThat(result[0].bool).isTrue()
Assertions.assertThat(result[0].fraction).isEqualTo(42.0f)
}
@Test
fun readFailsOnInvalidJson() {
val payload = """
this is an invalid JSON document
""".trimIndent()
val message = MessageBuilder.withPayload(payload.toByteArray(StandardCharsets.UTF_8)).build()
Assertions.assertThatExceptionOfType(MessageConversionException::class.java).isThrownBy {
converter.fromMessage(message, SerializableBean::class.java)
}
}
@Test
fun writeObject() {
val serializableBean = SerializableBean(byteArrayOf(0x1, 0x2), arrayOf("Foo", "Bar"), 42, "Foo", true, 42.0f)
val message = converter.toMessage(serializableBean, null)
val result = String((message!!.payload as ByteArray), StandardCharsets.UTF_8)
Assertions.assertThat(result)
.contains("\"bytes\":[1,2]")
.contains("\"array\":[\"Foo\",\"Bar\"]")
.contains("\"number\":42")
.contains("\"string\":\"Foo\"")
.contains("\"bool\":true")
.contains("\"fraction\":42.0")
}
@Test
fun writeObjectWithNullableProperty() {
val serializableBean = SerializableBean(byteArrayOf(0x1, 0x2), arrayOf("Foo", "Bar"), 42, null, true, 42.0f)
val message = converter.toMessage(serializableBean, null)
val result = String((message!!.payload as ByteArray), StandardCharsets.UTF_8)
Assertions.assertThat(result)
.contains("\"bytes\":[1,2]")
.contains("\"array\":[\"Foo\",\"Bar\"]")
.contains("\"number\":42")
.contains("\"string\":null")
.contains("\"bool\":true")
.contains("\"fraction\":42.0")
}
@Test
fun writeArrayOfObjects() {
val serializableBean = SerializableBean(byteArrayOf(0x1, 0x2), arrayOf("Foo", "Bar"), 42, "Foo", true, 42.0f)
val expectedJson = """
[{"bytes":[1,2],"array":["Foo","Bar"],"number":42,"string":"Foo","bool":true,"fraction":42.0}]
""".trimIndent()
val message = converter.toMessage(arrayOf(serializableBean), null)
val result = String((message!!.payload as ByteArray), StandardCharsets.UTF_8)
Assertions.assertThat(result).isEqualTo(expectedJson)
}
@Test
@ExperimentalStdlibApi
fun writeGenericCollection() {
val serializableBean = SerializableBean(byteArrayOf(0x1, 0x2), arrayOf("Foo", "Bar"), 42, "Foo", true, 42.0f)
val expectedJson = """
[{"bytes":[1,2],"array":["Foo","Bar"],"number":42,"string":"Foo","bool":true,"fraction":42.0}]
""".trimIndent()
val method = javaClass.getDeclaredMethod("handleList", List::class.java)
val param = MethodParameter(method, 0)
val message = converter.toMessage(arrayListOf(serializableBean), null, param)
val result = String((message!!.payload as ByteArray), StandardCharsets.UTF_8)
Assertions.assertThat(result).isEqualTo(expectedJson)
}
@Suppress("UNUSED_PARAMETER")
fun handleList(payload: List<SerializableBean>) {}
@Serializable
@Suppress("ArrayInDataClass")
data class SerializableBean(
val bytes: ByteArray,
val array: Array<String>,
val number: Int,
val string: String?,
val bool: Boolean,
val fraction: Float
)
}

View File

@ -389,7 +389,7 @@ 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 and Spring WebFlux. The builtin support currently only targets JSON format.
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.