diff --git a/benchmark/pom.xml b/benchmark/pom.xml index d723304..f25a1d5 100644 --- a/benchmark/pom.xml +++ b/benchmark/pom.xml @@ -14,7 +14,7 @@ UTF-8 - 1.25.2 + 1.37 benchmarks @@ -58,15 +58,38 @@ com.fasterxml.jackson.module jackson-module-afterburner + + com.fasterxml.jackson.module + jackson-module-blackbird + com.google.code.gson gson - 2.8.6 + 2.13.2 compile + + com.dslplatform + dsl-json + 2.0.2 + + + org.apache.maven.plugins + maven-compiler-plugin + 3.12.1 + + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + + + + org.apache.maven.plugins maven-shade-plugin @@ -84,6 +107,12 @@ org.openjdk.jmh.Main + + META-INF/BenchmarkList + + + META-INF/CompilerHints + diff --git a/benchmark/src/main/java/org/sample/MyBenchmark.java b/benchmark/src/main/java/org/sample/MyBenchmark.java index 7afaf3e..239938b 100644 --- a/benchmark/src/main/java/org/sample/MyBenchmark.java +++ b/benchmark/src/main/java/org/sample/MyBenchmark.java @@ -31,11 +31,15 @@ package org.sample; +import com.dslplatform.json.DslJson; +import com.dslplatform.json.runtime.Settings; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.module.afterburner.AfterburnerModule; +import com.fasterxml.jackson.module.blackbird.BlackbirdModule; import com.google.gson.Gson; import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; @@ -50,6 +54,7 @@ import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.annotations.Warmup; +import org.sample.jackson.Person2Deserializer; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -180,6 +185,24 @@ public void setup() { } } + @State(Scope.Benchmark) + public static class BlackbirdWriter { + public com.fasterxml.jackson.databind.ObjectWriter objectWriter; + public Person2 person; + + @Setup(Level.Trial) + public void setup() { + ObjectMapper mapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, false); + mapper.registerModule(new BlackbirdModule()); + objectWriter = mapper.writerFor(Person2.class); + try { + person = mapper.readerFor(Person2.class).readValue(new StringReader(json)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } @State(Scope.Benchmark) public static class SimpleQsonWriter { @@ -290,6 +313,62 @@ public void setup() { } } + @State(Scope.Benchmark) + public static class BlackbirdParser { + public ObjectReader reader; + public byte[] jsonBytes; + + @Setup(Level.Trial) + public void setup() { + ObjectMapper mapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, false); + mapper.registerModule(new BlackbirdModule()); + reader = mapper.readerFor(Person2.class); + try { + jsonBytes = json.getBytes("UTF-8"); + } catch (Exception e) { + throw new RuntimeException(); + } + } + } + + @State(Scope.Benchmark) + public static class JacksonCustomParser { + public ObjectReader reader; + public byte[] jsonBytes; + + @Setup(Level.Trial) + public void setup() { + ObjectMapper mapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, false); + SimpleModule module = new SimpleModule(); + module.addDeserializer(Person2.class, new Person2Deserializer()); + mapper.registerModule(module); + reader = mapper.readerFor(Person2.class); + try { + jsonBytes = json.getBytes("UTF-8"); + } catch (Exception e) { + throw new RuntimeException(); + } + } + } + + @State(Scope.Benchmark) + public static class DslJsonParser { + public DslJson reader; + public byte[] jsonBytes; + + @Setup(Level.Trial) + public void setup() { + reader = new DslJson<>(Settings.basicSetup()); + try { + jsonBytes = json.getBytes("UTF-8"); + } catch (Exception e) { + throw new RuntimeException(); + } + } + } + @State(Scope.Benchmark) public static class GsonParser { public Gson gson; @@ -349,7 +428,32 @@ public Object testParserAfterburner(AfterburnerParser a) { } } + @Benchmark + public Object testParserBlackbird(BlackbirdParser a) { + try { + return a.reader.readValue(a.jsonBytes); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Benchmark + public Object testParserJacksonCustom(JacksonCustomParser a) { + try { + return a.reader.readValue(a.jsonBytes); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + @Benchmark + public Object testParserDslJson(DslJsonParser a) { + try { + return a.reader.deserialize(Person2.class, new ByteArrayInputStream(a.jsonBytes)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } @Benchmark public Object testWriterQson(QsonWriter q) { @@ -373,4 +477,13 @@ public Object testWriterAfterburner(AfterburnerWriter q) { throw new RuntimeException(e); } } + + @Benchmark + public Object testWriterBlackbird(BlackbirdWriter q) { + try { + return q.objectWriter.writeValueAsBytes(q.person); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } diff --git a/benchmark/src/main/java/org/sample/jackson/Person2Deserializer.java b/benchmark/src/main/java/org/sample/jackson/Person2Deserializer.java new file mode 100644 index 0000000..89d8df0 --- /dev/null +++ b/benchmark/src/main/java/org/sample/jackson/Person2Deserializer.java @@ -0,0 +1,220 @@ +package org.sample.jackson; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import org.sample.Person2; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Custom Jackson deserializer for Person2 using streaming API for better performance. + */ +public class Person2Deserializer extends JsonDeserializer { + + @Override + public Person2 deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + if (p.currentToken() == JsonToken.VALUE_NULL) { + return null; + } + + Person2 person = new Person2(); + + if (p.currentToken() != JsonToken.START_OBJECT) { + p.nextToken(); + } + + while (p.nextToken() != JsonToken.END_OBJECT) { + String fieldName = p.currentName(); + p.nextToken(); // move to value + + if (fieldName == null) continue; + + switch (fieldName) { + case "name": + person.setName(p.getValueAsString()); + break; + case "age": + person.setAge(p.getIntValue()); + break; + case "married": + person.setMarried(p.getBooleanValue()); + break; + case "money": + person.setMoney(p.getFloatValue()); + break; + case "intMap": + person.setIntMap(deserializeIntMap(p)); + break; + case "dad": + person.setDad(deserialize(p, ctxt)); + break; + case "kids": + person.setKids(deserializePersonMap(p, ctxt)); + break; + case "siblings": + person.setSiblings(deserializePersonList(p, ctxt)); + break; + case "pets": + person.setPets(deserializeStringList(p)); + break; + case "genericMap": + person.setGenericMap(deserializeGenericMap(p, ctxt)); + break; + case "genericBag": + person.setGenericBag(deserializeGenericValue(p, ctxt)); + break; + case "genericList": + person.setGenericList(deserializeGenericList(p, ctxt)); + break; + case "nested": + person.setNested(deserializeNestedMap(p, ctxt)); + break; + default: + p.skipChildren(); + break; + } + } + + return person; + } + + private Map deserializeIntMap(JsonParser p) throws IOException { + if (p.currentToken() == JsonToken.VALUE_NULL) { + return null; + } + + Map map = new LinkedHashMap<>(); + + while (p.nextToken() != JsonToken.END_OBJECT) { + String key = p.currentName(); + p.nextToken(); + map.put(key, p.getIntValue()); + } + + return map; + } + + private Map deserializePersonMap(JsonParser p, DeserializationContext ctxt) throws IOException { + if (p.currentToken() == JsonToken.VALUE_NULL) { + return null; + } + + Map map = new LinkedHashMap<>(); + + while (p.nextToken() != JsonToken.END_OBJECT) { + String key = p.currentName(); + p.nextToken(); + map.put(key, deserialize(p, ctxt)); + } + + return map; + } + + private List deserializePersonList(JsonParser p, DeserializationContext ctxt) throws IOException { + if (p.currentToken() == JsonToken.VALUE_NULL) { + return null; + } + + List list = new ArrayList<>(); + + while (p.nextToken() != JsonToken.END_ARRAY) { + list.add(deserialize(p, ctxt)); + } + + return list; + } + + private List deserializeStringList(JsonParser p) throws IOException { + if (p.currentToken() == JsonToken.VALUE_NULL) { + return null; + } + + List list = new ArrayList<>(); + + while (p.nextToken() != JsonToken.END_ARRAY) { + list.add(p.getValueAsString()); + } + + return list; + } + + @SuppressWarnings("rawtypes") + private Map deserializeGenericMap(JsonParser p, DeserializationContext ctxt) throws IOException { + if (p.currentToken() == JsonToken.VALUE_NULL) { + return null; + } + + Map map = new LinkedHashMap<>(); + + while (p.nextToken() != JsonToken.END_OBJECT) { + String key = p.currentName(); + p.nextToken(); + map.put(key, deserializeGenericValue(p, ctxt)); + } + + return map; + } + + @SuppressWarnings("rawtypes") + private List deserializeGenericList(JsonParser p, DeserializationContext ctxt) throws IOException { + if (p.currentToken() == JsonToken.VALUE_NULL) { + return null; + } + + List list = new ArrayList<>(); + + while (p.nextToken() != JsonToken.END_ARRAY) { + list.add(deserializeGenericValue(p, ctxt)); + } + + return list; + } + + private Object deserializeGenericValue(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonToken token = p.currentToken(); + + switch (token) { + case VALUE_NULL: + return null; + case VALUE_STRING: + return p.getText(); + case VALUE_NUMBER_INT: + return p.getLongValue(); + case VALUE_NUMBER_FLOAT: + return p.getDoubleValue(); + case VALUE_TRUE: + return Boolean.TRUE; + case VALUE_FALSE: + return Boolean.FALSE; + case START_ARRAY: + return deserializeGenericList(p, ctxt); + case START_OBJECT: + return deserializeGenericMap(p, ctxt); + default: + return null; + } + } + + private Map> deserializeNestedMap(JsonParser p, DeserializationContext ctxt) throws IOException { + if (p.currentToken() == JsonToken.VALUE_NULL) { + return null; + } + + Map> map = new LinkedHashMap<>(); + + while (p.nextToken() != JsonToken.END_OBJECT) { + String key = p.currentName(); + p.nextToken(); + map.put(key, deserializePersonList(p, ctxt)); + } + + return map; + } +} +