Binäres JSON mit bson4jackson

JSON hat sich inzwischen als hervorragende Alternative zu XML etabliert. Die meisten JSON-Parser in Java sind jedoch noch relativ langsam. Auf meiner Suche nach schnelleren Libraries bin ich auf zwei Dinge gestoßen: BSON und Jackson.

BSON ist binär kodiertes JSON. Das Format ist besonders darauf ausgelegt, schnell und einfach maschinell lesbar zu sein. BSON wurde bekannt durch seine Verwendung als primäres Austauschformat für das dokumentenorientierte Datenbankmanagementsystem MongoDB. Jackson ist laut dem JVM Serializers Benchmark einer der schnellsten JSON-Prozessoren, die es gibt. Außerdem bietet Jackson die Möglichkeit, eigene Erweiterungen hinzuzufügen und somit die Bibliothek um weitere Austauschformate zu ergänzen.

bson4jackson

Das ist der Punkt, an dem bson4jackson ins Spiel kommt. Die Bibliothek erweitert Jackson um die Möglichkeit, BSON-Dokumente zu lesen und zu schreiben. Dadurch, dass bson4jackson vollständig integriert ist, kann man die wirklich sehr gelungene API von Jackson nutzen. Somit lassen sich einfache POJOs sehr schnell serialisieren. Angenommen man möchte Objekte der folgende Klasse mittels BSON austauschen:

public class Person {
  private String _name;
 
  public void setName(String name) {
    _name = name;
  }
 
  public String getName() {
    return _name;
  }
}

Dann kann man den so genannten ObjectMapper benutzen, der das schnelle Serialisieren von POJOs erlaubt:

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.undercouch.bson4jackson.BsonFactory;
 
public class ObjectMapperSample {
  public static void main(String[] args) throws Exception {
    //create dummy POJO
    Person bob = new Person();
    bob.setName("Bob");
 
    //serialize data
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectMapper mapper = new ObjectMapper(new BsonFactory());
    mapper.writeValue(baos, bob);
 
    //deserialize data
    ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
    Person clone_of_bob = mapper.readValue(bais, Person.class);
 
    assert bob.getName().equals(clone_of_bob.getName());
  }
}

Oder man verwendet die Streaming API von Jackson und serialisiert das Objekt manuell:

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import de.undercouch.bson4jackson.BsonFactory;
 
public class ManualSample {
  public static void main(String[] args) throws Exception {
    //create dummy POJO
    Person bob = new Person();
    bob.setName("Bob");
 
    //create factory
    BsonFactory factory = new BsonFactory();
 
    //serialize data
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    JsonGenerator gen = factory.createJsonGenerator(baos);
    gen.writeStartObject();
    gen.writeFieldName("name");
    gen.writeString(bob.getName());
    gen.close();
 
    //deserialize data
    ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
    JsonParser parser = factory.createJsonParser(bais);
    Person clone_of_bob = new Person();
    parser.nextToken();
    while (parser.nextToken() != JsonToken.END_OBJECT) {
      String fieldname = parser.getCurrentName();
      parser.nextToken();
      if ("name".equals(fieldname)) {
        clone_of_bob.setName(parser.getText());
      }
    }
 
    assert bob.getName().equals(clone_of_bob.getName());
  }
}

Optimierung fürs Streaming

Eine Einschränkung des BSON-Formats ist, dass jedes Dokument mit einer Längenangabe beginnt. Beim Schreiben muss die Länge des gesamten Dokuments also im Voraus bekannt sein. bson4jackson ist gezwungen, das Dokument zu puffern bevor es in den OutputStream geschrieben werden kann. Der Parser von bson4jackson ignoriert die Längeangabe jedoch, sodass man sie auch weglassen kann. Dazu muss man die BsonFactory folgendermaßen erzeugen:

BsonFactory fac = new BsonFactory();
fac.enable(BsonGenerator.Feature.ENABLE_STREAMING);

Dies kann den Serialisierungsvorgang für große Dokumente sehr beschleunigen und den Speicherverbrauch stark reduzieren. Der offizielle MongoDB-Treiber für Java ignoriert die Längenangabe übrigens auch. Die Optimierung lässt sich also ebenfalls nutzen, wenn die von bson4jackson geschriebenen Dokumente mit dem MongoDB-Treiber gelesen werden sollen.

Performance

In der Version 1.1.0 von bson4jackson wurden neben der Unterstützung für Jackson 1.7 auch einige Verbesserungen hinsichtlich der Performance vorgenommen. Inzwischen ist bson4jackson deutlich schneller als der offizielle MongoDB-Treiber für Java (Stand: Januar 2011). Dies trifft beim Serialisieren jedoch nur zu, wenn die Streaming-API verwendet wird, da durch den ObjectMapper von Jackson noch ein kleiner Overhead dazu addiert werden muss (der MongoDB-Treiber arbeitet allerdings auch mit einer Art Streaming-API). Beim Deserialisieren ist bson4jackson immer schneller. Die aktuellen Ergebnisse des Benchmarks kann man unter folgender Seite nachschauen:

https://github.com/eishay/jvm-serializers/wiki

Kompatibilität mit MongoDB

In Version 1.2.0 wurde die Kompatibilität von bson4jackson zu MongoDB deutlich verbessert. Dank der Beteiligung von James Roper unterstützt die BsonParser-Klasse nun das Feature HONOR_DOCUMENT_LENGTH. Es bringt den Parser dazu, die ersten 4 Bytes, also die Größe eines Dokuments, auszuwerten und zu beachten. Natürlich funktioniert dies nur, wenn das Feature BsonGenerator.Feature.ENABLE_STREAMING nicht während der Erzeugung des Dokuments aktiviert war.

Das Feature ist besonders nützlich, um mehrere Dokumente aus einem Input-Stream von MongoDB zu lesen. Es kann wie folgt aktiviert werden:

BsonFactory fac = new BsonFactory();
fac.enable(BsonParser.Feature.HONOR_DOCUMENT_LENGTH);
BsonParser parser = (BsonParser)fac.createJsonParser(...);

Unterstützung für Jackson 2.0

Jackson 2.0 wird seit bson4jackson 2.0 unterstützt. Falls Sie nach einer Version suchen, die mit Jackson 1.x läuft, dann laden Sie bitte bson4jackson 1.3.0 herunter.

Download

Vorkompilierte Binaries

Vorkompilierte Binärdateien von bson4jackson kann man sich in meinem GitHub-Repository herunterladen. Zusätzlich benötigt man nur noch eine Kopie von Jackson und schon kann es losgehen.

Maven/sbt/buildr

Alternativ kann man auch Maven benutzen, um bson4jackson herunterzuladen:

<dependencies>
  <dependency>
    <groupId>de.undercouch</groupId>
    <artifactId>bson4jackson</artifactId>
    <version>2.0.0</version>
  </dependency>
</dependencies>

Für sbt muss man folgende Zeile zu seiner Projektbeschreibung hinzufügen:

val bson4jackson = "de.undercouch" % "bson4jackson" % "2.0.0"

Für buildr kann man folgenden Code nutzen:

compile.with 'de.undercouch:bson4jackson:jar:2.0.0'
blog comments powered by Disqus