Bootstrap

gson解析出现IllegalArgumentException: TypeToken type argument must not contain a type variable

结论先行:
对于List<XXXBean>class MyXXXBean() { T xxData},这两种都是嵌套泛型,我们经常会疏忽掉。有时候会忘记List其实也是一个类,List<Bean>就已经是一个二级嵌套泛型了。对于二级嵌套泛型,我们需要格外小心。最后有通用代码。(kotlin参考新帖子。)

三天后:订正:
参考我的新帖子:https://blog.csdn.net/jzlhll123/article/details/139398967

让我们从头来捋一下。
我们现在有如下一些代码:

data class MyCmd<T : Any> (
    val cmdId: String? = null,
    var deviceId: String? = null,
    val cmdData: T? = null,
)

val gson = GsonBuilder().create()
abstract class GsonParameterizedType<T> : ParameterizedType

fun <T : Any> parseCmd(jsonStr: String): MyCmd<T> {
    return gson.fromJson(jsonStr, object : TypeToken<T>() {}.type)
}

val jsonString = """
    {
     "cmdId": "aaaa",
     "cmdType": "type",
     "cmdData": {
         "cmdType": "fromNetwork",
         "errorCode": 0,
         "cmdError": 10,
         "deviceStatus": "normal"
     }
 }
 """.trimIndent()

 fun test() {
     val cmd = parseCmd<Any>(jsonString)
     cmd.deviceId = "DeviceId001"
     val cmd2 = gson.toJson(cmd)

     Log.d("", "debug $cmd")
     Log.d("", "debug $cmd2")
 }

上述代码呢,我们的目的是这样的:
我们有一个实体类MyCmd, 由于cmdData的类,可变,他的真实类可能是Class1,Class2…
我们想要做的事情呢,就是把MyCmd里面的基础字段,比如deviceId等进行修改。
再还原回去。

在gson2.10.1的库,上述代码是不会报错的。
经过调试:我们可以看到,
请添加图片描述
请添加图片描述
由于泛型传入的是Any(java则是Object),gson帮我们将cmdData解析成了LinkedTreeMap,这是他内部的结构。(如果,jsonString cmdData是string,则会被解析为String。)

