Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adjust ProGuard default rules and shrinking tests #2420

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,8 @@ Note: For newer Gson versions these rules might be applied automatically; make s

**Symptom:** A `JsonIOException` with the message 'Abstract classes can't be instantiated!' is thrown; the class mentioned in the exception message is not actually `abstract` in your source code, and you are using the code shrinking tool R8 (Android app builds normally have this configured by default).

Note: If the class which you are trying to deserialize is actually abstract, then this exception is probably unrelated to R8 and you will have to implement a custom [`InstanceCreator`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/InstanceCreator.html) or [`TypeAdapter`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/TypeAdapter.html) which creates an instance of a non-abstract subclass of the class.

**Reason:** The code shrinking tool R8 performs optimizations where it removes the no-args constructor from a class and makes the class `abstract`. Due to this Gson cannot create an instance of the class.

**Solution:** Make sure the class has a no-args constructor, then adjust your R8 configuration file to keep the constructor of the class. For example:
Expand All @@ -324,6 +326,10 @@ Note: For newer Gson versions these rules might be applied automatically; make s
}
```

You can also use `<init>(...);` to keep all constructors of that class, but then you might actually rely on `sun.misc.Unsafe` on both JDK and Android to create classes without no-args constructor, see [`GsonBuilder.disableJdkUnsafe()`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/GsonBuilder.html#disableJdkUnsafe()) for more information.

For Android you can add this rule to the `proguard-rules.pro` file, see also the [Android documentation](https://developer.android.com/build/shrink-code#keep-code). In case the class name in the exception message is obfuscated, see the Android documentation about [retracing](https://developer.android.com/build/shrink-code#retracing).

Note: If the class which you are trying to deserialize is actually abstract, then this exception is probably unrelated to R8 and you will have to implement a custom [`InstanceCreator`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/InstanceCreator.html) or [`TypeAdapter`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/TypeAdapter.html) which creates an instance of a non-abstract subclass of the class.
For Android you can alternatively use the [`@Keep` annotation](https://developer.android.com/studio/write/annotations#keep) on the class or constructor you want to keep. That might be easier than having to maintain a custom R8 configuration.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not only for Android. You can also use R8 (or ProGuard) to shrink class files.

Also please change @Keep to @androidx.annotation.Keep.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this paragraph is worded a bit misleading then. What I meant was that Android developers can use Android's @Keep annotation, which has explicit ProGuard / R8 support by the Android build tooling. Or do ProGuard / R8 in general consider any @Keep annotations, regardless of package name and without having to manually configure this first? Do you have any ideas how to better describe this then?

Because this is also written in the context of Android, I am not sure if it is necessary to specify the fully qualified package name. I also preferred linking to the Android Developers page instead of the Javadoc for that @Keep annotation because it has links to more information about shrinking.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, linking to DAC is a good idea, and also keeping Android is actually fine, as the annotation is part of an Android library.


Note that the latest Gson versions (> 2.10.1) specify a default R8 configuration. If your class is a top-level class or is `static`, has a no-args constructor and its fields are annotated with Gson's [`@SerializedName`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/annotations/SerializedName.html), you might not have to perform any additional R8 configuration.
7 changes: 5 additions & 2 deletions examples/android-proguard-example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ details on how ProGuard can be configured.
The R8 code shrinker uses the same rule format as ProGuard, but there are differences between these two
tools. Have a look at R8's Compatibility FAQ, and especially at the [Gson section](https://r8.googlesource.com/r8/+/refs/heads/main/compatibility-faq.md#gson).

Note that newer Gson versions apply some of the rules shown in `proguard.cfg` automatically by default,
Note that the latest Gson versions (> 2.10.1) apply some of the rules shown in `proguard.cfg` automatically by default,
see the file [`gson/META-INF/proguard/gson.pro`](/gson/src/main/resources/META-INF/proguard/gson.pro) for
the Gson version you are using.
the Gson version you are using. In general if your classes are top-level classes or are `static`, have a no-args constructor and their fields are annotated with Gson's [`@SerializedName`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/annotations/SerializedName.html), you might not have to perform any additional ProGuard or R8 configuration.

An alternative to writing custom keep rules for your classes in the ProGuard configuration can be to use
Android's [`@Keep` annotation](https://developer.android.com/studio/write/annotations#keep).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, please change change @Keep to [@androidx.annotation.Keep].

Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,15 @@ static String checkInstantiable(Class<?> c) {
if (Modifier.isAbstract(modifiers)) {
// R8 performs aggressive optimizations where it removes the default constructor of a class
// and makes the class `abstract`; check for that here explicitly
if (c.getDeclaredConstructors().length == 0) {
return "Abstract classes can't be instantiated! Adjust the R8 configuration or register"
+ " an InstanceCreator or a TypeAdapter for this type. Class name: " + c.getName()
+ "\nSee " + TroubleshootingGuide.createUrl("r8-abstract-class");
}

return "Abstract classes can't be instantiated! Register an InstanceCreator"
+ " or a TypeAdapter for this type. Class name: " + c.getName();
/*
* Note: Ideally should only show this R8-specific message when it is clear that R8 was
* used (e.g. when `c.getDeclaredConstructors().length == 0`), but on Android where this
* issue with R8 occurs most, R8 seems to keep some constructors for some reason while
* still making the class abstract
*/
return "Abstract classes can't be instantiated! Adjust the R8 configuration or register"
+ " an InstanceCreator or a TypeAdapter for this type. Class name: " + c.getName()
+ "\nSee " + TroubleshootingGuide.createUrl("r8-abstract-class");
}
return null;
}
Expand Down
28 changes: 20 additions & 8 deletions gson/src/main/resources/META-INF/proguard/gson.pro
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

# Keep Gson annotations
# Note: Cannot perform finer selection here to only cover Gson annotations, see also https://stackoverflow.com/q/47515093
-keepattributes *Annotation*
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault


### The following rules are needed for R8 in "full mode" which only adheres to `-keepattribtues` if
Expand All @@ -24,18 +24,20 @@
-keep class com.google.gson.reflect.TypeToken { *; }

# Keep any (anonymous) classes extending TypeToken
-keep class * extends com.google.gson.reflect.TypeToken
-keep,allowobfuscation class * extends com.google.gson.reflect.TypeToken

# Keep classes with @JsonAdapter annotation
-keep @com.google.gson.annotations.JsonAdapter class *
-keep,allowobfuscation,allowoptimization @com.google.gson.annotations.JsonAdapter class *

# Keep fields with @SerializedName annotation, but allow obfuscation of their names
-keepclassmembers,allowobfuscation class * {
@com.google.gson.annotations.SerializedName <fields>;
}

# Keep fields with any other Gson annotation
-keepclassmembers class * {
# Also allow obfuscation, assuming that users will additionally use @SerializedName or
# other means to preserve the field names
-keepclassmembers,allowobfuscation class * {
@com.google.gson.annotations.Expose <fields>;
@com.google.gson.annotations.JsonAdapter <fields>;
@com.google.gson.annotations.Since <fields>;
Expand All @@ -44,15 +46,25 @@

# Keep no-args constructor of classes which can be used with @JsonAdapter
# By default their no-args constructor is invoked to create an adapter instance
-keep class * extends com.google.gson.TypeAdapter {
-keepclassmembers class * extends com.google.gson.TypeAdapter {
<init>();
}
-keep class * implements com.google.gson.TypeAdapterFactory {
-keepclassmembers class * implements com.google.gson.TypeAdapterFactory {
<init>();
}
-keep class * implements com.google.gson.JsonSerializer {
-keepclassmembers class * implements com.google.gson.JsonSerializer {
<init>();
}
-keep class * implements com.google.gson.JsonDeserializer {
-keepclassmembers class * implements com.google.gson.JsonDeserializer {
<init>();
}

# If a class is used in some way by the application, and has fields annotated with @SerializedName
# and a no-args constructor, keep those fields and the constructor
# Based on https://issuetracker.google.com/issues/150189783#comment11
# See also https://github.com/google/gson/pull/2420#discussion_r1241813541 for a more detailed explanation
-if class *
-keepclasseswithmembers,allowobfuscation,allowoptimization class <1> {
<init>();
@com.google.gson.annotations.SerializedName <fields>;
}
27 changes: 14 additions & 13 deletions gson/src/test/java/com/google/gson/functional/Java17RecordTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public int i() {

var exception = assertThrows(JsonIOException.class, () -> gson.getAdapter(LocalRecord.class));
assertThat(exception).hasMessageThat()
.isEqualTo("@SerializedName on method '" + LocalRecord.class.getName() + "#i()' is not supported");
.isEqualTo("@SerializedName on method '" + LocalRecord.class.getName() + "#i()' is not supported");
}

@Test
Expand Down Expand Up @@ -154,7 +154,7 @@ record LocalRecord(String s) {
// TODO: Adjust this once Gson throws more specific exception type
catch (RuntimeException e) {
assertThat(e).hasMessageThat()
.isEqualTo("Failed to invoke constructor '" + LocalRecord.class.getName() + "(String)' with args [value]");
.isEqualTo("Failed to invoke constructor '" + LocalRecord.class.getName() + "(String)' with args [value]");
assertThat(e).hasCauseThat().isSameInstanceAs(LocalRecord.thrownException);
}
}
Expand Down Expand Up @@ -227,7 +227,7 @@ public void testPrimitiveJsonNullValue() {
String s = "{'aString': 's', 'aByte': null, 'aShort': 0}";
var e = assertThrows(JsonParseException.class, () -> gson.fromJson(s, RecordWithPrimitives.class));
assertThat(e).hasMessageThat()
.isEqualTo("null is not allowed as value for record component 'aByte' of primitive type; at path $.aByte");
.isEqualTo("null is not allowed as value for record component 'aByte' of primitive type; at path $.aByte");
}

/**
Expand Down Expand Up @@ -384,8 +384,8 @@ record Blocked(int b) {}

var exception = assertThrows(JsonIOException.class, () -> gson.toJson(new Blocked(1)));
assertThat(exception).hasMessageThat()
.isEqualTo("ReflectionAccessFilter does not permit using reflection for class " + Blocked.class.getName() +
". Register a TypeAdapter for this type or adjust the access filter.");
.isEqualTo("ReflectionAccessFilter does not permit using reflection for class " + Blocked.class.getName() +
". Register a TypeAdapter for this type or adjust the access filter.");
}

@Test
Expand All @@ -396,15 +396,15 @@ public void testReflectionFilterBlockInaccessible() {

var exception = assertThrows(JsonIOException.class, () -> gson.toJson(new PrivateRecord(1)));
assertThat(exception).hasMessageThat()
.isEqualTo("Constructor 'com.google.gson.functional.Java17RecordTest$PrivateRecord(int)' is not accessible and"
+ " ReflectionAccessFilter does not permit making it accessible. Register a TypeAdapter for the declaring"
+ " type, adjust the access filter or increase the visibility of the element and its declaring type.");
.isEqualTo("Constructor 'com.google.gson.functional.Java17RecordTest$PrivateRecord(int)' is not accessible and"
+ " ReflectionAccessFilter does not permit making it accessible. Register a TypeAdapter for the declaring"
+ " type, adjust the access filter or increase the visibility of the element and its declaring type.");

exception = assertThrows(JsonIOException.class, () -> gson.fromJson("{}", PrivateRecord.class));
assertThat(exception).hasMessageThat()
.isEqualTo("Constructor 'com.google.gson.functional.Java17RecordTest$PrivateRecord(int)' is not accessible and"
+ " ReflectionAccessFilter does not permit making it accessible. Register a TypeAdapter for the declaring"
+ " type, adjust the access filter or increase the visibility of the element and its declaring type.");
.isEqualTo("Constructor 'com.google.gson.functional.Java17RecordTest$PrivateRecord(int)' is not accessible and"
+ " ReflectionAccessFilter does not permit making it accessible. Register a TypeAdapter for the declaring"
+ " type, adjust the access filter or increase the visibility of the element and its declaring type.");

assertThat(gson.toJson(new PublicRecord(1))).isEqualTo("{\"i\":1}");
assertThat(gson.fromJson("{\"i\":2}", PublicRecord.class)).isEqualTo(new PublicRecord(2));
Expand All @@ -427,7 +427,8 @@ record LocalRecord(int i) {}

var exception = assertThrows(JsonIOException.class, () -> gson.fromJson("{}", Record.class));
assertThat(exception).hasMessageThat()
.isEqualTo("Abstract classes can't be instantiated! Register an InstanceCreator or a TypeAdapter for"
+ " this type. Class name: java.lang.Record");
.isEqualTo("Abstract classes can't be instantiated! Adjust the R8 configuration or register an InstanceCreator"
+ " or a TypeAdapter for this type. Class name: java.lang.Record"
+ "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#r8-abstract-class");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,14 @@
import static com.google.common.truth.Truth.assertThat;
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 org.junit.Test;

public class ConstructorConstructorTest {
private ConstructorConstructor constructorConstructor = new ConstructorConstructor(
Collections.<Type, InstanceCreator<?>>emptyMap(), true,
Collections.<ReflectionAccessFilter>emptyList()
Collections.emptyMap(), true,
Collections.emptyList()
);

private abstract static class AbstractClass {
Expand All @@ -39,7 +36,7 @@ public AbstractClass() { }
private interface Interface { }

/**
* Verify that ConstructorConstructor does not try to invoke no-arg constructor
* Verify that ConstructorConstructor does not try to invoke no-args constructor
* of abstract class.
*/
@Test
Expand All @@ -49,9 +46,10 @@ public void testGet_AbstractClassNoArgConstructor() {
constructor.construct();
fail("Expected exception");
} catch (RuntimeException exception) {
assertThat(exception).hasMessageThat().isEqualTo("Abstract classes can't be instantiated! "
+ "Register an InstanceCreator or a TypeAdapter for this type. "
+ "Class name: com.google.gson.internal.ConstructorConstructorTest$AbstractClass");
assertThat(exception).hasMessageThat().isEqualTo("Abstract classes can't be instantiated!"
+ " Adjust the R8 configuration or register an InstanceCreator or a TypeAdapter for this type."
+ " Class name: com.google.gson.internal.ConstructorConstructorTest$AbstractClass"
+ "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#r8-abstract-class");
}
}

Expand All @@ -62,9 +60,9 @@ public void testGet_Interface() {
constructor.construct();
fail("Expected exception");
} catch (RuntimeException exception) {
assertThat(exception).hasMessageThat().isEqualTo("Interfaces can't be instantiated! "
+ "Register an InstanceCreator or a TypeAdapter for this type. "
+ "Interface name: com.google.gson.internal.ConstructorConstructorTest$Interface");
assertThat(exception).hasMessageThat().isEqualTo("Interfaces can't be instantiated!"
+ " Register an InstanceCreator or a TypeAdapter for this type."
+ " Interface name: com.google.gson.internal.ConstructorConstructorTest$Interface");
}
}
}
2 changes: 2 additions & 0 deletions shrinker-test/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@
<dependencies>
<dependency>
<!-- R8 dependency used above -->
<!-- Note: For some reason Maven shows the warning "Missing POM for com.android.tools:r8:jar",
but it appears that can be ignored -->
<groupId>com.android.tools</groupId>
<artifactId>r8</artifactId>
<version>8.0.40</version>
Expand Down
19 changes: 19 additions & 0 deletions shrinker-test/proguard.pro
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
-keep class com.example.DefaultConstructorMain {
public static java.lang.String runTest();
public static java.lang.String runTestNoJdkUnsafe();
public static java.lang.String runTestNoDefaultConstructor();
}


Expand All @@ -27,3 +28,21 @@
-keepclassmembers class com.example.ClassWithNamedFields {
!transient <fields>;
}

-keepclassmembernames class com.example.ClassWithExposeAnnotation {
<fields>;
}
-keepclassmembernames class com.example.ClassWithJsonAdapterAnnotation {
** f;
}
-keepclassmembernames class com.example.ClassWithVersionAnnotations {
<fields>;
}


-keepclassmembernames class com.example.DefaultConstructorMain$TestClass {
<fields>;
}
-keepclassmembernames class com.example.DefaultConstructorMain$TestClassNotAbstract {
<fields>;
}
18 changes: 3 additions & 15 deletions shrinker-test/r8.pro
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,21 @@
### The following rules are needed for R8 in "full mode", which performs more aggressive optimizations than ProGuard
### See https://r8.googlesource.com/r8/+/refs/heads/main/compatibility-faq.md#r8-full-mode

# Keep the no-args constructor of deserialized classes
-keepclassmembers class com.example.ClassWithDefaultConstructor {
<init>();
}
-keepclassmembers class com.example.GenericClasses$GenericClass {
<init>();
}
-keepclassmembers class com.example.GenericClasses$UsingGenericClass {
<init>();
}
-keepclassmembers class com.example.GenericClasses$GenericUsingGenericClass {
<init>();
}

# For classes with generic type parameter R8 in "full mode" requires to have a keep rule to
# preserve the generic signature
-keep,allowshrinking,allowoptimization,allowobfuscation,allowaccessmodification class com.example.GenericClasses$GenericClass
-keep,allowshrinking,allowoptimization,allowobfuscation,allowaccessmodification class com.example.GenericClasses$GenericUsingGenericClass

# Don't obfuscate class name, to check it in exception message
-keep,allowshrinking,allowoptimization class com.example.DefaultConstructorMain$TestClass
-keep,allowshrinking,allowoptimization class com.example.DefaultConstructorMain$TestClassWithoutDefaultConstructor

# This rule has the side-effect that R8 still removes the no-args constructor, but does not make the class abstract
-keep class com.example.DefaultConstructorMain$TestClassNotAbstract {
@com.google.gson.annotations.SerializedName <fields>;
}

# Keep enum constants which are not explicitly used in code
-keep class com.example.EnumClass {
-keepclassmembers class com.example.EnumClass {
** SECOND;
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
*/
public class ClassWithJsonAdapterAnnotation {
// For this field don't use @SerializedName and ignore it for deserialization
// Has custom ProGuard rule to keep the field name
@JsonAdapter(value = Adapter.class, nullSafe = false)
DummyClass f;

Expand Down
Loading