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
This commit is contained in:
Ståle Undheim 2022-10-11 18:13:49 +02:00 committed by GitHub
parent 8451c1fa63
commit a0dc7bfddd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 584 additions and 66 deletions

View File

@ -38,12 +38,17 @@ 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.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -94,22 +99,32 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
return fieldNames;
}
@Override public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
@Override
public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
Class<? super T> raw = type.getRawType();
if (!Object.class.isAssignableFrom(raw)) {
return null; // it's a primitive!
}
FilterResult filterResult = ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, raw);
FilterResult filterResult =
ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, raw);
if (filterResult == FilterResult.BLOCK_ALL) {
throw new JsonIOException("ReflectionAccessFilter does not permit using reflection for "
+ raw + ". Register a TypeAdapter for this type or adjust the access filter.");
throw new JsonIOException(
"ReflectionAccessFilter does not permit using reflection for "
+ raw
+ ". Register a TypeAdapter for this type or adjust the access filter.");
}
boolean blockInaccessible = filterResult == FilterResult.BLOCK_INACCESSIBLE;
// If the type is actually a Java Record, we need to use the RecordAdapter instead. This will always be false
// on JVMs that do not support records.
if (ReflectionHelper.isRecord(raw)) {
return new RecordAdapter<>(raw, getBoundFields(gson, type, raw, true, true));
}
ObjectConstructor<T> constructor = constructorConstructor.get(type);
return new Adapter<>(constructor, getBoundFields(gson, type, raw, blockInaccessible));
return new FieldReflectionAdapter<>(constructor, getBoundFields(gson, type, raw, blockInaccessible, false));
}
private static void checkAccessible(Object object, Field field) {
@ -122,7 +137,7 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
}
private ReflectiveTypeAdapterFactory.BoundField createBoundField(
final Gson context, final Field field, final String name,
final Gson context, final Field field, final Method accessor, final String name,
final TypeToken<?> fieldType, boolean serialize, boolean deserialize,
final boolean blockInaccessible) {
final boolean isPrimitive = Primitives.isPrimitive(fieldType.getRawType());
@ -138,16 +153,18 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
@SuppressWarnings("unchecked")
final TypeAdapter<Object> typeAdapter = (TypeAdapter<Object>) mapped;
return new ReflectiveTypeAdapterFactory.BoundField(name, serialize, deserialize) {
@Override void write(JsonWriter writer, Object value)
throws IOException, IllegalAccessException {
return new ReflectiveTypeAdapterFactory.BoundField(name, field.getName(), serialize, deserialize) {
@Override void write(JsonWriter writer, Object source)
throws IOException, ReflectiveOperationException {
if (!serialized) return;
if (blockInaccessible) {
checkAccessible(value, field);
if (blockInaccessible && accessor == null) {
checkAccessible(source, field);
}
Object fieldValue = field.get(value);
if (fieldValue == value) {
Object fieldValue = (accessor != null)
? accessor.invoke(source)
: field.get(source);
if (fieldValue == source) {
// avoid direct recursion
return;
}
@ -156,20 +173,31 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
: new TypeAdapterRuntimeTypeWrapper<>(context, typeAdapter, fieldType.getType());
t.write(writer, fieldValue);
}
@Override void read(JsonReader reader, Object value)
@Override
void readIntoArray(JsonReader reader, int index, Object[] target) throws IOException {
Object fieldValue = typeAdapter.read(reader);
if (fieldValue != null || !isPrimitive) {
target[index] = fieldValue;
}
}
@Override
void readIntoField(JsonReader reader, Object target)
throws IOException, IllegalAccessException {
Object fieldValue = typeAdapter.read(reader);
if (fieldValue != null || !isPrimitive) {
if (blockInaccessible) {
checkAccessible(value, field);
checkAccessible(target, field);
}
field.set(value, fieldValue);
field.set(target, fieldValue);
}
}
};
}
private Map<String, BoundField> getBoundFields(Gson context, TypeToken<?> type, Class<?> raw, boolean blockInaccessible) {
private Map<String, BoundField> getBoundFields(Gson context, TypeToken<?> type, Class<?> raw,
boolean blockInaccessible, boolean isRecord) {
Map<String, BoundField> result = new LinkedHashMap<>();
if (raw.isInterface()) {
return result;
@ -197,8 +225,19 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
if (!serialize && !deserialize) {
continue;
}
// The accessor method is only used for records. If the type is a record, we will read out values
// via its accessor method instead of via reflection. This way we will bypass the accessible restrictions
// If there is a static field on a record, there will not be an accessor. Instead we will use the default
// field logic for dealing with statics.
Method accessor = null;
if (isRecord && !Modifier.isStatic(field.getModifiers())) {
accessor = ReflectionHelper.getAccessor(raw, field);
}
// If blockInaccessible, skip and perform access check later
// If blockInaccessible, skip and perform access check later. When constructing a BoundedField for a Record
// field, blockInaccessible is always true, thus makeAccessible will never get called. This is not an issue
// though, as we will use the accessor method instead for reading record fields, and the constructor for
// writing fields.
if (!blockInaccessible) {
ReflectionHelper.makeAccessible(field);
}
@ -208,7 +247,7 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
for (int i = 0, size = fieldNames.size(); i < size; ++i) {
String name = fieldNames.get(i);
if (i != 0) serialize = false; // only serialize the default name
BoundField boundField = createBoundField(context, field, name,
BoundField boundField = createBoundField(context, field, accessor, name,
TypeToken.get(fieldType), serialize, deserialize, blockInaccessible);
BoundField replaced = result.put(name, boundField);
if (previous == null) previous = replaced;
@ -226,56 +265,50 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
static abstract class BoundField {
final String name;
/** Name of the underlying field */
final String fieldName;
final boolean serialized;
final boolean deserialized;
protected BoundField(String name, boolean serialized, boolean deserialized) {
protected BoundField(String name, String fieldName, boolean serialized, boolean deserialized) {
this.name = name;
this.fieldName = fieldName;
this.serialized = serialized;
this.deserialized = deserialized;
}
abstract void write(JsonWriter writer, Object value) throws IOException, IllegalAccessException;
abstract void read(JsonReader reader, Object value) throws IOException, IllegalAccessException;
/** Read this field value from the source, and append its JSON value to the writer */
abstract void write(JsonWriter writer, Object source) throws IOException, ReflectiveOperationException;
/** Read the value into the target array, used to provide constructor arguments for records */
abstract void readIntoArray(JsonReader reader, int index, Object[] target) throws IOException;
/** Read the value from the reader, and set it on the corresponding field on target via reflection */
abstract void readIntoField(JsonReader reader, Object target) throws IOException, IllegalAccessException;
}
public static final class Adapter<T> extends TypeAdapter<T> {
private final ObjectConstructor<T> constructor;
private final Map<String, BoundField> boundFields;
/**
* Base class for Adapters produced by this factory.
*
* <p>The {@link RecordAdapter} is a special case to handle records for JVMs that support it, for
* all other types we use the {@link FieldReflectionAdapter}. This class encapsulates the common
* logic for serialization and deserialization. During deserialization, we construct an
* accumulator A, which we use to accumulate values from the source JSON. After the object has been read in
* full, the {@link #finalize(Object)} method is used to convert the accumulator to an instance
* of T.
*
* @param <T> type of objects that this Adapter creates.
* @param <A> type of accumulator used to build the deserialization result.
*/
public static abstract class Adapter<T, A> extends TypeAdapter<T> {
protected final Map<String, BoundField> boundFields;
Adapter(ObjectConstructor<T> constructor, Map<String, BoundField> boundFields) {
this.constructor = constructor;
protected Adapter(Map<String, BoundField> boundFields) {
this.boundFields = boundFields;
}
@Override public T read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
T instance = constructor.construct();
try {
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
BoundField field = boundFields.get(name);
if (field == null || !field.deserialized) {
in.skipValue();
} else {
field.read(in, instance);
}
}
} catch (IllegalStateException e) {
throw new JsonSyntaxException(e);
} catch (IllegalAccessException e) {
throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
}
in.endObject();
return instance;
}
@Override public void write(JsonWriter out, T value) throws IOException {
@Override
public void write(JsonWriter out, T value) throws IOException {
if (value == null) {
out.nullValue();
return;
@ -288,8 +321,143 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
}
} catch (IllegalAccessException e) {
throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
} catch (ReflectiveOperationException e) {
throw ReflectionHelper.createExceptionForRecordReflectionException(e);
}
out.endObject();
}
@Override
public T read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
A accumulator = createAccumulator();
try {
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
BoundField field = boundFields.get(name);
if (field == null || !field.deserialized) {
in.skipValue();
} else {
readField(accumulator, in, field);
}
}
} catch (IllegalStateException e) {
throw new JsonSyntaxException(e);
} catch (IllegalAccessException e) {
throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
}
in.endObject();
return finalize(accumulator);
}
/** Create the Object that will be used to collect each field value */
abstract A createAccumulator();
/**
* Read a single BoundedField into the accumulator. The JsonReader will be pointed at the
* start of the value for the BoundField to read from.
*/
abstract void readField(A accumulator, JsonReader in, BoundField field)
throws IllegalAccessException, IOException;
/** Convert the accumulator to a final instance of T. */
abstract T finalize(A accumulator);
}
private static final class FieldReflectionAdapter<T> extends Adapter<T, T> {
private final ObjectConstructor<T> constructor;
FieldReflectionAdapter(ObjectConstructor<T> constructor, Map<String, BoundField> boundFields) {
super(boundFields);
this.constructor = constructor;
}
@Override
T createAccumulator() {
return constructor.construct();
}
@Override
void readField(T accumulator, JsonReader in, BoundField field)
throws IllegalAccessException, IOException {
field.readIntoField(in, accumulator);
}
@Override
T finalize(T accumulator) {
return accumulator;
}
}
private static final class RecordAdapter<T> extends Adapter<T, Object[]> {
// The actual record constructor.
private final Constructor<? super T> constructor;
// Array of arguments to the constructor, initialized with default values for primitives
private final Object[] constructorArgsDefaults;
// Map from component names to index into the constructors arguments.
private final Map<String, Integer> componentIndices = new HashMap<>();
RecordAdapter(Class<? super T> raw, Map<String, BoundField> boundFields) {
super(boundFields);
this.constructor = ReflectionHelper.getCanonicalRecordConstructor(raw);
// Ensure the constructor is accessible
ReflectionHelper.makeAccessible(this.constructor);
String[] componentNames = ReflectionHelper.getRecordComponentNames(raw);
for (int i = 0; i < componentNames.length; i++) {
componentIndices.put(componentNames[i], i);
}
Class<?>[] parameterTypes = constructor.getParameterTypes();
// We need to ensure that we are passing non-null values to primitive fields in the constructor. To do this,
// we create an Object[] where all primitives are initialized to non-null values.
constructorArgsDefaults = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
if (parameterTypes[i].isPrimitive()) {
// Voodoo magic, we create a new instance of this primitive type using reflection via an
// array. The array has 1 element, that of course will be initialized to the primitives
// default value. We then retrieve this value back from the array to get the properly
// initialized default value for the primitve type.
constructorArgsDefaults[i] = Array.get(Array.newInstance(parameterTypes[i], 1), 0);
}
}
}
@Override
Object[] createAccumulator() {
return constructorArgsDefaults.clone();
}
@Override
void readField(Object[] accumulator, JsonReader in, BoundField field) throws IOException {
Integer fieldIndex = componentIndices.get(field.fieldName);
if (fieldIndex == null) {
throw new IllegalStateException(
"Could not find the index in the constructor "
+ constructor
+ " for field with name "
+ field.name
+ ", unable to determine which argument in the constructor the field corresponds"
+ " to. This is unexpected behaviour, as we expect the RecordComponents to have the"
+ " same names as the fields in the Java class, and that the order of the"
+ " RecordComponents is the same as the order of the canonical arguments.");
}
field.readIntoArray(in, fieldIndex, accumulator);
}
@Override
@SuppressWarnings("unchecked")
T finalize(Object[] accumulator) {
try {
return (T) constructor.newInstance(accumulator);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(
"Failed to invoke " + constructor + " with args " + Arrays.toString(accumulator), e);
}
}
}
}

View File

@ -2,26 +2,64 @@ package com.google.gson.internal.reflect;
import com.google.gson.JsonIOException;
import com.google.gson.internal.GsonBuildConfig;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.*;
public class ReflectionHelper {
private ReflectionHelper() { }
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() {}
/**
* Tries making the field accessible, wrapping any thrown exception in a
* {@link JsonIOException} with descriptive message.
* Tries making the field accessible, wrapping any thrown exception in a {@link JsonIOException}
* with descriptive message.
*
* @param field field to make accessible
* @throws JsonIOException if making the field accessible fails
*/
public static void makeAccessible(Field field) throws JsonIOException {
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 {
try {
field.setAccessible(true);
object.setAccessible(true);
} catch (Exception exception) {
throw new JsonIOException("Failed making field '" + field.getDeclaringClass().getName() + "#"
+ field.getName() + "' accessible; either change its visibility or write a custom "
+ "TypeAdapter for its declaring type", exception);
throw new JsonIOException("Failed making " + description + "' accessible; either change its visibility "
+ "or write a custom TypeAdapter for its declaring type", exception);
}
}
@ -65,10 +103,149 @@ public class ReflectionHelper {
}
}
public static RuntimeException createExceptionForUnexpectedIllegalAccess(IllegalAccessException exception) {
/** 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) {
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);
}
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");
}
}
}

View File

@ -0,0 +1,83 @@
package com.google.gson.internal.bind;
import static org.junit.Assert.*;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.internal.reflect.ReflectionHelperTest;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.nio.file.attribute.GroupPrincipal;
import java.nio.file.attribute.UserPrincipal;
import java.security.Principal;
import org.junit.AssumptionViolatedException;
import org.junit.Before;
import org.junit.Test;
public class ReflectiveTypeAdapterFactoryTest {
// The class jdk.net.UnixDomainPrincipal is one of the few Record types that are included in the
// JDK.
// We use this to test serialization and deserialization of Record classes, so we do not need to
// have
// record support at the language level for these tests. This class was added in JDK 16.
Class<?> unixDomainPrincipalClass;
@Before
public void setUp() throws Exception {
try {
Class.forName("java.lang.Record");
unixDomainPrincipalClass = Class.forName("jdk.net.UnixDomainPrincipal");
} catch (ClassNotFoundException e) {
// Records not supported, ignore
throw new AssumptionViolatedException("java.lang.Record not supported");
}
}
@Test
public void testCustomAdapterForRecords() {
Gson gson = new Gson();
TypeAdapter<?> recordAdapter = gson.getAdapter(unixDomainPrincipalClass);
TypeAdapter<?> defaultReflectionAdapter = gson.getAdapter(UserPrincipal.class);
assertNotEquals(recordAdapter.getClass(), defaultReflectionAdapter.getClass());
}
@Test
public void testSerializeRecords() throws ReflectiveOperationException {
Gson gson =
new GsonBuilder()
.registerTypeAdapter(UserPrincipal.class, new PrincipalTypeAdapter<>())
.registerTypeAdapter(GroupPrincipal.class, new PrincipalTypeAdapter<>())
.create();
UserPrincipal userPrincipal = gson.fromJson("\"user\"", UserPrincipal.class);
GroupPrincipal groupPrincipal = gson.fromJson("\"group\"", GroupPrincipal.class);
Object recordInstance =
unixDomainPrincipalClass
.getDeclaredConstructor(UserPrincipal.class, GroupPrincipal.class)
.newInstance(userPrincipal, groupPrincipal);
String serialized = gson.toJson(recordInstance);
Object deserializedRecordInstance = gson.fromJson(serialized, unixDomainPrincipalClass);
assertEquals(recordInstance, deserializedRecordInstance);
assertEquals("{\"user\":\"user\",\"group\":\"group\"}", serialized);
}
private static class PrincipalTypeAdapter<T extends Principal> extends TypeAdapter<T> {
@Override
public void write(JsonWriter out, T principal) throws IOException {
out.value(principal.getName());
}
@Override
public T read(JsonReader in) throws IOException {
final String name = in.nextString();
// This type adapter is only used for Group and User Principal, both of which are implemented by PrincipalImpl.
@SuppressWarnings("unchecked")
T principal = (T) new ReflectionHelperTest.PrincipalImpl(name);
return principal;
}
}
}

View File

@ -0,0 +1,90 @@
package com.google.gson.internal.reflect;
import static org.junit.Assert.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.attribute.GroupPrincipal;
import java.nio.file.attribute.UserPrincipal;
import java.util.Objects;
import org.junit.AssumptionViolatedException;
import org.junit.Before;
import org.junit.Test;
public class ReflectionHelperTest {
@Before
public void setUp() throws Exception {
try {
Class.forName("java.lang.Record");
} catch (ClassNotFoundException e) {
// Records not supported, ignore
throw new AssumptionViolatedException("java.lang.Record not supported");
}
}
@Test
public void testJava17Record() throws ClassNotFoundException {
Class<?> unixDomainPrincipalClass = Class.forName("jdk.net.UnixDomainPrincipal");
// UnixDomainPrincipal is a record
assertTrue(ReflectionHelper.isRecord(unixDomainPrincipalClass));
// with 2 components
assertArrayEquals(
new String[] {"user", "group"},
ReflectionHelper.getRecordComponentNames(unixDomainPrincipalClass));
// Check canonical constructor
Constructor<?> constructor =
ReflectionHelper.getCanonicalRecordConstructor(unixDomainPrincipalClass);
assertNotNull(constructor);
assertArrayEquals(
new Class<?>[] {UserPrincipal.class, GroupPrincipal.class},
constructor.getParameterTypes());
}
@Test
public void testJava17RecordAccessors() throws ReflectiveOperationException {
// Create an instance of UnixDomainPrincipal, using our custom implementation of UserPrincipal,
// and GroupPrincipal. Then attempt to access each component of the record using our accessor
// methods.
Class<?> unixDomainPrincipalClass = Class.forName("jdk.net.UnixDomainPrincipal");
Object unixDomainPrincipal =
ReflectionHelper.getCanonicalRecordConstructor(unixDomainPrincipalClass)
.newInstance(new PrincipalImpl("user"), new PrincipalImpl("group"));
for (String componentName :
ReflectionHelper.getRecordComponentNames(unixDomainPrincipalClass)) {
Field componentField = unixDomainPrincipalClass.getDeclaredField(componentName);
Method accessor = ReflectionHelper.getAccessor(unixDomainPrincipalClass, componentField);
Object principal = accessor.invoke(unixDomainPrincipal);
assertEquals(new PrincipalImpl(componentName), principal);
}
}
/** Implementation of {@link UserPrincipal} and {@link GroupPrincipal} just for record tests. */
public static class PrincipalImpl implements UserPrincipal, GroupPrincipal {
private final String name;
public PrincipalImpl(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PrincipalImpl principal = (PrincipalImpl) o;
return Objects.equals(name, principal.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
}
}