-
Notifications
You must be signed in to change notification settings - Fork 40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bugfix/leaks and performance loss observed using jsonfactory #361
base: master
Are you sure you want to change the base?
Changes from all commits
6671eb0
4d5917a
e9c9147
b64f34c
ae84826
398865b
309579c
3dd1c54
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,48 +10,55 @@ | |
import java.lang.reflect.InvocationTargetException; | ||
import java.lang.reflect.Method; | ||
import java.util.ArrayList; | ||
import java.util.HashMap; | ||
import java.util.Iterator; | ||
import java.util.List; | ||
import java.util.Map; | ||
|
||
/** | ||
The JSONFactory class helps converting json objects into Java objects, using reflection. | ||
|
||
Some examples: | ||
|
||
<pre> | ||
class Car | ||
{ | ||
private int id; | ||
private String description; | ||
|
||
public int getId() | ||
{ | ||
return id; | ||
} | ||
public void setId(int id) | ||
{ | ||
this.id = id; | ||
} | ||
public String getDescription() | ||
{ | ||
return description; | ||
} | ||
public void setDescription(String description) | ||
{ | ||
this.description = description; | ||
} | ||
} | ||
</pre> | ||
|
||
You can retrieve a new Car object using: | ||
<pre> | ||
Car cc = JSONFactory.parse("{\"carro\":{\"id\":-1,\"descricao\":\"GOL\"}}", Carro.class); | ||
</pre> | ||
|
||
You may also retrieve a list or an array. See the JSONSample in the TotalCrossAPI. | ||
* The JSONFactory class helps converting json objects into Java objects, using | ||
* reflection. | ||
* | ||
* Some examples: | ||
* | ||
* <pre> | ||
* class Car { | ||
* private int id; | ||
* private String description; | ||
* | ||
* public int getId() { | ||
* return id; | ||
* } | ||
* | ||
* public void setId(int id) { | ||
* this.id = id; | ||
* } | ||
* | ||
* public String getDescription() { | ||
* return description; | ||
* } | ||
* | ||
* public void setDescription(String description) { | ||
* this.description = description; | ||
* } | ||
* } | ||
* </pre> | ||
* | ||
* You can retrieve a new Car object using: | ||
* | ||
* <pre> | ||
* Car cc = JSONFactory.parse("{\"carro\":{\"id\":-1,\"descricao\":\"GOL\"}}", Carro.class); | ||
* </pre> | ||
* | ||
* You may also retrieve a list or an array. See the JSONSample in the | ||
* TotalCrossAPI. | ||
*/ | ||
public class JSONFactory { | ||
public static <T> List<T> asList(String json, Class<T> classOfT) throws InstantiationException, | ||
IllegalAccessException, IllegalArgumentException, InvocationTargetException, JSONException, ArrayIndexOutOfBoundsException, NoSuchMethodException, SecurityException { | ||
private static Map<Class<?>, Map<String, Method>> classes = new HashMap<>(); | ||
|
||
public static <T> List<T> asList(String json, Class<T> classOfT) | ||
throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, | ||
JSONException, ArrayIndexOutOfBoundsException, NoSuchMethodException, SecurityException { | ||
List<T> list = new ArrayList<T>(); | ||
try { | ||
JSONArray jsonArray = new JSONArray(json); | ||
|
@@ -88,13 +95,15 @@ public static <T> T parse(String json, Class<T> classOfT) throws InstantiationEx | |
return parse(new JSONObject(json), classOfT); | ||
} | ||
|
||
public static <T> T parse(JSONArray jsonArray, Class<T> classOfT) throws InstantiationException, | ||
IllegalAccessException, IllegalArgumentException, InvocationTargetException, JSONException, ArrayIndexOutOfBoundsException, NoSuchMethodException, SecurityException { | ||
return parse(null, jsonArray, classOfT); | ||
public static <T> T parse(JSONArray jsonArray, Class<T> classOfT) | ||
throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, | ||
JSONException, ArrayIndexOutOfBoundsException, NoSuchMethodException, SecurityException { | ||
return parse(null, jsonArray, classOfT); | ||
} | ||
|
||
private static <T> T parse(Object outerObject, JSONArray jsonArray, Class<T> classOfT) throws InstantiationException, | ||
IllegalAccessException, IllegalArgumentException, InvocationTargetException, JSONException, ArrayIndexOutOfBoundsException, NoSuchMethodException, SecurityException { | ||
|
||
private static <T> T parse(Object outerObject, JSONArray jsonArray, Class<T> classOfT) | ||
throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, | ||
JSONException, ArrayIndexOutOfBoundsException, NoSuchMethodException, SecurityException { | ||
if (classOfT.isArray()) { | ||
T array; | ||
try { | ||
|
@@ -110,95 +119,112 @@ private static <T> T parse(Object outerObject, JSONArray jsonArray, Class<T> cla | |
} | ||
return parse(outerObject, jsonArray, classOfT); | ||
} | ||
|
||
public static <T> T parse(JSONObject jsonObject, Class<T> classOfT) throws InstantiationException, | ||
IllegalAccessException, IllegalArgumentException, InvocationTargetException, JSONException, NoSuchMethodException, SecurityException { | ||
return parse(null, jsonObject, classOfT); | ||
|
||
public static <T> T parse(JSONObject jsonObject, Class<T> classOfT) | ||
throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, | ||
JSONException, NoSuchMethodException, SecurityException { | ||
return parse(null, jsonObject, classOfT); | ||
} | ||
|
||
private static <T> T parse(Object outerObject, JSONObject jsonObject, Class<T> classOfT) throws InstantiationException, | ||
IllegalAccessException, IllegalArgumentException, InvocationTargetException, JSONException, NoSuchMethodException, SecurityException { | ||
private static <T> T parse(Object outerObject, JSONObject jsonObject, Class<T> classOfT) | ||
throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, | ||
JSONException, NoSuchMethodException, SecurityException { | ||
if (classOfT.isArray()) { | ||
throw new IllegalArgumentException(); | ||
} | ||
T object = null; | ||
try { | ||
object = classOfT.newInstance(); | ||
object = classOfT.newInstance(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems to me that the creation of a single object based on its class could also be cached, in a similar way that it is possible to cache methods. It should be easier to read if the creation of the object was simply: T object = createNewInstanceOfT(classOfT, outerObject); and let private static final Supplier<Object> FORBIDEN = () -> {
throw new RuntimeException("oops");
};
private static final Function<Object, Object> FORBIDEN_FUNCTION = () -> {
throw new RuntimeException("oops");
};
private static <T> T createNewInstanceOfT(Class<T> classOfT, Object outerObject) {
Supplier<T> supplierOfT = (Supplier<T>) cacheCtor.get(claffOfT);
if (supplierOfT != null && supplierOfT != FORBIDEN) {
return supplierOfT .get();
} else if (supplierOfT == null) {
try {
T obj = classOfT.newInstance();
cacheCtor,put(classOfT, classOfT::newInstance); // working around exceptions, not literally RACE-CONDITION-AWARE
return obj;
} catch (Exception e) {
cacheCtor.put(classOfT, FORBIDEN); // mark such that won't try to fetch default ctor again RACE-CONDITION-AWARE
}
}
if (outerObject == null || classOfT.getName().indexOf(outerObject.getClass().getName()) == -1) {
// CANNOT INSTANTIATE
throw new ProperCantInstantiateException();
}
HashMap<Class<?>, Function<Object, T>> mapConstructorOfTWithParam = (Function<Object, T>) cacheCtorWithParam(classOfT);
if (mapConstructorOfTWithParam != null) {
Function<Object, T> constructorOfTWithParam = mapConstructorOfTWithParam .get(outerObject.getClass());
if (constructorOfTWithParam == FORBIDEN_FUNCTION ) {
// CANNOT INSTANTIATE
throw new ProperCantInstantiateException();
} else if (constructorOfTWithParam != null) {
return constructorOfTWithParam.apply(outerObject);
}
} else {
mapConstructorOfTWithParam = new HashMap<>();
cacheCtorWithParam.put(classOfT, mapConstructorOfTWithParam); // RACE-CONDITION-AWARE
}
Constructor<T> constructorOfT = classOfT.getDeclaredConstructor(outerObject.getClass());
if (constructorOfT == null) {
mapConstructorOfTWithParam.put(outerObject.getClass(), FORBIDEN_FUNCTION); // mark such that won't try to fetch ctor with this class again RACE-CONDITION-AWARE
// CANNOT INSTANTIATE
throw new ProperCantInstantiateException();
}
mapConstructorOfTWithParam.put(outerObject.getClass(), oObject -> constructorOfT.newInstance(oObject)); // working around exceptions, not literally RACE-CONDITION-AWARE
return constructorOfT.newInstance(outerObject);
}
|
||
} catch (InstantiationException e) { | ||
if (outerObject != null && classOfT.getName().indexOf(outerObject.getClass().getName()) != -1) { | ||
Constructor<T> constructorOfT = classOfT.getDeclaredConstructor(outerObject.getClass()); | ||
if (constructorOfT != null) { | ||
object = constructorOfT.newInstance(outerObject); | ||
} | ||
if (outerObject != null && classOfT.getName().indexOf(outerObject.getClass().getName()) != -1) { | ||
Constructor<T> constructorOfT = classOfT.getDeclaredConstructor(outerObject.getClass()); | ||
if (constructorOfT != null) { | ||
object = constructorOfT.newInstance(outerObject); | ||
} | ||
if (object == null) { | ||
throw e; | ||
} | ||
if (object == null) { | ||
throw e; | ||
} | ||
} | ||
|
||
Map<String, Method> methodCache = classes.get(classOfT); | ||
if (methodCache == null) { | ||
methodCache = mapClassMethodsToJsonNames(classOfT); | ||
classes.put(classOfT, methodCache); | ||
} | ||
Comment on lines
+150
to
+154
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here you got a race condition Maybe trying a double-checked lock? |
||
|
||
Iterator<String> keyIterator = jsonObject.keys(); | ||
while (keyIterator.hasNext()) { | ||
final String name = keyIterator.next(); | ||
Method method = methodCache.get(name); | ||
if (method == null || jsonObject.isNull(name)) { | ||
continue; | ||
} | ||
|
||
Class<?> parameterType = method.getParameterTypes()[0]; | ||
if (parameterType.isPrimitive()) { | ||
if (parameterType.isAssignableFrom(boolean.class)) { | ||
method.invoke(object, jsonObject.getBoolean(name)); | ||
} else if (parameterType.isAssignableFrom(int.class)) { | ||
method.invoke(object, jsonObject.getInt(name)); | ||
} else if (parameterType.isAssignableFrom(long.class)) { | ||
method.invoke(object, jsonObject.getLong(name)); | ||
} else if (parameterType.isAssignableFrom(double.class)) { | ||
method.invoke(object, jsonObject.getDouble(name)); | ||
} | ||
} else if (parameterType.isAssignableFrom(String.class)) { | ||
method.invoke(object, jsonObject.getString(name)); | ||
} else if (parameterType.isAssignableFrom(Double.class)) { | ||
method.invoke(object, jsonObject.getDouble(name)); | ||
} else if (parameterType.isAssignableFrom(Integer.class)) { | ||
method.invoke(object, jsonObject.getInt(name)); | ||
} else if (parameterType.isAssignableFrom(Long.class)) { | ||
method.invoke(object, jsonObject.getLong(name)); | ||
} else if (parameterType.isAssignableFrom(Boolean.class)) { | ||
method.invoke(object, jsonObject.getBoolean(name)); | ||
} else if (parameterType.isArray()) { | ||
method.invoke(object, parse(object, jsonObject.getJSONArray(name), parameterType)); | ||
} else { | ||
method.invoke(object, parse(object, jsonObject.getJSONObject(name), parameterType)); | ||
} | ||
} | ||
return object; | ||
} | ||
|
||
private static Map<String, Method> mapClassMethodsToJsonNames(Class<?> classOfT) { | ||
final Map<String, Method> methodCache = new HashMap<>(); | ||
Method[] methods = classOfT.getMethods(); | ||
for (Method method : methods) { | ||
String methodName = method.getName(); | ||
Class<?>[] paramTypes = method.getParameterTypes(); | ||
if (paramTypes != null && paramTypes.length == 1 && methodName.length() > 3 && methodName.startsWith("set")) { | ||
final String originalName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4); | ||
String name = null; | ||
// look for the field name in the json based on the method name | ||
if (jsonObject.has(originalName)) { | ||
name = originalName; | ||
} else if (!jsonObject.has(name = originalName.toLowerCase())) { | ||
// not found as-is or lowercased? try replacing camel case with underscore | ||
/* | ||
* originally done using regex, but totalcross implementation has some bugs and | ||
* until they are fixed this is done looping through the characters | ||
* originalName.replaceAll("(.)(\\p{Upper})", "$1_$2").toLowerCase(); | ||
*/ | ||
StringBuilder sb = new StringBuilder(); | ||
boolean lastWasUnderscored = false; | ||
for(int i = 0 ; i < originalName.length() ; i++) { | ||
char c = originalName.charAt(i); | ||
if (!lastWasUnderscored && Character.isUpperCase(c)) { | ||
lastWasUnderscored = true; | ||
sb.append('_'); | ||
} else { | ||
lastWasUnderscored = false; | ||
} | ||
sb.append(Character.toLowerCase(c)); | ||
} | ||
final String underscoredName = sb.toString(); | ||
if (jsonObject.has(underscoredName)) { | ||
name = underscoredName; | ||
} | ||
} | ||
if (!jsonObject.isNull(name)) { | ||
Class<?> parameterType = method.getParameterTypes()[0]; | ||
if (parameterType.isPrimitive()) { | ||
if (parameterType.isAssignableFrom(boolean.class)) { | ||
method.invoke(object, jsonObject.getBoolean(name)); | ||
} else if (parameterType.isAssignableFrom(int.class)) { | ||
method.invoke(object, jsonObject.getInt(name)); | ||
} else if (parameterType.isAssignableFrom(long.class)) { | ||
method.invoke(object, jsonObject.getLong(name)); | ||
} else if (parameterType.isAssignableFrom(double.class)) { | ||
method.invoke(object, jsonObject.getDouble(name)); | ||
} | ||
} else if (parameterType.isAssignableFrom(String.class)) { | ||
method.invoke(object, jsonObject.getString(name)); | ||
} else if (parameterType.isAssignableFrom(Double.class)) { | ||
method.invoke(object, jsonObject.getDouble(name)); | ||
} else if (parameterType.isAssignableFrom(Integer.class)) { | ||
method.invoke(object, jsonObject.getInt(name)); | ||
} else if (parameterType.isAssignableFrom(Long.class)) { | ||
method.invoke(object, jsonObject.getLong(name)); | ||
} else if (parameterType.isAssignableFrom(Boolean.class)) { | ||
method.invoke(object, jsonObject.getBoolean(name)); | ||
} else if (parameterType.isArray()) { | ||
method.invoke(object, parse(object, jsonObject.getJSONArray(name), parameterType)); | ||
// name as-is | ||
methodCache.put(originalName, method); | ||
// lowercased name | ||
methodCache.put(originalName.toLowerCase(), method); | ||
// not found as-is or lowercased? try replacing camel case with underscore | ||
/* | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that it is safe to assume that a private method will be inlined by the javac compiler with proper given optimization, so for code clarity I think that would be better it this snippet that gives snakecase names was removed to a proper method Also, I think that |
||
* originally done using regex, but totalcross implementation has some bugs and | ||
* until they are fixed this is done looping through the characters | ||
* originalName.replaceAll("(.)(\\p{Upper})", "$1_$2").toLowerCase(); | ||
*/ | ||
StringBuilder sb = new StringBuilder(); | ||
boolean lastWasUnderscored = false; | ||
for (int i = 0; i < originalName.length(); i++) { | ||
char c = originalName.charAt(i); | ||
if (!lastWasUnderscored && Character.isUpperCase(c)) { | ||
lastWasUnderscored = true; | ||
sb.append('_'); | ||
} else { | ||
method.invoke(object, parse(object, jsonObject.getJSONObject(name), parameterType)); | ||
lastWasUnderscored = false; | ||
} | ||
sb.append(Character.toLowerCase(c)); | ||
} | ||
final String underscoredName = sb.toString(); | ||
methodCache.put(underscoredName, method); | ||
} | ||
} | ||
return object; | ||
return methodCache; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
// Copyright (C) 2021 TotalCross Global Mobile Platform Ltda. | ||
// | ||
// SPDX-License-Identifier: LGPL-2.1-only | ||
|
||
#ifndef Class_h | ||
#define Class_h | ||
|
||
#include "tcvm.h" | ||
|
||
// java.lang.Class | ||
#define Class_nativeStruct(o) FIELD_OBJ(o, OBJ_CLASS(o), 0) | ||
#define Class_targetName(o) FIELD_OBJ(o, OBJ_CLASS(o), 1) | ||
#define Class_methods(o) FIELD_OBJ(o, OBJ_CLASS(o), 4) | ||
|
||
#endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using this as a source of cache, one should be aware that it isn't thread safe OR the thread safety must be provided by the implementor