Azrael is a serialization library for Java. It allows Java objects to be saved ("serialized") to some data format, and to be reconstructed ("deserialized") from that saved data at a later time.
The form to which objects are serialized is parameterizable. By default, Azrael comes with a serializer for JSON and XML, but its API allows you to easily implement serialization to your own custom data format.
Suppose you have the following class:
class MyClass {
int x = 0;
String s = "";
List<Float> f = new LinkedList<Float>();
public add(Float x) {
f.add(x);
}
}
You would like to serialize the state of objects of class MyClass
to
JSON. With Azrael, you can do the following:
// Create an object and fill with values
MyClass my_obj = new MyClass();
my_obj.x = 1; my_obj.s = "abc"; my_obj.add(1.5);
// Create a serializer for JSON
JsonPrinter p = new JsonPrinter();
JsonElement e = p.print(my_obj);
The contents of my_obj
are saved in a JSON element, which you can save
somewhere as a string using its toString()
method.
Now suppose you want to reconstruct an object of MyClass
with the exact
data that was contained in the saved JSON. You first reconstruct the JSON
element e
(from a String, etc.), and then call the read()
method:
JsonReader d = new JsonReader();
MyClass my_new_obj = (MyClass) d.read(e);
You could check for yourself that the member fields of my_obj
and
my_new_obj
are identical. Note that you don't need to use the same
instance serializer for both operations, or be in the same program when
saving and loading.
This, in a nutshell, is how Azrael works (and how other serialization libraries work too, although with some peculiarities).
Azrael was developed out of insatisfaction with existing (JSON) serialization libraries, mostly Java Google Gson (Gson) and Genson. Here are a couple of features of Azrael for which other libraries didn't fit the author's needs.
Existing serialization libraries are tied to a single specific output format. Hence, if you want to serialize objects to another format than JSON (XML, strings, whatever custom format you wish), a JSON library can't help you. You need to use yet another library (such as XStream for XML).
Not so with Azrael. Its default ObjectPrinter
and ObjectReader
classes take care of much of the work of decomposing an object and
determining type information. JSON is just one possible way of implementing
the abstract methods of these two classes. To serialize to another format,
simply override them in a different way to produce the output you wish. (As
a matter of fact, the code specific to JSON serialization is a mere 600
lines long.) The serialized object is not even aware of the format it is
being serialized into.
This means that, in the example above, if you want to serialize my_obj
into XML, you simply pass my_obj
to another type of object printer:
XmlPrinter p = new XmlSerializer();
XmlElement e = p.print(my_obj);
Another nice consequence of Azrael's structure is that you can write serializers that do not even perform serialization. For example:
- The
Core
folder provides an implementation of a serializer that prints an object as itself. The deserializer deserializes primitive values as themselves, and deserializes other objects as new instances of themselves. The end result is a process that performs a deep copy of an object. - The
Size
folder contains an implementation of a serializer that turns any object into a number. This can be used to compute the size of an arbitrary object using just 350 lines of code.
Most libraries use reflection to serialize an object. Some may argue that this is a "brutal" process: the object is not aware it is being serialized, its internal contents are revealed bare (totally disregarding visibility modifiers), and has no control over what and how things are serialized. The reverse operation creates an empty object skeleton, and forcefully populates its member fields --effectively treating the object as an inert "bag of data".
In contrast, an object can choose to cooperate with Azrael by implementing
the Printable
interface to serialize itself of its own will, and the
Readable
interface to create a new instance from a serialized version.
What is more, the object does not need to be aware of the format to which it
is serialized: Azrael takes care of that.
As an example, consider again the class MyClass
:
class MyClass implements Printable, Readable {
int x = 0;
String s = "";
List<Float> f = new LinkedList<Float>();
public add(Float x) {
f.add(x);
}
public Object print(ObjectPrinter<?> printer) {
List<Object> list = new ArrayList<Object>();
list.add(System.currentTimeMillis() / 1000);
list.add(x);
list.add(s);
list.add(f);
return printer.print(list);
}
public Object read(ObjectReader<?> reader, Object o) throws ReadException {
List<Object> list = (List<Object>) reader.read(o);
long now = System.currentTimeMillis() / 1000;
if (now - (Long) list.get(0) > 600) {
throw new ReadException("Copy is too old");
}
MyClass mc = new MyClass((Integer) list.get(1), (String) list.get(2));
for (float f : (List<Float>) list.get(3)) {
mc.add(f);
}
return mc;
}
}
This time, the class implements Printable
, and decides what and how to
print its contents. In this case, the choice is to use a list; notice how
the first element of that list is not even part of the object's state
(in this case, a timestamp indicating when the serialization was made).
The object simply asks an anonymous ObjectPrinter
to print the contents of
this list --whether this is done with JSON, XML or something else is
completely irrelevant to the class.
Similarly, the read
method implements the Readable
interface. Notice
how the object asks an ObjectReader
to deserialize an arbitrary object
o
, which recovers the list that was saved by print
. Again, exactly what
is o
(XML? JSON? something else?) is irrelevant. The contents of the
list are used to recreate a new instance of MyClass
, but through means
that the object itself controls. Remark how the timestamp that was
serialized by the object is used to throw an exception when the copy is
too old.
When serializing member fields of an object, Azrael inserts information about the actual type of an object, and not the type that is declared in a class. Consider the following example:
abstract class A {
}
class B extends A {
int x = 0;
}
class C {
A my_b = new B();
}
When deserializing an object of class C
, other libraries run into a
problem, as they try to instantiate an object of class A
, since this is
the declared type of field my_b
. But A
is an abstract class, and
cannot be instantiated. To handle this case with Gson, you need to write
yet more custom code to take care of this (not quite) exceptional
situation.
Not so with Azrael, which takes care of adding to the serialization that
the actual class of my_b
is B
, enabling it to properly deserialize the
object. Works out of the box, period.
This means that you can serialize generic collections such as lists, sets and maps easily. In many other serialization libraries, "collections require special treatment since the Collections are of generic type and the type information is lost while converting to JSON due to Java type erasure" (says the Gson documentation).
Not so with Azrael, which does takes care of serializing the exact class of each element in a collection (list, map, set). Hence you can write, as you would for any other object:
List<Integer> list1 = new LinkedList<Integer>();
(...Fill the list with stuff...)
Object o = serializer.print(list1);
List<Integer> list2 = (List<Integer>) serializer.read(o);
The contents of list2
recreate precisely the original objects with their
actual (not declared) type. No custom code is needed (contrary to what
Gson requires).
Azrael also defines an interface called a Fridge
, which is an
implementation of the memento pattern.
A fridge can be used to store an object (using a method called store
),
and retrieve this object at a later time (using a method called fetch
).
Exactly how and where this object is stored is transparent to the user.
For example, a FileFridge
serializes the object (using an arbitrary
format) and saves it as a local text file. The fetch operation loads that
file and deserializes its content to recreate the original object. Consider
the following code:
MyClass mc = ...
XmlFileFridge fridge = new XmlFileFridge("/path/to/file.xml");
fridge.store(mc);
(... Some time later ...)
MyClass mc_new = (MyClass) fridge.fetch();
The generic Fridge
interface can be implemented for other purposes. For
example, one could imagine a fridge that sends the object's contents to
a remote server using an HTTP request, or that stores it into a database.
If you use Azrael as a library within your own project, it cannot instantiate objects outside of the basic Java classes out of the box. You need to give one or more class loaders that will enable it to create instances of your objects.
Suppose for example that you have a package called my.package
; to
help Azrael create objects from this package, do the following:
my_serializer.addClassLoader(my.package.MyClass.class.getClassLoader());
where MyClass
is any of the classes of my.package
. This should normally
be enough for all classes of that package. You can add more than one
class loader to Azrael; when it attempts to instantiate an object, it tries
them all until one of them works.
If you would like to serialize objects that do not implement Printable
and
Readable
in a special way, you can do so by creating a custom
PrintHandler
and ReadHandler
. The print handler must implement a method
called canHandle
, which must return true
when given an object it can
serialize (and false
otherwise). The method handle
should contain custom
code that takes care of printing the content of that object. Conversely, the
read handler also has a method called canHandle
, and another called
read
which should should contain custom code that takes care of reading
the content of that object.
In version 9 of Java onwards, serialization using reflection can sometimes cause an exception that looks like this:
java.lang.reflect.InaccessibleObjectException: Unable to make field private
static final jdk.internal.misc.Unsafe jdk.internal.misc.InnocuousThread.UNSAFE accessible:
module java.base does not "opens jdk.internal.misc" to unnamed module @5cba847b
...
As per this StackOverflow answer, this is caused by the fact that "one of the dependencies of your project is trying to access a JVM API which was moved to an internal Java module, and is no longer exposed". The workaround is to run the application with the following flag:
--add-opens jdk.management/xxx.xxx.xxx=ALL-UNNAMED
Where xxx.xxx.xxx
is the name of the offending package in the exception's
message (here "jdk.internal.misc").
Another possibility is to make Azrael silently ignore these warnings. In
order to do so, call ignoreAccessChecks(true)
on an ObjectPrinter
or
an ObjectReader
.
This project is separated in two parts:
- The
Core
folder generates a small JAR file that only defines the interfaces to support serialization. If you develop a library and want your objects to be serialized, simply include this JAR in your project. - The other folders implement serialization in a variety of formats. For
example, the
Json
folder provides a JSON serializer; theXml
folder provides an XML serializer.
These JARs may themselves have dependencies.
- JSON serialization requires that the library json-lif be in your classpath.
- XML serialization requires that the library xml-lif be in your classpath.
- Cornipickle, a web testing tool
- LabPal, a library for running experiments on a computer
- The Serialization palette and Hibernate palette of the BeepBeep 3 event stream processing engine.
All the letters of "Azrael" are contained in the word "serialization". Anything to add?
Azrael was written by Sylvain Hallé, professor at Université du Québec à Chicoutimi, Canada.