Adjust ProGuard default rules and shrinking tests (#2420)
* Adjust ProGuard default rules and shrinking tests * Adjust comment * Add shrinking test for class without no-args constructor; improve docs * Improve Unsafe mention in Troubleshooting Guide * Improve comment for `-if class *`
This commit is contained in:
parent
6d9c3566b7
commit
ecb9f8c8ad
|
@ -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).
|
**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.
|
**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:
|
**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:
|
||||||
|
@ -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).
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
|
@ -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
|
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).
|
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
|
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).
|
||||||
|
|
|
@ -76,14 +76,15 @@ public final class ConstructorConstructor {
|
||||||
if (Modifier.isAbstract(modifiers)) {
|
if (Modifier.isAbstract(modifiers)) {
|
||||||
// R8 performs aggressive optimizations where it removes the default constructor of a class
|
// R8 performs aggressive optimizations where it removes the default constructor of a class
|
||||||
// and makes the class `abstract`; check for that here explicitly
|
// 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"
|
* Note: Ideally should only show this R8-specific message when it is clear that R8 was
|
||||||
+ " an InstanceCreator or a TypeAdapter for this type. Class name: " + c.getName()
|
* used (e.g. when `c.getDeclaredConstructors().length == 0`), but on Android where this
|
||||||
+ "\nSee " + TroubleshootingGuide.createUrl("r8-abstract-class");
|
* 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! Register an InstanceCreator"
|
return "Abstract classes can't be instantiated! Adjust the R8 configuration or register"
|
||||||
+ " or a TypeAdapter for this type. Class name: " + c.getName();
|
+ " an InstanceCreator or a TypeAdapter for this type. Class name: " + c.getName()
|
||||||
|
+ "\nSee " + TroubleshootingGuide.createUrl("r8-abstract-class");
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
|
|
||||||
# Keep Gson annotations
|
# Keep Gson annotations
|
||||||
# Note: Cannot perform finer selection here to only cover Gson annotations, see also https://stackoverflow.com/q/47515093
|
# 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
|
### The following rules are needed for R8 in "full mode" which only adheres to `-keepattribtues` if
|
||||||
|
@ -24,10 +24,10 @@
|
||||||
-keep class com.google.gson.reflect.TypeToken { *; }
|
-keep class com.google.gson.reflect.TypeToken { *; }
|
||||||
|
|
||||||
# Keep any (anonymous) classes extending 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 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
|
# Keep fields with @SerializedName annotation, but allow obfuscation of their names
|
||||||
-keepclassmembers,allowobfuscation class * {
|
-keepclassmembers,allowobfuscation class * {
|
||||||
|
@ -35,7 +35,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
# Keep fields with any other Gson annotation
|
# 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.Expose <fields>;
|
||||||
@com.google.gson.annotations.JsonAdapter <fields>;
|
@com.google.gson.annotations.JsonAdapter <fields>;
|
||||||
@com.google.gson.annotations.Since <fields>;
|
@com.google.gson.annotations.Since <fields>;
|
||||||
|
@ -44,15 +46,25 @@
|
||||||
|
|
||||||
# Keep no-args constructor of classes which can be used with @JsonAdapter
|
# 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
|
# 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>();
|
<init>();
|
||||||
}
|
}
|
||||||
-keep class * implements com.google.gson.TypeAdapterFactory {
|
-keepclassmembers class * implements com.google.gson.TypeAdapterFactory {
|
||||||
<init>();
|
<init>();
|
||||||
}
|
}
|
||||||
-keep class * implements com.google.gson.JsonSerializer {
|
-keepclassmembers class * implements com.google.gson.JsonSerializer {
|
||||||
<init>();
|
<init>();
|
||||||
}
|
}
|
||||||
-keep class * implements com.google.gson.JsonDeserializer {
|
-keepclassmembers class * implements com.google.gson.JsonDeserializer {
|
||||||
<init>();
|
<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>;
|
||||||
|
}
|
||||||
|
|
|
@ -89,7 +89,7 @@ public final class Java17RecordTest {
|
||||||
|
|
||||||
var exception = assertThrows(JsonIOException.class, () -> gson.getAdapter(LocalRecord.class));
|
var exception = assertThrows(JsonIOException.class, () -> gson.getAdapter(LocalRecord.class));
|
||||||
assertThat(exception).hasMessageThat()
|
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
|
@Test
|
||||||
|
@ -154,7 +154,7 @@ public final class Java17RecordTest {
|
||||||
// TODO: Adjust this once Gson throws more specific exception type
|
// TODO: Adjust this once Gson throws more specific exception type
|
||||||
catch (RuntimeException e) {
|
catch (RuntimeException e) {
|
||||||
assertThat(e).hasMessageThat()
|
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);
|
assertThat(e).hasCauseThat().isSameInstanceAs(LocalRecord.thrownException);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -227,7 +227,7 @@ public final class Java17RecordTest {
|
||||||
String s = "{'aString': 's', 'aByte': null, 'aShort': 0}";
|
String s = "{'aString': 's', 'aByte': null, 'aShort': 0}";
|
||||||
var e = assertThrows(JsonParseException.class, () -> gson.fromJson(s, RecordWithPrimitives.class));
|
var e = assertThrows(JsonParseException.class, () -> gson.fromJson(s, RecordWithPrimitives.class));
|
||||||
assertThat(e).hasMessageThat()
|
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");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -384,8 +384,8 @@ public final class Java17RecordTest {
|
||||||
|
|
||||||
var exception = assertThrows(JsonIOException.class, () -> gson.toJson(new Blocked(1)));
|
var exception = assertThrows(JsonIOException.class, () -> gson.toJson(new Blocked(1)));
|
||||||
assertThat(exception).hasMessageThat()
|
assertThat(exception).hasMessageThat()
|
||||||
.isEqualTo("ReflectionAccessFilter does not permit using reflection for class " + Blocked.class.getName() +
|
.isEqualTo("ReflectionAccessFilter does not permit using reflection for class " + Blocked.class.getName() +
|
||||||
". Register a TypeAdapter for this type or adjust the access filter.");
|
". Register a TypeAdapter for this type or adjust the access filter.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -396,15 +396,15 @@ public final class Java17RecordTest {
|
||||||
|
|
||||||
var exception = assertThrows(JsonIOException.class, () -> gson.toJson(new PrivateRecord(1)));
|
var exception = assertThrows(JsonIOException.class, () -> gson.toJson(new PrivateRecord(1)));
|
||||||
assertThat(exception).hasMessageThat()
|
assertThat(exception).hasMessageThat()
|
||||||
.isEqualTo("Constructor 'com.google.gson.functional.Java17RecordTest$PrivateRecord(int)' is not accessible and"
|
.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"
|
+ " 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.");
|
+ " type, adjust the access filter or increase the visibility of the element and its declaring type.");
|
||||||
|
|
||||||
exception = assertThrows(JsonIOException.class, () -> gson.fromJson("{}", PrivateRecord.class));
|
exception = assertThrows(JsonIOException.class, () -> gson.fromJson("{}", PrivateRecord.class));
|
||||||
assertThat(exception).hasMessageThat()
|
assertThat(exception).hasMessageThat()
|
||||||
.isEqualTo("Constructor 'com.google.gson.functional.Java17RecordTest$PrivateRecord(int)' is not accessible and"
|
.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"
|
+ " 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.");
|
+ " 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.toJson(new PublicRecord(1))).isEqualTo("{\"i\":1}");
|
||||||
assertThat(gson.fromJson("{\"i\":2}", PublicRecord.class)).isEqualTo(new PublicRecord(2));
|
assertThat(gson.fromJson("{\"i\":2}", PublicRecord.class)).isEqualTo(new PublicRecord(2));
|
||||||
|
@ -427,7 +427,8 @@ public final class Java17RecordTest {
|
||||||
|
|
||||||
var exception = assertThrows(JsonIOException.class, () -> gson.fromJson("{}", Record.class));
|
var exception = assertThrows(JsonIOException.class, () -> gson.fromJson("{}", Record.class));
|
||||||
assertThat(exception).hasMessageThat()
|
assertThat(exception).hasMessageThat()
|
||||||
.isEqualTo("Abstract classes can't be instantiated! Register an InstanceCreator or a TypeAdapter for"
|
.isEqualTo("Abstract classes can't be instantiated! Adjust the R8 configuration or register an InstanceCreator"
|
||||||
+ " this type. Class name: java.lang.Record");
|
+ " or a TypeAdapter for this type. Class name: java.lang.Record"
|
||||||
|
+ "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#r8-abstract-class");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,17 +19,14 @@ package com.google.gson.internal;
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
import com.google.gson.InstanceCreator;
|
|
||||||
import com.google.gson.ReflectionAccessFilter;
|
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
import java.lang.reflect.Type;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
public class ConstructorConstructorTest {
|
public class ConstructorConstructorTest {
|
||||||
private ConstructorConstructor constructorConstructor = new ConstructorConstructor(
|
private ConstructorConstructor constructorConstructor = new ConstructorConstructor(
|
||||||
Collections.<Type, InstanceCreator<?>>emptyMap(), true,
|
Collections.emptyMap(), true,
|
||||||
Collections.<ReflectionAccessFilter>emptyList()
|
Collections.emptyList()
|
||||||
);
|
);
|
||||||
|
|
||||||
private abstract static class AbstractClass {
|
private abstract static class AbstractClass {
|
||||||
|
@ -39,7 +36,7 @@ public class ConstructorConstructorTest {
|
||||||
private interface Interface { }
|
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.
|
* of abstract class.
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
|
@ -49,9 +46,10 @@ public class ConstructorConstructorTest {
|
||||||
constructor.construct();
|
constructor.construct();
|
||||||
fail("Expected exception");
|
fail("Expected exception");
|
||||||
} catch (RuntimeException exception) {
|
} catch (RuntimeException exception) {
|
||||||
assertThat(exception).hasMessageThat().isEqualTo("Abstract classes can't be instantiated! "
|
assertThat(exception).hasMessageThat().isEqualTo("Abstract classes can't be instantiated!"
|
||||||
+ "Register an InstanceCreator or a TypeAdapter for this type. "
|
+ " Adjust the R8 configuration or register an InstanceCreator or a TypeAdapter for this type."
|
||||||
+ "Class name: com.google.gson.internal.ConstructorConstructorTest$AbstractClass");
|
+ " Class name: com.google.gson.internal.ConstructorConstructorTest$AbstractClass"
|
||||||
|
+ "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#r8-abstract-class");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,9 +60,9 @@ public class ConstructorConstructorTest {
|
||||||
constructor.construct();
|
constructor.construct();
|
||||||
fail("Expected exception");
|
fail("Expected exception");
|
||||||
} catch (RuntimeException exception) {
|
} catch (RuntimeException exception) {
|
||||||
assertThat(exception).hasMessageThat().isEqualTo("Interfaces can't be instantiated! "
|
assertThat(exception).hasMessageThat().isEqualTo("Interfaces can't be instantiated!"
|
||||||
+ "Register an InstanceCreator or a TypeAdapter for this type. "
|
+ " Register an InstanceCreator or a TypeAdapter for this type."
|
||||||
+ "Interface name: com.google.gson.internal.ConstructorConstructorTest$Interface");
|
+ " Interface name: com.google.gson.internal.ConstructorConstructorTest$Interface");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -199,6 +199,8 @@
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<!-- R8 dependency used above -->
|
<!-- 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>
|
<groupId>com.android.tools</groupId>
|
||||||
<artifactId>r8</artifactId>
|
<artifactId>r8</artifactId>
|
||||||
<version>8.0.40</version>
|
<version>8.0.40</version>
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
-keep class com.example.DefaultConstructorMain {
|
-keep class com.example.DefaultConstructorMain {
|
||||||
public static java.lang.String runTest();
|
public static java.lang.String runTest();
|
||||||
public static java.lang.String runTestNoJdkUnsafe();
|
public static java.lang.String runTestNoJdkUnsafe();
|
||||||
|
public static java.lang.String runTestNoDefaultConstructor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -27,3 +28,21 @@
|
||||||
-keepclassmembers class com.example.ClassWithNamedFields {
|
-keepclassmembers class com.example.ClassWithNamedFields {
|
||||||
!transient <fields>;
|
!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>;
|
||||||
|
}
|
||||||
|
|
|
@ -4,20 +4,6 @@
|
||||||
### The following rules are needed for R8 in "full mode", which performs more aggressive optimizations than ProGuard
|
### 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
|
### 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
|
# For classes with generic type parameter R8 in "full mode" requires to have a keep rule to
|
||||||
# preserve the generic signature
|
# preserve the generic signature
|
||||||
-keep,allowshrinking,allowoptimization,allowobfuscation,allowaccessmodification class com.example.GenericClasses$GenericClass
|
-keep,allowshrinking,allowoptimization,allowobfuscation,allowaccessmodification class com.example.GenericClasses$GenericClass
|
||||||
|
@ -25,12 +11,14 @@
|
||||||
|
|
||||||
# Don't obfuscate class name, to check it in exception message
|
# 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$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
|
# 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 {
|
-keep class com.example.DefaultConstructorMain$TestClassNotAbstract {
|
||||||
@com.google.gson.annotations.SerializedName <fields>;
|
@com.google.gson.annotations.SerializedName <fields>;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Keep enum constants which are not explicitly used in code
|
# Keep enum constants which are not explicitly used in code
|
||||||
-keep class com.example.EnumClass {
|
-keepclassmembers class com.example.EnumClass {
|
||||||
** SECOND;
|
** SECOND;
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import java.lang.reflect.Type;
|
||||||
*/
|
*/
|
||||||
public class ClassWithJsonAdapterAnnotation {
|
public class ClassWithJsonAdapterAnnotation {
|
||||||
// For this field don't use @SerializedName and ignore it for deserialization
|
// 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)
|
@JsonAdapter(value = Adapter.class, nullSafe = false)
|
||||||
DummyClass f;
|
DummyClass f;
|
||||||
|
|
||||||
|
|
|
@ -8,14 +8,24 @@ import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
public class DefaultConstructorMain {
|
public class DefaultConstructorMain {
|
||||||
static class TestClass {
|
static class TestClass {
|
||||||
@SerializedName("s")
|
|
||||||
public String s;
|
public String s;
|
||||||
}
|
}
|
||||||
|
|
||||||
// R8 rule for this class still removes no-args constructor, but doesn't make class abstract
|
// R8 rule for this class still removes no-args constructor, but doesn't make class abstract
|
||||||
static class TestClassNotAbstract {
|
static class TestClassNotAbstract {
|
||||||
|
public String s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current Gson ProGuard rules only keep default constructor (and only then prevent R8 from
|
||||||
|
// making class abstract); other constructors are ignored to suggest to user adding default
|
||||||
|
// constructor instead of implicitly relying on JDK Unsafe
|
||||||
|
static class TestClassWithoutDefaultConstructor {
|
||||||
@SerializedName("s")
|
@SerializedName("s")
|
||||||
public String s;
|
public String s;
|
||||||
|
|
||||||
|
public TestClassWithoutDefaultConstructor(String s) {
|
||||||
|
this.s = s;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -34,4 +44,12 @@ public class DefaultConstructorMain {
|
||||||
TestClassNotAbstract deserialized = gson.fromJson("{\"s\": \"value\"}", same(TestClassNotAbstract.class));
|
TestClassNotAbstract deserialized = gson.fromJson("{\"s\": \"value\"}", same(TestClassNotAbstract.class));
|
||||||
return deserialized.s;
|
return deserialized.s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main entrypoint, called by {@code ShrinkingIT.testNoDefaultConstructor()}.
|
||||||
|
*/
|
||||||
|
public static String runTestNoDefaultConstructor() {
|
||||||
|
TestClassWithoutDefaultConstructor deserialized = new Gson().fromJson("{\"s\":\"value\"}", same(TestClassWithoutDefaultConstructor.class));
|
||||||
|
return deserialized.s;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -220,4 +220,24 @@ public class ShrinkingIT {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNoDefaultConstructor() throws Exception {
|
||||||
|
runTest("com.example.DefaultConstructorMain", c -> {
|
||||||
|
Method m = c.getMethod("runTestNoDefaultConstructor");
|
||||||
|
|
||||||
|
if (jarToTest.equals(PROGUARD_RESULT_PATH)) {
|
||||||
|
Object result = m.invoke(null);
|
||||||
|
assertThat(result).isEqualTo("value");
|
||||||
|
} else {
|
||||||
|
// R8 performs more aggressive optimizations
|
||||||
|
Exception e = assertThrows(InvocationTargetException.class, () -> m.invoke(null));
|
||||||
|
assertThat(e).hasCauseThat().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.example.DefaultConstructorMain$TestClassWithoutDefaultConstructor"
|
||||||
|
+ "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#r8-abstract-class"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue