Browse Source

Add kotlinx.serialization JSON support to Spring Messaging

Closes gh-25883
pull/25991/head
Sébastien Deleuze 5 years ago
parent
commit
3f01af6f7c
  1. 2
      spring-messaging/spring-messaging.gradle
  2. 109
      spring-messaging/src/main/java/org/springframework/messaging/converter/KotlinSerializationJsonMessageConverter.java
  3. 7
      spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java
  4. 214
      spring-messaging/src/test/kotlin/org/springframework/messaging/converter/KotlinSerializationJsonMessageConverterTests.kt
  5. 2
      src/docs/asciidoc/languages/kotlin.adoc

2
spring-messaging/spring-messaging.gradle

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
description = "Spring Messaging"
apply plugin: "kotlin"
apply plugin: "kotlinx-serialization"
dependencies {
compile(project(":spring-beans"))
@ -17,6 +18,7 @@ dependencies { @@ -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")

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

@ -0,0 +1,109 @@ @@ -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;
}
}

7
spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java

@ -36,6 +36,7 @@ import org.springframework.messaging.converter.CompositeMessageConverter; @@ -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 @@ -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 @@ -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 @@ -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);
}

214
spring-messaging/src/test/kotlin/org/springframework/messaging/converter/KotlinSerializationJsonMessageConverterTests.kt

@ -0,0 +1,214 @@ @@ -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
)
}

2
src/docs/asciidoc/languages/kotlin.adoc

@ -389,7 +389,7 @@ project for more details. @@ -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…
Cancel
Save