2021-11-09 16:16:35 +01:00
|
|
|
package com.google.gson.internal.reflect;
|
|
|
|
|
|
|
|
import com.google.gson.JsonIOException;
|
2022-04-17 18:05:18 +02:00
|
|
|
import com.google.gson.internal.GsonBuildConfig;
|
Support Java Records when present in JVM. (#2201)
* Support Java Records when present in JVM.
Fixes google/gson#1794
Added support in the ReflectionHelper to detect if a class is a record
on the JVM (via reflection), and if so, we will create a special
RecordAdapter to deserialize records, using the canoncial constructor.
The ReflectionTypeAdapterFactory had to be refactored a bit to support
this. The Adapter class inside the factory is now abstract, with
concrete implementations for normal field reflection and for Records.
The common code is in the Adapter, with each implementation
deserializing values into an intermediary object.
For the FieldReflectionAdapter, the intermediary is actually the final
result, and field access is used to write to fields as before. For the
RecordAdapter the intermediary is the Object[] to pass to the Record
constructor.
* Fixed comments from @Marcono1234
Also updated so that we now use the record accessor method to read out
values from a record, so that direct field access is not necessary.
Also added some tests, that should only execute on Java versions with
record support, and be ignored for other JVMs
* Fixed additional comments from @Marcono1234
* Made Adapter in ReflectiveTypeAdapterFactory public
Fix comment from @eamonnmcmanus
2022-10-11 18:13:49 +02:00
|
|
|
|
|
|
|
import java.lang.reflect.*;
|
2021-11-09 16:16:35 +01:00
|
|
|
|
|
|
|
public class ReflectionHelper {
|
Support Java Records when present in JVM. (#2201)
* Support Java Records when present in JVM.
Fixes google/gson#1794
Added support in the ReflectionHelper to detect if a class is a record
on the JVM (via reflection), and if so, we will create a special
RecordAdapter to deserialize records, using the canoncial constructor.
The ReflectionTypeAdapterFactory had to be refactored a bit to support
this. The Adapter class inside the factory is now abstract, with
concrete implementations for normal field reflection and for Records.
The common code is in the Adapter, with each implementation
deserializing values into an intermediary object.
For the FieldReflectionAdapter, the intermediary is actually the final
result, and field access is used to write to fields as before. For the
RecordAdapter the intermediary is the Object[] to pass to the Record
constructor.
* Fixed comments from @Marcono1234
Also updated so that we now use the record accessor method to read out
values from a record, so that direct field access is not necessary.
Also added some tests, that should only execute on Java versions with
record support, and be ignored for other JVMs
* Fixed additional comments from @Marcono1234
* Made Adapter in ReflectiveTypeAdapterFactory public
Fix comment from @eamonnmcmanus
2022-10-11 18:13:49 +02:00
|
|
|
|
|
|
|
private static final RecordHelper RECORD_HELPER;
|
|
|
|
|
|
|
|
static {
|
|
|
|
RecordHelper instance;
|
|
|
|
try {
|
|
|
|
// Try to construct the RecordSupportedHelper, if this fails, records are not supported on this JVM.
|
|
|
|
instance = new RecordSupportedHelper();
|
|
|
|
} catch (NoSuchMethodException e) {
|
|
|
|
instance = new RecordNotSupportedHelper();
|
|
|
|
}
|
|
|
|
RECORD_HELPER = instance;
|
|
|
|
}
|
|
|
|
|
|
|
|
private ReflectionHelper() {}
|
2021-11-09 16:16:35 +01:00
|
|
|
|
|
|
|
/**
|
Support Java Records when present in JVM. (#2201)
* Support Java Records when present in JVM.
Fixes google/gson#1794
Added support in the ReflectionHelper to detect if a class is a record
on the JVM (via reflection), and if so, we will create a special
RecordAdapter to deserialize records, using the canoncial constructor.
The ReflectionTypeAdapterFactory had to be refactored a bit to support
this. The Adapter class inside the factory is now abstract, with
concrete implementations for normal field reflection and for Records.
The common code is in the Adapter, with each implementation
deserializing values into an intermediary object.
For the FieldReflectionAdapter, the intermediary is actually the final
result, and field access is used to write to fields as before. For the
RecordAdapter the intermediary is the Object[] to pass to the Record
constructor.
* Fixed comments from @Marcono1234
Also updated so that we now use the record accessor method to read out
values from a record, so that direct field access is not necessary.
Also added some tests, that should only execute on Java versions with
record support, and be ignored for other JVMs
* Fixed additional comments from @Marcono1234
* Made Adapter in ReflectiveTypeAdapterFactory public
Fix comment from @eamonnmcmanus
2022-10-11 18:13:49 +02:00
|
|
|
* Tries making the field accessible, wrapping any thrown exception in a {@link JsonIOException}
|
|
|
|
* with descriptive message.
|
2021-11-09 16:16:35 +01:00
|
|
|
*
|
|
|
|
* @param field field to make accessible
|
|
|
|
* @throws JsonIOException if making the field accessible fails
|
|
|
|
*/
|
|
|
|
public static void makeAccessible(Field field) throws JsonIOException {
|
Support Java Records when present in JVM. (#2201)
* Support Java Records when present in JVM.
Fixes google/gson#1794
Added support in the ReflectionHelper to detect if a class is a record
on the JVM (via reflection), and if so, we will create a special
RecordAdapter to deserialize records, using the canoncial constructor.
The ReflectionTypeAdapterFactory had to be refactored a bit to support
this. The Adapter class inside the factory is now abstract, with
concrete implementations for normal field reflection and for Records.
The common code is in the Adapter, with each implementation
deserializing values into an intermediary object.
For the FieldReflectionAdapter, the intermediary is actually the final
result, and field access is used to write to fields as before. For the
RecordAdapter the intermediary is the Object[] to pass to the Record
constructor.
* Fixed comments from @Marcono1234
Also updated so that we now use the record accessor method to read out
values from a record, so that direct field access is not necessary.
Also added some tests, that should only execute on Java versions with
record support, and be ignored for other JVMs
* Fixed additional comments from @Marcono1234
* Made Adapter in ReflectiveTypeAdapterFactory public
Fix comment from @eamonnmcmanus
2022-10-11 18:13:49 +02:00
|
|
|
makeAccessible("field '" + field.getDeclaringClass().getName() + "#" + field.getName() + "'", field);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Tries making the constructor accessible, wrapping any thrown exception in a {@link JsonIOException}
|
|
|
|
* with descriptive message.
|
|
|
|
*
|
|
|
|
* @param constructor constructor to make accessible
|
|
|
|
* @throws JsonIOException if making the constructor accessible fails
|
|
|
|
*/
|
|
|
|
public static void makeAccessible(Constructor<?> constructor) throws JsonIOException {
|
|
|
|
makeAccessible(
|
|
|
|
"constructor " + constructor + " in " + constructor.getDeclaringClass().getName(),
|
|
|
|
constructor
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Internal implementation of making an {@link AccessibleObject} accessible.
|
|
|
|
*
|
|
|
|
* @param description describe what we are attempting to make accessible
|
|
|
|
* @param object the object that {@link AccessibleObject#setAccessible(boolean)} should be called on.
|
|
|
|
* @throws JsonIOException if making the object accessible fails
|
|
|
|
*/
|
|
|
|
private static void makeAccessible(String description, AccessibleObject object) throws JsonIOException {
|
2021-11-09 16:16:35 +01:00
|
|
|
try {
|
Support Java Records when present in JVM. (#2201)
* Support Java Records when present in JVM.
Fixes google/gson#1794
Added support in the ReflectionHelper to detect if a class is a record
on the JVM (via reflection), and if so, we will create a special
RecordAdapter to deserialize records, using the canoncial constructor.
The ReflectionTypeAdapterFactory had to be refactored a bit to support
this. The Adapter class inside the factory is now abstract, with
concrete implementations for normal field reflection and for Records.
The common code is in the Adapter, with each implementation
deserializing values into an intermediary object.
For the FieldReflectionAdapter, the intermediary is actually the final
result, and field access is used to write to fields as before. For the
RecordAdapter the intermediary is the Object[] to pass to the Record
constructor.
* Fixed comments from @Marcono1234
Also updated so that we now use the record accessor method to read out
values from a record, so that direct field access is not necessary.
Also added some tests, that should only execute on Java versions with
record support, and be ignored for other JVMs
* Fixed additional comments from @Marcono1234
* Made Adapter in ReflectiveTypeAdapterFactory public
Fix comment from @eamonnmcmanus
2022-10-11 18:13:49 +02:00
|
|
|
object.setAccessible(true);
|
2021-11-09 16:16:35 +01:00
|
|
|
} catch (Exception exception) {
|
Support Java Records when present in JVM. (#2201)
* Support Java Records when present in JVM.
Fixes google/gson#1794
Added support in the ReflectionHelper to detect if a class is a record
on the JVM (via reflection), and if so, we will create a special
RecordAdapter to deserialize records, using the canoncial constructor.
The ReflectionTypeAdapterFactory had to be refactored a bit to support
this. The Adapter class inside the factory is now abstract, with
concrete implementations for normal field reflection and for Records.
The common code is in the Adapter, with each implementation
deserializing values into an intermediary object.
For the FieldReflectionAdapter, the intermediary is actually the final
result, and field access is used to write to fields as before. For the
RecordAdapter the intermediary is the Object[] to pass to the Record
constructor.
* Fixed comments from @Marcono1234
Also updated so that we now use the record accessor method to read out
values from a record, so that direct field access is not necessary.
Also added some tests, that should only execute on Java versions with
record support, and be ignored for other JVMs
* Fixed additional comments from @Marcono1234
* Made Adapter in ReflectiveTypeAdapterFactory public
Fix comment from @eamonnmcmanus
2022-10-11 18:13:49 +02:00
|
|
|
throw new JsonIOException("Failed making " + description + "' accessible; either change its visibility "
|
|
|
|
+ "or write a custom TypeAdapter for its declaring type", exception);
|
2021-11-09 16:16:35 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a string representation for a constructor.
|
|
|
|
* E.g.: {@code java.lang.String#String(char[], int, int)}
|
|
|
|
*/
|
|
|
|
private static String constructorToString(Constructor<?> constructor) {
|
|
|
|
StringBuilder stringBuilder = new StringBuilder(constructor.getDeclaringClass().getName())
|
|
|
|
.append('#')
|
|
|
|
.append(constructor.getDeclaringClass().getSimpleName())
|
|
|
|
.append('(');
|
|
|
|
Class<?>[] parameters = constructor.getParameterTypes();
|
|
|
|
for (int i = 0; i < parameters.length; i++) {
|
|
|
|
if (i > 0) {
|
|
|
|
stringBuilder.append(", ");
|
|
|
|
}
|
|
|
|
stringBuilder.append(parameters[i].getSimpleName());
|
|
|
|
}
|
|
|
|
|
|
|
|
return stringBuilder.append(')').toString();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Tries making the constructor accessible, returning an exception message
|
|
|
|
* if this fails.
|
|
|
|
*
|
|
|
|
* @param constructor constructor to make accessible
|
|
|
|
* @return exception message; {@code null} if successful, non-{@code null} if
|
|
|
|
* unsuccessful
|
|
|
|
*/
|
|
|
|
public static String tryMakeAccessible(Constructor<?> constructor) {
|
|
|
|
try {
|
|
|
|
constructor.setAccessible(true);
|
|
|
|
return null;
|
|
|
|
} catch (Exception exception) {
|
|
|
|
return "Failed making constructor '" + constructorToString(constructor) + "' accessible; "
|
|
|
|
+ "either change its visibility or write a custom InstanceCreator or TypeAdapter for its declaring type: "
|
|
|
|
// Include the message since it might contain more detailed information
|
|
|
|
+ exception.getMessage();
|
|
|
|
}
|
|
|
|
}
|
2022-04-17 18:05:18 +02:00
|
|
|
|
Support Java Records when present in JVM. (#2201)
* Support Java Records when present in JVM.
Fixes google/gson#1794
Added support in the ReflectionHelper to detect if a class is a record
on the JVM (via reflection), and if so, we will create a special
RecordAdapter to deserialize records, using the canoncial constructor.
The ReflectionTypeAdapterFactory had to be refactored a bit to support
this. The Adapter class inside the factory is now abstract, with
concrete implementations for normal field reflection and for Records.
The common code is in the Adapter, with each implementation
deserializing values into an intermediary object.
For the FieldReflectionAdapter, the intermediary is actually the final
result, and field access is used to write to fields as before. For the
RecordAdapter the intermediary is the Object[] to pass to the Record
constructor.
* Fixed comments from @Marcono1234
Also updated so that we now use the record accessor method to read out
values from a record, so that direct field access is not necessary.
Also added some tests, that should only execute on Java versions with
record support, and be ignored for other JVMs
* Fixed additional comments from @Marcono1234
* Made Adapter in ReflectiveTypeAdapterFactory public
Fix comment from @eamonnmcmanus
2022-10-11 18:13:49 +02:00
|
|
|
/** If records are supported on the JVM, this is equivalent to a call to Class.isRecord() */
|
|
|
|
public static boolean isRecord(Class<?> raw) {
|
|
|
|
return RECORD_HELPER.isRecord(raw);
|
|
|
|
}
|
|
|
|
|
|
|
|
public static String[] getRecordComponentNames(Class<?> raw) {
|
|
|
|
return RECORD_HELPER.getRecordComponentNames(raw);
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Looks up the record accessor method that corresponds to the given record field */
|
|
|
|
public static Method getAccessor(Class<?> raw, Field field) {
|
|
|
|
return RECORD_HELPER.getAccessor(raw, field);
|
|
|
|
}
|
|
|
|
|
|
|
|
public static <T> Constructor<T> getCanonicalRecordConstructor(Class<T> raw) {
|
|
|
|
return RECORD_HELPER.getCanonicalRecordConstructor(raw);
|
|
|
|
}
|
|
|
|
|
|
|
|
public static RuntimeException createExceptionForUnexpectedIllegalAccess(
|
|
|
|
IllegalAccessException exception) {
|
2022-04-17 18:05:18 +02:00
|
|
|
throw new RuntimeException("Unexpected IllegalAccessException occurred (Gson " + GsonBuildConfig.VERSION + "). "
|
|
|
|
+ "Certain ReflectionAccessFilter features require Java >= 9 to work correctly. If you are not using "
|
|
|
|
+ "ReflectionAccessFilter, report this to the Gson maintainers.",
|
|
|
|
exception);
|
|
|
|
}
|
Support Java Records when present in JVM. (#2201)
* Support Java Records when present in JVM.
Fixes google/gson#1794
Added support in the ReflectionHelper to detect if a class is a record
on the JVM (via reflection), and if so, we will create a special
RecordAdapter to deserialize records, using the canoncial constructor.
The ReflectionTypeAdapterFactory had to be refactored a bit to support
this. The Adapter class inside the factory is now abstract, with
concrete implementations for normal field reflection and for Records.
The common code is in the Adapter, with each implementation
deserializing values into an intermediary object.
For the FieldReflectionAdapter, the intermediary is actually the final
result, and field access is used to write to fields as before. For the
RecordAdapter the intermediary is the Object[] to pass to the Record
constructor.
* Fixed comments from @Marcono1234
Also updated so that we now use the record accessor method to read out
values from a record, so that direct field access is not necessary.
Also added some tests, that should only execute on Java versions with
record support, and be ignored for other JVMs
* Fixed additional comments from @Marcono1234
* Made Adapter in ReflectiveTypeAdapterFactory public
Fix comment from @eamonnmcmanus
2022-10-11 18:13:49 +02:00
|
|
|
|
|
|
|
|
|
|
|
public static RuntimeException createExceptionForRecordReflectionException(
|
|
|
|
ReflectiveOperationException exception) {
|
|
|
|
throw new RuntimeException("Unexpected ReflectiveOperationException occurred "
|
|
|
|
+ "(Gson " + GsonBuildConfig.VERSION + "). "
|
|
|
|
+ "To support Java records, reflection is utilized to read out information "
|
|
|
|
+ "about records. All these invocations happens after it is established "
|
|
|
|
+ "that records exists in the JVM. This exception is unexpected behaviour.",
|
|
|
|
exception);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Internal abstraction over reflection when Records are supported.
|
|
|
|
*/
|
|
|
|
private abstract static class RecordHelper {
|
|
|
|
abstract boolean isRecord(Class<?> clazz);
|
|
|
|
|
|
|
|
abstract String[] getRecordComponentNames(Class<?> clazz);
|
|
|
|
|
|
|
|
abstract <T> Constructor<T> getCanonicalRecordConstructor(Class<T> raw);
|
|
|
|
|
|
|
|
public abstract Method getAccessor(Class<?> raw, Field field);
|
|
|
|
}
|
|
|
|
|
|
|
|
private static class RecordSupportedHelper extends RecordHelper {
|
|
|
|
private final Method isRecord;
|
|
|
|
private final Method getRecordComponents;
|
|
|
|
private final Method getName;
|
|
|
|
private final Method getType;
|
|
|
|
private final Method getAccessor;
|
|
|
|
|
|
|
|
private RecordSupportedHelper() throws NoSuchMethodException {
|
|
|
|
isRecord = Class.class.getMethod("isRecord");
|
|
|
|
getRecordComponents = Class.class.getMethod("getRecordComponents");
|
|
|
|
Class<?> recordComponentType = getRecordComponents.getReturnType().getComponentType();
|
|
|
|
getName = recordComponentType.getMethod("getName");
|
|
|
|
getType = recordComponentType.getMethod("getType");
|
|
|
|
getAccessor = recordComponentType.getMethod("getAccessor");
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
boolean isRecord(Class<?> raw) {
|
|
|
|
try {
|
|
|
|
return Boolean.class.cast(isRecord.invoke(raw)).booleanValue();
|
|
|
|
} catch (ReflectiveOperationException e) {
|
|
|
|
throw createExceptionForRecordReflectionException(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
String[] getRecordComponentNames(Class<?> raw) {
|
|
|
|
try {
|
|
|
|
Object[] recordComponents = (Object[]) getRecordComponents.invoke(raw);
|
|
|
|
String[] componentNames = new String[recordComponents.length];
|
|
|
|
for (int i = 0; i < recordComponents.length; i++) {
|
|
|
|
componentNames[i] = (String) getName.invoke(recordComponents[i]);
|
|
|
|
}
|
|
|
|
return componentNames;
|
|
|
|
} catch (ReflectiveOperationException e) {
|
|
|
|
throw createExceptionForRecordReflectionException(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public <T> Constructor<T> getCanonicalRecordConstructor(Class<T> raw) {
|
|
|
|
try {
|
|
|
|
Object[] recordComponents = (Object[]) getRecordComponents.invoke(raw);
|
|
|
|
Class<?>[] recordComponentTypes = new Class<?>[recordComponents.length];
|
|
|
|
for (int i = 0; i < recordComponents.length; i++) {
|
|
|
|
recordComponentTypes[i] = (Class<?>) getType.invoke(recordComponents[i]);
|
|
|
|
}
|
|
|
|
// Uses getDeclaredConstructor because implicit constructor has same visibility as record and might
|
|
|
|
// therefore not be public
|
|
|
|
return raw.getDeclaredConstructor(recordComponentTypes);
|
|
|
|
} catch (ReflectiveOperationException e) {
|
|
|
|
throw createExceptionForRecordReflectionException(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Method getAccessor(Class<?> raw, Field field) {
|
|
|
|
try {
|
|
|
|
// Records consists of record components, each with a unique name, a corresponding field and accessor method
|
|
|
|
// with the same name. Ref.: https://docs.oracle.com/javase/specs/jls/se17/html/jls-8.html#jls-8.10.3
|
|
|
|
return raw.getMethod(field.getName());
|
|
|
|
} catch (ReflectiveOperationException e) {
|
|
|
|
throw createExceptionForRecordReflectionException(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Instance used when records are not supported
|
|
|
|
*/
|
|
|
|
private static class RecordNotSupportedHelper extends RecordHelper {
|
|
|
|
|
|
|
|
@Override
|
|
|
|
boolean isRecord(Class<?> clazz) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
String[] getRecordComponentNames(Class<?> clazz) {
|
|
|
|
throw new UnsupportedOperationException(
|
|
|
|
"Records are not supported on this JVM, this method should not be called");
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
<T> Constructor<T> getCanonicalRecordConstructor(Class<T> raw) {
|
|
|
|
throw new UnsupportedOperationException(
|
|
|
|
"Records are not supported on this JVM, this method should not be called");
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Method getAccessor(Class<?> raw, Field field) {
|
|
|
|
throw new UnsupportedOperationException(
|
|
|
|
"Records are not supported on this JVM, this method should not be called");
|
|
|
|
}
|
|
|
|
}
|
2021-11-09 16:16:35 +01:00
|
|
|
}
|