Merge pull request #2383 from corda/kat/feature/deterministicSerilaizer

CORDA-914 - Deterministic property ordering for AMQP serialization
This commit is contained in:
Katelyn Baker 2018-01-18 10:29:57 +00:00 committed by GitHub
commit 9df35ae5d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 465 additions and 176 deletions

View File

@ -70,8 +70,8 @@ class CorDappCustomSerializer(
data.withDescribed(descriptor) { data.withDescribed(descriptor) {
data.withList { data.withList {
for (property in proxySerializer.propertySerializers.getters) { proxySerializer.propertySerializers.serializationOrder.forEach {
property.writeProperty(proxy, this, output) it.getter.writeProperty(proxy, this, output)
} }
} }
} }

View File

@ -61,7 +61,14 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor {
override fun isSerializerFor(clazz: Class<*>): Boolean = clazz == this.clazz override fun isSerializerFor(clazz: Class<*>): Boolean = clazz == this.clazz
override val type: Type get() = clazz override val type: Type get() = clazz
override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForDescriptors(superClassSerializer.typeDescriptor.toString(), nameForType(clazz))}") override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForDescriptors(superClassSerializer.typeDescriptor.toString(), nameForType(clazz))}")
private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(clazz), null, emptyList(), SerializerFactory.nameForType(superClassSerializer.type), Descriptor(typeDescriptor), emptyList()) private val typeNotation: TypeNotation = RestrictedType(
SerializerFactory.nameForType(clazz),
null,
emptyList(),
SerializerFactory.nameForType(superClassSerializer.type),
Descriptor(typeDescriptor),
emptyList())
override fun writeClassInfo(output: SerializationOutput) { override fun writeClassInfo(output: SerializationOutput) {
output.writeTypeNotations(typeNotation) output.writeTypeNotations(typeNotation)
} }
@ -132,8 +139,8 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor {
override fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput) { override fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput) {
val proxy = toProxy(obj) val proxy = toProxy(obj)
data.withList { data.withList {
for (property in proxySerializer.propertySerializers.getters) { proxySerializer.propertySerializers.serializationOrder.forEach {
property.writeProperty(proxy, this, output) it.getter.writeProperty(proxy, this, output)
} }
} }
} }

View File

@ -20,7 +20,7 @@ class EvolutionSerializer(
override val kotlinConstructor: KFunction<Any>?) : ObjectSerializer(clazz, factory) { override val kotlinConstructor: KFunction<Any>?) : ObjectSerializer(clazz, factory) {
// explicitly set as empty to indicate it's unused by this type of serializer // explicitly set as empty to indicate it's unused by this type of serializer
override val propertySerializers = ConstructorDestructorMethods (emptyList(), emptyList()) override val propertySerializers = PropertySerializersEvolution()
/** /**
* Represents a parameter as would be passed to the constructor of the class as it was * Represents a parameter as would be passed to the constructor of the class as it was

View File

@ -11,7 +11,8 @@ import java.lang.reflect.Type
import kotlin.reflect.jvm.javaConstructor import kotlin.reflect.jvm.javaConstructor
/** /**
* Responsible for serializing and deserializing a regular object instance via a series of properties (matched with a constructor). * Responsible for serializing and deserializing a regular object instance via a series of properties
* (matched with a constructor).
*/ */
open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPSerializer<Any> { open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPSerializer<Any> {
override val type: Type get() = clazz override val type: Type get() = clazz
@ -22,7 +23,7 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
private val logger = contextLogger() private val logger = contextLogger()
} }
open internal val propertySerializers: ConstructorDestructorMethods by lazy { open internal val propertySerializers: PropertySerializers by lazy {
propertiesForSerialization(kotlinConstructor, clazz, factory) propertiesForSerialization(kotlinConstructor, clazz, factory)
} }
@ -31,17 +32,22 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
private val typeName = nameForType(clazz) private val typeName = nameForType(clazz)
override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}") override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}")
private val interfaces = interfacesForSerialization(clazz, factory) // We restrict to only those annotated or whitelisted
open internal val typeNotation: TypeNotation by lazy { CompositeType(typeName, null, generateProvides(), Descriptor(typeDescriptor), generateFields()) } // We restrict to only those annotated or whitelisted
private val interfaces = interfacesForSerialization(clazz, factory)
open internal val typeNotation: TypeNotation by lazy {
CompositeType(typeName, null, generateProvides(), Descriptor(typeDescriptor), generateFields())
}
override fun writeClassInfo(output: SerializationOutput) { override fun writeClassInfo(output: SerializationOutput) {
if (output.writeTypeNotations(typeNotation)) { if (output.writeTypeNotations(typeNotation)) {
for (iface in interfaces) { for (iface in interfaces) {
output.requireSerializer(iface) output.requireSerializer(iface)
} }
for (property in propertySerializers.getters) {
property.writeClassInfo(output) propertySerializers.serializationOrder.forEach { property ->
property.getter.writeClassInfo(output)
} }
} }
} }
@ -51,8 +57,8 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
data.withDescribed(typeNotation.descriptor) { data.withDescribed(typeNotation.descriptor) {
// Write list // Write list
withList { withList {
for (property in propertySerializers.getters) { propertySerializers.serializationOrder.forEach { property ->
property.writeProperty(obj, this, output) property.getter.writeProperty(obj, this, output)
} }
} }
} }
@ -63,16 +69,18 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
schemas: SerializationSchemas, schemas: SerializationSchemas,
input: DeserializationInput): Any = ifThrowsAppend({ clazz.typeName }) { input: DeserializationInput): Any = ifThrowsAppend({ clazz.typeName }) {
if (obj is List<*>) { if (obj is List<*>) {
if (obj.size > propertySerializers.getters.size) { if (obj.size > propertySerializers.size) {
throw NotSerializableException("Too many properties in described type $typeName") throw NotSerializableException("Too many properties in described type $typeName")
} }
return if (propertySerializers.setters.isEmpty()) { return if (propertySerializers.byConstructor) {
readObjectBuildViaConstructor(obj, schemas, input) readObjectBuildViaConstructor(obj, schemas, input)
} else { } else {
readObjectBuildViaSetters(obj, schemas, input) readObjectBuildViaSetters(obj, schemas, input)
} }
} else throw NotSerializableException("Body of described type is unexpected $obj") } else {
throw NotSerializableException("Body of described type is unexpected $obj")
}
} }
private fun readObjectBuildViaConstructor( private fun readObjectBuildViaConstructor(
@ -81,7 +89,11 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
input: DeserializationInput) : Any = ifThrowsAppend({ clazz.typeName }){ input: DeserializationInput) : Any = ifThrowsAppend({ clazz.typeName }){
logger.trace { "Calling construction based construction for ${clazz.typeName}" } logger.trace { "Calling construction based construction for ${clazz.typeName}" }
return construct(obj.zip(propertySerializers.getters).map { it.second.readProperty(it.first, schemas, input) }) return construct (propertySerializers.serializationOrder
.zip(obj)
.map { Pair(it.first.initialPosition, it.first.getter.readProperty(it.second, schemas, input)) }
.sortedWith(compareBy({it.first}))
.map { it.second })
} }
private fun readObjectBuildViaSetters( private fun readObjectBuildViaSetters(
@ -93,22 +105,23 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
val instance : Any = javaConstructor?.newInstance() ?: throw NotSerializableException ( val instance : Any = javaConstructor?.newInstance() ?: throw NotSerializableException (
"Failed to instantiate instance of object $clazz") "Failed to instantiate instance of object $clazz")
// read the properties out of the serialised form // read the properties out of the serialised form, since we're invoking the setters the order we
// do it in doesn't matter
val propertiesFromBlob = obj val propertiesFromBlob = obj
.zip(propertySerializers.getters) .zip(propertySerializers.serializationOrder)
.map { it.second.readProperty(it.first, schemas, input) } .map { it.second.getter.readProperty(it.first, schemas, input) }
// one by one take a property and invoke the setter on the class // one by one take a property and invoke the setter on the class
propertySerializers.setters.zip(propertiesFromBlob).forEach { propertySerializers.serializationOrder.zip(propertiesFromBlob).forEach {
it.first?.invoke(instance, *listOf(it.second).toTypedArray()) it.first.set(instance, it.second)
} }
return instance return instance
} }
private fun generateFields(): List<Field> { private fun generateFields(): List<Field> {
return propertySerializers.getters.map { return propertySerializers.serializationOrder.map {
Field(it.name, it.type, it.requires, it.default, null, it.mandatory, false) Field(it.getter.name, it.getter.type, it.getter.requires, it.getter.default, null, it.getter.mandatory, false)
} }
} }

