Add support for reflection access filter (#1905)

* Add support for reflection access filter

* Improve documentation

* Fix compilation errors

* Relax handling for BLOCK_ALL when invoking default constructor

* Improve handling for inherited fields

* Fix accessible test failing for static fields

* Simplify ReflectiveTypeAdapterFactory field writing

* Fix GsonBuilder changes affecting created Gson instances

* Improve documentation

* Improve handling for IllegalAccessException

For Java < 9, AccessibleObject.canAccess is not available and therefore checks
might pass even if object is not accessible, causing IllegalAccessException
later.

* Fix incorrect GsonBuilder.addReflectionAccessFilter documentation
This commit is contained in:
Marcono1234 2022-04-17 18:05:18 +02:00 committed by GitHub
parent f79ea208b1
commit e82637c485
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1034 additions and 135 deletions

View File

@ -20,6 +20,7 @@ import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.InstanceCreator;
import com.google.gson.JsonElement;
import com.google.gson.ReflectionAccessFilter;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.ConstructorConstructor;
@ -30,6 +31,7 @@ import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.LinkedList;
@ -47,7 +49,7 @@ public final class GraphAdapterBuilder {
public GraphAdapterBuilder() {
this.instanceCreators = new HashMap<Type, InstanceCreator<?>>();
this.constructorConstructor = new ConstructorConstructor(instanceCreators, true);
this.constructorConstructor = new ConstructorConstructor(instanceCreators, true, Collections.<ReflectionAccessFilter>emptyList());
}
public GraphAdapterBuilder addType(Type type) {
final ObjectConstructor<?> objectConstructor = constructorConstructor.get(TypeToken.get(type));

View File

@ -156,6 +156,7 @@ public final class Gson {
final List<TypeAdapterFactory> builderHierarchyFactories;
final ToNumberStrategy objectToNumberStrategy;
final ToNumberStrategy numberToNumberStrategy;
final List<ReflectionAccessFilter> reflectionFilters;
/**
* Constructs a Gson object with default configuration. The default configuration has the
@ -199,7 +200,8 @@ public final class Gson {
DEFAULT_USE_JDK_UNSAFE,
LongSerializationPolicy.DEFAULT, DEFAULT_DATE_PATTERN, DateFormat.DEFAULT, DateFormat.DEFAULT,
Collections.<TypeAdapterFactory>emptyList(), Collections.<TypeAdapterFactory>emptyList(),
Collections.<TypeAdapterFactory>emptyList(), DEFAULT_OBJECT_TO_NUMBER_STRATEGY, DEFAULT_NUMBER_TO_NUMBER_STRATEGY);
Collections.<TypeAdapterFactory>emptyList(), DEFAULT_OBJECT_TO_NUMBER_STRATEGY, DEFAULT_NUMBER_TO_NUMBER_STRATEGY,
Collections.<ReflectionAccessFilter>emptyList());
}
Gson(Excluder excluder, FieldNamingStrategy fieldNamingStrategy,
@ -211,11 +213,12 @@ public final class Gson {
int timeStyle, List<TypeAdapterFactory> builderFactories,
List<TypeAdapterFactory> builderHierarchyFactories,
List<TypeAdapterFactory> factoriesToBeAdded,
ToNumberStrategy objectToNumberStrategy, ToNumberStrategy numberToNumberStrategy) {
ToNumberStrategy objectToNumberStrategy, ToNumberStrategy numberToNumberStrategy,
List<ReflectionAccessFilter> reflectionFilters) {
this.excluder = excluder;
this.fieldNamingStrategy = fieldNamingStrategy;
this.instanceCreators = instanceCreators;
this.constructorConstructor = new ConstructorConstructor(instanceCreators, useJdkUnsafe);
this.constructorConstructor = new ConstructorConstructor(instanceCreators, useJdkUnsafe, reflectionFilters);
this.serializeNulls = serializeNulls;
this.complexMapKeySerialization = complexMapKeySerialization;
this.generateNonExecutableJson = generateNonExecutableGson;
@ -232,6 +235,7 @@ public final class Gson {
this.builderHierarchyFactories = builderHierarchyFactories;
this.objectToNumberStrategy = objectToNumberStrategy;
this.numberToNumberStrategy = numberToNumberStrategy;
this.reflectionFilters = reflectionFilters;
List<TypeAdapterFactory> factories = new ArrayList<TypeAdapterFactory>();
@ -296,7 +300,7 @@ public final class Gson {
factories.add(jsonAdapterFactory);
factories.add(TypeAdapters.ENUM_FACTORY);
factories.add(new ReflectiveTypeAdapterFactory(
constructorConstructor, fieldNamingStrategy, excluder, jsonAdapterFactory));
constructorConstructor, fieldNamingStrategy, excluder, jsonAdapterFactory, reflectionFilters));
this.factories = Collections.unmodifiableList(factories);
}

View File

@ -22,6 +22,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@ -101,6 +102,7 @@ public final class GsonBuilder {
private boolean useJdkUnsafe = DEFAULT_USE_JDK_UNSAFE;
private ToNumberStrategy objectToNumberStrategy = DEFAULT_OBJECT_TO_NUMBER_STRATEGY;
private ToNumberStrategy numberToNumberStrategy = DEFAULT_NUMBER_TO_NUMBER_STRATEGY;
private final LinkedList<ReflectionAccessFilter> reflectionFilters = new LinkedList<ReflectionAccessFilter>();
/**
* Creates a GsonBuilder instance that can be used to build Gson with various configuration
@ -137,6 +139,7 @@ public final class GsonBuilder {
this.useJdkUnsafe = gson.useJdkUnsafe;
this.objectToNumberStrategy = gson.objectToNumberStrategy;
this.numberToNumberStrategy = gson.numberToNumberStrategy;
this.reflectionFilters.addAll(gson.reflectionFilters);
}
/**
@ -632,6 +635,28 @@ public final class GsonBuilder {
return this;
}
/**
* Adds a reflection access filter. A reflection access filter prevents Gson from using
* reflection for the serialization and deserialization of certain classes. The logic in
* the filter specifies which classes those are.
*
* <p>Filters will be invoked in reverse registration order, that is, the most recently
* added filter will be invoked first.
*
* <p>By default Gson has no filters configured and will try to use reflection for
* all classes for which no {@link TypeAdapter} has been registered, and for which no
* built-in Gson {@code TypeAdapter} exists.
*
* @param filter filter to add
* @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
*/
public GsonBuilder addReflectionAccessFilter(ReflectionAccessFilter filter) {
if (filter == null) throw new NullPointerException();
reflectionFilters.addFirst(filter);
return this;
}
/**
* Creates a {@link Gson} instance based on the current configuration. This method is free of
* side-effects to this {@code GsonBuilder} instance and hence can be called multiple times.
@ -649,12 +674,13 @@ public final class GsonBuilder {
addTypeAdaptersForDate(datePattern, dateStyle, timeStyle, factories);
return new Gson(excluder, fieldNamingPolicy, instanceCreators,
return new Gson(excluder, fieldNamingPolicy, new HashMap<Type, InstanceCreator<?>>(instanceCreators),
serializeNulls, complexMapKeySerialization,
generateNonExecutableJson, escapeHtmlChars, prettyPrinting, lenient,
serializeSpecialFloatingPointValues, useJdkUnsafe, longSerializationPolicy,
datePattern, dateStyle, timeStyle,
this.factories, this.hierarchyFactories, factories, objectToNumberStrategy, numberToNumberStrategy);
datePattern, dateStyle, timeStyle, new ArrayList<TypeAdapterFactory>(this.factories),
new ArrayList<TypeAdapterFactory>(this.hierarchyFactories), factories,
objectToNumberStrategy, numberToNumberStrategy, new ArrayList<ReflectionAccessFilter>(reflectionFilters));
}
private void addTypeAdaptersForDate(String datePattern, int dateStyle, int timeStyle,

View File

@ -0,0 +1,194 @@
package com.google.gson;
import java.lang.reflect.AccessibleObject;
import com.google.gson.internal.ReflectionAccessFilterHelper;
/**
* Filter for determining whether reflection based serialization and
* deserialization is allowed for a class.
*
* <p>A filter can be useful in multiple scenarios, for example when
* upgrading to newer Java versions which use the Java Platform Module
* System (JPMS). A filter then allows to {@linkplain FilterResult#BLOCK_INACCESSIBLE
* prevent making inaccessible members accessible}, even if the used
* Java version might still allow illegal access (but logs a warning),
* or if {@code java} command line arguments are used to open the inaccessible
* packages to other parts of the application. This interface defines some
* convenience filters for this task, such as {@link #BLOCK_INACCESSIBLE_JAVA}.
*
* <p>A filter can also be useful to prevent mixing model classes of a
* project with other non-model classes; the filter could
* {@linkplain FilterResult#BLOCK_ALL block all reflective access} to
* non-model classes.
*
* <p>A reflection access filter is similar to an {@link ExclusionStrategy}
* with the major difference that a filter will cause an exception to be
* thrown when access is disallowed while an exclusion strategy just skips
* fields and classes.
*
* @see GsonBuilder#addReflectionAccessFilter(ReflectionAccessFilter)
*/
public interface ReflectionAccessFilter {
/**
* Result of a filter check.
*/
enum FilterResult {
/**
* Reflection access for the class is allowed.
*
* <p>Note that this does not affect the Java access checks in any way,
* it only permits Gson to try using reflection for a class. The Java
* runtime might still deny such access.
*/
ALLOW,
/**
* The filter is indecisive whether reflection access should be allowed.
* The next registered filter will be consulted to get the result. If
* there is no next filter, this result acts like {@link #ALLOW}.
*/
INDECISIVE,
/**
* Blocks reflection access if a member of the class is not accessible
* by default and would have to be made accessible. This is unaffected
* by any {@code java} command line arguments being used to make packages
* accessible, or by module declaration directives which <i>open</i> the
* complete module or certain packages for reflection and will consider
* such packages inaccessible.
*
* <p>Note that this <b>only works for Java 9 and higher</b>, for older
* Java versions its functionality will be limited and it might behave like
* {@link #ALLOW}. Access checks are only performed as defined by the Java
* Language Specification (<a href="https://docs.oracle.com/javase/specs/jls/se11/html/jls-6.html#jls-6.6">JLS 11 &sect;6.6</a>),
* restrictions imposed by a {@link SecurityManager} are not considered.
*
* <p>This result type is mainly intended to help enforce the access checks of
* the Java Platform Module System. It allows detecting illegal access, even if
* the used Java version would only log a warning, or is configured to open
* packages for reflection using command line arguments.
*
* @see AccessibleObject#canAccess(Object)
*/
BLOCK_INACCESSIBLE,
/**
* Blocks all reflection access for the class. Other means for serializing
* and deserializing the class, such as a {@link TypeAdapter}, have to
* be used.
*/
BLOCK_ALL
}
/**
* Blocks all reflection access to members of standard Java classes which are
* not accessible by default. However, reflection access is still allowed for
* classes for which all fields are accessible and which have an accessible
* no-args constructor (or for which an {@link InstanceCreator} has been registered).
*
* <p>If this filter encounters a class other than a standard Java class it
* returns {@link FilterResult#INDECISIVE}.
*
* <p>This filter is mainly intended to help enforcing the access checks of
* Java Platform Module System. It allows detecting illegal access, even if
* the used Java version would only log a warning, or is configured to open
* packages for reflection. However, this filter <b>only works for Java 9 and
* higher</b>, when using an older Java version its functionality will be
* limited.
*
* <p>Note that this filter might not cover all standard Java classes. Currently
* only classes in a {@code java.*} or {@code javax.*} package are considered. The
* set of detected classes might be expanded in the future without prior notice.
*
* @see FilterResult#BLOCK_INACCESSIBLE
*/
ReflectionAccessFilter BLOCK_INACCESSIBLE_JAVA = new ReflectionAccessFilter() {
@Override public FilterResult check(Class<?> rawClass) {
return ReflectionAccessFilterHelper.isJavaType(rawClass)
? FilterResult.BLOCK_INACCESSIBLE
: FilterResult.INDECISIVE;
}
};
/**
* Blocks all reflection access to members of standard Java classes.
*
* <p>If this filter encounters a class other than a standard Java class it
* returns {@link FilterResult#INDECISIVE}.
*
* <p>This filter is mainly intended to prevent depending on implementation
* details of the Java platform and to help applications prepare for upgrading
* to the Java Platform Module System.
*
* <p>Note that this filter might not cover all standard Java classes. Currently
* only classes in a {@code java.*} or {@code javax.*} package are considered. The
* set of detected classes might be expanded in the future without prior notice.
*
* @see #BLOCK_INACCESSIBLE_JAVA
* @see FilterResult#BLOCK_ALL
*/
ReflectionAccessFilter BLOCK_ALL_JAVA = new ReflectionAccessFilter() {
@Override public FilterResult check(Class<?> rawClass) {
return ReflectionAccessFilterHelper.isJavaType(rawClass)
? FilterResult.BLOCK_ALL
: FilterResult.INDECISIVE;
}
};
/**
* Blocks all reflection access to members of standard Android classes.
*
* <p>If this filter encounters a class other than a standard Android class it
* returns {@link FilterResult#INDECISIVE}.
*
* <p>This filter is mainly intended to prevent depending on implementation
* details of the Android platform.
*
* <p>Note that this filter might not cover all standard Android classes. Currently
* only classes in an {@code android.*} or {@code androidx.*} package, and standard
* Java classes in a {@code java.*} or {@code javax.*} package are considered. The
* set of detected classes might be expanded in the future without prior notice.
*
* @see FilterResult#BLOCK_ALL
*/
ReflectionAccessFilter BLOCK_ALL_ANDROID = new ReflectionAccessFilter() {
@Override public FilterResult check(Class<?> rawClass) {
return ReflectionAccessFilterHelper.isAndroidType(rawClass)
? FilterResult.BLOCK_ALL
: FilterResult.INDECISIVE;
}
};
/**
* Blocks all reflection access to members of classes belonging to programming
* language platforms, such as Java, Android, Kotlin or Scala.
*
* <p>If this filter encounters a class other than a standard platform class it
* returns {@link FilterResult#INDECISIVE}.
*
* <p>This filter is mainly intended to prevent depending on implementation
* details of the platform classes.
*
* <p>Note that this filter might not cover all platform classes. Currently it
* combines the filters {@link #BLOCK_ALL_JAVA} and {@link #BLOCK_ALL_ANDROID},
* and checks for other language-specific platform classes like {@code kotlin.*}.
* The set of detected classes might be expanded in the future without prior notice.
*
* @see FilterResult#BLOCK_ALL
*/
ReflectionAccessFilter BLOCK_ALL_PLATFORM = new ReflectionAccessFilter() {
@Override public FilterResult check(Class<?> rawClass) {
return ReflectionAccessFilterHelper.isAnyPlatformType(rawClass)
? FilterResult.BLOCK_ALL
: FilterResult.INDECISIVE;
}
};
/**
* Checks if reflection access should be allowed for a class.
*
* @param rawClass
* Class to check
* @return
* Result indicating whether reflection access is allowed
*/
FilterResult check(Class<?> rawClass);
}

View File

@ -16,6 +16,12 @@
package com.google.gson.internal;
import com.google.gson.InstanceCreator;
import com.google.gson.JsonIOException;
import com.google.gson.ReflectionAccessFilter;
import com.google.gson.ReflectionAccessFilter.FilterResult;
import com.google.gson.internal.reflect.ReflectionHelper;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
@ -28,6 +34,7 @@ import java.util.EnumMap;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
@ -40,21 +47,18 @@ import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
import com.google.gson.InstanceCreator;
import com.google.gson.JsonIOException;
import com.google.gson.internal.reflect.ReflectionHelper;
import com.google.gson.reflect.TypeToken;
/**
* Returns a function that can construct an instance of a requested type.
*/
public final class ConstructorConstructor {
private final Map<Type, InstanceCreator<?>> instanceCreators;
private final boolean useJdkUnsafe;
private final List<ReflectionAccessFilter> reflectionFilters;
public ConstructorConstructor(Map<Type, InstanceCreator<?>> instanceCreators, boolean useJdkUnsafe) {
public ConstructorConstructor(Map<Type, InstanceCreator<?>> instanceCreators, boolean useJdkUnsafe, List<ReflectionAccessFilter> reflectionFilters) {
this.instanceCreators = instanceCreators;
this.useJdkUnsafe = useJdkUnsafe;
this.reflectionFilters = reflectionFilters;
}
public <T> ObjectConstructor<T> get(TypeToken<T> typeToken) {
@ -85,7 +89,16 @@ public final class ConstructorConstructor {
};
}
ObjectConstructor<T> defaultConstructor = newDefaultConstructor(rawType);
// First consider special constructors before checking for no-args constructors
// below to avoid matching internal no-args constructors which might be added in
// future JDK versions
ObjectConstructor<T> specialConstructor = newSpecialCollectionConstructor(type, rawType);
if (specialConstructor != null) {
return specialConstructor;
}
FilterResult filterResult = ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, rawType);
ObjectConstructor<T> defaultConstructor = newDefaultConstructor(rawType, filterResult);
if (defaultConstructor != null) {
return defaultConstructor;
}
@ -95,11 +108,81 @@ public final class ConstructorConstructor {
return defaultImplementation;
}
// finally try unsafe
return newUnsafeAllocator(rawType);
// Check whether type is instantiable; otherwise ReflectionAccessFilter recommendation
// of adjusting filter suggested below is irrelevant since it would not solve the problem
final String exceptionMessage = UnsafeAllocator.checkInstantiable(rawType);
if (exceptionMessage != null) {
return new ObjectConstructor<T>() {
@Override public T construct() {
throw new JsonIOException(exceptionMessage);
}
};
}
// Consider usage of Unsafe as reflection, so don't use if BLOCK_ALL
// Additionally, since it is not calling any constructor at all, don't use if BLOCK_INACCESSIBLE
if (filterResult == FilterResult.ALLOW) {
// finally try unsafe
return newUnsafeAllocator(rawType);
} else {
final String message = "Unable to create instance of " + rawType + "; ReflectionAccessFilter "
+ "does not permit using reflection or Unsafe. Register an InstanceCreator or a TypeAdapter "
+ "for this type or adjust the access filter to allow using reflection.";
return new ObjectConstructor<T>() {
@Override public T construct() {
throw new JsonIOException(message);
}
};
}
}
private <T> ObjectConstructor<T> newDefaultConstructor(Class<? super T> rawType) {
/**
* Creates constructors for special JDK collection types which do not have a public no-args constructor.
*/
private static <T> ObjectConstructor<T> newSpecialCollectionConstructor(final Type type, Class<? super T> rawType) {
if (EnumSet.class.isAssignableFrom(rawType)) {
return new ObjectConstructor<T>() {
@Override public T construct() {
if (type instanceof ParameterizedType) {
Type elementType = ((ParameterizedType) type).getActualTypeArguments()[0];
if (elementType instanceof Class) {
@SuppressWarnings({"unchecked", "rawtypes"})
T set = (T) EnumSet.noneOf((Class)elementType);
return set;
} else {
throw new JsonIOException("Invalid EnumSet type: " + type.toString());
}
} else {
throw new JsonIOException("Invalid EnumSet type: " + type.toString());
}
}
};
}
// Only support creation of EnumMap, but not of custom subtypes; for them type parameters
// and constructor parameter might have completely different meaning
else if (rawType == EnumMap.class) {
return new ObjectConstructor<T>() {
@Override public T construct() {
if (type instanceof ParameterizedType) {
Type elementType = ((ParameterizedType) type).getActualTypeArguments()[0];
if (elementType instanceof Class) {
@SuppressWarnings({"unchecked", "rawtypes"})
T map = (T) new EnumMap((Class) elementType);
return map;
} else {
throw new JsonIOException("Invalid EnumMap type: " + type.toString());
}
} else {
throw new JsonIOException("Invalid EnumMap type: " + type.toString());
}
}
};
}
return null;
}
private static <T> ObjectConstructor<T> newDefaultConstructor(Class<? super T> rawType, FilterResult filterResult) {
// Cannot invoke constructor of abstract class
if (Modifier.isAbstract(rawType.getModifiers())) {
return null;
@ -112,27 +195,47 @@ public final class ConstructorConstructor {
return null;
}
final String exceptionMessage = ReflectionHelper.tryMakeAccessible(constructor);
if (exceptionMessage != null) {
/*
* Create ObjectConstructor which throws exception.
* This keeps backward compatibility (compared to returning `null` which
* would then choose another way of creating object).
* And it supports types which are only serialized but not deserialized
* (compared to directly throwing exception here), e.g. when runtime type
* of object is inaccessible, but compile-time type is accessible.
*/
boolean canAccess = filterResult == FilterResult.ALLOW || (ReflectionAccessFilterHelper.canAccess(constructor, null)
// Be a bit more lenient here for BLOCK_ALL; if constructor is accessible and public then allow calling it
&& (filterResult != FilterResult.BLOCK_ALL || Modifier.isPublic(constructor.getModifiers())));
if (!canAccess) {
final String message = "Unable to invoke no-args constructor of " + rawType + "; "
+ "constructor is not accessible and ReflectionAccessFilter does not permit making "
+ "it accessible. Register an InstanceCreator or a TypeAdapter for this type, change "
+ "the visibility of the constructor or adjust the access filter.";
return new ObjectConstructor<T>() {
@Override
public T construct() {
// New exception is created every time to avoid keeping reference
// to exception with potentially long stack trace, causing a
// memory leak
throw new JsonIOException(exceptionMessage);
@Override public T construct() {
throw new JsonIOException(message);
}
};
}
// Only try to make accessible if allowed; in all other cases checks above should
// have verified that constructor is accessible
if (filterResult == FilterResult.ALLOW) {
final String exceptionMessage = ReflectionHelper.tryMakeAccessible(constructor);
if (exceptionMessage != null) {
/*
* Create ObjectConstructor which throws exception.
* This keeps backward compatibility (compared to returning `null` which
* would then choose another way of creating object).
* And it supports types which are only serialized but not deserialized
* (compared to directly throwing exception here), e.g. when runtime type
* of object is inaccessible, but compile-time type is accessible.
*/
return new ObjectConstructor<T>() {
@Override
public T construct() {
// New exception is created every time to avoid keeping reference
// to exception with potentially long stack trace, causing a
// memory leak
throw new JsonIOException(exceptionMessage);
}
};
}
}
return new ObjectConstructor<T>() {
@Override public T construct() {
try {
@ -148,7 +251,7 @@ public final class ConstructorConstructor {
throw new RuntimeException("Failed to invoke " + constructor + " with no args",
e.getTargetException());
} catch (IllegalAccessException e) {
throw new AssertionError(e);
throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
}
}
};
@ -159,8 +262,17 @@ public final class ConstructorConstructor {
* subtypes.
*/
@SuppressWarnings("unchecked") // use runtime checks to guarantee that 'T' is what it is
private <T> ObjectConstructor<T> newDefaultImplementationConstructor(
private static <T> ObjectConstructor<T> newDefaultImplementationConstructor(
final Type type, Class<? super T> rawType) {
/*
* IMPORTANT: Must only create instances for classes with public no-args constructor.
* For classes with special constructors / factory methods (e.g. EnumSet)
* `newSpecialCollectionConstructor` defined above must be used, to avoid no-args
* constructor check (which is called before this method) detecting internal no-args
* constructors which might be added in a future JDK version
*/
if (Collection.class.isAssignableFrom(rawType)) {
if (SortedSet.class.isAssignableFrom(rawType)) {
return new ObjectConstructor<T>() {
@ -168,22 +280,6 @@ public final class ConstructorConstructor {
return (T) new TreeSet<Object>();
}
};
} else if (EnumSet.class.isAssignableFrom(rawType)) {
return new ObjectConstructor<T>() {
@SuppressWarnings("rawtypes")
@Override public T construct() {
if (type instanceof ParameterizedType) {
Type elementType = ((ParameterizedType) type).getActualTypeArguments()[0];
if (elementType instanceof Class) {
return (T) EnumSet.noneOf((Class)elementType);
} else {
throw new JsonIOException("Invalid EnumSet type: " + type.toString());
}
} else {
throw new JsonIOException("Invalid EnumSet type: " + type.toString());
}
}
};
} else if (Set.class.isAssignableFrom(rawType)) {
return new ObjectConstructor<T>() {
@Override public T construct() {
@ -206,26 +302,7 @@ public final class ConstructorConstructor {
}
if (Map.class.isAssignableFrom(rawType)) {
// Only support creation of EnumMap, but not of custom subtypes; for them type parameters
// and constructor parameter might have completely different meaning
if (rawType == EnumMap.class) {
return new ObjectConstructor<T>() {
@Override public T construct() {
if (type instanceof ParameterizedType) {
Type elementType = ((ParameterizedType) type).getActualTypeArguments()[0];
if (elementType instanceof Class) {
@SuppressWarnings("rawtypes")
T map = (T) new EnumMap((Class) elementType);
return map;
} else {
throw new JsonIOException("Invalid EnumMap type: " + type.toString());
}
} else {
throw new JsonIOException("Invalid EnumMap type: " + type.toString());
}
}
};
} else if (ConcurrentNavigableMap.class.isAssignableFrom(rawType)) {
if (ConcurrentNavigableMap.class.isAssignableFrom(rawType)) {
return new ObjectConstructor<T>() {
@Override public T construct() {
return (T) new ConcurrentSkipListMap<Object, Object>();

View File

@ -0,0 +1,101 @@
package com.google.gson.internal;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Method;
import java.util.List;
import com.google.gson.ReflectionAccessFilter;
import com.google.gson.ReflectionAccessFilter.FilterResult;
/**
* Internal helper class for {@link ReflectionAccessFilter}.
*/
public class ReflectionAccessFilterHelper {
private ReflectionAccessFilterHelper() { }
// Platform type detection is based on Moshi's Util.isPlatformType(Class)
// See https://github.com/square/moshi/blob/3c108919ee1cce88a433ffda04eeeddc0341eae7/moshi/src/main/java/com/squareup/moshi/internal/Util.java#L141
public static boolean isJavaType(Class<?> c) {
return isJavaType(c.getName());
}
private static boolean isJavaType(String className) {
return className.startsWith("java.") || className.startsWith("javax.");
}
public static boolean isAndroidType(Class<?> c) {
return isAndroidType(c.getName());
}
private static boolean isAndroidType(String className) {
return className.startsWith("android.")
|| className.startsWith("androidx.")
|| isJavaType(className);
}
public static boolean isAnyPlatformType(Class<?> c) {
String className = c.getName();
return isAndroidType(className) // Covers Android and Java
|| className.startsWith("kotlin.")
|| className.startsWith("kotlinx.")
|| className.startsWith("scala.");
}
/**
* Gets the result of applying all filters until the first one returns a result
* other than {@link FilterResult#INDECISIVE}, or {@link FilterResult#ALLOW} if
* the list of filters is empty or all returned {@code INDECISIVE}.
*/
public static FilterResult getFilterResult(List<ReflectionAccessFilter> reflectionFilters, Class<?> c) {
for (ReflectionAccessFilter filter : reflectionFilters) {
FilterResult result = filter.check(c);
if (result != FilterResult.INDECISIVE) {
return result;
}
}
return FilterResult.ALLOW;
}
/**
* See {@link AccessibleObject#canAccess(Object)} (Java >= 9)
*/
public static boolean canAccess(AccessibleObject accessibleObject, Object object) {
return AccessChecker.INSTANCE.canAccess(accessibleObject, object);
}
private static abstract class AccessChecker {
public static final AccessChecker INSTANCE;
static {
AccessChecker accessChecker = null;
// TODO: Ideally should use Multi-Release JAR for this version specific code
if (JavaVersion.isJava9OrLater()) {
try {
final Method canAccessMethod = AccessibleObject.class.getDeclaredMethod("canAccess", Object.class);
accessChecker = new AccessChecker() {
@Override public boolean canAccess(AccessibleObject accessibleObject, Object object) {
try {
return (Boolean) canAccessMethod.invoke(accessibleObject, object);
} catch (Exception e) {
throw new RuntimeException("Failed invoking canAccess", e);
}
}
};
} catch (NoSuchMethodException ignored) {
}
}
if (accessChecker == null) {
accessChecker = new AccessChecker() {
@Override public boolean canAccess(AccessibleObject accessibleObject, Object object) {
// Cannot determine whether object can be accessed, so assume it can be accessed
return true;
}
};
}
INSTANCE = accessChecker;
}
public abstract boolean canAccess(AccessibleObject accessibleObject, Object object);
}
}

View File

@ -31,6 +31,37 @@ import java.lang.reflect.Modifier;
public abstract class UnsafeAllocator {
public abstract <T> T newInstance(Class<T> c) throws Exception;
/**
* Check if the class can be instantiated by Unsafe allocator. If the instance has interface or abstract modifiers
* return an exception message.
* @param c instance of the class to be checked
* @return if instantiable {@code null}, else a non-{@code null} exception message
*/
static String checkInstantiable(Class<?> c) {
int modifiers = c.getModifiers();
if (Modifier.isInterface(modifiers)) {
return "Interfaces can't be instantiated! Register an InstanceCreator "
+ "or a TypeAdapter for this type. Interface name: " + c.getName();
}
if (Modifier.isAbstract(modifiers)) {
return "Abstract classes can't be instantiated! Register an InstanceCreator "
+ "or a TypeAdapter for this type. Class name: " + c.getName();
}
return null;
}
/**
* Asserts that the class is instantiable. This check should have already occurred
* in {@link ConstructorConstructor}; this check here acts as safeguard since trying
* to use Unsafe for non-instantiable classes might crash the JVM on some devices.
*/
private static void assertInstantiable(Class<?> c) {
String exceptionMessage = checkInstantiable(c);
if (exceptionMessage != null) {
throw new AssertionError("UnsafeAllocator is used for non-instantiable type: " + exceptionMessage);
}
}
public static UnsafeAllocator create() {
// try JVM
// public class Unsafe {
@ -106,19 +137,4 @@ public abstract class UnsafeAllocator {
}
};
}
/**
* Check if the class can be instantiated by unsafe allocator. If the instance has interface or abstract modifiers
* throw an {@link java.lang.UnsupportedOperationException}
* @param c instance of the class to be checked
*/
static void assertInstantiable(Class<?> c) {
int modifiers = c.getModifiers();
if (Modifier.isInterface(modifiers)) {
throw new UnsupportedOperationException("Interface can't be instantiated! Interface name: " + c.getName());
}
if (Modifier.isAbstract(modifiers)) {
throw new UnsupportedOperationException("Abstract class can't be instantiated! Class name: " + c.getName());
}
}
}

View File

@ -18,7 +18,10 @@ package com.google.gson.internal.bind;
import com.google.gson.FieldNamingStrategy;
import com.google.gson.Gson;
import com.google.gson.JsonIOException;
import com.google.gson.JsonSyntaxException;
import com.google.gson.ReflectionAccessFilter;
import com.google.gson.ReflectionAccessFilter.FilterResult;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.annotations.JsonAdapter;
@ -28,6 +31,7 @@ import com.google.gson.internal.ConstructorConstructor;
import com.google.gson.internal.Excluder;
import com.google.gson.internal.ObjectConstructor;
import com.google.gson.internal.Primitives;
import com.google.gson.internal.ReflectionAccessFilterHelper;
import com.google.gson.internal.reflect.ReflectionHelper;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
@ -35,6 +39,7 @@ import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
@ -50,14 +55,17 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
private final FieldNamingStrategy fieldNamingPolicy;
private final Excluder excluder;
private final JsonAdapterAnnotationTypeAdapterFactory jsonAdapterFactory;
private final List<ReflectionAccessFilter> reflectionFilters;
public ReflectiveTypeAdapterFactory(ConstructorConstructor constructorConstructor,
FieldNamingStrategy fieldNamingPolicy, Excluder excluder,
JsonAdapterAnnotationTypeAdapterFactory jsonAdapterFactory) {
JsonAdapterAnnotationTypeAdapterFactory jsonAdapterFactory,
List<ReflectionAccessFilter> reflectionFilters) {
this.constructorConstructor = constructorConstructor;
this.fieldNamingPolicy = fieldNamingPolicy;
this.excluder = excluder;
this.jsonAdapterFactory = jsonAdapterFactory;
this.reflectionFilters = reflectionFilters;
}
public boolean excludeField(Field f, boolean serialize) {
@ -97,13 +105,30 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
return null; // it's a primitive!
}
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.");
}
boolean blockInaccessible = filterResult == FilterResult.BLOCK_INACCESSIBLE;
ObjectConstructor<T> constructor = constructorConstructor.get(type);
return new Adapter<T>(constructor, getBoundFields(gson, type, raw));
return new Adapter<T>(constructor, getBoundFields(gson, type, raw, blockInaccessible));
}
private static void checkAccessible(Object object, Field field) {
if (!ReflectionAccessFilterHelper.canAccess(field, Modifier.isStatic(field.getModifiers()) ? null : object)) {
throw new JsonIOException("Field '" + field.getDeclaringClass().getName() + "#"
+ field.getName() + "' is not accessible and ReflectionAccessFilter does not "
+ "permit making it accessible. Register a TypeAdapter for the declaring type "
+ "or adjust the access filter.");
}
}
private ReflectiveTypeAdapterFactory.BoundField createBoundField(
final Gson context, final Field field, final String name,
final TypeToken<?> fieldType, boolean serialize, boolean deserialize) {
final TypeToken<?> fieldType, boolean serialize, boolean deserialize,
final boolean blockInaccessible) {
final boolean isPrimitive = Primitives.isPrimitive(fieldType.getRawType());
// special casing primitives here saves ~5% on Android...
JsonAdapter annotation = field.getAnnotation(JsonAdapter.class);
@ -120,7 +145,17 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
@SuppressWarnings({"unchecked", "rawtypes"}) // the type adapter and field type always agree
@Override void write(JsonWriter writer, Object value)
throws IOException, IllegalAccessException {
if (!serialized) return;
if (blockInaccessible) {
checkAccessible(value, field);
}
Object fieldValue = field.get(value);
if (fieldValue == value) {
// avoid direct recursion
return;
}
writer.name(name);
TypeAdapter t = jsonAdapterPresent ? typeAdapter
: new TypeAdapterRuntimeTypeWrapper(context, typeAdapter, fieldType.getType());
t.write(writer, fieldValue);
@ -129,33 +164,48 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
throws IOException, IllegalAccessException {
Object fieldValue = typeAdapter.read(reader);
if (fieldValue != null || !isPrimitive) {
if (blockInaccessible) {
checkAccessible(value, field);
}
field.set(value, fieldValue);
}
}
@Override public boolean writeField(Object value) throws IOException, IllegalAccessException {
if (!serialized) return false;
Object fieldValue = field.get(value);
return fieldValue != value; // avoid recursion for example for Throwable.cause
}
};
}
private Map<String, BoundField> getBoundFields(Gson context, TypeToken<?> type, Class<?> raw) {
private Map<String, BoundField> getBoundFields(Gson context, TypeToken<?> type, Class<?> raw, boolean blockInaccessible) {
Map<String, BoundField> result = new LinkedHashMap<String, BoundField>();
if (raw.isInterface()) {
return result;
}
Type declaredType = type.getType();
Class<?> originalRaw = raw;
while (raw != Object.class) {
Field[] fields = raw.getDeclaredFields();
// For inherited fields, check if access to their declaring class is allowed
if (raw != originalRaw && fields.length > 0) {
FilterResult filterResult = ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, raw);
if (filterResult == FilterResult.BLOCK_ALL) {
throw new JsonIOException("ReflectionAccessFilter does not permit using reflection for "
+ raw + " (supertype of " + originalRaw + "). Register a TypeAdapter for this type "
+ "or adjust the access filter.");
}
blockInaccessible = filterResult == FilterResult.BLOCK_INACCESSIBLE;
}
for (Field field : fields) {
boolean serialize = excludeField(field, true);
boolean deserialize = excludeField(field, false);
if (!serialize && !deserialize) {
continue;
}
ReflectionHelper.makeAccessible(field);
// If blockInaccessible, skip and perform access check later
if (!blockInaccessible) {
ReflectionHelper.makeAccessible(field);
}
Type fieldType = $Gson$Types.resolve(type.getType(), raw, field.getGenericType());
List<String> fieldNames = getFieldNames(field);
BoundField previous = null;
@ -163,7 +213,7 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
String name = fieldNames.get(i);
if (i != 0) serialize = false; // only serialize the default name
BoundField boundField = createBoundField(context, field, name,
TypeToken.get(fieldType), serialize, deserialize);
TypeToken.get(fieldType), serialize, deserialize, blockInaccessible);
BoundField replaced = result.put(name, boundField);
if (previous == null) previous = replaced;
}
@ -188,7 +238,6 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
this.serialized = serialized;
this.deserialized = deserialized;
}
abstract boolean writeField(Object value) throws IOException, IllegalAccessException;
abstract void write(JsonWriter writer, Object value) throws IOException, IllegalAccessException;
abstract void read(JsonReader reader, Object value) throws IOException, IllegalAccessException;
}
@ -224,7 +273,7 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
} catch (IllegalStateException e) {
throw new JsonSyntaxException(e);
} catch (IllegalAccessException e) {
throw new AssertionError(e);
throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
}
in.endObject();
return instance;
@ -239,13 +288,10 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
out.beginObject();
try {
for (BoundField boundField : boundFields.values()) {
if (boundField.writeField(value)) {
out.name(boundField.name);
boundField.write(out, value);
}
boundField.write(out, value);
}
} catch (IllegalAccessException e) {
throw new AssertionError(e);
throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
}
out.endObject();
}

View File

@ -1,6 +1,7 @@
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;
@ -63,4 +64,11 @@ public class ReflectionHelper {
+ exception.getMessage();
}
}
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);
}
}

View File

@ -27,6 +27,7 @@ import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import junit.framework.TestCase;
@ -56,7 +57,8 @@ public final class GsonTest extends TestCase {
true, true, false, true, LongSerializationPolicy.DEFAULT, null, DateFormat.DEFAULT,
DateFormat.DEFAULT, new ArrayList<TypeAdapterFactory>(),
new ArrayList<TypeAdapterFactory>(), new ArrayList<TypeAdapterFactory>(),
CUSTOM_OBJECT_TO_NUMBER_STRATEGY, CUSTOM_NUMBER_TO_NUMBER_STRATEGY);
CUSTOM_OBJECT_TO_NUMBER_STRATEGY, CUSTOM_NUMBER_TO_NUMBER_STRATEGY,
Collections.<ReflectionAccessFilter>emptyList());
assertEquals(CUSTOM_EXCLUDER, gson.excluder);
assertEquals(CUSTOM_FIELD_NAMING_STRATEGY, gson.fieldNamingStrategy());
@ -70,7 +72,8 @@ public final class GsonTest extends TestCase {
true, true, false, true, LongSerializationPolicy.DEFAULT, null, DateFormat.DEFAULT,
DateFormat.DEFAULT, new ArrayList<TypeAdapterFactory>(),
new ArrayList<TypeAdapterFactory>(), new ArrayList<TypeAdapterFactory>(),
CUSTOM_OBJECT_TO_NUMBER_STRATEGY, CUSTOM_NUMBER_TO_NUMBER_STRATEGY);
CUSTOM_OBJECT_TO_NUMBER_STRATEGY, CUSTOM_NUMBER_TO_NUMBER_STRATEGY,
Collections.<ReflectionAccessFilter>emptyList());
Gson clone = original.newBuilder()
.registerTypeAdapter(Object.class, new TestTypeAdapter())

View File

@ -0,0 +1,426 @@
package com.google.gson.functional;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.InstanceCreator;
import com.google.gson.JsonElement;
import com.google.gson.JsonIOException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.ReflectionAccessFilter;
import com.google.gson.ReflectionAccessFilter.FilterResult;
import com.google.gson.TypeAdapter;
import com.google.gson.internal.ConstructorConstructor;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.awt.Point;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.Type;
import java.util.LinkedList;
import java.util.List;
import org.junit.Test;
public class ReflectionAccessFilterTest {
// Reader has protected `lock` field which cannot be accessed
private static class ClassExtendingJdkClass extends Reader {
@Override
public int read(char[] cbuf, int off, int len) throws IOException {
return 0;
}
@Override
public void close() throws IOException {
}
}
@Test
public void testBlockInaccessibleJava() {
Gson gson = new GsonBuilder()
.addReflectionAccessFilter(ReflectionAccessFilter.BLOCK_INACCESSIBLE_JAVA)
.create();
// Serialization should fail for classes with non-public fields
try {
gson.toJson(new File("a"));
fail("Expected exception; test needs to be run with Java >= 9");
} catch (JsonIOException expected) {
// Note: This test is rather brittle and depends on the JDK implementation
assertEquals(
"Field 'java.io.File#path' is not accessible and ReflectionAccessFilter does not permit "
+ "making it accessible. Register a TypeAdapter for the declaring type or adjust the access filter.",
expected.getMessage()
);
}
// But serialization should succeed for classes with only public fields
String json = gson.toJson(new Point(1, 2));
assertEquals("{\"x\":1,\"y\":2}", json);
}
@Test
public void testBlockInaccessibleJavaExtendingJdkClass() {
Gson gson = new GsonBuilder()
.addReflectionAccessFilter(ReflectionAccessFilter.BLOCK_INACCESSIBLE_JAVA)
.create();
try {
gson.toJson(new ClassExtendingJdkClass());
fail("Expected exception; test needs to be run with Java >= 9");
} catch (JsonIOException expected) {
assertEquals(
"Field 'java.io.Reader#lock' is not accessible and ReflectionAccessFilter does not permit "
+ "making it accessible. Register a TypeAdapter for the declaring type or adjust the access filter.",
expected.getMessage()
);
}
}
@Test
public void testBlockAllJava() {
Gson gson = new GsonBuilder()
.addReflectionAccessFilter(ReflectionAccessFilter.BLOCK_ALL_JAVA)
.create();
// Serialization should fail for any Java class
try {
gson.toJson(Thread.currentThread());
fail();
} catch (JsonIOException expected) {
assertEquals(
"ReflectionAccessFilter does not permit using reflection for class java.lang.Thread. "
+ "Register a TypeAdapter for this type or adjust the access filter.",
expected.getMessage()
);
}
}
@Test
public void testBlockAllJavaExtendingJdkClass() {
Gson gson = new GsonBuilder()
.addReflectionAccessFilter(ReflectionAccessFilter.BLOCK_ALL_JAVA)
.create();
try {
gson.toJson(new ClassExtendingJdkClass());
fail();
} catch (JsonIOException expected) {
assertEquals(
"ReflectionAccessFilter does not permit using reflection for class java.io.Reader "
+ "(supertype of class com.google.gson.functional.ReflectionAccessFilterTest$ClassExtendingJdkClass). "
+ "Register a TypeAdapter for this type or adjust the access filter.",
expected.getMessage()
);
}
}
private static class ClassWithStaticField {
@SuppressWarnings("unused")
private static int i = 1;
}
@Test
public void testBlockInaccessibleStaticField() {
Gson gson = new GsonBuilder()
.addReflectionAccessFilter(new ReflectionAccessFilter() {
@Override public FilterResult check(Class<?> rawClass) {
return FilterResult.BLOCK_INACCESSIBLE;
}
})
// Include static fields
.excludeFieldsWithModifiers(0)
.create();
try {
gson.toJson(new ClassWithStaticField());
fail("Expected exception; test needs to be run with Java >= 9");
} catch (JsonIOException expected) {
assertEquals(
"Field 'com.google.gson.functional.ReflectionAccessFilterTest$ClassWithStaticField#i' "
+ "is not accessible and ReflectionAccessFilter does not permit making it accessible. "
+ "Register a TypeAdapter for the declaring type or adjust the access filter.",
expected.getMessage()
);
}
}
private static class SuperTestClass {
}
private static class SubTestClass extends SuperTestClass {
@SuppressWarnings("unused")
public int i = 1;
}
private static class OtherClass {
@SuppressWarnings("unused")
public int i = 2;
}
@Test
public void testDelegation() {
Gson gson = new GsonBuilder()
.addReflectionAccessFilter(new ReflectionAccessFilter() {
@Override public FilterResult check(Class<?> rawClass) {
// INDECISIVE in last filter should act like ALLOW
return SuperTestClass.class.isAssignableFrom(rawClass) ? FilterResult.BLOCK_ALL : FilterResult.INDECISIVE;
}
})
.addReflectionAccessFilter(new ReflectionAccessFilter() {
@Override public FilterResult check(Class<?> rawClass) {
// INDECISIVE should delegate to previous filter
return rawClass == SubTestClass.class ? FilterResult.ALLOW : FilterResult.INDECISIVE;
}
})
.create();
// Filter disallows SuperTestClass
try {
gson.toJson(new SuperTestClass());
fail();
} catch (JsonIOException expected) {
assertEquals(
"ReflectionAccessFilter does not permit using reflection for class "
+ "com.google.gson.functional.ReflectionAccessFilterTest$SuperTestClass. "
+ "Register a TypeAdapter for this type or adjust the access filter.",
expected.getMessage()
);
}
// But registration order is reversed, so filter for SubTestClass allows reflection
String json = gson.toJson(new SubTestClass());
assertEquals("{\"i\":1}", json);
// And unrelated class should not be affected
json = gson.toJson(new OtherClass());
assertEquals("{\"i\":2}", json);
}
private static class ClassWithPrivateField {
@SuppressWarnings("unused")
private int i = 1;
}
private static class ExtendingClassWithPrivateField extends ClassWithPrivateField {
}
@Test
public void testAllowForSupertype() {
Gson gson = new GsonBuilder()
.addReflectionAccessFilter(new ReflectionAccessFilter() {
@Override public FilterResult check(Class<?> rawClass) {
return FilterResult.BLOCK_INACCESSIBLE;
}
})
.create();
// First make sure test is implemented correctly and access is blocked
try {
gson.toJson(new ExtendingClassWithPrivateField());
fail("Expected exception; test needs to be run with Java >= 9");
} catch (JsonIOException expected) {
assertEquals(
"Field 'com.google.gson.functional.ReflectionAccessFilterTest$ClassWithPrivateField#i' "
+ "is not accessible and ReflectionAccessFilter does not permit making it accessible. "
+ "Register a TypeAdapter for the declaring type or adjust the access filter.",
expected.getMessage()
);
}
gson = gson.newBuilder()
// Allow reflective access for supertype
.addReflectionAccessFilter(new ReflectionAccessFilter() {
@Override public FilterResult check(Class<?> rawClass) {
return rawClass == ClassWithPrivateField.class ? FilterResult.ALLOW : FilterResult.INDECISIVE;
}
})
.create();
// Inherited (inaccessible) private field should have been made accessible
String json = gson.toJson(new ExtendingClassWithPrivateField());
assertEquals("{\"i\":1}", json);
}
private static class ClassWithPrivateNoArgsConstructor {
private ClassWithPrivateNoArgsConstructor() {
}
}
@Test
public void testInaccessibleNoArgsConstructor() {
Gson gson = new GsonBuilder()
.addReflectionAccessFilter(new ReflectionAccessFilter() {
@Override public FilterResult check(Class<?> rawClass) {
return FilterResult.BLOCK_INACCESSIBLE;
}
})
.create();
try {
gson.fromJson("{}", ClassWithPrivateNoArgsConstructor.class);
fail("Expected exception; test needs to be run with Java >= 9");
} catch (JsonIOException expected) {
assertEquals(
"Unable to invoke no-args constructor of class com.google.gson.functional.ReflectionAccessFilterTest$ClassWithPrivateNoArgsConstructor; "
+ "constructor is not accessible and ReflectionAccessFilter does not permit making it accessible. Register an "
+ "InstanceCreator or a TypeAdapter for this type, change the visibility of the constructor or adjust the access filter.",
expected.getMessage()
);
}
}
private static class ClassWithoutNoArgsConstructor {
public String s;
public ClassWithoutNoArgsConstructor(String s) {
this.s = s;
}
}
@Test
public void testClassWithoutNoArgsConstructor() {
GsonBuilder gsonBuilder = new GsonBuilder()
.addReflectionAccessFilter(new ReflectionAccessFilter() {
@Override public FilterResult check(Class<?> rawClass) {
// Even BLOCK_INACCESSIBLE should prevent usage of Unsafe for object creation
return FilterResult.BLOCK_INACCESSIBLE;
}
});
Gson gson = gsonBuilder.create();
try {
gson.fromJson("{}", ClassWithoutNoArgsConstructor.class);
fail();
} catch (JsonIOException expected) {
assertEquals(
"Unable to create instance of class com.google.gson.functional.ReflectionAccessFilterTest$ClassWithoutNoArgsConstructor; "
+ "ReflectionAccessFilter does not permit using reflection or Unsafe. Register an InstanceCreator "
+ "or a TypeAdapter for this type or adjust the access filter to allow using reflection.",
expected.getMessage()
);
}
// But should not fail when custom TypeAdapter is specified
gson = gson.newBuilder()
.registerTypeAdapter(ClassWithoutNoArgsConstructor.class, new TypeAdapter<ClassWithoutNoArgsConstructor>() {
@Override public ClassWithoutNoArgsConstructor read(JsonReader in) throws IOException {
in.skipValue();
return new ClassWithoutNoArgsConstructor("TypeAdapter");
}
@Override public void write(JsonWriter out, ClassWithoutNoArgsConstructor value) throws IOException {
throw new AssertionError("Not needed for test");
};
})
.create();
ClassWithoutNoArgsConstructor deserialized = gson.fromJson("{}", ClassWithoutNoArgsConstructor.class);
assertEquals("TypeAdapter", deserialized.s);
// But should not fail when custom InstanceCreator is specified
gson = gsonBuilder
.registerTypeAdapter(ClassWithoutNoArgsConstructor.class, new InstanceCreator<ClassWithoutNoArgsConstructor>() {
@Override public ClassWithoutNoArgsConstructor createInstance(Type type) {
return new ClassWithoutNoArgsConstructor("InstanceCreator");
}
})
.create();
deserialized = gson.fromJson("{}", ClassWithoutNoArgsConstructor.class);
assertEquals("InstanceCreator", deserialized.s);
}
/**
* When using {@link FilterResult#BLOCK_ALL}, registering only a {@link JsonSerializer}
* but not performing any deserialization should not throw any exception.
*/
@Test
public void testBlockAllPartial() {
Gson gson = new GsonBuilder()
.addReflectionAccessFilter(new ReflectionAccessFilter() {
@Override public FilterResult check(Class<?> rawClass) {
return FilterResult.BLOCK_ALL;
}
})
.registerTypeAdapter(OtherClass.class, new JsonSerializer<OtherClass>() {
@Override public JsonElement serialize(OtherClass src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(123);
}
})
.create();
String json = gson.toJson(new OtherClass());
assertEquals("123", json);
// But deserialization should fail
try {
gson.fromJson("{}", OtherClass.class);
fail();
} catch (JsonIOException expected) {
assertEquals(
"ReflectionAccessFilter does not permit using reflection for class com.google.gson.functional.ReflectionAccessFilterTest$OtherClass. "
+ "Register a TypeAdapter for this type or adjust the access filter.",
expected.getMessage()
);
}
}
/**
* Should not fail when deserializing collection interface
* (Even though this goes through {@link ConstructorConstructor} as well)
*/
@Test
public void testBlockAllCollectionInterface() {
Gson gson = new GsonBuilder()
.addReflectionAccessFilter(new ReflectionAccessFilter() {
@Override public FilterResult check(Class<?> rawClass) {
return FilterResult.BLOCK_ALL;
}
})
.create();
List<?> deserialized = gson.fromJson("[1.0]", List.class);
assertEquals(1.0, deserialized.get(0));
}
/**
* Should not fail when deserializing specific collection implementation
* (Even though this goes through {@link ConstructorConstructor} as well)
*/
@Test
public void testBlockAllCollectionImplementation() {
Gson gson = new GsonBuilder()
.addReflectionAccessFilter(new ReflectionAccessFilter() {
@Override public FilterResult check(Class<?> rawClass) {
return FilterResult.BLOCK_ALL;
}
})
.create();
List<?> deserialized = gson.fromJson("[1.0]", LinkedList.class);
assertEquals(1.0, deserialized.get(0));
}
/**
* When trying to deserialize interface an exception for that should
* be thrown, even if {@link FilterResult#BLOCK_INACCESSIBLE} is used
*/
@Test
public void testBlockInaccessibleInterface() {
Gson gson = new GsonBuilder()
.addReflectionAccessFilter(new ReflectionAccessFilter() {
@Override public FilterResult check(Class<?> rawClass) {
return FilterResult.BLOCK_INACCESSIBLE;
}
})
.create();
try {
gson.fromJson("{}", Runnable.class);
fail();
} catch (JsonIOException expected) {
assertEquals(
"Interfaces can't be instantiated! Register an InstanceCreator or a TypeAdapter for "
+ "this type. Interface name: java.lang.Runnable",
expected.getMessage()
);
}
}
}

View File

@ -3,17 +3,18 @@ package com.google.gson.internal;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import com.google.gson.InstanceCreator;
import com.google.gson.ReflectionAccessFilter;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.Map;
import org.junit.Test;
import com.google.gson.InstanceCreator;
import com.google.gson.reflect.TypeToken;
public class ConstructorConstructorTest {
private static final Map<Type, InstanceCreator<?>> NO_INSTANCE_CREATORS = Collections.emptyMap();
private ConstructorConstructor constructorConstructor = new ConstructorConstructor(
Collections.<Type, InstanceCreator<?>>emptyMap(), true,
Collections.<ReflectionAccessFilter>emptyList()
);
private abstract static class AbstractClass {
@SuppressWarnings("unused")
@ -27,15 +28,14 @@ public class ConstructorConstructorTest {
*/
@Test
public void testGet_AbstractClassNoArgConstructor() {
ConstructorConstructor constructorFactory = new ConstructorConstructor(NO_INSTANCE_CREATORS, true);
ObjectConstructor<AbstractClass> constructor = constructorFactory.get(TypeToken.get(AbstractClass.class));
ObjectConstructor<AbstractClass> constructor = constructorConstructor.get(TypeToken.get(AbstractClass.class));
try {
constructor.construct();
fail("Expected exception");
} catch (RuntimeException exception) {
assertEquals(
"Unable to create instance of class com.google.gson.internal.ConstructorConstructorTest$AbstractClass. "
+ "Registering an InstanceCreator or a TypeAdapter for this type, or adding a no-args constructor may fix this problem.",
"Abstract classes can't be instantiated! Register an InstanceCreator or a TypeAdapter for this "
+ "type. Class name: com.google.gson.internal.ConstructorConstructorTest$AbstractClass",
exception.getMessage()
);
}
@ -43,15 +43,14 @@ public class ConstructorConstructorTest {
@Test
public void testGet_Interface() {
ConstructorConstructor constructorFactory = new ConstructorConstructor(NO_INSTANCE_CREATORS, true);
ObjectConstructor<Interface> constructor = constructorFactory.get(TypeToken.get(Interface.class));
ObjectConstructor<Interface> constructor = constructorConstructor.get(TypeToken.get(Interface.class));
try {
constructor.construct();
fail("Expected exception");
} catch (RuntimeException exception) {
assertEquals(
"Unable to create instance of interface com.google.gson.internal.ConstructorConstructorTest$Interface. "
+ "Registering an InstanceCreator or a TypeAdapter for this type, or adding a no-args constructor may fix this problem.",
"Interfaces can't be instantiated! Register an InstanceCreator or a TypeAdapter for "
+ "this type. Interface name: com.google.gson.internal.ConstructorConstructorTest$Interface",
exception.getMessage()
);
}

View File

@ -33,42 +33,39 @@ public final class UnsafeAllocatorInstantiationTest extends TestCase {
}
/**
* Ensure that the {@link java.lang.UnsupportedOperationException} is thrown when trying
* Ensure that an {@link AssertionError} is thrown when trying
* to instantiate an interface
*/
public void testInterfaceInstantiation() {
public void testInterfaceInstantiation() throws Exception {
UnsafeAllocator unsafeAllocator = UnsafeAllocator.create();
try {
unsafeAllocator.newInstance(Interface.class);
fail();
} catch (Exception e) {
assertEquals(e.getClass(), UnsupportedOperationException.class);
} catch (AssertionError e) {
assertTrue(e.getMessage().startsWith("UnsafeAllocator is used for non-instantiable type"));
}
}
/**
* Ensure that the {@link java.lang.UnsupportedOperationException} is thrown when trying
* Ensure that an {@link AssertionError} is thrown when trying
* to instantiate an abstract class
*/
public void testAbstractClassInstantiation() {
public void testAbstractClassInstantiation() throws Exception {
UnsafeAllocator unsafeAllocator = UnsafeAllocator.create();
try {
unsafeAllocator.newInstance(AbstractClass.class);
fail();
} catch (Exception e) {
assertEquals(e.getClass(), UnsupportedOperationException.class);
} catch (AssertionError e) {
assertTrue(e.getMessage().startsWith("UnsafeAllocator is used for non-instantiable type"));
}
}
/**
* Ensure that no exception is thrown when trying to instantiate a concrete class
*/
public void testConcreteClassInstantiation() {
public void testConcreteClassInstantiation() throws Exception {
UnsafeAllocator unsafeAllocator = UnsafeAllocator.create();
try {
unsafeAllocator.newInstance(ConcreteClass.class);
} catch (Exception e) {
fail();
}
ConcreteClass instance = unsafeAllocator.newInstance(ConcreteClass.class);
assertNotNull(instance);
}
}