-
Notifications
You must be signed in to change notification settings - Fork 9
Add kotlin.serialization
implementation of Serializer
#124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 11 commits
87ee5ff
43c5d0e
4bf4ddf
0af12e5
2b7735b
1256d2d
f7d4ee9
798b3fd
edb6903
b8fbd02
c113040
7acce4c
aa6804f
20479a8
e0f411a
8d7b70d
34844c8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
/* | ||
* Copyright (c) 2010-2021. Axon Framework | ||
* | ||
* 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 | ||
* | ||
* http://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.axonframework.extensions.kotlin.serialization | ||
|
||
import kotlinx.serialization.ContextualSerializer | ||
import kotlinx.serialization.KSerializer | ||
import kotlinx.serialization.SerializationException | ||
import kotlinx.serialization.json.Json | ||
import kotlinx.serialization.json.JsonElement | ||
import kotlinx.serialization.json.JsonNull | ||
import org.axonframework.common.ObjectUtils | ||
import org.axonframework.serialization.AnnotationRevisionResolver | ||
import org.axonframework.serialization.ChainingConverter | ||
import org.axonframework.serialization.Converter | ||
import org.axonframework.serialization.RevisionResolver | ||
import org.axonframework.serialization.SerializedObject | ||
import org.axonframework.serialization.SerializedType | ||
import org.axonframework.serialization.Serializer | ||
import org.axonframework.serialization.SimpleSerializedObject | ||
import org.axonframework.serialization.SimpleSerializedType | ||
import org.axonframework.serialization.UnknownSerializedType | ||
import kotlin.reflect.full.companionObject | ||
import kotlin.reflect.full.companionObjectInstance | ||
import org.axonframework.serialization.SerializationException as AxonSerializationException | ||
|
||
/** | ||
* Implementation of Axon Serializer that uses a kotlinx.serialization implementation. | ||
* The serialized format is JSON. | ||
* | ||
* The DSL function kotlinSerializer can be used to easily configure the parameters | ||
* for this serializer. | ||
* | ||
* @see kotlinx.serialization.Serializer | ||
* @see org.axonframework.serialization.Serializer | ||
* @see kotlinSerializer | ||
* | ||
* @since 0.2.0 | ||
* @author Hidde Wieringa | ||
*/ | ||
class KotlinSerializer( | ||
private val revisionResolver: RevisionResolver = AnnotationRevisionResolver(), | ||
private val converter: Converter = ChainingConverter(), | ||
private val json: Json = Json, | ||
) : Serializer { | ||
|
||
private val serializerCache: MutableMap<Class<*>, KSerializer<*>> = mutableMapOf() | ||
|
||
override fun <T> serialize(value: Any?, expectedRepresentation: Class<T>): SerializedObject<T> { | ||
try { | ||
val type = ObjectUtils.nullSafeTypeOf(value) | ||
|
||
if (expectedRepresentation.isAssignableFrom(JsonElement::class.java)) { | ||
return SimpleSerializedObject( | ||
(if (value == null) JsonNull else json.encodeToJsonElement(type.serializer(), value)) as T, | ||
expectedRepresentation, | ||
typeForClass(type) | ||
) | ||
} | ||
|
||
// By default, encode to String. This can be converted to other types by the converter | ||
val stringSerialized: SerializedObject<String> = SimpleSerializedObject( | ||
(if (value == null) "null" else json.encodeToString(type.serializer(), value)), | ||
String::class.java, | ||
typeForClass(type) | ||
) | ||
|
||
return converter.convert(stringSerialized, expectedRepresentation) | ||
} catch (ex: SerializationException) { | ||
throw AxonSerializationException("Cannot serialize type ${value?.javaClass?.name} to representation $expectedRepresentation.", ex) | ||
} | ||
} | ||
|
||
override fun <T> canSerializeTo(expectedRepresentation: Class<T>): Boolean = | ||
expectedRepresentation == JsonElement::class.java || | ||
hiddewie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
converter.canConvert(String::class.java, expectedRepresentation) | ||
|
||
override fun <S, T> deserialize(serializedObject: SerializedObject<S>?): T? { | ||
try { | ||
if (serializedObject == null) { | ||
return null | ||
} | ||
|
||
if (serializedObject.type == SerializedType.emptyType()) { | ||
return null | ||
} | ||
|
||
val foundType = classForType(serializedObject.type) | ||
if (UnknownSerializedType::class.java.isAssignableFrom(foundType)) { | ||
return UnknownSerializedType(this, serializedObject) as T | ||
} | ||
|
||
val serializer: KSerializer<T> = foundType.serializer() as KSerializer<T> | ||
|
||
if (serializedObject.contentType.isAssignableFrom(JsonElement::class.java)) { | ||
return json.decodeFromJsonElement(serializer, serializedObject.data as JsonElement) | ||
} | ||
|
||
val stringSerialized: SerializedObject<String> = converter.convert(serializedObject, String::class.java) | ||
return json.decodeFromString(serializer, stringSerialized.data) | ||
} catch (ex: SerializationException) { | ||
throw AxonSerializationException( | ||
"Could not deserialize from content type ${serializedObject?.contentType} to type ${serializedObject?.type}", | ||
ex | ||
) | ||
} | ||
} | ||
|
||
private fun <S, T> SerializedObject<S>.serializer(): KSerializer<T> = | ||
classForType(type).serializer() as KSerializer<T> | ||
|
||
/** | ||
* When a type is compiled by the Kotlin compiler extension, a companion object | ||
* is created which contains a method `serializer()`. This method should be called | ||
* to get the serializer of the class. | ||
* | ||
* In a 'normal' serialization environment, you would call the MyClass.serializer() | ||
* method directly. Here we are in a generic setting, and need reflection to call | ||
* the method. | ||
* | ||
* If there is no `serializer` method, a ContextualSerializer will be created. This | ||
* serializer requires manual configuration of the SerializersModule containing a | ||
* KSerializer which will be used when this class is serialized. | ||
* | ||
* This method caches the reflection mapping from class to serializer for efficiency. | ||
*/ | ||
private fun <T> Class<T>.serializer(): KSerializer<T> = | ||
serializerCache.computeIfAbsent(this) { | ||
// Class<T>: T must be non-null | ||
val kClass = (this as Class<Any>).kotlin | ||
|
||
val companion = kClass.companionObject | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I assume that the hard requirement on the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the toughest question :) As far as I can see, there are two solutions:
To illustrate option 2, I added a small example and custom serializer with test. The third option is abandoning this approach because the Kotlin serialization is not wanted in the core framework, and it is not worth the effort to maintain the custom serializers. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (After thinking about option 1 a bit more: I have not tested adding There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the insights here. In the meantime, I'll start an internal discussion about whether we want to add the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried a few things to test how the interoperability of Java classes with Kotlin annotations, and the Kotlin annotation processors work together. What I did:
What I saw:
What I did:
What I saw:
What I did:
What I saw:
So my conclusion is that the Kotlin serialization compiler plugin only works for Kotlin source files, and that the That is very unfortunate, I might open an issue with the Kotlin Serialization team to ask if that is supposed to work (ref Kotlin/kotlinx.serialization#1687). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Either way, we can not assume that every class that comes here will have a serializer attached to it (nor be a Kotlin class) If there is no serializer defined, or just based on types, we can fall back on a registry that we can keep as a map of Type (Class, Class name, etc) and a Serializer. That map could be configurable so someone might add support for classes without This and |
||
?: return@computeIfAbsent ContextualSerializer(kClass) | ||
|
||
val serializerMethod = companion.java.getMethod("serializer") | ||
?: return@computeIfAbsent ContextualSerializer(kClass) | ||
|
||
serializerMethod.invoke(kClass.companionObjectInstance) as KSerializer<*> | ||
} as KSerializer<T> | ||
|
||
override fun classForType(type: SerializedType): Class<*> = | ||
if (SerializedType.emptyType() == type) { | ||
Void.TYPE | ||
} else { | ||
try { | ||
Class.forName(type.name) | ||
} catch (e: ClassNotFoundException) { | ||
UnknownSerializedType::class.java | ||
} | ||
} | ||
|
||
override fun typeForClass(type: Class<*>?): SerializedType = | ||
if (type == null || Void.TYPE == type || Void::class.java == type) { | ||
SimpleSerializedType.emptyType() | ||
} else { | ||
SimpleSerializedType(type.name, revisionResolver.revisionOf(type)) | ||
} | ||
|
||
override fun getConverter(): Converter = | ||
converter | ||
|
||
} | ||
|
||
/** | ||
* Configuration which will be used to construct a KotlinSerializer. | ||
* This class is used in the kotlinSerializer DSL function. | ||
* | ||
* @see KotlinSerializer | ||
* @see kotlinSerializer | ||
*/ | ||
class KotlinSerializerConfiguration { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the driving force to use this format instead of a constructor with default values? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These properties are all There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Honestly, I wouldn't know what's more "Kotlin-like". If I'd look at the other Axon components, the settings are done through the Builder of a piece of infrastructure. I'd wager that such a separate config object does not align very well with that idea. @sandjelkovic, what's your opinion on this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I removed this Configuration class, and used the constructor with default values. It is no problem to add it back later if it is needed. The Configuration class could be seen as a builder pattern for a Kotlin-style DSL. The Configuration class contains the same properties as the constructor, and all mutable. In the DSL builder function you could configure the Configuration class (or builder, however you want to call it), and that will then invoke the constructor of the actual (immutable) service. |
||
var revisionResolver: RevisionResolver = AnnotationRevisionResolver() | ||
var converter: Converter = ChainingConverter() | ||
var json: Json = Json | ||
} | ||
|
||
/** | ||
* DSL function to configure a new KotlinSerializer. | ||
* | ||
* Usage example: | ||
* <code> | ||
* val serializer: KotlinSerializer = kotlinSerializer { | ||
* json = Json | ||
* converter = ChainingConverter() | ||
* revisionResolver = AnnotationRevisionResolver() | ||
* } | ||
* </code> | ||
* | ||
* @see KotlinSerializer | ||
*/ | ||
fun kotlinSerializer(init: KotlinSerializerConfiguration.() -> Unit = {}): KotlinSerializer { | ||
val configuration = KotlinSerializerConfiguration() | ||
configuration.init() | ||
return KotlinSerializer( | ||
configuration.revisionResolver, | ||
configuration.converter, | ||
configuration.json, | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package org.axonframework.extensions.kotlin.serialization.serializer | ||
|
||
import kotlinx.serialization.ExperimentalSerializationApi | ||
import kotlinx.serialization.KSerializer | ||
import kotlinx.serialization.SerializationException | ||
import kotlinx.serialization.Serializer | ||
import kotlinx.serialization.builtins.MapSerializer | ||
import kotlinx.serialization.builtins.serializer | ||
import kotlinx.serialization.descriptors.SerialDescriptor | ||
import kotlinx.serialization.descriptors.buildClassSerialDescriptor | ||
import kotlinx.serialization.descriptors.element | ||
import kotlinx.serialization.encoding.CompositeDecoder.Companion.DECODE_DONE | ||
import kotlinx.serialization.encoding.Decoder | ||
import kotlinx.serialization.encoding.Encoder | ||
import kotlinx.serialization.encoding.decodeStructure | ||
import kotlinx.serialization.encoding.encodeStructure | ||
import org.axonframework.eventhandling.tokenstore.ConfigToken | ||
|
||
@OptIn(ExperimentalSerializationApi::class) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would you be able to elaborate a little on the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The We might wait to merge this feature until the used serialization API is marked fully stable, although the Axon There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So to clarify, only There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes exactly. The following is stable:
But the following needs experimental opt in:
In particular doing this for Java classes gives compiler errors, and it might simply not be supported (see Kotlin/kotlinx.serialization#1687) |
||
@Serializer(forClass = ConfigToken::class) | ||
class ConfigTokenSerializer : KSerializer<ConfigToken> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the custom serializer implemented manually for the internal Axon type. These type of implementations are needed if the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd assume this should be constructed for every concrete implementation of serializable Axon objects in that case, correct? This would at least enable moving further with this serializer, so that's good. If we go this route, it will require some KDoc, of course ;-) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just saw https://github.com/Kotlin/kotlinx.serialization/blob/f673bc2/docs/serializers.md#deriving-external-serializer-for-another-kotlin-class-experimental for the first time. This would allow us to skip the 'manual' implementation of the serializer. Unfortunately I can't yet get it to work, I run into this bug in the Kotlin complier plugin (Kotlin/kotlinx.serialization#1680). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have you tried Surrogate serializers and Delegating serializers? They look much simpler and might just get the job done. Hopefully, they might just work and there would be no hidden issues like you encountered previously. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (#124 (comment)) I changed the fully-custom example The work means, for every Axon class:
The serializers for surrogates are very simple and similar (only a few lines of code). However the surrogates themselves need to contain exactly the public data fields of the class they are a surrogate for. This will cause bugs if a field is forgotten in the surrogate. |
||
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ConfigToken") { | ||
element<Map<String, String>>("config") | ||
} | ||
|
||
override fun serialize(encoder: Encoder, value: ConfigToken) { | ||
encoder.encodeStructure(descriptor) { | ||
encodeSerializableElement(descriptor, 0, MapSerializer(String.serializer(), String.serializer()), value.config) | ||
} | ||
} | ||
|
||
override fun deserialize(decoder: Decoder): ConfigToken { | ||
return decoder.decodeStructure(descriptor) { | ||
var config: Map<String, String>? = null | ||
|
||
loop@ while (true) { | ||
when (val index = decodeElementIndex(descriptor)) { | ||
DECODE_DONE -> break@loop | ||
|
||
0 -> config = decodeSerializableElement(descriptor, 0, MapSerializer(String.serializer(), String.serializer())) | ||
|
||
else -> throw SerializationException("Unexpected index $index") | ||
} | ||
} | ||
|
||
ConfigToken(config) | ||
} | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.