但是呢,gson升级到2.11.0,由于它内部代码增加了判断:

  private static boolean isCapturingTypeVariablesForbidden() {
    return !Objects.equals(System.getProperty("gson.allowCapturingTypeVariables"), "true");
  }

  if (isCapturingTypeVariablesForbidden()) {
    verifyNoTypeVariable(typeArgument);
  }

  private static void verifyNoTypeVariable(Type type) {
    if (type instanceof TypeVariable) {
      TypeVariable<?> typeVariable = (TypeVariable<?>) type;
      throw new IllegalArgumentException(
          "TypeToken type argument must not contain a type variable; captured type variable "
              + typeVariable.getName()
              + " declared by "
              + typeVariable.getGenericDeclaration()
              + "\nSee "
              + TroubleshootingGuide.createUrl("typetoken-type-variable"));
    } else if (type instanceof GenericArrayType) {
   ...........
   ...........

官方文档中解释:

https://github.com/google/gson/blob/main/Troubleshooting.md#typetoken-type-variable
Symptom: An exception with the message ‘TypeToken type argument must not contain a type variable’ is thrown
Reason: This exception is thrown when you create an anonymous TypeToken subclass which captures a type variable, for example new TypeToken<List>() {} (where T is a type variable). At compile time such code looks safe and you can use the type List without any warnings. However, this code is not actually type-safe because at runtime due to type erasure only the upper bound of the type variable is available. For the previous example that would be List. When using such a TypeToken with any Gson methods performing deserialization this would lead to confusing and difficult to debug ClassCastExceptions. For serialization it can in some cases also lead to undesired results.
Note: Earlier version of Gson unfortunately did not prevent capturing type variables, which caused many users to unwittingly write type-unsafe code.
Solution:
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 to , if possible. If you have a chain of functions with type parameters you will probably have to make all of them reified.
If you don’t actually use Gson’s TypeToken for any Gson method, use a general purpose ‘type token’ implementation provided by a different library instead, for example Guava’s com.google.common.reflect.TypeToken.
For backward compatibility it is possible to restore Gson’s old behavior of allowing TypeToken to capture type variables by setting the system property gson.allowCapturingTypeVariables to “true”, however:
This does not solve any of the type-safety problems mentioned above; in the long term you should prefer one of the other solutions listed above. This system property might be removed in future Gson versions.
You should only ever set the property to “true”, but never to any other value or manually clear it. Otherwise this might counteract any libraries you are using which might have deliberately set the system property because they rely on its behavior.

它的意思就是说,以前的写法不够安全,容易出现cast错误。根本原因就是二级嵌套泛型获取的解析方式问题。(三天后订正:对于kotlin项目inline+reified就能自行解决这些问题。)

解决方案0:>=kotlin1.8.20 + inline+reified

参考我的新帖子:https://blog.csdn.net/jzlhll123/article/details/139398967

解决方案1: TypeToken.getParameterized()

对于直接操作这个MyCmd类而言,我们就可以通过:

	//java版本, 解析MyCmd里面的二级类型T2. 
    public static <T2> MyCmd<T2> parseCmd(String jsonStr, Class<T2> t) {
        Type typeToken = TypeToken.getParameterized(MyCmd.class, t).getType();
        return gson.fromJson(jsonStr, typeToken);
    }

	//kotlin版本,同上java版本
    fun <T2 : Any> parseCmd2(jsonStr: String, t:Class<T2>): MyCmd<T2> {
        val typeToken = TypeToken.getParameterized(MyCmd::class.java, t).type
        return gson.fromJson(jsonStr, typeToken)
    }

     //kotlin版本,inline+reified来解决。与上差不多。
    inline fun <reified T2 : Any> parseCmd3(jsonStr: String): MyCmd<T2> {
        val typeToken = TypeToken.getParameterized(MyCmd::class.java, T2::class.java).type
        return gson.fromJson(jsonStr, typeToken)
    }

//使用方法:
try {
    val cmd = parseCmd3<CmdData>(jsonString)
} catch (e:Exception) {//如果确认是json则可以不做下面的解析。
    e.printStackTrace()
    val cmd = parseCmd3<String>(jsonString)
}

这样后,你就可以给泛型传入Any(Object)了。
上述效果呢,在gson老版本不会报错,把它当做了object来解析的,而gson2.11.0版本默认已经拦截并抛出异常。
(通用解析方案我放到最后去。)

解决方案2:ParameterizedType

//二级解析办法。与上述效果相同。
 val cmd2 = try {
    gson.fromJson<MyCmd<CmdData>>(jsonString2, object : ParameterizedType {
         override fun getActualTypeArguments(): Array<Type> {
             return arrayOf(CmdData::class.java)
         }

         override fun getRawType(): Type {
             return MyCmd::class.java
         }

         override fun getOwnerType(): Type? {
             return null
         }
     })
 }catch (e:Exception) { //如果确认是json则可以不做下面的解析。
     gson.fromJson<MyCmd<String>>(jsonString2, object : ParameterizedType {
         override fun getActualTypeArguments(): Array<Type> {
             return arrayOf(String::class.java)
         }

         override fun getRawType(): Type {
             return MyCmd::class.java
         }

         override fun getOwnerType(): Type? {
             return null
         }
     })
 }


gson.fromJson<List<ApiDeviceModel>>(text, object : ParameterizedType {
  override fun getActualTypeArguments(): Array<Type> {
          return arrayOf(ApiDeviceModel::class.java)
      }

      override fun getRawType(): Type {
          return List::class.java
      }

      override fun getOwnerType(): Type? {
          return null
      }
  })

这种写法,稍微复杂一丢丢,注意rawType就是外层bean类,actualTypeArgs,你看它是Array其实就是二级可以多个泛型。这里也列出了2种。
注意,List解析也需要如此。

解决方案3: 移除二级嵌套泛型

那么,就直接修改外层bean类,自然,List<Bean>是不行的。没有嵌套的泛型,顺次解决后续的:

data class MyCmd (
    val cmdId: String? = null,
    var deviceId: String? = null,
    val cmdData: Any? = null,
)

//就可以这样去解析了。
gson.fromJson(jsonStr, object : TypeToken<T>() {}.type)

解析二级泛型:

想要得到二级的结果:
第一种:

  val cmd = parseJsonLv2<MyCmd<Any>, Any>(jsonString)

  if (cmd.cmdData is String) { //cmdData是String类型
      //...
      Log.d("", "" + cmd.cmdData)
  } else if (cmd.cmdData is LinkedTreeMap<*, *>) { //cmdData继续是json对象,进而转成具体的某个类。
      val cmdData = gson.fromJson(gson.toJson(cmd.cmdData), CmdData1::class.java)
      //val cmdData = gson.fromJson(gson.toJson(cmd.cmdData), CmdData2::class.java)
      //val cmdData = gson.fromJson(gson.toJson(cmd.cmdData), CmdData3::class.java)
      Log.d("", "cmdData $cmdData")
  }

第二种似乎更好一点:

  try {
      val cmd = parseJsonLv2<MyCmd<CmdData>, CmdData>(jsonString2)
  } catch (e:Exception) {
      e.printStackTrace()
      val cmd = parseJsonLv2<MyCmd<String>, String>(jsonString2)
  }

上述2种都可以通配String和对象类。而且必须这样写,不然就要有问题啦。
所以懂了吧,
对于有嵌套的泛型gson解析,需要引起注意。

通用代码函数

可以用作GsonUtil工具类。如下:

//一级泛型解析:不支持List<xxxBean>, 
//org.jetbrains.kotlin.android 1.8.10 有bug不支持二级嵌套xxxBean<CmdData>
//新版就没问题了。 
inline fun <reified T> String.parseJson(): T {
    if (T::class.java == String::class.java) {
        return this as T
    }
    return gson.fromJson(this, object : TypeToken<T>() {}.type)
}

//二级泛型解析:使用TypeToken的版本。org.jetbrains.kotlin.android >1.8.10 则无需如此。
inline fun <reified T, reified TLv2> String.parseJsonLv2(): T {
    if (T::class.java == String::class.java) {
        return this as T
    }
    val typeToken = TypeToken.getParameterized(T::class.java, TLv2::class.java).type
    return gson.fromJson(this, typeToken)
}

abstract class GsonParameterizedType<T> : ParameterizedType

//二级泛型解析:使用ParameterizedType的版本。org.jetbrains.kotlin.android >1.8.10 则无需如此。
inline fun <reified T, reified T2> parseJsonLv2_v2(jsonStr: String): T {
    return gson.fromJson(jsonStr, object : GsonParameterizedType<T>() {
        override fun getActualTypeArguments(): Array<Type> {
            return arrayOf(T2::class.java)
        }

        override fun getRawType(): Type {
            return T::class.java
        }

        override fun getOwnerType(): Type? {
            return null
        }
    })
}

//二级泛型有2个的解析:平常比较少见,我这里注释掉了。
//inline fun <reified T, reified TLv21, reified TLv22> String.parseJsonLv22(): T {
//    if (T::class.java == String::class.java) {
//        return this as T
//    }
//    val typeToken = TypeToken.getParameterized(T::class.java, TLv21::class.java, TLv22::class.java).type
//    return gson.fromJson(this, typeToken)
//}

//java版本
//通用二级嵌套解析
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);
}

使用的样例代码:

jsonStr.formJsonStringLv2One<CustomBean<T>, T>()

text?.parseJsonLv2<List<ModelBean>, ModelBean>()

org.jetbrains.kotlin.android >1.8.10 则不用担心,嵌套的泛型解析。

;