View File

@ -1,69 +1,8 @@
package net.corda.nodeapi.internal.serialization.amqp package net.corda.nodeapi.internal.serialization.amqp
import net.corda.core.utilities.loggerFor
import org.apache.qpid.proton.amqp.Binary import org.apache.qpid.proton.amqp.Binary
import org.apache.qpid.proton.codec.Data import org.apache.qpid.proton.codec.Data
import java.lang.reflect.Method
import java.lang.reflect.Type import java.lang.reflect.Type
import java.lang.reflect.Field
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.javaGetter
import kotlin.reflect.jvm.kotlinProperty
abstract class PropertyReader {
abstract fun read(obj: Any?): Any?
abstract fun isNullable(): Boolean
}
class PublicPropertyReader(private val readMethod: Method?) : PropertyReader() {
init {
readMethod?.isAccessible = true
}
private fun Method.returnsNullable(): Boolean {
try {
val returnTypeString = this.declaringClass.kotlin.memberProperties.firstOrNull { it.javaGetter == this }?.returnType?.toString() ?: "?"
return returnTypeString.endsWith('?') || returnTypeString.endsWith('!')
} catch (e: kotlin.reflect.jvm.internal.KotlinReflectionInternalError) {
// This might happen for some types, e.g. kotlin.Throwable? - the root cause of the issue is: https://youtrack.jetbrains.com/issue/KT-13077
// TODO: Revisit this when Kotlin issue is fixed.
loggerFor<PropertySerializer>().error("Unexpected internal Kotlin error", e)
return true
}
}
override fun read(obj: Any?): Any? {
return readMethod!!.invoke(obj)
}
override fun isNullable(): Boolean = readMethod?.returnsNullable() ?: false
}
class PrivatePropertyReader(val field: Field, parentType: Type) : PropertyReader() {
init {
loggerFor<PropertySerializer>().warn("Create property Serializer for private property '${field.name}' not "
+ "exposed by a getter on class '$parentType'\n"
+ "\tNOTE: This behaviour will be deprecated at some point in the future and a getter required")
}
override fun read(obj: Any?): Any? {
field.isAccessible = true
val rtn = field.get(obj)
field.isAccessible = false
return rtn
}
override fun isNullable() = try {
field.kotlinProperty?.returnType?.isMarkedNullable ?: false
} catch (e: kotlin.reflect.jvm.internal.KotlinReflectionInternalError) {
// This might happen for some types, e.g. kotlin.Throwable? - the root cause of the issue is: https://youtrack.jetbrains.com/issue/KT-13077
// TODO: Revisit this when Kotlin issue is fixed.
loggerFor<PropertySerializer>().error("Unexpected internal Kotlin error", e)
true
}
}
/** /**
* Base class for serialization of a property of an object. * Base class for serialization of a property of an object.
@ -106,13 +45,13 @@ sealed class PropertySerializer(val name: String, val propertyReader: PropertyRe
companion object { companion object {
fun make(name: String, readMethod: PropertyReader, resolvedType: Type, factory: SerializerFactory): PropertySerializer { fun make(name: String, readMethod: PropertyReader, resolvedType: Type, factory: SerializerFactory): PropertySerializer {
if (SerializerFactory.isPrimitive(resolvedType)) { return if (SerializerFactory.isPrimitive(resolvedType)) {
return when (resolvedType) { when (resolvedType) {
Char::class.java, Character::class.java -> AMQPCharPropertySerializer(name, readMethod) Char::class.java, Character::class.java -> AMQPCharPropertySerializer(name, readMethod)
else -> AMQPPrimitivePropertySerializer(name, readMethod, resolvedType) else -> AMQPPrimitivePropertySerializer(name, readMethod, resolvedType)
} }
} else { } else {
return DescribedTypePropertySerializer(name, readMethod, resolvedType) { factory.get(null, resolvedType) } DescribedTypePropertySerializer(name, readMethod, resolvedType) { factory.get(null, resolvedType) }
} }
} }
} }

View File

@ -0,0 +1,179 @@
package net.corda.nodeapi.internal.serialization.amqp
import net.corda.core.utilities.loggerFor
import java.io.NotSerializableException
import java.lang.reflect.Method
import java.lang.reflect.Type
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.javaGetter
import kotlin.reflect.jvm.kotlinProperty
import java.lang.reflect.Field
abstract class PropertyReader {
abstract fun read(obj: Any?): Any?
abstract fun isNullable(): Boolean
}
class PublicPropertyReader(private val readMethod: Method?) : PropertyReader() {
init {
readMethod?.isAccessible = true
}
private fun Method.returnsNullable(): Boolean {
try {
val returnTypeString = this.declaringClass.kotlin.memberProperties.firstOrNull {
it.javaGetter == this
}?.returnType?.toString() ?: "?"
return returnTypeString.endsWith('?') || returnTypeString.endsWith('!')
} catch (e: kotlin.reflect.jvm.internal.KotlinReflectionInternalError) {
// This might happen for some types, e.g. kotlin.Throwable? - the root cause of the issue
// is: https://youtrack.jetbrains.com/issue/KT-13077
// TODO: Revisit this when Kotlin issue is fixed.
loggerFor<PropertySerializer>().error("Unexpected internal Kotlin error", e)
return true
}
}
override fun read(obj: Any?): Any? {
return readMethod!!.invoke(obj)
}
override fun isNullable(): Boolean = readMethod?.returnsNullable() ?: false
}
class PrivatePropertyReader(val field: Field, parentType: Type) : PropertyReader() {
init {
loggerFor<PropertySerializer>().warn("Create property Serializer for private property '${field.name}' not "
+ "exposed by a getter on class '$parentType'\n"
+ "\tNOTE: This behaviour will be deprecated at some point in the future and a getter required")
}
override fun read(obj: Any?): Any? {
field.isAccessible = true
val rtn = field.get(obj)
field.isAccessible = false
return rtn
}
override fun isNullable() = try {
field.kotlinProperty?.returnType?.isMarkedNullable ?: false
} catch (e: kotlin.reflect.jvm.internal.KotlinReflectionInternalError) {
// This might happen for some types, e.g. kotlin.Throwable? - the root cause of the issue
// is: https://youtrack.jetbrains.com/issue/KT-13077
// TODO: Revisit this when Kotlin issue is fixed.
loggerFor<PropertySerializer>().error("Unexpected internal Kotlin error", e)
true
}
}
/**
* Represents a generic interface to a serializable property of an object.
*
* @property initialPosition where in the constructor used for serialization the property occurs.
* @property getter a [PropertySerializer] wrapping access to the property. This will either be a
* method invocation on the getter or, if not publicly accessible, reflection based by temporally
* making the property accessible.
*/
abstract class PropertyAccessor(
val initialPosition: Int,
open val getter: PropertySerializer) {
companion object : Comparator<PropertyAccessor> {
override fun compare(p0: PropertyAccessor?, p1: PropertyAccessor?): Int {
return p0?.getter?.name?.compareTo(p1?.getter?.name ?: "") ?: 0
}
}
/**
* Override to control how the property is set on the object.
*/
abstract fun set(instance: Any, obj: Any?)
override fun toString(): String {
return "${getter.name}($initialPosition)"
}
}
/**
* Implementation of [PropertyAccessor] representing a property of an object that
* is serialized and deserialized via JavaBean getter and setter style methods.
*/
class PropertyAccessorGetterSetter(
initialPosition: Int,
getter: PropertySerializer,
private val setter: Method?) : PropertyAccessor(initialPosition, getter) {
/**
* Invokes the setter on the underlying object passing in the serialized value.
*/
override fun set(instance: Any, obj: Any?) {
setter?.invoke(instance, *listOf(obj).toTypedArray())
}
}
/**
* Implementation of [PropertyAccessor] representing a property of an object that
* is serialized via a JavaBean getter but deserialized using the constructor
* of the object the property belongs to.
*/
class PropertyAccessorConstructor(
initialPosition: Int,
override val getter: PropertySerializer) : PropertyAccessor(initialPosition, getter) {
/**
* Because the property should be being set on the obejct through the constructor any
* calls to the explicit setter should be an error.
*/
override fun set(instance: Any, obj: Any?) {
NotSerializableException ("Attempting to access a setter on an object being instantiated " +
"via its constructor.")
}
}
/**
* Represents a collection of [PropertyAccessor]s that represent the serialized form
* of an object.
*
* @property serializationOrder a list of [PropertyAccessor]. For deterministic serialization
* should be sorted.
* @property size how many properties are being serialized.
* @property byConstructor are the properties of the class represented by this set of properties populated
* on deserialization via the object's constructor or the corresponding setter functions. Should be
* overridden and set appropriately by child types.
*/
abstract class PropertySerializers(
val serializationOrder: List<PropertyAccessor>) {
companion object {
fun make(serializationOrder: List<PropertyAccessor>) =
when (serializationOrder.firstOrNull()) {
is PropertyAccessorConstructor -> PropertySerializersConstructor(serializationOrder)
is PropertyAccessorGetterSetter -> PropertySerializersSetter(serializationOrder)
null -> PropertySerializersNoProperties()
else -> {
throw NotSerializableException ("Unknown Property Accessor type, cannot create set")
}
}
}
val size get() = serializationOrder.size
abstract val byConstructor: Boolean
}
class PropertySerializersNoProperties : PropertySerializers (emptyList()) {
override val byConstructor get() = true
}
class PropertySerializersConstructor(
serializationOrder: List<PropertyAccessor>) : PropertySerializers(serializationOrder) {
override val byConstructor get() = true
}
class PropertySerializersSetter(
serializationOrder: List<PropertyAccessor>) : PropertySerializers(serializationOrder) {
override val byConstructor get() = false
}
class PropertySerializersEvolution : PropertySerializers(emptyList()) {
override val byConstructor get() = false
}

