Finally found and removed the performance bottleneck, so now performance is equal to jackson

updated dependencies and surefire config for java 9+
This commit is contained in:
Shautvast 2023-06-03 10:13:07 +02:00
parent 2a484909ed
commit 8d8160d341
5 changed files with 80 additions and 105 deletions

View file

@ -4,3 +4,6 @@ a JSON serializer based on bytecode manipulation
* creates a Json serializer for a java type using javassist * creates a Json serializer for a java type using javassist
* deserializing not yet implemented * deserializing not yet implemented
* see the unit tests to see how it works * see the unit tests to see how it works
* as of java9 it needs `--add-opens java.base/java.lang=ALL-UNNAMED` as java commandline option.

45
pom.xml
View file

@ -15,47 +15,21 @@
</properties> </properties>
<dependencies> <dependencies>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
<dependency> <dependency>
<groupId>org.javassist</groupId> <groupId>org.javassist</groupId>
<artifactId>javassist</artifactId> <artifactId>javassist</artifactId>
<version>3.26.0-GA</version> <version>3.26.0-GA</version>
</dependency> </dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency> <dependency>
<groupId>junit</groupId> <groupId>junit</groupId>
<artifactId>junit</artifactId> <artifactId>junit</artifactId>
<version>[4.13.1,)</version> <version>4.13.2</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.assertj</groupId> <groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId> <artifactId>assertj-core</artifactId>
<version>1.6.0</version> <version>3.23.1</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
@ -68,8 +42,21 @@
<dependency> <dependency>
<groupId>com.fasterxml.jackson.core</groupId> <groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId> <artifactId>jackson-databind</artifactId>
<version>2.10.5.1</version> <version>2.15.1</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
</dependencies> </dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<argLine>--add-opens java.base/java.lang=ALL-UNNAMED</argLine>
</configuration>
</plugin>
</plugins>
</build>
</project> </project>

View file

@ -11,9 +11,9 @@ public abstract class JSONSerializer<T> {
if (object == null) { if (object == null) {
return ""; return "";
} else if (object instanceof Number || object instanceof Boolean) { } else if (object instanceof Number || object instanceof Boolean) {
return "" + object.toString(); return "" + object;
} else if (object instanceof CharSequence || object instanceof Character) { } else if (object instanceof CharSequence || object instanceof Character) {
return "\"" + object.toString() + "\""; return "\"" + object + "\"";
} else { } else {
return handle(object); return handle(object);
} }

View file

@ -1,5 +1,6 @@
package nl.jssl.jsontoy.serialize; package nl.jssl.jsontoy.serialize;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
@ -36,7 +37,7 @@ class SynthSerializerFactory {
private static final String SET = "java.util.Set"; private static final String SET = "java.util.Set";
private static final String MAP = "java.util.Map"; private static final String MAP = "java.util.Map";
private static final Map<String, JSONSerializer<?>> serializers = new HashMap<>(); private static final Map<Class<?>, JSONSerializer<?>> serializers = new HashMap<>();
private static final String ROOT_PACKAGE = "serializer."; private static final String ROOT_PACKAGE = "serializer.";
private final ClassPool pool = ClassPool.getDefault(); private final ClassPool pool = ClassPool.getDefault();
@ -55,37 +56,29 @@ class SynthSerializerFactory {
} }
} }
<T> JSONSerializer<T> createSerializer(Class<T> beanjavaClass) { @SuppressWarnings("unchecked")
<T> JSONSerializer<T> createSerializer(Class<T> beanjavaClass) {
if (serializers.containsKey(beanjavaClass)) {
return (JSONSerializer<T>) serializers.get(beanjavaClass);
}
try { try {
CtClass beanClass = pool.get(beanjavaClass.getName()); CtClass beanClass = pool.get(beanjavaClass.getName());
return tryCreateSerializer(beanjavaClass, beanClass);
return createSerializer2(beanClass); } catch (NotFoundException | CannotCompileException | InstantiationException | IllegalAccessException |
} catch (NotFoundException e) { InvocationTargetException | NoSuchMethodException e) {
throw new SerializerCreationException(e); throw new SerializerCreationException(e);
} }
} }
@SuppressWarnings("unchecked") private <T> JSONSerializer<T> tryCreateSerializer(Class<?> javaClass, CtClass beanClass) throws NotFoundException, CannotCompileException, InstantiationException,
private <T> JSONSerializer<T> createSerializer2(CtClass beanClass) { IllegalAccessException, InvocationTargetException, NoSuchMethodException {
if (serializers.containsKey(createSerializerName(beanClass))) {
return (JSONSerializer<T>) serializers.get(createSerializerName(beanClass));
}
try {
return tryCreateSerializer(beanClass);
} catch (NotFoundException | CannotCompileException | InstantiationException | IllegalAccessException e) {
throw new SerializerCreationException(e);
}
}
private <T> JSONSerializer<T> tryCreateSerializer(CtClass beanClass) throws NotFoundException, CannotCompileException, InstantiationException,
IllegalAccessException {
CtClass serializerClass = pool.makeClass(createSerializerName(beanClass), serializerBase); CtClass serializerClass = pool.makeClass(createSerializerName(beanClass), serializerBase);
addToJsonStringMethod(beanClass, serializerClass); addToJsonStringMethod(beanClass, serializerClass);
JSONSerializer<T> jsonSerializer = createSerializerInstance(serializerClass); JSONSerializer<T> jsonSerializer = createSerializerInstance(serializerClass);
serializers.put(createSerializerName(beanClass), jsonSerializer); serializers.put(javaClass, jsonSerializer);
return jsonSerializer; return jsonSerializer;
} }
@ -99,16 +92,16 @@ class SynthSerializerFactory {
/* /*
* Creates the source, handling the for JSON different types of classes * Creates the source, handling the for JSON different types of classes
*/ */
private <T> String createToJSONStringMethodSource(CtClass beanClass) throws NotFoundException { private String createToJSONStringMethodSource(CtClass beanClass) throws NotFoundException {
String source = "public String handle(Object object){\n"; String source = "public String handle(Object object){\n";
if (beanClass.isArray()) { if (beanClass.isArray()) {
source += "\tObject[] array=(Object[])object;\n"; source += "\tObject[] array=(Object[])object;\n";
source += handleArray(beanClass); source += handleArray();
} else if (isCollection(beanClass)) { } else if (isCollection(beanClass)) {
source += "\tObject[] array=((java.util.Collection)object).toArray();\n"; source += "\tObject[] array=((java.util.Collection)object).toArray();\n";
source += handleArray(beanClass); source += handleArray();
} else if (isMap(beanClass)) { } else if (isMap(beanClass)) {
source += handleMap(beanClass); source += handleMap();
} else if (!isPrimitiveOrWrapperOrString(beanClass)) { } else if (!isPrimitiveOrWrapperOrString(beanClass)) {
List<CtMethod> getters = getGetters(beanClass); List<CtMethod> getters = getGetters(beanClass);
if (shouldAddGetterCallers(getters)) { if (shouldAddGetterCallers(getters)) {
@ -117,18 +110,17 @@ class SynthSerializerFactory {
} else { } else {
source += "\treturn \"\";}"; source += "\treturn \"\";}";
} }
System.out.println(source);
return source; return source;
} }
/* /*
* Any Collection is converted to an array, after which code is generated to handle the single elements. * Any Collection is converted to an array, after which code is generated to handle the single elements.
* *
* A subserializer is created for every single element, but most of the time it will be the same cached instance. * A subserializer is created for every single element, but most of the time it will be the same cached instance.
* *
* The generated code fills a StringBuilder. The values are generated by the subserializers * The generated code fills a StringBuilder. The values are generated by the subserializers
*/ */
private String handleArray(CtClass beanClass) { private String handleArray() {
String source = "\tStringBuilder result=new StringBuilder(\"[\");\n"; String source = "\tStringBuilder result=new StringBuilder(\"[\");\n";
source += "\tfor (int i=0; i<array.length; i++){\n"; source += "\tfor (int i=0; i<array.length; i++){\n";
source += "\t\tresult.append(" + Serializer.class.getName() + ".toJSONString(array[i]));\n"; source += "\t\tresult.append(" + Serializer.class.getName() + ".toJSONString(array[i]));\n";
@ -141,7 +133,7 @@ class SynthSerializerFactory {
return source; return source;
} }
private String handleMap(CtClass beanClass) { private String handleMap() {
String source = "StringBuilder result=new StringBuilder(\"{\");\n"; String source = "StringBuilder result=new StringBuilder(\"{\");\n";
source += "\tfor (java.util.Iterator entries=((java.util.Map)object).entrySet().iterator();entries.hasNext();){\n"; source += "\tfor (java.util.Iterator entries=((java.util.Map)object).entrySet().iterator();entries.hasNext();){\n";
source += "\t\tjava.util.Map.Entry entry=(java.util.Map.Entry)entries.next();\n"; source += "\t\tjava.util.Map.Entry entry=(java.util.Map.Entry)entries.next();\n";
@ -160,7 +152,7 @@ class SynthSerializerFactory {
/* /*
* If the class contains fields for which public getters are available, then these will be called in the generated code. * If the class contains fields for which public getters are available, then these will be called in the generated code.
*/ */
private String addGetterCallers(CtClass beanClass, String source, List<CtMethod> getters) throws NotFoundException { private String addGetterCallers(CtClass beanClass, String source, List<CtMethod> getters) {
int index = 0; int index = 0;
source += "\treturn "; source += "\treturn ";
source += "\"{"; source += "\"{";
@ -176,13 +168,13 @@ class SynthSerializerFactory {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private <T> JSONSerializer<T> createSerializerInstance(CtClass serializerClass) throws InstantiationException, IllegalAccessException, private <T> JSONSerializer<T> createSerializerInstance(CtClass serializerClass) throws InstantiationException, IllegalAccessException,
CannotCompileException { CannotCompileException, NoSuchMethodException, InvocationTargetException {
return (JSONSerializer<T>) serializerClass.toClass().newInstance(); return (JSONSerializer<T>) serializerClass.toClass().getConstructor().newInstance();
} }
/* /*
* custom root package is prepended to avoid the java.lang class in which it's illegal to create new classes * custom root package is prepended to avoid the java.lang class in which it's illegal to create new classes
* *
* Array marks ( '[]' ) are replaced by the 'Array', Otherwise the SerializerClassName would be syntactically incorrect * Array marks ( '[]' ) are replaced by the 'Array', Otherwise the SerializerClassName would be syntactically incorrect
*/ */
public String createSerializerName(CtClass beanClass) { public String createSerializerName(CtClass beanClass) {
@ -218,7 +210,7 @@ class SynthSerializerFactory {
/* /*
* The JSON vernacular for key:value is pair... * The JSON vernacular for key:value is pair...
*/ */
private String addPair(CtClass classToSerialize, String source, CtMethod getter) throws NotFoundException { private String addPair(CtClass classToSerialize, String source, CtMethod getter) {
source += jsonKey(getter); source += jsonKey(getter);
source += ": "; // what is the rule when it comes to spaces in json? source += ": "; // what is the rule when it comes to spaces in json?
source += jsonValue(classToSerialize, getter); source += jsonValue(classToSerialize, getter);
@ -232,20 +224,14 @@ class SynthSerializerFactory {
return "\\\"" + toFieldName(getter.getName()) + "\\\""; return "\\\"" + toFieldName(getter.getName()) + "\\\"";
} }
private String jsonValue(CtClass classToSerialize, CtMethod getter) throws NotFoundException { private String jsonValue(CtClass classToSerialize, CtMethod getter) {
String source = "";
CtClass returnType = getter.getReturnType();
/* primitives are wrapped so the produced methods adhere to the JSONSerializer interface */ /* primitives are wrapped so the produced methods adhere to the JSONSerializer interface */
source = createSubSerializerForReturnTypeAndAddInvocationToSource(classToSerialize, getter, source, returnType); return createSubSerializerForReturnTypeAndAddInvocationToSource(classToSerialize, getter);
return source;
} }
private String createSubSerializerForReturnTypeAndAddInvocationToSource(CtClass classToSerialize, CtMethod getter, String source, CtClass returnType) { private String createSubSerializerForReturnTypeAndAddInvocationToSource(CtClass classToSerialize, CtMethod getter) {
/* NB there does not seem to be auto(un))boxing nor generic types (or other jdk1.5 stuff) in javassist compileable code */ /* NB there does not seem to be auto(un))boxing nor generic types (or other jdk1.5 stuff) in javassist compileable code */
String source = "\"+" + Serializer.class.getName() + ".toJSONString(";
source += "\"+" + Serializer.class.getName() + ".toJSONString(";
// cast because of lack of generics // cast because of lack of generics
source += "(" + cast(regularClassname(classToSerialize.getName())) + "object)." + getter.getName() + "()"; source += "(" + cast(regularClassname(classToSerialize.getName())) + "object)." + getter.getName() + "()";
@ -331,22 +317,22 @@ class SynthSerializerFactory {
String asPrimitive(String name) { String asPrimitive(String name) {
switch (name) { switch (name) {
case "int": case "int":
return "I"; return "I";
case "byte": case "byte":
return "B"; return "B";
case "float": case "float":
return "F"; return "F";
case "long": case "long":
return "J"; return "J";
case "boolean": case "boolean":
return "Z"; return "Z";
case "char": case "char":
return "C"; return "C";
case "double": case "double":
return "D"; return "D";
case "short": case "short":
return "S"; return "S";
} }
return ""; return "";
} }

View file

@ -13,50 +13,49 @@ import org.junit.Test;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
public class Jackson { public class JacksonComparisonTest {
List<String> trashbin = new ArrayList<String>(); private static final int ITERATIONS = 20;
private static final int INNERLOOP_COUNT = 100000;
List<String> trashbin = new ArrayList<>();
@Test @Test
public void jackson() throws JsonProcessingException { public void testPerformance() throws JsonProcessingException {
System.out.println("jackson,jsontoy");
ObjectMapper objectMapper = new ObjectMapper(); ObjectMapper objectMapper = new ObjectMapper();
Bean1 bean1 = new Bean1(); Bean1 bean1 = new Bean1();
Bean2 bean2 = new Bean2(); Bean2 bean2 = new Bean2();
bean1.setData1(UUID.randomUUID().toString()); bean1.setData1(UUID.randomUUID().toString());
bean1.setBean2(bean2); bean1.setBean2(bean2);
bean2.setData2(UUID.randomUUID().toString()); bean2.setData2(UUID.randomUUID().toString());
String valueAsString = objectMapper.writeValueAsString(bean1); String valueAsString;
String jsonString = Serializer.toJSONString(bean1); String jsonString;
for (int c = 0; c < 20; c++) { for (int c = 0; c < ITERATIONS; c++) {
trashbin.clear(); trashbin.clear();
System.gc();
long t0 = System.currentTimeMillis(); long t0 = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) { for (int i = 0; i < INNERLOOP_COUNT; i++) {
bean1 = new Bean1(); bean1 = new Bean1();
bean2 = new Bean2(); bean2 = new Bean2();
bean1.setData1(UUID.randomUUID().toString()); bean1.setData1(UUID.randomUUID().toString());
bean1.setBean2(bean2); bean1.setBean2(bean2);
bean2.setData2(UUID.randomUUID().toString()); bean2.setData2(UUID.randomUUID().toString());
valueAsString = objectMapper.writeValueAsString(bean1); valueAsString = objectMapper.writeValueAsString(bean1);
// System.out.println(valueAsString);
trashbin.add(valueAsString); trashbin.add(valueAsString);
} }
System.out.print(System.currentTimeMillis() - t0); System.out.printf("% 7d",(System.currentTimeMillis() - t0));
System.out.print(","); System.out.print(",");
trashbin.clear(); trashbin.clear();
System.gc();
long tt0 = System.currentTimeMillis(); long tt0 = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) { for (int i = 0; i < INNERLOOP_COUNT; i++) {
bean1 = new Bean1(); bean1 = new Bean1();
bean2 = new Bean2(); bean2 = new Bean2();
bean1.setData1(UUID.randomUUID().toString()); bean1.setData1(UUID.randomUUID().toString());
bean1.setBean2(bean2); bean1.setBean2(bean2);
bean2.setData2(UUID.randomUUID().toString()); bean2.setData2(UUID.randomUUID().toString());
jsonString = Serializer.toJSONString(bean1); jsonString = Serializer.toJSONString(bean1);
// System.out.println(jsonString);
trashbin.add(jsonString); trashbin.add(jsonString);
} }
System.out.println(System.currentTimeMillis() - tt0); System.out.printf("% 7d%n",System.currentTimeMillis() - tt0);
} }
} }