From 9259c9ec458e3ad82c4e0d9175daa3a98b545607 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Fri, 13 Jul 2018 14:44:03 +0100 Subject: [PATCH] Serialization documentation backport (#3594) --- docs/source/cordapp-custom-serializers.rst | 169 +++++++++++++++++- .../amqp/JavaCustomSerializerTests.java | 108 +++++++++++ 2 files changed, 268 insertions(+), 9 deletions(-) create mode 100644 serialization/src/test/java/net/corda/serialization/internal/amqp/JavaCustomSerializerTests.java diff --git a/docs/source/cordapp-custom-serializers.rst b/docs/source/cordapp-custom-serializers.rst index 4541d18f3d..733989bf03 100644 --- a/docs/source/cordapp-custom-serializers.rst +++ b/docs/source/cordapp-custom-serializers.rst @@ -1,3 +1,9 @@ +.. highlight:: kotlin +.. raw:: html + + + + Pluggable Serializers for CorDapps ================================== @@ -25,8 +31,7 @@ Serializers inheriting from SerializationCustomSerializer have to implement two Example ------- -Consider this example class - +Consider the following class: .. sourcecode:: java @@ -34,6 +39,9 @@ Consider this example class private final Int a private final Int b + // Because this is marked private the serialization framework will not + // consider it when looking to see which constructor should be used + // when serializing instances of this class. private Example(Int a, Int b) { this.a = a; this.b = b; @@ -48,23 +56,166 @@ Consider this example class Without a custom serializer we cannot serialize this class as there is no public constructor that facilitates the initialisation of all of its properties. -To be serializable by Corda this would require a custom serializer as follows: +.. note:: This is clearly a contrived example, simply making the constructor public would alleviate the issues. + However, for the purposes of this example we are assuming that for external reasons this cannot be done. + +To be serializable by Corda this would require a custom serializer to be written that can transform the unserializable +class into a form we can serialize. Continuing the above example, this could be written as follows: + +.. container:: codeset + + .. sourcecode:: java + + /** + * The class lacks a public constructor that takes parameters it can associate + * with its properties and is thus not serializable by the CORDA serialization + * framework. + */ + class Example { + private int a; + private int b; + + public int getA() { return a; } + public int getB() { return b; } + + public Example(List l) { + this.a = l.get(0); + this.b = l.get(1); + } + } + + /** + * This is the class that will Proxy instances of Example within the serializer + */ + public class ExampleProxy { + /** + * These properties will be serialized into the byte stream, this is where we choose how to + * represent instances of the object we're proxying. In this example, which is somewhat + * contrived, this choice is obvious. In your own classes / 3rd party libraries, however, this + * may require more thought. + */ + private int proxiedA; + private int proxiedB; + + /** + * The proxy class itself must be serializable by the framework, it must thus have a constructor that + * can be mapped to the properties of the class via getter methods. + */ + public int getProxiedA() { return proxiedA; } + public int getProxiedB() { return proxiedB; } + + public ExampleProxy(int proxiedA, int proxiedB) { + this.proxiedA = proxiedA; + this.proxiedB = proxiedB; + } + } + + /** + * Finally this is the custom serializer that will automatically loaded into the serialization + * framework when the CorDapp Jar is scanned at runtime. + */ + public class ExampleSerializer implements SerializationCustomSerializer { + + /** + * Given an instance of the Example class, create an instance of the proxying object ExampleProxy. + * + * Essentially convert Example -> ExampleProxy + */ + public ExampleProxy toProxy(Example obj) { + return new ExampleProxy(obj.getA(), obj.getB()); + } + + /** + * Conversely, given an instance of the proxy object, revert that back to an instance of the + * type being proxied. + * + * Essentially convert ExampleProxy -> Example + */ + public Example fromProxy(ExampleProxy proxy) { + List l = new ArrayList(2); + l.add(proxy.getProxiedA()); + l.add(proxy.getProxiedB()); + return new Example(l); + } + } + + .. sourcecode:: kotlin + + class ExampleSerializer : SerializationCustomSerializer { + /** + * This is the actual proxy class that is used as an intermediate representation + * of the Example class + */ + data class Proxy(val a: Int, val b: Int) + + /** + * This method should be able to take an instance of the type being proxied and + * transpose it into that form, instantiating an instance of the Proxy object (it + * is this class instance that will be serialized into the byte stream. + */ + override fun toProxy(obj: Example) = Proxy(obj.a, obj.b) + + /** + * This method is used during deserialization. The bytes will have been read + * from the serialized blob and an instance of the Proxy class returned, we must + * now be able to transform that back into an instance of our original class. + * + * In our example this requires us to evoke the static "of" method on the + * Example class, transforming the serialized properties of the Proxy instance + * into a form expected by the construction method of Example. + */ + override fun fromProxy(proxy: Proxy) : Example { + val constructorArg = IntArray(2); + constructorArg[0] = proxy.a + constructorArg[1] = proxy.b + return Example.of(constructorArg) + } + } + +In the above examples + +- ``ExampleSerializer`` is the actual serializer that will be loaded by the framework to serialize instances of the ``Example`` type. +- ``ExampleSerializer.Proxy``, in the Kotlin example, and ``ExampleProxy`` in the Java example, is the intermediate representation used by the framework to represent instances of ``Example`` within the wire format. + +The Proxy Object +---------------- + +The proxy object should be thought of as an intermediate representation that the serialization framework +can reason about. One is being written for a class because, for some reason, that class cannot be +introspected successfully but that framework. It is therefore important to note that the proxy class must +only contain elements that the framework can reason about. + +The proxy class itself is distinct from the proxy serializer. The serializer must refer to the unserializable +type in the ``toProxy`` and ``fromProxy`` methods. + +For example, the first thought a developer may have when implementing a proxy class is to simply *wrap* an +instance of the object being proxied. This is shown below .. sourcecode:: kotlin class ExampleSerializer : SerializationCustomSerializer { - data class Proxy(val a: Int, val b: Int) + /** + * In this example, we are trying to wrap the Example type to make it serializable + */ + data class Proxy(val e: Example) - override fun toProxy(obj: Example) = Proxy(obj.a, obj.b) + override fun toProxy(obj: Example) = Proxy(obj) override fun fromProxy(proxy: Proxy) : Example { - val constructorArg = IntArray(2); - constructorArg[0] = proxy.a - constructorArg[1] = proxy.b - return Example.create(constructorArg) + return proxy.e } } +However, this will not work because what we've created is a recursive loop whereby synthesising a serializer +for the ``Example`` type requires synthesising one for ``ExampleSerializer.Proxy``. However, that requires +one for ``Example`` and so on and so forth until we get a ``StackOverflowException``. + +The solution, as shown initially, is to create the intermediate form (the Proxy object) purely in terms +the serialization framework can reason about. + +.. important:: When composing a proxy object for a class be aware that everything within that structure will be written + into the serialized byte stream. + Whitelisting ------------ By writing a custom serializer for a class it has the effect of adding that class to the whitelist, meaning such diff --git a/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaCustomSerializerTests.java b/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaCustomSerializerTests.java new file mode 100644 index 0000000000..b6248d9c16 --- /dev/null +++ b/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaCustomSerializerTests.java @@ -0,0 +1,108 @@ +package net.corda.serialization.internal.amqp; + +import net.corda.core.serialization.SerializationCustomSerializer; +import net.corda.serialization.internal.AllWhitelist; +import net.corda.serialization.internal.amqp.testutils.TestSerializationContext; +import org.junit.Test; + +import java.io.NotSerializableException; +import java.util.ArrayList; +import java.util.List; + +public class JavaCustomSerializerTests { + /** + * The class lacks a public constructor that takes parameters it can associate + * with its properties and is thus not serializable by the CORDA serialization + * framework. + */ + static class Example { + private Integer a; + private Integer b; + + Integer getA() { return a; } + Integer getB() { return b; } + + public Example(List l) { + this.a = l.get(0); + this.b = l.get(1); + } + } + + /** + * This is the class that will Proxy instances of Example within the serializer + */ + public static class ExampleProxy { + /** + * These properties will be serialized into the byte stream, this is where we choose how to + * represent instances of the object we're proxying. In this example, which is somewhat + * contrived, this choice is obvious. In your own classes / 3rd party libraries, however, this + * may require more thought. + */ + private Integer proxiedA; + private Integer proxiedB; + + /** + * The proxu class itself must be serializable by the framework, it must thus have a constructor that + * can be mapped to the properties of the class via getter methods. + */ + public Integer getProxiedA() { return proxiedA; } + public Integer getProxiedB() { return proxiedB; } + + + public ExampleProxy(Integer proxiedA, Integer proxiedB) { + this.proxiedA = proxiedA; + this.proxiedB = proxiedB; + } + } + + /** + * Finally this is the custom serializer that will automatically loaded into the serialization + * framework when the CorDapp Jar is scanned at runtime. + */ + public static class ExampleSerializer implements SerializationCustomSerializer { + + /** + * Given an instance of the Example class, create an instance of the proxying object ExampleProxy. + * + * Essentially convert Example -> ExampleProxy + */ + public ExampleProxy toProxy(Example obj) { + return new ExampleProxy(obj.getA(), obj.getB()); + } + + /** + * Conversely, given an instance of the proxy object, revert that back to an instance of the + * type being proxied. + * + * Essentially convert ExampleProxy -> Example + * + */ + public Example fromProxy(ExampleProxy proxy) { + List l = new ArrayList(2); + l.add(proxy.getProxiedA()); + l.add(proxy.getProxiedB()); + return new Example(l); + } + + } + + @Test + public void serializeExample() throws NotSerializableException, NoSuchFieldException, IllegalAccessException { + SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(), + new EvolutionSerializerGetter(), + new SerializerFingerPrinter()); + SerializationOutput ser = new SerializationOutput(factory); + + List l = new ArrayList(2); + l.add(10); + l.add(20); + Example e = new Example(l); + + CorDappCustomSerializer ccs = new CorDappCustomSerializer(new ExampleSerializer(), factory); + factory.registerExternal(ccs); + + ser.serialize(e, TestSerializationContext.testSerializationContext); + + + } +}