【问题标题】:Moshi with kotlin generics throws No JsonAdapter for interface带有 kotlin 泛型的 Moshi 为接口抛出 No JsonAdapter
【发布时间】:2020-03-10 05:29:53
【问题描述】:

假设我有一个接口IRunnable 和两个实现CatDog

interface IRunnable {
  fun run()
}

class Cat : IRunnable {
  override fun run() { println("cat running") }
}

class Dog : IRunnable {
  override fun run() { println("dog running") }
}

还有一个接口可以将IRunnable转换成Map<String,String>,key=runnable

interface IMapConverter<T> {
  fun getMap(impl : T) : Map<String , String>
  fun getImpl(map : Map<String , String>) : T?
}
class RunnableConverter : IMapConverter<IRunnable> {
  private val key = "runnable"

  override fun getMap(impl: IRunnable): Map<String, String> {
    val value = when(impl) {
      is Cat -> "C"
      is Dog -> "D"
      else -> throw RuntimeException("error")
    }
    return mapOf(key to value)
  }

  override fun getImpl(map: Map<String, String>): IRunnable? {
    return map[key]?.let { value ->
      when (value) {
        "C" -> Cat()
        "D" -> Dog()
        else -> throw RuntimeException("error")
      }
    }
  }
}

然后使用 moshi ,我希望 IMapConverter 变成 JsonAdapter ,所以我在 RunnableConverter 中添加了一个 getAdapter() 函数:

fun getAdapter() : JsonAdapter<IRunnable> {
    return object : JsonAdapter<IRunnable>() {

      @ToJson
      override fun toJson(writer: JsonWriter, runnable: IRunnable?) {
        runnable?.also { impl ->
          writer.beginObject()
          getMap(impl).forEach { (key , value) ->
            writer.name(key).value(value)
          }
          writer.endObject()
        }
      }

      @FromJson
      override fun fromJson(reader: JsonReader): IRunnable? {
        reader.beginObject()

        val map = mutableMapOf<String , String>().apply {
          while (reader.hasNext()) {
            put(reader.nextName() , reader.nextString())
          }
        }
        val result = getImpl(map)

        reader.endObject()
        return result
      }
    }
  }

效果很好:

  @Test
  fun testConverter1() {
    val converter = RunnableConverter()

    val moshi = Moshi.Builder()
      .add(converter.getAdapter())
      .build()

    val adapter = moshi.adapter<IRunnable>(IRunnable::class.java)
    adapter.toJson(Dog()).also { json ->
      assertEquals("""{"runnable":"D"}""" , json)
      adapter.fromJson(json).also { runnable ->
        assertTrue(runnable is Dog)
      }
    }
  }

但是,当我想将适配器外部化到扩展功能时:

fun <T> IMapConverter<T>.toJsonAdapter() : JsonAdapter<T> {
  return object : JsonAdapter<T>() {

    @ToJson
    override fun toJson(writer: JsonWriter, value: T?) {
      value?.also { impl ->
        writer.beginObject()
        getMap(impl).forEach { (key , value) ->
          writer.name(key).value(value)
        }
        writer.endObject()
      }
    }

    @FromJson
    override fun fromJson(reader: JsonReader): T? {
      reader.beginObject()

      val map = mutableMapOf<String , String>().apply {
        while (reader.hasNext()) {
          put(reader.nextName() , reader.nextString())
        }
      }

      val result = getImpl(map)
      reader.endObject()
      return result
    }
  }
}

当我想测试功能时:

  @Test
  fun testConverter2() {
    val converter = RunnableConverter()

    val moshi = Moshi.Builder()
      .add(converter.toJsonAdapter()) // this is extension function
      .build()

    val adapter = moshi.adapter<IRunnable>(IRunnable::class.java)
    adapter.toJson(Dog()).also { json ->
      logger.info("json of dog = {}", json)
      assertEquals("""{"runnable":"D"}""" , json)
      adapter.fromJson(json).also { runnable ->
        assertTrue(runnable is Dog)
      }
    }
  }

编译正常,但报错:

java.lang.IllegalArgumentException: No JsonAdapter for interface moshi.IRunnable (with no annotations)

    at com.squareup.moshi.Moshi.adapter(Moshi.java:148)
    at com.squareup.moshi.Moshi.adapter(Moshi.java:98)
    at com.squareup.moshi.Moshi.adapter(Moshi.java:72)
    at moshi.MoshiTest.testConverter2(MoshiTest.kt:39)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)

我尝试在 Moshi.java 的第 137 行设置断点 https://github.com/square/moshi/blob/40a829ef181e7097087ec0e95cdcf3e3fbba3156/moshi/src/main/java/com/squareup/moshi/Moshi.java#L137

这是调试截图:

好的(定义在RunnableConverter):

这是由扩展函数创建的通用版本:

似乎因为类型擦除,它无法工作...... 有什么办法可以解决吗?

对于任何对此感兴趣的人,完整的代码在这里: https://gist.github.com/smallufo/985db4719c5434a5f57f06b14011de78

环境:

<dependency>
  <groupId>com.squareup.moshi</groupId>
  <artifactId>moshi-kotlin</artifactId>
  <version>1.9.2</version>
</dependency>

谢谢。

【问题讨论】:

    标签: generics kotlin type-erasure moshi


    【解决方案1】:

    选项 1(我希望它可以工作,但不确定):尝试将 toJsonAdapter 更改为 inline fun &lt;reified T&gt; IMapConverter&lt;T&gt;.toJsonAdapter() : JsonAdapter&lt;T&gt; { ... }

    选项 2:使用允许指定类型的 Moshi.Builder.add 重载之一(Class&lt;T&gt; 扩展 Type):

    .add(IRunnable::class.java, converter.toJsonAdapter())
    

    为避免输入错误类型,您可以再次使用reified

    inline fun <reified T> Moshi.Builder.add(converter: IMapConverter<T>) = add(T::class.java, converter.toJsonAdapter())
    

    然后

    Moshi.Builder().add(converter)
    

    【讨论】:

    • 是的,Moshi 需要能够反省 T 在这里。我推荐选项 2 的变体;返回 JsonAdapter 的实例,而不是带有注释方法的对象。
    • 选项1也使用了JsonAdapter的实例,reified的重点是它应该继承JsonAdapter&lt;Runnable&gt;而不是JsonAdapter&lt;T&gt;
    • 谢谢,这两个选项都很好用!我从没想过reified 解决方案。
    【解决方案2】:

    您需要使用@JvmSuppressWildcards 禁止通配符。

    所以你的情况应该是这样的

    interface IMapConverter<@JvmSuppressWildcards T> {
      fun getMap(impl : T) : Map<String , String>
      fun getImpl(map : Map<String , String>) : T?
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2023-03-31
      • 2022-01-26
      • 1970-01-01
      • 1970-01-01
      • 2020-10-18
      • 2021-09-30
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多