书接上回,https://blog.****.net/jzlhll123/article/details/139302991。
之前我发现gson报错后:
gson在2.11.0给我的kotlin项目代码报错了。
IllegalArgumentException: TypeToken type argument must not contain a type variable
上次解释原因是因为,gson内部有了报错,根本原因就是二级嵌套泛型获取的解析方式问题。而且写下了解决办法和参考代码。 是错误的。
我后来又通过在demo工程中尝试,怎么都无法复现,困扰了我好几天。我又去研究了字节码和反编译。
最后发现,
public class ApiBean {
public String type;
public String name;
public String url;
}
public class CmdBean<T> { //T就可以传入ApiBean做为二层嵌套泛型
public String cmdId;
public long ts;
public String pro;
public T apiBean;
}
//二层嵌套解析 //暂时有问题。后面会讲到他没问题
inline fun <reified T> parse(jsonStr: String?): CmdBean<T>? {
return jsonStr?.fromJson<CmdBean<T>>()
}
//二层嵌套解析 //暂时有问题。后面会讲到他没问题
inline fun <reified T> parseList(jsonStr: String?): List<T>? {
return jsonStr?.fromJson<List<T>>()
}
//通用函数 //暂时有问题。后面会讲到他没问题
inline fun <reified T> String.fromJson(): T {
return Globals.gson.fromJson(this, object : TypeToken<T>() {}.type)
}
class Action {
fun test() {
val json = """
{"apiBean":{"name":"nameX","type":"typeX","url":"htpps://api.....com/.../..xxx"},"cmdId":"cmdId010102394","pro":"aaaa","ts":1029301291023}
""".trimIndent()
val cmd = parse<ApiBean>(json)
println("cmd $cmd")
}
fun testList() {
val json = """
[{"name":"icon1","type":"type1","url":"https://.......png"},{"name":"icon2","type":"type2","url":"https://..xx.....png"}]
""".trimIndent()
val cmd = parseList<ApiBean>(json)
println("list $cmd")
}
}
上述代码是很简单的,主要就是有一个嵌套的解析:parse<ApiBean>(json)
, parse函数内部,就是jsonStr?.fromJson<CmdBean<T>>()
这样嵌套去解析。然后这个函数又经过一个普通的封装函数fromJson
来解析。
在本地kotlin版本
id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
//也可能写作:
kotlin = "1.8.10"
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
在1.8.10之下:
编译查看class:
由此,我们就能知道为什么会报错了。原因就是字节码写了new出来的匿名内部类是个带T的,所以因为JVM上泛型擦除出现了T,gson的TypeToken就无法解析到真实的类,因此出现了问题。当我反复对比找各种原因,最后终于怀疑到了版本上。修改到kotlin1.8.20,kotlin1.9.0, kotlin 1.9.24均发现ok 了。对比的图如下:
很明显,对于这种嵌套型的inline+reified从kotlin1.8.20开始,生成的内部类就不会再有问题。
并且,gson官方文档提到的解决方案:
Use TypeToken.getParameterized(...), for example TypeToken.getParameterized(List.class, elementType) where elementType is a type you have to provide separately.
For Kotlin users: Use reified type parameters, that means change <T> to <reified T>, if possible. If you have a chain of functions with type parameters you will probably have to make all of them reified.
其实对于kotlin项目而言,
inline fun <reified T> String.fromJson(): T {
return Globals.gson.fromJson(this, object : TypeToken<T>() {}.type)
}
这个函数就是完美的,
你可以直接传入fromJson<List<Bean>()
, 可以传入fromJson<CmdBean<InnerBean>()
的形式,他就能给你处理好。
而1.8.10的kotlin,在inline+reified嵌套后就会出现问题。
大于1.8.10的kotlin版本,则可以嵌套传导正确,就不会出现问题了。
而非kotlin的java项目,你就不得不按照官方
public static <T> T parse(String jsonStr, Class<T> t) {
return new GsonBuilder().create().fromJson(jsonStr, t);
}
public static <T, T2> MyCmd<T2> parseLv2(String jsonStr, Class<T> t, Class<T2> t2) {
Type typeToken = TypeToken.getParameterized(t, t2).getType();
return new GsonBuilder().create().fromJson(jsonStr, typeToken);
}
或者参考我上篇帖子https://blog.****.net/jzlhll123/article/details/139302991的三个解决方案。
java中就不会遇到kotlin编译内部类的问题了。而是可能会犯低级错误:
比如
var json = """
{"apiBean":{"name":"nameX","type":"typeX","url":"htpps://api.....com/.../..xxx"},"cmdId":"cmdId010102394","pro":"aaaa","ts":1029301291023}
""";
var cmd = parse(json, CmdBean.class); //错误,没有提供二级泛型。需要TypeToken.getParameterized等方式解析。
System.out.printf("cmd " + cmd);
val listCmd = parse(json, List.class) //错误,没有提供二级泛型。需要TypeToken.getParameterized等方式解析。
而kotlin,由于inline和reified就可以解决它,前提是你要注意kotlin的版本哟,1.8.10有坑!1.8.20开始好的。