Arreglando la serialización de objetos en Kotlin de una vez por todas


Recientemente me encontré con un artículo sobre un problema con la serialización Java de objetos en Kotlin. El autor sugirió resolverlo agregando un método readResolvea cada objeto que hereda java.io.Serializable.


, . , Bundle, is when- sealed .


, , , Kotlin readResolve , singleton- . , , . Kotlin , ! .



, :


object Example : java.io.Serializable {
   // TODO: should be generated
   fun readResolve(): Any? = Example
}

readResolve , java.io.Serializable. , Any?.


.class- IDE. , .



. , .


, ; Kotlin .


, JetBrains “kotlin-compiler-embeddable”:


// kotlin-plugin/build.gradle
apply plugin: "org.jetbrains.kotlin.jvm"
dependencies {
   implementation "org.jetbrains.kotlin:kotlin-stdlib"
   compileOnly "org.jetbrains.kotlin:kotlin-compiler-embeddable"
}

ComponentRegistrar, :


class ObjectSerializationComponentRegistrar: ComponentRegistrar {
   override fun registerProjectComponents(
       project: MockProject, 
       configuration: CompilerConfiguration
   ) {
       println("Works")
   }
}

Kotlin ServiceLoader, ComponentRegistrar. META-INF/services. AutoService Google, .


# resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
me.shika.ObjectSerializationComponentRegistrar

, :


// integration-test/build.gradle
apply plugin: "org.jetbrains.kotlin.jvm"
dependencies {
   kotlinCompilerPluginClasspath project(':kotlin-plugin')
}

Kotlin , . - , “Works” .


, , . Kotlin , JVM ( java.io.Serializable ). ExpressionCodegenExtension.


-. , , . — , , readResolve:


class ObjectSerializationJvmGeneration : ExpressionCodegenExtension {
   override fun generateClassSyntheticParts(codegen: ImplementationBodyCodegen) {
       println("Found ${codegen.descriptor}")
       // todo: generate
   }
}

, .


ProjectExtensionDescriptor<T>. registerExtension . - ExpressionCodegenExtension, .


ComponentRegistrar:


override fun registerProjectComponents(
   project: MockProject, 
   configuration: CompilerConfiguration
) {
   ExpressionCodegenExtension.registerExtension(
       project,
       ObjectSerializationJvmGeneration()
   )
}

integration-test , .


-


, , . , , .


fun ClassDescriptor.needsSerializableFix() =
   DescriptorUtils.isObject(this)
       && isSerializable()
       && !hasReadMethod()

:


  1. object-?
  2. java.io.Serializable?
  3. readResolve?

. DescriptorUtils :


fun ClassDescriptor.isSerializable(): Boolean =
   getSuperInterfaces().any {
       it.fqNameSafe == SERIALIZABLE_FQ_NAME
       || it.isSerializable()
   } || getSuperClassNotAny()?.isSerializable() == true
val SERIALIZABLE_FQ_NAME = FqName("java.io.Serializable")

Serializable.


readResolve :


fun ClassDescriptor.hasReadMethod() =
    unsubstitutedMemberScope
        .getContributedFunctions(
            SERIALIZABLE_READ, 
            NoLookupLocation.FROM_BACKEND
        )
        .any { 
            it.name == SERIALIZABLE_READ 
            && it.valueParameters.isEmpty()
        }
val SERIALIZABLE_READ = Name.identifier("readResolve")

, . .


, , , . Kotlin ASM - ClassBuilder :


private fun ImplementationBodyCodegen.addReadResolveFunction(
   block: InstructionAdapter.() -> Unit
) {
   val visitor = v.newMethod(
       NO_ORIGIN,
       ACC_PUBLIC or ACC_SYNTHETIC,
       SERIALIZABLE_READ.identifier,
       "()Ljava/lang/Object;",
       null,
       EMPTY_STRING_ARRAY
   )

   visitor.visitCode()
   val iv = InstructionAdapter(visitor)
   iv.apply(block)
   FunctionCodegen.endVisit(iv, "JVM serialization bindings")
}

public synthetic, IDE. ()Ljava/lang/Object; . , , -.


- — Example :


GETSTATIC Example.INSTANCE : LExample;
ARETURN

InstructionAdapter, , , -, . , :


if (codegen.descriptor.needsSerializableFix()) {
   val selfType = codegen.typeMapper.mapType(codegen.descriptor)

   codegen.addReadResolveFunction {
       getstatic(codegen.className, "INSTANCE", selfType.descriptor)
       areturn(selfType)
   }
}


Kotlin , -. (, -) , .


: , .
kotlin-compile-testing. Java-. (, test/resources/), .


private val SERIALIZABLE_OBJECT = """
   import java.io.Serializable

   object Serial : Serializable
""".source()

@Test
fun `adds readResolve to obj extending Serializable`() {
   compiler.sources = listOf(SERIALIZABLE_OBJECT)
   val result = compiler.compile()

   val klass = result.classLoader.loadClass("Serial")
   assertTrue(klass.methods.any { it.addedReadResolve()})
}
private fun Method.addedReadResolve() =
   name == "readResolve"
       && parameterCount == 0
       && returnType == Object::class.java
       && isSynthetic

readResolve .


. . , , — :


private object TestObject : Serializable

@Test
fun `object instance is the same after deserialization`() {
   assertEquals(TestObject, serializeDeserialize(TestObject))
}

private fun serializeDeserialize(instance: Serializable): Serializable {
   val out = ByteArrayOutputStream()
   ObjectOutputStream(out).use {
       it.writeObject(instance)
   }
   return ObjectInputStream(
       ByteArrayInputStream(out.toByteArray())
   ).use {
       it.readObject() as TestObject
   }
}


Kotlin — . , , .


, , : , API - . , Kotlin 1.4.


GitHub. Maven ( ).


All Articles