diff --git a/README.md b/README.md index 1a33554..9d8f1b6 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ allprojects { ```groovy dependencyResolutionManagement { repositories { - // JitPack 远程仓库:https://jitpack.io[NameThatColor-1.7.4-fix.jar](..%2FStudioPlugins%2Fplugin%2FNameThatColor-1.7.4-fix.jar) + // JitPack 远程仓库:https://jitpack.io maven { url 'https://jitpack.io' } } } @@ -39,13 +39,19 @@ android { dependencies { // Gson 解析容错:https://github.com/getActivity/GsonFactory - implementation 'com.github.getActivity:GsonFactory:9.0' + implementation 'com.github.getActivity:GsonFactory:9.2' // Json 解析框架:https://github.com/google/gson implementation 'com.google.code.gson:gson:2.10.1' + // Kotlin 反射库:用于反射 Kotlin data class 类对象 + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.5.10' } ``` -* 需要注意的是:Gson 框架必须使用 **2.9.0** 及以上版本,否则将会出现版本兼容问题 +* 需要注意两点: + + * 当前项目必须要有 Kotlin 环境,否则会编译不通过 + + * Gson 框架必须使用 **2.9.0** 及以上版本,否则将会出现版本兼容问题 #### 使用文档 @@ -166,246 +172,16 @@ class XxxBean { * 如果你在 Kotlin 中定义了以下内容的 Bean 类 ```kotlin -data class DataClassBean(val name: String?, val age: Int = 18) +data class DataClassBean(val name: String = "Hello") ``` * 如果丢给 Gson 解析,最终会得到以下结果 ```text name = null -age = 0 -``` - -* age 为什么不等于 18?为什么会等于 0 呢?要知道这个问题的原因,我们需要反编译看一下 DataClassBean 的源码 - -```java -public final class DataClassBean { - private final int age; - private final String name; - - public static /* synthetic */ DataClassBean copy$default(DataClassBean bean, String str, int i, int i2, Object obj) { - if ((i2 & 1) != 0) { - str = bean.name; - } - if ((i2 & 2) != 0) { - i = bean.age; - } - return bean.copy(str, i); - } - - public final String component1() { - return this.name; - } - - public final int component2() { - return this.age; - } - - public final DataClassBean copy(String str, int i) { - return new DataClassBean(str, i); - } - - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj instanceof DataClassBean) { - DataClassBean bean = (DataClassBean) obj; - return Intrinsics.areEqual(this.name, bean.name) && this.age == bean.age; - } - return false; - } - - public int hashCode() { - String str = this.name; - return ((str == null ? 0 : str.hashCode()) * 31) + this.age; - } - - public String toString() { - return "DataClassBean(name=" + ((Object) this.name) + ", age=" + this.age + ')'; - } - - public DataClassBean(String name, int age) { - this.name = name; - this.age = age; - } - - public /* synthetic */ DataClassBean(String str, int i, int i2, DefaultConstructorMarker defaultConstructorMarker) { - this(str, (i2 & 2) != 0 ? 18 : i); - } - - public final int getAge() { - return this.age; - } - - public final String getName() { - return this.name; - } -} -``` - -* 不知道大家发现问题没有?DataClassBean 类里面并没有空参构造函数,那 Gson 到底是怎么创建对象的呢?让我们看一段源码 - -```java -package com.google.gson.internal; - -public final class ConstructorConstructor { - - public ObjectConstructor get(TypeToken typeToken) { - - ...... - - ObjectConstructor defaultConstructor = newDefaultConstructor(rawType, filterResult); - if (defaultConstructor != null) { - return defaultConstructor; - } - - ...... - - if (filterResult == FilterResult.ALLOW) { - // finally try unsafe - return newUnsafeAllocator(rawType); - } else { - ........ - } - } - - private ObjectConstructor newUnsafeAllocator(final Class rawType) { - - ...... - - ObjectConstructor defaultConstructor = newDefaultConstructor(rawType, filterResult); - if (defaultConstructor != null) { - return defaultConstructor; - } - - ...... - - if (useJdkUnsafe) { - return new ObjectConstructor() { - @Override public T construct() { - try { - @SuppressWarnings("unchecked") - T newInstance = (T) UnsafeAllocator.INSTANCE.newInstance(rawType); - return newInstance; - } catch (Exception e) { - throw new RuntimeException(("Unable to create instance of " + rawType + "." - + " Registering an InstanceCreator or a TypeAdapter for this type, or adding a no-args" - + " constructor may fix this problem."), e); - } - } - }; - } else { - ...... - } - } - - private static ObjectConstructor newDefaultConstructor(Class rawType, FilterResult filterResult) { - - ...... - - final Constructor constructor; - try { - constructor = rawType.getDeclaredConstructor(); - } catch (NoSuchMethodException e) { - return null; - } - - ...... - - return new ObjectConstructor() { - @Override public T construct() { - try { - @SuppressWarnings("unchecked") // T is the same raw type as is requested - T newInstance = (T) constructor.newInstance(); - return newInstance; - } - // Note: InstantiationException should be impossible because check at start of method made sure - // that class is not abstract - catch (InstantiationException e) { - throw new RuntimeException("Failed to invoke constructor '" + ReflectionHelper.constructorToString(constructor) + "'" - + " with no args", e); - } catch (InvocationTargetException e) { - // TODO: don't wrap if cause is unchecked? - // TODO: JsonParseException ? - throw new RuntimeException("Failed to invoke constructor '" + ReflectionHelper.constructorToString(constructor) + "'" - + " with no args", e.getCause()); - } catch (IllegalAccessException e) { - throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e); - } - } - }; - } -} -``` - -```java -package com.google.gson.internal; - -public abstract class UnsafeAllocator { - - public abstract T newInstance(Class c) throws Exception; - - public static final UnsafeAllocator INSTANCE = create(); - - private static UnsafeAllocator create() { - // try JVM - // public class Unsafe { - // public Object allocateInstance(Class type); - // } - try { - Class unsafeClass = Class.forName("sun.misc.Unsafe"); - Field f = unsafeClass.getDeclaredField("theUnsafe"); - f.setAccessible(true); - final Object unsafe = f.get(null); - final Method allocateInstance = unsafeClass.getMethod("allocateInstance", Class.class); - return new UnsafeAllocator() { - @Override - @SuppressWarnings("unchecked") - public T newInstance(Class c) throws Exception { - assertInstantiable(c); - return (T) allocateInstance.invoke(unsafe, c); - } - }; - } catch (Exception ignored) { - // OK: try the next way - } - - ...... - } -} -``` - -* 相信你看完就懂了,Gson 确实是反射创建无参构造函数来创建对象,但是如果没有空参构造函数的情况下,它也会通过另外的手段创建对象,借助 `sun.misc.Unsafe` 创建对象,这样会有一个问题,这样创建出来的对象它不会走任何构造函数,通过查看刚刚反编译出来的 DataClassBean 类,就知道为什么这样 Kotlin 默认值都不会生效了 - -* 框架的做法很简单,既然没有无参构造函数,那我就通过其他构造函数来创建,就拿 Kotlin 生成的 `DataClassBean(String str, int i, int i2, DefaultConstructorMarker defaultConstructorMarker)` 来创建对象 - -这个构造函数特别有意思,最后第一个参数是 DefaultConstructorMarker 类,里面啥也没有 - -```java -public final class DefaultConstructorMarker { - private DefaultConstructorMarker() { - } -} -``` - -* 最后第二个参数是参数标记,标记是否使用 data class 定义的默认值 - -```java -public final class DataClassBean { - - public /* synthetic */ DataClassBean(String str, int i, int i2, DefaultConstructorMarker defaultConstructorMarker) { - this(str, (i2 & 2) != 0 ? 18 : i); - } - - public DataClassBean(String name, int age) { - this.name = name; - this.age = age; - } -} ``` -* 框架的解决方案是:反射最后第一个参数类型为 DefaultConstructorMarker,然后传入空对象即可,最后第二个参数类型为 int 的构造函数,并且让最后第二个参数的位运算逻辑为 true,让它走到默认值赋值那里,这样可以选择传入 `Integer.MAX_VALUE`,这样每次使用它去 & 不大于 0 的某个值,都会等于某个值,也就是不会等于 0,这样就能保证它的运算条件一直为 true,也就是使用默认值,其他参数传值的话,如果是基本数据类型,就传入基本数据类型的默认值,如果是对象类型,则直接传入 null。这样就解决了 Gson 反射 Kotlin Data Class 类出现字段默认值不生效的问题。 +* `name` 字段为什么不等于 `Hello` ?为什么会等于 `null` 值呢?这是因为 Gson 默认只初始化了 DataClassBean 类的空参构造函数,框架的解决方案很简单粗暴,直接引入 Kotlin 反射库,让它找到 Kotlin data class 自动生成的主构造函数,然后反射创建 Kotlin 类,这样得到的对象,非空字段的默认值都会被保留,这样就解决了 Gson 反射 Kotlin Data Class 类出现字段默认值不生效的问题,目前框架内部已经处理了该问题,外部使用的人无需额外处理该问题,只需要调用框架进行解析即可。 ## 常见疑问解答 diff --git a/app/build.gradle b/app/build.gradle index cedb5db..62d4b57 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId "com.hjq.gson.factory.demo" minSdkVersion 16 targetSdkVersion 31 - versionCode 900 - versionName "9.0" + versionCode 920 + versionName "9.2" testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } diff --git a/build.gradle b/build.gradle index cf2fb91..0cbe102 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ buildscript { classpath 'com.android.tools.build:gradle:4.1.2' // Kotlin 插件:https://plugins.jetbrains.com/plugin/6954-kotlin // noinspection GradleDependency - classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31' + classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.32' } } diff --git a/library/build.gradle b/library/build.gradle index b3463cf..f251f16 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -1,12 +1,13 @@ apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' android { compileSdkVersion 31 defaultConfig { - minSdkVersion 11 - versionCode 900 - versionName "9.0" + minSdkVersion 12 + versionCode 920 + versionName "9.2" } // 使用 JDK 1.8 @@ -35,6 +36,8 @@ dependencies { // Json 解析框架:https://github.com/google/gson // noinspection GradleDependency implementation 'com.google.code.gson:gson:2.10.1' + // Kotlin 反射依赖:用于反射 Kotlin data class 类对象 + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.5.10' } // 防止编码问题 diff --git a/library/src/main/java/com/hjq/gson/factory/ParseExceptionCallback.java b/library/src/main/java/com/hjq/gson/factory/ParseExceptionCallback.java index f388784..3981f5b 100644 --- a/library/src/main/java/com/hjq/gson/factory/ParseExceptionCallback.java +++ b/library/src/main/java/com/hjq/gson/factory/ParseExceptionCallback.java @@ -25,7 +25,7 @@ public interface ParseExceptionCallback { * * @param typeToken 类型 Token * @param fieldName 字段名称(可能为空) - * @param listItemJsonToken List 条目类型 + * @param listItemJsonToken List 条目类型(可能为空) */ void onParseListException(TypeToken typeToken, String fieldName, JsonToken listItemJsonToken); @@ -35,7 +35,7 @@ public interface ParseExceptionCallback { * @param typeToken 类型 Token * @param fieldName 字段名称(可能为空) * @param mapItemKey Map 集合中的 key 值,如果等于为 "null" 字符串,则证明后端返回了错误类型的 key 过来 - * @param mapItemJsonToken Map 条目类型 + * @param mapItemJsonToken Map 条目类型(可能为空) */ void onParseMapException(TypeToken typeToken, String fieldName, String mapItemKey, JsonToken mapItemJsonToken); } \ No newline at end of file diff --git a/library/src/main/java/com/hjq/gson/factory/constructor/KotlinDataClassDefaultValueConstructor.java b/library/src/main/java/com/hjq/gson/factory/constructor/KotlinDataClassDefaultValueConstructor.java deleted file mode 100644 index 89c68c3..0000000 --- a/library/src/main/java/com/hjq/gson/factory/constructor/KotlinDataClassDefaultValueConstructor.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.hjq.gson.factory.constructor; - -import com.google.gson.internal.ObjectConstructor; -import com.google.gson.internal.Primitives; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/GsonFactory - * time : 2023/08/01 - * desc : Kotlin Data Class 创建器,用于处理反射创建 data class 类导致默认值不生效的问题 - */ -public final class KotlinDataClassDefaultValueConstructor implements ObjectConstructor { - - private static final String DEFAULT_CONSTRUCTOR_MARKER_CLASS_NAME = "kotlin.jvm.internal.DefaultConstructorMarker"; - - private final Class mRawType; - - public KotlinDataClassDefaultValueConstructor(Class rawType) { - mRawType = rawType; - } - - @SuppressWarnings("unchecked") - @Override - public T construct() { - T instance = null; - try { - Constructor[] constructors = mRawType.getConstructors(); - for (Constructor constructor : constructors) { - Class[] parameterTypes = constructor.getParameterTypes(); - - int parameterLength = parameterTypes.length; - - // 如果参数小于 3 个,证明不是我们要的目标构造函数,就继续下一个循环 - // 如果最后一个参数是 DefaultConstructorMarker 类型,并且最后第二个参数是 int 类型 - if (parameterLength < 3) { - continue; - } - - // 如果最后一个参数必须是 DefaultConstructorMarker 类型 - if (!DEFAULT_CONSTRUCTOR_MARKER_CLASS_NAME.equals(parameterTypes[parameterTypes.length - 1].getName())) { - continue; - } - - Field[] fields = mRawType.getDeclaredFields(); - - // kotlin data class 构造函数中的参数一定比字段数量要多 - // 因为里面的构造函数中的参数,除了类原有的字段外 - // 还有 N 个 int 标记位和 DefaultConstructorMarker 类型对象 - if (parameterTypes.length <= fields.length) { - continue; - } - - Object[] parameterValue = new Object[parameterLength]; - // 这个参数 DefaultConstructorMarker 类型,用于标记构造函数是 Kotlin 语法自动生成的 - parameterValue[parameterLength - 1] = null; - - for (int i = fields.length; i < parameterTypes.length - 1; i++) { - if (!int.class.isAssignableFrom(parameterTypes[i])) { - continue; - } - - // 这个参数是标记位,用于判断有没有设置默认值进去,会进行 & 位运算进行判断 - // 这里设置成 Integer.MAX_VALUE,Integer.MAX_VALUE & 大于 0 的值,都会等于那个值 - // 这样就能每次都能走到设置默认值那里,需要注意的是,这样的标记位在参数多的时候可能不止一个 - // 一个 int 标记位最多只能用于 32 个参数,如果超出了的话,就会新增多一个 int 标记位 - // 1. (i & 1) != 0 ? 0 : i - // 2. (i & 2) != 0 ? 0 : i - // 3. (i & 4) != 0 ? 0 : i - // 4. (i & 8) != 0 ? 0 : i - // 5. (i & 16) != 0 ? 0 : i - // 6. (i & 32) != 0 ? 0 : i - // 7. (i & 64) != 0 ? 0 : i - // 8. (i & 128) != 0 ? 0 : i - // 9. (i & 256) != 0 ? 0 : i - // 10. (i & 512) != 0 ? 0 : i - // 11. (i & 1024) != 0 ? 0 : i - // 12. (i & 2048) != 0 ? 0 : i - // 13. (i & 4096) != 0 ? 0 : i - // 14. (i & 8192) != 0 ? 0 : i - // 15. (i & 16384) != 0 ? 0 : i - // 16. (i & 32768) != 0 ? 0 : i - // 17. (i & 65536) != 0 ? 0 : i - // 18. (i & 131072) != 0 ? 0 : i - // 19. (i & 262144) != 0 ? 0 : i - // 20. (i & 524288) != 0 ? 0 : i - // 21. (i & 1048576) != 0 ? 0 : i - // 22. (i & 2097152) != 0 ? 0 : i - // 23. (i & 4194304) != 0 ? 0 : i - // 24. (i & 8388608) != 0 ? 0 : i - // 25. (i & 16777216) != 0 ? 0 : i - // 26. (i & 33554432) != 0 ? 0 : i - // 27. (i & 67108864) != 0 ? 0 : i - // 28. (i & 134217728) != 0 ? 0 : i - // 29. (i & 268435456) != 0 ? 0 : i - // 30. (i & 536870912) != 0 ? 0 : i - // 31. (i & 1073741824) != 0 ? 0 : i - // 32. (i & -2147483648) != 0 ? 0 : i(注意:-2147483648 = Integer.MIN_VALUE) - // Github 地址:https://github.com/getActivity/GsonFactory/issues/27 - parameterValue[i] = Integer.MAX_VALUE; - } - - for (int i = 0; i < parameterTypes.length - 2; i++) { - Class parameterType = parameterTypes[i]; - parameterValue[i] = getTypeDefaultValue(parameterType); - } - - instance = (T) constructor.newInstance(parameterValue); - break; - } - } catch (Exception e) { - e.printStackTrace(); - } - - return instance; - } - - private Object getTypeDefaultValue(Class clazz) { - if (Primitives.isWrapperType(clazz)) { - // 如果是包装类,使用默认值为 null - return null; - } - - if (clazz.isPrimitive()) { - // 如果是基本数据类型,使用对应的包装类的默认值 - if (clazz == byte.class) { - return (byte) 0; - } else if (clazz == short.class) { - return (short) 0; - } else if (clazz == int.class) { - return 0; - } else if (clazz == long.class) { - return 0L; - } else if (clazz == float.class) { - return 0.0f; - } else if (clazz == double.class) { - return 0.0; - } else if (clazz == char.class) { - return '\u0000'; - } else if (clazz == boolean.class) { - return false; - } - } - return null; - } -} \ No newline at end of file diff --git a/library/src/main/java/com/hjq/gson/factory/constructor/KotlinDataClassDefaultValueConstructor.kt b/library/src/main/java/com/hjq/gson/factory/constructor/KotlinDataClassDefaultValueConstructor.kt new file mode 100644 index 0000000..cc0edd7 --- /dev/null +++ b/library/src/main/java/com/hjq/gson/factory/constructor/KotlinDataClassDefaultValueConstructor.kt @@ -0,0 +1,78 @@ +package com.hjq.gson.factory.constructor + +import com.google.gson.internal.ObjectConstructor +import kotlin.reflect.KParameter +import kotlin.reflect.full.primaryConstructor +import kotlin.reflect.jvm.isAccessible + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/GsonFactory + * time : 2023/11/25 + * desc : Kotlin Data Class 创建器,用于处理反射创建 data class 类导致默认值不生效的问题 + */ +class KotlinDataClassDefaultValueConstructor(private val rawType: Class<*>) : ObjectConstructor { + + companion object { + /** 构造函数的字段的默认值 */ + private val ABSENT_VALUE = Any() + } + + override fun construct(): T? { + val rawTypeKotlin = rawType.kotlin + // 寻找 Kotlin 主构造函数,如果找不到就不往下执行 + val constructor = rawTypeKotlin.primaryConstructor ?: return null + constructor.isAccessible = true + + var fullInitialized = true + val constructorSize = constructor.parameters.size + val values = Array(constructorSize) { ABSENT_VALUE } + + for (i in 0 until constructorSize) { + if (values[i] !== ABSENT_VALUE) { + continue + } + + when { + // 判断这个参数是不是可选的 + constructor.parameters[i].isOptional -> fullInitialized = false + // 判断这个参数是否标记为空的 + constructor.parameters[i].type.isMarkedNullable -> values[i] = null + } + } + + val result = if (fullInitialized) { + constructor.call(*values) + } else { + constructor.callBy(IndexedParameterMap(constructor.parameters, values)) + } + + return result as T + } + + /** 一个简单的 Map,它使用参数索引而不是排序或哈希。 */ + class IndexedParameterMap( + private val parameterKeys: List, + private val parameterValues: Array, + ) : AbstractMutableMap() { + + override fun put(key: KParameter, value: Any?): Any? = null + + override val entries: MutableSet> + get() { + val allPossibleEntries = parameterKeys.mapIndexed { index, value -> + SimpleEntry(value, parameterValues[index]) + } + return allPossibleEntries.filterTo(mutableSetOf()) { + it.value !== ABSENT_VALUE + } + } + + override fun containsKey(key: KParameter) = parameterValues[key.index] !== ABSENT_VALUE + + override fun get(key: KParameter): Any? { + val value = parameterValues[key.index] + return if (value !== ABSENT_VALUE) value else null + } + } +} \ No newline at end of file diff --git a/library/src/main/java/com/hjq/gson/factory/constructor/ReflectCreatorConstructor.java b/library/src/main/java/com/hjq/gson/factory/constructor/ReflectCreatorConstructor.java index c5a742b..98364e7 100644 --- a/library/src/main/java/com/hjq/gson/factory/constructor/ReflectCreatorConstructor.java +++ b/library/src/main/java/com/hjq/gson/factory/constructor/ReflectCreatorConstructor.java @@ -13,7 +13,7 @@ */ public final class ReflectCreatorConstructor implements ObjectConstructor { - private final KotlinDataClassDefaultValueConstructor mKotlinDataClassDefaultValueConstructor; + private final ObjectConstructor mKotlinDataClassDefaultValueConstructor; private final Constructor mConstructor; diff --git a/library/src/main/java/com/hjq/gson/factory/constructor/ReflectSafeCreatorConstructor.java b/library/src/main/java/com/hjq/gson/factory/constructor/ReflectSafeCreatorConstructor.java index d05e5e2..3f609cf 100644 --- a/library/src/main/java/com/hjq/gson/factory/constructor/ReflectSafeCreatorConstructor.java +++ b/library/src/main/java/com/hjq/gson/factory/constructor/ReflectSafeCreatorConstructor.java @@ -11,7 +11,7 @@ */ public final class ReflectSafeCreatorConstructor implements ObjectConstructor { - private final KotlinDataClassDefaultValueConstructor mKotlinDataClassDefaultValueConstructor; + private final ObjectConstructor mKotlinDataClassDefaultValueConstructor; private final Class mRawType;