Skip to content

Instantly share code, notes, and snippets.

@bristermitten
Created July 24, 2021 15:47
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bristermitten/cf26d9931d32c78c5d777cc719658639 to your computer and use it in GitHub Desktop.
Save bristermitten/cf26d9931d32c78c5d777cc719658639 to your computer and use it in GitHub Desktop.
Gson support for java 16 records
package me.bristermitten.warzone.config.loading;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.RecordComponent;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* Gson support for Java 16+ record types.
* Taken from https://github.com/google/gson/issues/1794 and adjusted for performance and proper handling of
* {@link SerializedName} annotations
*/
public class RecordTypeAdapterFactory implements TypeAdapterFactory {
private static final Map<Class<?>, Object> PRIMITIVE_DEFAULTS = new HashMap<>();
static {
PRIMITIVE_DEFAULTS.put(byte.class, (byte) 0);
PRIMITIVE_DEFAULTS.put(int.class, 0);
PRIMITIVE_DEFAULTS.put(long.class, 0L);
PRIMITIVE_DEFAULTS.put(short.class, (short) 0);
PRIMITIVE_DEFAULTS.put(double.class, 0D);
PRIMITIVE_DEFAULTS.put(float.class, 0F);
PRIMITIVE_DEFAULTS.put(char.class, '\0');
PRIMITIVE_DEFAULTS.put(boolean.class, false);
}
private final Map<RecordComponent, List<String>> recordComponentNameCache = new ConcurrentHashMap<>();
/**
* Get all names of a record component
* If annotated with {@link SerializedName} the list returned will be the primary name first, then any alternative names
* Otherwise, the component name will be returned.
*/
private List<String> getRecordComponentNames(final RecordComponent recordComponent) {
List<String> inCache = recordComponentNameCache.get(recordComponent);
if (inCache != null) {
return inCache;
}
List<String> names = new ArrayList<>();
// The @SerializedName is compiled to be part of the componentName() method
// The use of a loop is also deliberate, getAnnotation seemed to return null if Gson's package was relocated
SerializedName annotation = null;
for (Annotation a : recordComponent.getAccessor().getAnnotations()) {
if (a.annotationType() == SerializedName.class) {
annotation = (SerializedName) a;
break;
}
}
if (annotation != null) {
names.add(annotation.value());
names.addAll(Arrays.asList(annotation.alternate()));
} else {
names.add(recordComponent.getName());
}
var namesList = List.copyOf(names);
recordComponentNameCache.put(recordComponent, namesList);
return namesList;
}
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
@SuppressWarnings("unchecked")
Class<T> clazz = (Class<T>) type.getRawType();
if (!clazz.isRecord()) {
return null;
}
TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
return new TypeAdapter<>() {
@Override
public void write(JsonWriter out, T value) throws IOException {
delegate.write(out, value);
}
@Override
public T read(JsonReader reader) throws IOException {
if (reader.peek() == JsonToken.NULL) {
reader.nextNull();
return null;
}
var recordComponents = clazz.getRecordComponents();
var typeMap = new HashMap<String, TypeToken<?>>();
for (RecordComponent recordComponent : recordComponents) {
for (String name : getRecordComponentNames(recordComponent)) {
typeMap.put(name, TypeToken.get(recordComponent.getGenericType()));
}
}
var argsMap = new HashMap<String, Object>();
reader.beginObject();
while (reader.hasNext()) {
String name = reader.nextName();
var type = typeMap.get(name);
if (type != null) {
argsMap.put(name, gson.getAdapter(type).read(reader));
} else {
gson.getAdapter(Object.class).read(reader);
}
}
reader.endObject();
var argTypes = new Class<?>[recordComponents.length];
var args = new Object[recordComponents.length];
for (int i = 0; i < recordComponents.length; i++) {
argTypes[i] = recordComponents[i].getType();
List<String> names = getRecordComponentNames(recordComponents[i]);
Object value = null;
TypeToken<?> type = null;
// Find the first matching type and value
for (String name : names) {
value = argsMap.get(name);
type = typeMap.get(name);
if (value != null && type != null) {
break;
}
}
if (value == null && (type != null && type.getRawType().isPrimitive())) {
value = PRIMITIVE_DEFAULTS.get(type.getRawType());
}
args[i] = value;
}
Constructor<T> constructor;
try {
constructor = clazz.getDeclaredConstructor(argTypes);
constructor.setAccessible(true);
return constructor.newInstance(args);
} catch (NoSuchMethodException | InstantiationException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
};
}
}
@gpluscb
Copy link

gpluscb commented Apr 21, 2022

Thank you for this, this is very helpful.

I found an issue with this implementation: The recordComponentNameCache doesn't seem to work like it should, it seems to leak.
In a heap dump I find an extreme amount of entries (> 2 million) which also seems to include multiple entries for what should be the same RecordComponent. Maybe equals isn't implemented as expected on RecordComponent?

@bristermitten
Copy link
Author

Thank you for this, this is very helpful.

I found an issue with this implementation: The recordComponentNameCache doesn't seem to work like it should, it seems to leak. In a heap dump I find an extreme amount of entries (> 2 million) which also seems to include multiple entries for what should be the same RecordComponent. Maybe equals isn't implemented as expected on RecordComponent?

Ah yes, it doesn't look like RecordComponent has an explicitly defined equals method, and presumably reference equality isn't enough. I'm not particularly well versed with the internals but comparing the declaring class and name could probably be enough for a well-defined equals. I'll see about adding a fix to this, thanks for making me aware!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment