Add kotlinx.serialization JSON support to Spring Messaging
Closes gh-25883
This commit is contained in:
parent
f329748657
commit
3f01af6f7c
|
@ -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")
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue