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
* deserializing not yet implemented
* 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>
<dependencies>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.26.0-GA</version>
</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>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>[4.13.1,)</version>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>1.6.0</version>
<version>3.23.1</version>
<scope>test</scope>
</dependency>
<dependency>
@ -68,8 +42,21 @@
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.5.1</version>
<version>2.15.1</version>
<scope>test</scope>
</dependency>
</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>

View file

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

View file

@ -1,5 +1,6 @@
package nl.jssl.jsontoy.serialize;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@ -36,7 +37,7 @@ class SynthSerializerFactory {
private static final String SET = "java.util.Set";
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 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 {
CtClass beanClass = pool.get(beanjavaClass.getName());
return createSerializer2(beanClass);
} catch (NotFoundException e) {
return tryCreateSerializer(beanjavaClass, beanClass);
} catch (NotFoundException | CannotCompileException | InstantiationException | IllegalAccessException |
InvocationTargetException | NoSuchMethodException e) {
throw new SerializerCreationException(e);
}
}
@SuppressWarnings("unchecked")
private <T> JSONSerializer<T> createSerializer2(CtClass beanClass) {
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 {
private <T> JSONSerializer<T> tryCreateSerializer(Class<?> javaClass, CtClass beanClass) throws NotFoundException, CannotCompileException, InstantiationException,
IllegalAccessException, InvocationTargetException, NoSuchMethodException {
CtClass serializerClass = pool.makeClass(createSerializerName(beanClass), serializerBase);
addToJsonStringMethod(beanClass, serializerClass);
JSONSerializer<T> jsonSerializer = createSerializerInstance(serializerClass);
serializers.put(createSerializerName(beanClass), jsonSerializer);
serializers.put(javaClass, jsonSerializer);
return jsonSerializer;
}
@ -99,16 +92,16 @@ class SynthSerializerFactory {
/*
* 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";
if (beanClass.isArray()) {
source += "\tObject[] array=(Object[])object;\n";
source += handleArray(beanClass);
source += handleArray();
} else if (isCollection(beanClass)) {
source += "\tObject[] array=((java.util.Collection)object).toArray();\n";
source += handleArray(beanClass);
source += handleArray();
} else if (isMap(beanClass)) {
source += handleMap(beanClass);
source += handleMap();
} else if (!isPrimitiveOrWrapperOrString(beanClass)) {
List<CtMethod> getters = getGetters(beanClass);
if (shouldAddGetterCallers(getters)) {
@ -117,7 +110,6 @@ class SynthSerializerFactory {
} else {
source += "\treturn \"\";}";
}
System.out.println(source);
return source;
}
@ -128,7 +120,7 @@ class SynthSerializerFactory {
*
* 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";
source += "\tfor (int i=0; i<array.length; i++){\n";
source += "\t\tresult.append(" + Serializer.class.getName() + ".toJSONString(array[i]));\n";
@ -141,7 +133,7 @@ class SynthSerializerFactory {
return source;
}
private String handleMap(CtClass beanClass) {
private String handleMap() {
String source = "StringBuilder result=new StringBuilder(\"{\");\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";
@ -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.
*/
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;
source += "\treturn ";
source += "\"{";
@ -176,8 +168,8 @@ class SynthSerializerFactory {
@SuppressWarnings("unchecked")
private <T> JSONSerializer<T> createSerializerInstance(CtClass serializerClass) throws InstantiationException, IllegalAccessException,
CannotCompileException {
return (JSONSerializer<T>) serializerClass.toClass().newInstance();
CannotCompileException, NoSuchMethodException, InvocationTargetException {
return (JSONSerializer<T>) serializerClass.toClass().getConstructor().newInstance();
}
/*
@ -218,7 +210,7 @@ class SynthSerializerFactory {
/*
* 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 += ": "; // what is the rule when it comes to spaces in json?
source += jsonValue(classToSerialize, getter);
@ -232,20 +224,14 @@ class SynthSerializerFactory {
return "\\\"" + toFieldName(getter.getName()) + "\\\"";
}
private String jsonValue(CtClass classToSerialize, CtMethod getter) throws NotFoundException {
String source = "";
CtClass returnType = getter.getReturnType();
private String jsonValue(CtClass classToSerialize, CtMethod getter) {
/* primitives are wrapped so the produced methods adhere to the JSONSerializer interface */
source = createSubSerializerForReturnTypeAndAddInvocationToSource(classToSerialize, getter, source, returnType);
return source;
return createSubSerializerForReturnTypeAndAddInvocationToSource(classToSerialize, getter);
}
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 */
source += "\"+" + Serializer.class.getName() + ".toJSONString(";
String source = "\"+" + Serializer.class.getName() + ".toJSONString(";
// cast because of lack of generics
source += "(" + cast(regularClassname(classToSerialize.getName())) + "object)." + getter.getName() + "()";
@ -331,22 +317,22 @@ class SynthSerializerFactory {
String asPrimitive(String name) {
switch (name) {
case "int":
return "I";
case "byte":
return "B";
case "float":
return "F";
case "long":
return "J";
case "boolean":
return "Z";
case "char":
return "C";
case "double":
return "D";
case "short":
return "S";
case "int":
return "I";
case "byte":
return "B";
case "float":
return "F";
case "long":
return "J";
case "boolean":
return "Z";
case "char":
return "C";
case "double":
return "D";
case "short":
return "S";
}
return "";
}

View file

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