View File

@ -446,11 +446,12 @@ private fun fingerprintForObject(
offset: Int = 0): Hasher { offset: Int = 0): Hasher {
// Hash the class + properties + interfaces // Hash the class + properties + interfaces
val name = type.asClass()?.name ?: throw NotSerializableException("Expected only Class or ParameterizedType but found $type") val name = type.asClass()?.name ?: throw NotSerializableException("Expected only Class or ParameterizedType but found $type")
propertiesForSerialization(constructorForDeserialization(type), contextType ?: type, factory).getters propertiesForSerialization(constructorForDeserialization(type), contextType ?: type, factory)
.serializationOrder
.fold(hasher.putUnencodedChars(name)) { orig, prop -> .fold(hasher.putUnencodedChars(name)) { orig, prop ->
fingerprintForType(prop.resolvedType, type, alreadySeen, orig, factory, offset+4) fingerprintForType(prop.getter.resolvedType, type, alreadySeen, orig, factory, offset+4)
.putUnencodedChars(prop.name) .putUnencodedChars(prop.getter.name)
.putUnencodedChars(if (prop.mandatory) NOT_NULLABLE_HASH else NULLABLE_HASH) .putUnencodedChars(if (prop.getter.mandatory) NOT_NULLABLE_HASH else NULLABLE_HASH)
} }
interfacesForSerialization(type, factory).map { fingerprintForType(it, type, alreadySeen, hasher, factory, offset+4) } interfacesForSerialization(type, factory).map { fingerprintForType(it, type, alreadySeen, hasher, factory, offset+4) }
return hasher return hasher

View File

@ -2,7 +2,6 @@ package net.corda.nodeapi.internal.serialization.amqp
import com.google.common.primitives.Primitives import com.google.common.primitives.Primitives
import com.google.common.reflect.TypeToken import com.google.common.reflect.TypeToken
import io.netty.util.internal.EmptyArrays
import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.ClassWhitelist
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationContext
@ -20,7 +19,6 @@ import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.primaryConstructor import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.isAccessible import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.javaType import kotlin.reflect.jvm.javaType
import kotlin.reflect.jvm.kotlinProperty
/** /**
* Annotation indicating a constructor to be used to reconstruct instances of a class during deserialization. * Annotation indicating a constructor to be used to reconstruct instances of a class during deserialization.
@ -29,10 +27,6 @@ import kotlin.reflect.jvm.kotlinProperty
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
annotation class ConstructorForDeserialization annotation class ConstructorForDeserialization
data class ConstructorDestructorMethods(
val getters: Collection<PropertySerializer>,
val setters: Collection<Method?>)
/** /**
* Code for finding the constructor we will use for deserialization. * Code for finding the constructor we will use for deserialization.
* *
@ -74,17 +68,22 @@ internal fun constructorForDeserialization(type: Type): KFunction<Any>? {
* Note, you will need any Java classes to be compiled with the `-parameters` option to ensure constructor parameters have * Note, you will need any Java classes to be compiled with the `-parameters` option to ensure constructor parameters have
* names accessible via reflection. * names accessible via reflection.
*/ */
internal fun <T : Any> propertiesForSerialization(kotlinConstructor: KFunction<T>?, type: Type, factory: SerializerFactory): ConstructorDestructorMethods { internal fun <T : Any> propertiesForSerialization(
val clazz = type.asClass()!! kotlinConstructor: KFunction<T>?,
return if (kotlinConstructor != null) propertiesForSerializationFromConstructor(kotlinConstructor, type, factory) else propertiesForSerializationFromAbstract(clazz, type, factory) type: Type,
} factory: SerializerFactory) = PropertySerializers.make (
if (kotlinConstructor != null) {
propertiesForSerializationFromConstructor(kotlinConstructor, type, factory)
} else {
propertiesForSerializationFromAbstract(type.asClass()!!, type, factory)
}.sortedWith(PropertyAccessor))
fun isConcrete(clazz: Class<*>): Boolean = !(clazz.isInterface || Modifier.isAbstract(clazz.modifiers)) fun isConcrete(clazz: Class<*>): Boolean = !(clazz.isInterface || Modifier.isAbstract(clazz.modifiers))
private fun <T : Any> propertiesForSerializationFromConstructor( internal fun <T : Any> propertiesForSerializationFromConstructor(
kotlinConstructor: KFunction<T>, kotlinConstructor: KFunction<T>,
type: Type, type: Type,
factory: SerializerFactory): ConstructorDestructorMethods { factory: SerializerFactory): List<PropertyAccessor> {
val clazz = (kotlinConstructor.returnType.classifier as KClass<*>).javaObjectType val clazz = (kotlinConstructor.returnType.classifier as KClass<*>).javaObjectType
// Kotlin reflection doesn't work with Java getters the way you might expect, so we drop back to good ol' beans. // Kotlin reflection doesn't work with Java getters the way you might expect, so we drop back to good ol' beans.
val properties = Introspector.getBeanInfo(clazz).propertyDescriptors val properties = Introspector.getBeanInfo(clazz).propertyDescriptors
@ -96,39 +95,45 @@ private fun <T : Any> propertiesForSerializationFromConstructor(
return propertiesForSerializationFromSetters(properties, type, factory) return propertiesForSerializationFromSetters(properties, type, factory)
} }
val rc: MutableList<PropertySerializer> = ArrayList(kotlinConstructor.parameters.size) return mutableListOf<PropertyAccessor>().apply {
kotlinConstructor.parameters.withIndex().forEach { param ->
val name = param.value.name ?: throw NotSerializableException("Constructor parameter of $clazz has no name.")
for (param in kotlinConstructor.parameters) { val propertyReader = if (name in properties) {
val name = param.name ?: throw NotSerializableException("Constructor parameter of $clazz has no name.") // it's a publicly accessible property
val matchingProperty = properties[name]!!
if (name in properties) { // Check that the method has a getter in java.
val matchingProperty = properties[name]!! val getter = matchingProperty.readMethod ?: throw NotSerializableException(
"Property has no getter method for $name of $clazz. If using Java and the parameter name"
+ "looks anonymous, check that you have the -parameters option specified in the Java compiler."
+ "Alternately, provide a proxy serializer (SerializationCustomSerializer) if "
+ "recompiling isn't an option.")
// Check that the method has a getter in java. val returnType = resolveTypeVariables(getter.genericReturnType, type)
val getter = matchingProperty.readMethod ?: throw NotSerializableException("Property has no getter method for $name of $clazz. " + if (!constructorParamTakesReturnTypeOfGetter(returnType, getter.genericReturnType, param.value)) {
"If using Java and the parameter name looks anonymous, check that you have the -parameters option specified in the Java compiler." + throw NotSerializableException(
"Alternately, provide a proxy serializer (SerializationCustomSerializer) if recompiling isn't an option") "Property type $returnType for $name of $clazz differs from constructor parameter "
val returnType = resolveTypeVariables(getter.genericReturnType, type) + "type ${param.value.type.javaType}")
if (constructorParamTakesReturnTypeOfGetter(returnType, getter.genericReturnType, param)) { }
rc += PropertySerializer.make(name, PublicPropertyReader(getter), returnType, factory)
Pair(PublicPropertyReader(getter), returnType)
} else { } else {
throw NotSerializableException("Property type $returnType for $name of $clazz differs from constructor parameter type ${param.type.javaType}") try {
val field = clazz.getDeclaredField(param.value.name)
Pair(PrivatePropertyReader(field, type), field.genericType)
} catch (e: NoSuchFieldException) {
throw NotSerializableException("No property matching constructor parameter named '$name' of '$clazz'. " +
"If using Java, check that you have the -parameters option specified in the Java compiler. " +
"Alternately, provide a proxy serializer (SerializationCustomSerializer) if recompiling isn't an option")
}
} }
} else {
try {
val field = (clazz.getDeclaredField(param.name))
rc += PropertySerializer.make(name, PrivatePropertyReader(field, type), field.genericType, factory) this += PropertyAccessorConstructor(
} catch (e: NoSuchFieldException) { param.index,
throw NotSerializableException("No property matching constructor parameter named '$name' of '$clazz'. " + PropertySerializer.make(name, propertyReader.first, propertyReader.second, factory))
"If using Java, check that you have the -parameters option specified in the Java compiler. " +
"Alternately, provide a proxy serializer (SerializationCustomSerializer) if recompiling isn't an option")
}
} }
} }
return ConstructorDestructorMethods(rc, emptyList())
} }
/** /**
@ -138,26 +143,26 @@ private fun <T : Any> propertiesForSerializationFromConstructor(
private fun propertiesForSerializationFromSetters( private fun propertiesForSerializationFromSetters(
properties: Map<String, PropertyDescriptor>, properties: Map<String, PropertyDescriptor>,
type: Type, type: Type,
factory: SerializerFactory): ConstructorDestructorMethods { factory: SerializerFactory): List<PropertyAccessor> {
val getters: MutableList<PropertySerializer> = ArrayList(properties.size) return mutableListOf<PropertyAccessorGetterSetter>().apply {
val setters: MutableList<Method?> = ArrayList(properties.size) var idx = 0
properties.forEach { property ->
val getter: Method? = property.value.readMethod
val setter: Method? = property.value.writeMethod
properties.forEach { property -> if (getter == null || setter == null) return@forEach
val getter: Method? = property.value.readMethod
val setter: Method? = property.value.writeMethod
if (getter == null || setter == null) return@forEach // NOTE: There is no need to check return and parameter types vs the underlying type for
// the getter / setter vs property as if there is a difference then that property isn't reported
// by the BEAN inspector and thus we don't consider that case here
// NOTE: There is no need to check return and parameter types vs the underlying type for this += PropertyAccessorGetterSetter (
// the getter / setter vs property as if there is a difference then that property isn't reported idx++,
// by the BEAN inspector and thus we don't consider that case here PropertySerializer.make(property.key, PublicPropertyReader(getter),
resolveTypeVariables(getter.genericReturnType, type), factory),
getters += PropertySerializer.make(property.key, PublicPropertyReader(getter), setter)
resolveTypeVariables(getter.genericReturnType, type), factory) }
setters += setter
} }
return ConstructorDestructorMethods(getters, setters)
} }
private fun constructorParamTakesReturnTypeOfGetter(getterReturnType: Type, rawGetterReturnType: Type, param: KParameter): Boolean { private fun constructorParamTakesReturnTypeOfGetter(getterReturnType: Type, rawGetterReturnType: Type, param: KParameter): Boolean {
@ -168,21 +173,24 @@ private fun constructorParamTakesReturnTypeOfGetter(getterReturnType: Type, rawG
private fun propertiesForSerializationFromAbstract( private fun propertiesForSerializationFromAbstract(
clazz: Class<*>, clazz: Class<*>,
type: Type, type: Type,
factory: SerializerFactory): ConstructorDestructorMethods { factory: SerializerFactory): List<PropertyAccessor> {
// Kotlin reflection doesn't work with Java getters the way you might expect, so we drop back to good ol' beans. // Kotlin reflection doesn't work with Java getters the way you might expect, so we drop back to good ol' beans.
val properties = Introspector.getBeanInfo(clazz).propertyDescriptors val properties = Introspector.getBeanInfo(clazz).propertyDescriptors
.filter { it.name != "class" } .filter { it.name != "class" }
.sortedBy { it.name } .sortedBy { it.name }
.filterNot { it is IndexedPropertyDescriptor } .filterNot { it is IndexedPropertyDescriptor }
val rc: MutableList<PropertySerializer> = ArrayList(properties.size)
for (property in properties) { return mutableListOf<PropertyAccessorConstructor>().apply {
// Check that the method has a getter in java. properties.withIndex().forEach { property ->
val getter = property.readMethod ?: throw NotSerializableException( // Check that the method has a getter in java.
"Property has no getter method for ${property.name} of $clazz.") val getter = property.value.readMethod ?: throw NotSerializableException(
val returnType = resolveTypeVariables(getter.genericReturnType, type) "Property has no getter method for ${property.value.name} of $clazz.")
rc += PropertySerializer.make(property.name, PublicPropertyReader(getter), returnType, factory) val returnType = resolveTypeVariables(getter.genericReturnType, type)
} this += PropertyAccessorConstructor(
return ConstructorDestructorMethods(rc, emptyList()) property.index,
PropertySerializer.make(property.value.name, PublicPropertyReader(getter), returnType, factory))
}
}
} }
internal fun interfacesForSerialization(type: Type, serializerFactory: SerializerFactory): List<Type> { internal fun interfacesForSerialization(type: Type, serializerFactory: SerializerFactory): List<Type> {

View File

@ -23,9 +23,8 @@ class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<T
// Try and find a constructor // Try and find a constructor
try { try {
val constructor = constructorForDeserialization(obj.javaClass) val constructor = constructorForDeserialization(obj.javaClass)
val props = propertiesForSerialization(constructor, obj.javaClass, factory) propertiesForSerializationFromConstructor(constructor!!, obj.javaClass, factory).forEach { property ->
for (prop in props.getters) { extraProperties[property.getter.name] = property.getter.propertyReader.read(obj)
extraProperties[prop.name] = prop.propertyReader.read(obj)
} }
} catch (e: NotSerializableException) { } catch (e: NotSerializableException) {
logger.warn("Unexpected exception", e) logger.warn("Unexpected exception", e)
@ -90,4 +89,4 @@ class StackTraceElementSerializer(factory: SerializerFactory) : CustomSerializer
override fun fromProxy(proxy: StackTraceElementProxy): StackTraceElement = StackTraceElement(proxy.declaringClass, proxy.methodName, proxy.fileName, proxy.lineNumber) override fun fromProxy(proxy: StackTraceElementProxy): StackTraceElement = StackTraceElement(proxy.declaringClass, proxy.methodName, proxy.fileName, proxy.lineNumber)
data class StackTraceElementProxy(val declaringClass: String, val methodName: String, val fileName: String?, val lineNumber: Int) data class StackTraceElementProxy(val declaringClass: String, val methodName: String, val fileName: String?, val lineNumber: Int)
} }

View File

@ -47,9 +47,9 @@ public class JavaPrivatePropertyTests {
assertEquals(1, serializersByDescriptor.size()); assertEquals(1, serializersByDescriptor.size());
ObjectSerializer cSerializer = ((ObjectSerializer)serializersByDescriptor.values().toArray()[0]); ObjectSerializer cSerializer = ((ObjectSerializer)serializersByDescriptor.values().toArray()[0]);
assertEquals(1, cSerializer.getPropertySerializers().component1().size()); assertEquals(1, cSerializer.getPropertySerializers().getSerializationOrder().size());
Object[] propertyReaders = cSerializer.getPropertySerializers().component1().toArray(); Object[] propertyReaders = cSerializer.getPropertySerializers().getSerializationOrder().toArray();
assertTrue (((PropertySerializer)propertyReaders[0]).getPropertyReader() instanceof PrivatePropertyReader); assertTrue (((PropertyAccessor)propertyReaders[0]).getGetter().getPropertyReader() instanceof PrivatePropertyReader);
} }
@Test @Test
@ -76,8 +76,8 @@ public class JavaPrivatePropertyTests {
assertEquals(1, serializersByDescriptor.size()); assertEquals(1, serializersByDescriptor.size());
ObjectSerializer cSerializer = ((ObjectSerializer)serializersByDescriptor.values().toArray()[0]); ObjectSerializer cSerializer = ((ObjectSerializer)serializersByDescriptor.values().toArray()[0]);
assertEquals(1, cSerializer.getPropertySerializers().component1().size()); assertEquals(1, cSerializer.getPropertySerializers().getSerializationOrder().size());
Object[] propertyReaders = cSerializer.getPropertySerializers().component1().toArray(); Object[] propertyReaders = cSerializer.getPropertySerializers().getSerializationOrder().toArray();
assertTrue (((PropertySerializer)propertyReaders[0]).getPropertyReader() instanceof PublicPropertyReader); assertTrue (((PropertyAccessor)propertyReaders[0]).getGetter().getPropertyReader() instanceof PublicPropertyReader);
} }
} }

View File

@ -2,12 +2,23 @@ package net.corda.nodeapi.internal.serialization.amqp
import junit.framework.TestCase.assertTrue import junit.framework.TestCase.assertTrue
import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertEquals
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.junit.Test import org.junit.Test
import org.apache.qpid.proton.amqp.Symbol import org.apache.qpid.proton.amqp.Symbol
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
class PrivatePropertyTests { class PrivatePropertyTests {
private val factory = testDefaultFactory() private val factory = testDefaultFactoryNoEvolution()
companion object {
val fields : Map<String, java.lang.reflect.Field> = mapOf (
"serializersByDesc" to SerializerFactory::class.java.getDeclaredField("serializersByDescriptor")).apply {
this.values.forEach {
it.isAccessible = true
}
}
}
@Test @Test
fun testWithOnePrivateProperty() { fun testWithOnePrivateProperty() {
@ -53,16 +64,14 @@ class PrivatePropertyTests {
val schemaAndBlob = SerializationOutput(factory).serializeAndReturnSchema(c1) val schemaAndBlob = SerializationOutput(factory).serializeAndReturnSchema(c1)
assertEquals(1, schemaAndBlob.schema.types.size) assertEquals(1, schemaAndBlob.schema.types.size)
val field = SerializerFactory::class.java.getDeclaredField("serializersByDescriptor")
field.isAccessible = true
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val serializersByDescriptor = field.get(factory) as ConcurrentHashMap<Any, AMQPSerializer<Any>> val serializersByDescriptor = fields["serializersByDesc"]?.get(factory) as ConcurrentHashMap<Any, AMQPSerializer<Any>>
val schemaDescriptor = schemaAndBlob.schema.types.first().descriptor.name val schemaDescriptor = schemaAndBlob.schema.types.first().descriptor.name
serializersByDescriptor.filterKeys { (it as Symbol) == schemaDescriptor }.values.apply { serializersByDescriptor.filterKeys { (it as Symbol) == schemaDescriptor }.values.apply {
assertEquals(1, this.size) assertEquals(1, this.size)
assertTrue(this.first() is ObjectSerializer) assertTrue(this.first() is ObjectSerializer)
val propertySerializers = (this.first() as ObjectSerializer).propertySerializers.getters.toList() val propertySerializers = (this.first() as ObjectSerializer).propertySerializers.serializationOrder.map { it.getter }
assertEquals(2, propertySerializers.size) assertEquals(2, propertySerializers.size)
// a was public so should have a synthesised getter // a was public so should have a synthesised getter
assertTrue(propertySerializers[0].propertyReader is PublicPropertyReader) assertTrue(propertySerializers[0].propertyReader is PublicPropertyReader)
@ -84,16 +93,14 @@ class PrivatePropertyTests {
val schemaAndBlob = SerializationOutput(factory).serializeAndReturnSchema(c1) val schemaAndBlob = SerializationOutput(factory).serializeAndReturnSchema(c1)
assertEquals(1, schemaAndBlob.schema.types.size) assertEquals(1, schemaAndBlob.schema.types.size)
val field = SerializerFactory::class.java.getDeclaredField("serializersByDescriptor")
field.isAccessible = true
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val serializersByDescriptor = field.get(factory) as ConcurrentHashMap<Any, AMQPSerializer<Any>> val serializersByDescriptor = fields["serializersByDesc"]?.get(factory) as ConcurrentHashMap<Any, AMQPSerializer<Any>>
val schemaDescriptor = schemaAndBlob.schema.types.first().descriptor.name val schemaDescriptor = schemaAndBlob.schema.types.first().descriptor.name
serializersByDescriptor.filterKeys { (it as Symbol) == schemaDescriptor }.values.apply { serializersByDescriptor.filterKeys { (it as Symbol) == schemaDescriptor }.values.apply {
assertEquals(1, this.size) assertEquals(1, this.size)
assertTrue(this.first() is ObjectSerializer) assertTrue(this.first() is ObjectSerializer)
val propertySerializers = (this.first() as ObjectSerializer).propertySerializers.getters.toList() val propertySerializers = (this.first() as ObjectSerializer).propertySerializers.serializationOrder.map { it.getter }
assertEquals(2, propertySerializers.size) assertEquals(2, propertySerializers.size)
// as before, a is public so we'll use the getter method // as before, a is public so we'll use the getter method
@ -105,13 +112,24 @@ class PrivatePropertyTests {
} }
} }
@Suppress("UNCHECKED_CAST")
@Test @Test
fun testNested() { fun testNested() {
data class Inner(private val a: Int) data class Inner(private val a: Int)
data class Outer(private val i: Inner) data class Outer(private val i: Inner)
val c1 = Outer(Inner(1010101)) val c1 = Outer(Inner(1010101))
val c2 = DeserializationInput(factory).deserialize(SerializationOutput(factory).serialize(c1)) val output = SerializationOutput(factory).serializeAndReturnSchema(c1)
println (output.schema)
val serializersByDescriptor = fields["serializersByDesc"]!!.get(factory) as ConcurrentHashMap<Any, AMQPSerializer<Any>>
// Inner and Outer
assertEquals(2, serializersByDescriptor.size)
val schemaDescriptor = output.schema.types.first().descriptor.name
val c2 = DeserializationInput(factory).deserialize(output.obj)
assertEquals(c1, c2) assertEquals(c1, c2)
} }
} }

View File

@ -0,0 +1,125 @@
package net.corda.nodeapi.internal.serialization.amqp
import org.junit.Test
import java.util.concurrent.ConcurrentHashMap
import kotlin.test.assertEquals
import org.apache.qpid.proton.amqp.Symbol
import java.lang.reflect.Method
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class SerializationPropertyOrdering {
companion object {
val VERBOSE get() = false
val sf = testDefaultFactoryNoEvolution()
}
// Force object references to be ued to ensure we go through that code path
// this test shows (not now it's fixed) a bug whereby deserializing objects
// would break where refferenced objects were accessed before they'd been
// processed thanks to the way the blob was deserialized
@Test
fun refferenceOrdering() {
data class Reffed(val c: String, val b: String, val a: String)
data class User(val b: List<Reffed>, val a: List<Reffed>)
val r1 = Reffed("do not", "or", "do")
val r2 = Reffed("do not", "or", "do")
val l = listOf(r1, r2, r1, r2, r1, r2)
val u = User(l,l)
val output = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(u)
val input = DeserializationInput(sf).deserialize(output.obj)
}
@Test
fun randomOrder() {
data class C(val c: Int, val d: Int, val b: Int, val e: Int, val a: Int)
val c = C(3,4,2,5,1)
val output = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(c)
// the schema should reflect the serialized order of properties, not the
// construction order
assertEquals(1, output.schema.types.size)
output.schema.types.firstOrNull()?.apply {
assertEquals(5, (this as CompositeType).fields.size)
assertEquals("a", this.fields[0].name)
assertEquals("b", this.fields[1].name)
assertEquals("c", this.fields[2].name)
assertEquals("d", this.fields[3].name)
assertEquals("e", this.fields[4].name)
}
// and deserializing it should construct the object as it was and not in the order prescribed
// by the serialized form
val input = DeserializationInput(sf).deserialize(output.obj)
assertEquals(1, input.a)
assertEquals(2, input.b)
assertEquals(3, input.c)
assertEquals(4, input.d)
assertEquals(5, input.e)
}
@Suppress("UNCHECKED_CAST")
@Test
fun randomOrderSetter() {
data class C(var c: Int, var d: Int, var b: Int, var e: Int, var a: Int) {
// This will force the serialization engine to use getter / setter
// instantiation for the object rather than construction
@ConstructorForDeserialization
@Suppress("UNUSED")
constructor() : this(0, 0, 0, 0, 0)
}
val c = C()
c.a = 100
c.b = 200
c.c = 300
c.d = 400
c.e = 500
val output = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(c)
// the schema should reflect the serialized order of properties, not the
// construction order
assertEquals(1, output.schema.types.size)
output.schema.types.firstOrNull()?.apply {
assertEquals(5, (this as CompositeType).fields.size)
assertEquals("a", this.fields[0].name)
assertEquals("b", this.fields[1].name)
assertEquals("c", this.fields[2].name)
assertEquals("d", this.fields[3].name)
assertEquals("e", this.fields[4].name)
}
// Test needs to look at a bunch of private variables, change the access semantics for them
val fields : Map<String, java.lang.reflect.Field> = mapOf (
"serializersByDesc" to SerializerFactory::class.java.getDeclaredField("serializersByDescriptor"),
"setter" to PropertyAccessorGetterSetter::class.java.getDeclaredField("setter")).apply {
this.values.forEach {
it.isAccessible = true
}
}
val serializersByDescriptor = fields["serializersByDesc"]!!.get(sf) as ConcurrentHashMap<Any, AMQPSerializer<Any>>
val schemaDescriptor = output.schema.types.first().descriptor.name
// make sure that each property accessor has a setter to ensure we're using getter / setter instantiation
serializersByDescriptor.filterKeys { (it as Symbol) == schemaDescriptor }.values.apply {
assertEquals(1, this.size)
assertTrue(this.first() is ObjectSerializer)
val propertyAccessors = (this.first() as ObjectSerializer).propertySerializers.serializationOrder as List<PropertyAccessorGetterSetter>
propertyAccessors.forEach { property -> assertNotNull(fields["setter"]!!.get(property) as Method?) }
}
val input = DeserializationInput(sf).deserialize(output.obj)
assertEquals(100, input.a)
assertEquals(200, input.b)
assertEquals(300, input.c)
assertEquals(400, input.d)
assertEquals(500, input.e)
}
}