Recently, in a customer project, we needed some Ajax functionality in an otherwise Struts-based application. For ease of communication we wanted JSON as the protocol between GUI and backend. Due to project constraints however, we were restricted to a Java 1.4 code-base and could not introduce new third-party libraries, otherwise we would have probably considered Json-lib, which we had used successfully in the past, or possibly Jersey's ability for JAXB-based JSON serialization. Being trapped in this emergency situation, we did what we usually try to avoid: We re-invented the wheel and wrote SOS, which very conveniently also translates to Simple Object Serialization. The outcome was a low-footprint, easy-to-use but very flexible framework for Java object serialization for JSON, XML and other data bindings. This article explains the usage of the SOS library, but more importantly it offers an insight in object serialization in general.
Freitag, 22. Mai 2009
Java Object Serialization
Availability and Usage
In case you are in a rush and want to see yourself, rather than read any details, we made SOS available under Apache 2.0 license. You can download the source, complete with unit tests and build script, as a single ZIP archive, or the binaries as a JAR. You can also browse the Javadoc online.
| Current version | ||
|---|---|---|
| Source Code | sos-0_5.zip | 48 KB |
| Binaries | sos-0_5.jar | 63 KB |
| Javadoc | sos-0_5-javadoc.zip | 336 KB |
For instructions on the usage of SOS, you might want to read this article, but in short, to serialize a Java object to JSON use the following line:
SosSerializer.getSerializer("application/json").toString(new SosBuilder().toSos(yourObject));
Use the mime type "application/xml", if you want XML rather than JSON.
Design Goals
Partially because of the constraints of the customer project, but also because we wanted to avoid certain disadvantages of other existing serialization tools, we decided on the following design goals:
- Compatibility to Java 1.4 onwards
- Minimal or zero dependencies
- Ability to serialize any Pojo
- Support for data construction without domain classes
- Easy to use
- Very customizable
- Support for more than one data binding
Reflection Issues
A simple and fail-safe approach for object serialization is of course to annotate the Java classes and members in the source code. However, since we wanted to support serialization of any Java object (for which the source code might not be available) and of Java 1.4 or higher, we had to drop this idea.
There are of course two obvious ways to serialize the state of a Java object:
- Direct field access
- Field access via getter-methods (bean pattern)
In an ideal world a getter-method would always simply return the value of the correspondent field, but there are many reasons why its behaviour might be different, deferred-loading being just one of them. We decided not to rely on just one of the methods, but to implement both methods as optional strategies. In fact, we implemented two further strategies derived from the ones named above:
- Field access if corresponding getter-method exists
- Getter-access if corresponding field exists
To maintain ease-of-use, SOS ist pre-configured to use getter-access, which we considered to be the most versatile method. The difference between the four methods is more significant than one might expect. Just take a quick look at the JSON representations of a java.util.GregorianCalendar object created with the four different methods:
Where the difference between the first two methods, "Getter" vs. "Field" access, is obvious, the difference between the latter two variants is more subtle. But, if you take a close look, you can see that the calendar has a field time of the datatype long to store the time in milliseconds and a method getTime() which returns a java.util.Date object.
With SOS, you set the different access strategies on the SosBuilder class or instance. In general, configurable strategies can be applied statically to the class with the setDefaultXXX methods.
PropertyAccessStrategy accessStrategy =
PropertyAccessStrategy.getStrategy(PropertyAccessStrategy.FIELD);
SosBuilder.setDefaultPropertyAccess(accessStrategy);
Of course you can provide your own access strategy: Simply extends the abstract class PropertyAccessStrategy and implement the method public HashMap getProperties(Object obj).
If you are very familiar with the class GregorianCalendar, you might have noticed in the examples above, that some fields or getter-methods are missing. This is the case, because the SosBuilder was configured to ignore private, protected or static getter-methods and static or transient fields. This is the default configuration of the SosBuilder, and you do not need set it, as long as you keep the default access strategy. However, if you do want to change something, here is how it is done:
accessStrategy.setExcludedMethodModifiers(Modifier.PRIVATE | Modifier.PROTECTED | Modifier.STATIC); accessStrategy.setExcludedFieldModifiers(Modifier.STATIC | Modifier.TRANSIENT);
Next to the modifier-based access limitations, there is one more possibility to limit field access of a property access strategy: you can specify a field name filter. The SosBuilder's default access strategy is configured to avoid accessing the property class:
accessStrategy.setFieldNameFilter(new FieldNameFilter() {
public boolean allow(Object obj, String fieldName) {
return !fieldName.equals("class");
}
});
If for some reason, you are not happy with the way SOS serializes one of your objects, and none of the customizing described above helps, you can define a custom converter. By default, the SosBuilder is configured to use a custom converter for the class java.util.Date:
SosBuilder.setDefaultCustomConverter(Date.class, DateConverter.DATE_OBJECT_CONVERTER);
In order to write your own custom converter, you have to implement the ObjectConverter interface and implement its method public Sos toSos(Object o). This can be done easily with the meta object model API.
Meta Object Model
When you call the SosBuilder's method toSos on a Java object, it traverses the object hierarchy and determines the property values of each object in the tree by the means of a configurable access strategy.
During this process the SosBuilder constructs a meta object model, which, in terms of its hierachical structure is equivalent to the original object tree, but is only constructed from objects which implement the Sos interface. Of course, we could have decided to serialize the data directly from the original object hierarchy instead of performing this intermediate step, but since we wanted to support multiple data bindings, we wanted to have this abstract data layer in order to separate the data extraction process from the data serialization process. A further advatage is, that you can directly construct a meta object hierarchy through the API and serialize it, thus avoiding the need for "artificial" domain classes.
The meta object model is made from one or more instances of the following classes, all implementing the Sos interface:
- SosValue
- SosObject
- SosMap
- SosArray
The SosValue class is the most primitive of all. It can only hold a Java primitive, the primitives' class equivalents or a string. For each of these types, SosValue provides its own constructor. A SosValue cannot have any child objects and is therefore always a leaf in the meta object tree.
A SosObject can be considered as a set of key/value pairs, where key is the name of the property of the originating Java object and value the Sos representation of the property's value. Therefore, if you use one of the set methods of SosObject with a value other than an object implementing the Sos interface, the value will be converted to it's Sos representation first.
A SosMap is basically nothing but a SosObject, with the difference, that it's method isMap will return true instead of false.
A SosArray is a collection of objects implementing the Sos interface. However, you can add any object or primitive to it, since SosArray will automatically convert it to it's Sos representation.
For an example on how to use the meta object model API, let's have a look at a simple custom converter, which converts a java.awt.Point:
import de.naxos.sos.*;
import de.naxos.sos.conversion.ObjectConverter;
import java.awt.Point;
public class PointConverter implements ObjectConverter {
public Sos toSos(Object o) {
return (o instanceof Point) ? new SosObject().set("x", ((Point)o).x).set("y", ((Point)o).y) : null;
}
}
The class java.awt.Point actually lends itself to introduce another important issue of object serialization: Cycle detection.
Cycle Detection
Let us have a look at the source code of the class java.awt.Point (reduced to fields and getters):
public class Point extends Point2D implements java.io.Serializable {
public int x;
public int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public double getX() {
return x;
}
public double getY() {
return y;
}
public Point getLocation() {
return new Point(x, y);
}
public boolean equals(Object obj) {
if (obj instanceof Point) {
Point pt = (Point)obj;
return (x == pt.x) && (y == pt.y);
}
return super.equals(obj);
}
}
If you used the getter-access method for serialization, you would determine three properties: x, y, and location. The values for x and y are of simple data types and therefore leaves in the object hierarchy, but the value of location is a child object and needs serialization itself. Since however, location is yet another java.awt.Point we would run into a cycle, which would finally terminate in a stack overflow exception.
Here is where cycle detection becomes important. SOS implements two different cycle detection strategies:
- IdentityCycleDetector
- EqualityCycleDetector
The latter strategy is configured by default, but you can use the other or define your own:
SosBuilder.setDefaultCycleDetector(new IdentityCycleDetector());
In case of java.awt.Point, identity cycle detection would not work, because the method getLocation() returns a new instance of java.awt.Point. If it simply returned this, identity cycle detection would be sufficient.
Data Binding
Once the meta object model is created, serialization to different data formats is pretty straight forward. In SOS this is the task of SosSerializer. Actually the class SosSerializer serves multiple purposes: it is the registry for new data bindings, the factory for the concrete serializer instances and the abstract base class for any SOS serializer. A concrete serializer has to implement the two abstract methods public String toString(Sos sos) and public void write(OutputStream out, Sos sos). So far SOS implements two data bindings, JSON and XML, with the classes
- JsonSerializer and
- XmlSerializer.
However, writing your own data binding is easy, because you work from the simple SOS meta object model.
Now, when we take a look at the actual serialization results, there are some issues worth mentioning. To start with, here are the XML and JSON representations of the class java.awt.Font, which lends itself as an example, because it has getter-methods returning simple datatypes as well as complex object, arrays and maps:
XML representation of the class java.awt.Font
<font>
<availableAttributes>
<textAttribute>
<name>family</name>
</textAttribute>
<textAttribute>
<name>weight</name>
</textAttribute>
<textAttribute>
<name>posture</name>
</textAttribute>
<textAttribute>
<name>size</name>
</textAttribute>
<textAttribute>
<name>transform</name>
</textAttribute>
<textAttribute>
<name>superscript</name>
</textAttribute>
<textAttribute>
<name>width</name>
</textAttribute>
</availableAttributes>
<family>SansSerif</family>
<transform>
<shearX>0.0</shearX>
<translateY>0.0</translateY>
<scaleX>1.0</scaleX>
<shearY>0.0</shearY>
<scaleY>1.0</scaleY>
<translateX>0.0</translateX>
<determinant>1.0</determinant>
</transform>
<missingGlyphCode>0</missingGlyphCode>
<size2D>10.0</size2D>
<numGlyphs>1674</numGlyphs>
<psName>SansSerif.plain</psName>
<attributes>
<entry>
<key>java.awt.font.TextAttribute(posture)</key>
<value>0.0</value>
</entry>
<entry>
<key>java.awt.font.TextAttribute(superscript)</key>
<value>0</value>
</entry>
<entry>
<key>java.awt.font.TextAttribute(size)</key>
<value>10.0</value>
</entry>
<entry>
<key>java.awt.font.TextAttribute(weight)</key>
<value>1.0</value>
</entry>
<entry>
<key>java.awt.font.TextAttribute(width)</key>
<value>1.0</value>
</entry>
<entry>
<key>java.awt.font.TextAttribute(family)</key>
<value>Helvetica</value>
</entry>
<entry>
<key>java.awt.font.TextAttribute(transform)</key>
<value>
<transformAttribute>
<transform>
<shearX>0.0</shearX>
<translateY>0.0</translateY>
<scaleX>1.0</scaleX>
<shearY>0.0</shearY>
<scaleY>1.0</scaleY>
<translateX>0.0</translateX>
<determinant>1.0</determinant>
</transform>
</transformAttribute>
</value>
</entry>
</attributes>
<italicAngle>0.0</italicAngle>
<style>0</style>
<size>10</size>
<name>Helvetica</name>
<fontName>SansSerif.plain</fontName>
</font>
JSON representation of the class java.awt.Font
{
"availableAttributes": [{
"name": "family"
},
{
"name": "weight"
},
{
"name": "posture"
},
{
"name": "size"
},
{
"name": "transform"
},
{
"name": "superscript"
},
{
"name": "width"
}],
"family": "SansSerif",
"transform": {
"shearX": 0.0,
"translateY": 0.0,
"scaleX": 1.0,
"shearY": 0.0,
"scaleY": 1.0,
"translateX": 0.0,
"determinant": 1.0
},
"missingGlyphCode": 0,
"size2D": 10.0,
"numGlyphs": 1674,
"psName": "SansSerif.plain",
"attributes": {
"java.awt.font.TextAttribute(posture)": 0.0,
"java.awt.font.TextAttribute(superscript)": 0,
"java.awt.font.TextAttribute(size)": 10.0,
"java.awt.font.TextAttribute(weight)": 1.0,
"java.awt.font.TextAttribute(width)": 1.0,
"java.awt.font.TextAttribute(family)": "Helvetica",
"java.awt.font.TextAttribute(transform)": {
"transform": {
"shearX": 0.0,
"translateY": 0.0,
"scaleX": 1.0,
"shearY": 0.0,
"scaleY": 1.0,
"translateX": 0.0,
"determinant": 1.0
}
}
},
"italicAngle": 0.0,
"style": 0,
"size": 10,
"name": "Helvetica",
"fontName": "SansSerif.plain"
}
Firstly noticable is the XML root element font: The XmlSerializer created that from the class name java.awt.Font of the Java object by skipping the package part and converting the first character of the remaining class name to lower case. For JSON, there is no need to do so, because the root does not have to be named, it is simply indicated by an opening curly brace.
Pretty much the same is true for the elements of the availableAttributes array: For XML, the element names (textAttribute) are synthesized from the objects' class names, for JSON it is again simply a curly brace.
The need for "synthetic" XML elements becomes most apparent, when serializing a map object. In the examples above, the element attributes is such a map object. In JSON, this is represented as set of key/value-pairs, actually just like any object with its fields. The XmlSerializer however, found it necessary to introduce the synthetic XML elements <entry>, <key>, and <value>.
But another serialization issue becomes apparent, when we look at the attributes map: In a Java HashMap or the like, any object can be a key, in JSON however, where maps are represented just like objects, a key must be a single value. Currently SOS therefore simply calls the toString() method on the key object and serializes the resulting value. A different approach would be to introduce entry, key and value elements for JSON just like in the XML representation, so that key could actually be an object. We might consider this approach as an option in a future version of SOS. Its use however is questionable, because object identity can not easily maintained with serialization.
Using SOS with Jersey or a different JSR311 (REST) framework
Jersey is my favourite framework, when it comes to REST services, however, it does not support serialization of "legacy" Java objects. Luckily, SOS or any other serializer can be used to fill this gap. For SOS, simply write the class SosWriter and make sure it is somewhere in the classpath, so that Jersey can find it.
import de.naxos.sos.Sos;
import de.naxos.sos.SosBuilder;
import de.naxos.sos.SosSerializer;
import de.naxos.sos.reflect.PropertyAccessStrategy;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import javax.ws.rs.ext.*;
import javax.xml.bind.annotation.XmlRootElement;
@Provider
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public class SosWriter implements MessageBodyWriter {
static {
PropertyAccessStrategy accessStrategy = PropertyAccessStrategy.getStrategy(PropertyAccessStrategy.FIELD_IF_GETTER_EXISTS);
accessStrategy.setExcludedFieldModifiers(Modifier.STATIC | Modifier.TRANSIENT);
SosBuilder.setDefaultPropertyAccess(accessStrategy);
}
public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return
type.getAnnotation(XmlRootElement.class) == null && (
MediaType.APPLICATION_JSON.toString().equals(mediaType.toString()) ||
MediaType.APPLICATION_XML.toString().equals(mediaType.toString())
);
}
public long getSize(Object t, Class type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return -1L;
}
public void writeTo(Object t, Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException {
SosBuilder builder = new SosBuilder();
Sos sos = builder.toSos(t);
SosSerializer.getSerializer(mediaType.toString()).write(entityStream, sos);
}
}
If you take a look at the implementation of the isWriteable method, you will see, that SOS is only used for non-JAXB objects (no annotation as XmlRootElement) and only when JSON or XML serialization is requested.
Outlook
Even though we consider SOS quite complete, there are a few features, which would be nice to have:
- Objects as map keys
- More influence on the actual JSON or XML representation (naming convention strategies)
- Deserialization (JSON / XML --> Java)

