SerializableAutoValueExtension
can be extended to support additional un-serializable types. Extensions are created by implementing SerializerExtension
.
To use an extension that‘s not bundled with SerializableAutoValueExtension
, include it in your AutoValue
class’s dependencies.
To add serialization support to a new type, write a class that extends SerializerExtension
. Put that class on the processorpath
along with SerializerExtension
.
See [AutoService
] for Java extension loading information.
SerializableAutoValueExtension
iterates through each property of the @AutoValue
class and tries to look up a SerializerExtension
for the property's type. If a SerializerExtension
is available, a Serializer
is returned. The Serializer
does three things:
We have a Foo
AutoValue class that we would like to serialize:
@SerializableAutoValue @AutoValue public abstract class Foo implements Serializable { public static Foo create(Bar bar) { return new AutoValue_Foo(bar); } // Bar is unserializable. abstract Bar bar(); } // An un-serializable class. public final class Bar { public int x; public Bar(int x) { this.x = x; } }
Unfortunately, Bar
is un-serializable, which makes the entire class un-serializable. We can extend SerializableAutoValue
to support Bar
by implementing a SerializerExtension
for Bar
.
// Use AutoService to make BarSerializerExtension discoverable by java.util.ServiceLoader. @AutoService(SerializerExtension.class) public final class BarSerializerExtension implements SerializerExtension { // Service providers must have a public constructor with no formal parameters. public BarSerializerExtension() {} // This method is called for each property in an AutoValue. // When this extension can handle a type (i.e. Bar), we return a Serializer. called on all SerializerExtensions. @Override public Optional<Serializer> getSerializer( TypeMirror type, SerializerFactory factory, ProcessingEnvironment env) { // BarSerializerExtension only handles Bar types. if (!isBar(type)) { return Optional.empty(); } return Optional.of(new BarSerializer(env)); } // Our implementation of how Bar should be serialized + de-serialized. private static class BarSerializer implements Serializer { private final ProcessingEnvironment env; BarSerializer(ProcessingEnvironment env) { this.env = env; } // One way to serialize Bar is to just serialize Bar.x. // We can do that by mapping Bar to an int. @Override public TypeMirror proxyFieldType() { return Types.getPrimitiveType(TypeKind.INT); } // We need to map Bar to the type we declared in {@link #proxyFieldType}. // Note that {@code expression} is a variable of type Bar. @Override public CodeBlock toProxy(CodeBlock expression) { return CodeBlock.of("$L.x", expression); } // We need to map the integer back to a Bar. @Override public CodeBlock fromProxy(CodeBlock expression) { return CodeBlock.of("new $T($L)", Bar.class, expression); } } }
BarSerializerExtension
enables AutoValue classes with Bar
properties to be serialized. For our example class Foo
, it would help SerializableAutoValue
generate the following code:
@Generated("SerializableAutoValueExtension") final class AutoValue_Foo extends $AutoValue_Foo { Object writeReplace() throws ObjectStreamException { return new Proxy$(this.x); } static class Proxy$ implements Serializable { // The type is generated by {@code BarSerializer#proxyFieldType}. private int bar; Proxy$(Bar bar) { // The assignment expression is generated by {@code BarSerializer#toProxy}. this.bar = bar.x; } Object readResolve() throws ObjectStreamException { // The reverse mapping expression is generated by {@code BarSerializer#fromProxy}. return new AutoValue_Foo(new Bar(bar)); } } }
Objects with type parameters are also supported by SerializerExtension
.
For example:
// A potentially un-serializable class, depending on the actual type of T. public final class Baz<T> implements Serializable { public T x; public Baz(int x) { this.x = x; } }
Baz
's type argument T
may not be serializable, but we could create a SerializerExtension
that supports Baz
by asking for a SerializerExtension
for T
.
@AutoService(SerializerExtension.class) public final class BazSerializerExtension implements SerializerExtension { public BazSerializerExtension() {} @Override public Optional<Serializer> getSerializer( TypeMirror type, SerializerFactory factory, ProcessingEnvironment env) { if (!isBaz(type)) { return Optional.empty(); } // Extract the T of Baz<T>. TypeMirror containedType = getContainedType(type); // Look up a serializer for the contained type T. Serializer containedTypeSerializer = factory.getSerializer(containedType); // If the serializer for the contained type T is an identity function, it // means the contained type is either serializable or unsupported. // Either way, nothing needs to be done. Baz can be serialized as is. if (containedTypeSerializer.isIdentity()) { return Optional.empty(); } // Make Baz serializable by using the contained type T serializer. return Optional.of(new BazSerializer(containedTypeSerializer)); } private static class BazSerializer implements Serializer { private Serializer serializer; BazSerializer(Serializer serialize) { this.serializer = serializer; } @Override public TypeMirror proxyFieldType() { // Since the contained type "T" is Baz's only field, we map Baz to "T"'s // proxy type. return serializer.proxyFieldType(); } @Override public CodeBlock toProxy(CodeBlock expression) { return serializer.toProxy(expression); } @Override public CodeBlock fromProxy(CodeBlock expression) { return serializer.fromProxy(expression); } } }
This implementation uses SerializerFactory
to find a Serializer
for T
. If a Serializer
is available, we use it to map Baz
to a serializable type. If no Serializer
is available, we can do nothing and let Baz
be serialized as-is.