【问题标题】:Scala - Instantiate classes dynamically from a StringScala - 从字符串动态实例化类
【发布时间】:2014-11-03 17:14:08
【问题描述】:

我正在尝试创建一个动态解析器​​,它允许我根据类名将 json 内容解析为不同的类。

我会得到 json 和类名(作为字符串),我想做这样的事情:

val theCaseClassName = "com.ardlema.JDBCDataProviderProperties" 
val myCaseClass = Class.forName(theCaseClassName)
val jsonJdbcProperties = """{"url":"myUrl","userName":"theUser","password":"thePassword"}"""
val json = Json.parse(jsonJdbcProperties)
val value = Try(json.as[myClass])

上述代码显然无法编译,因为 json.as[] 方法试图将节点转换为“T”(我为我的案例类定义了一个隐式 Reads[T])

从原始字符串中获取正确的“T”以传递给 json.as[] 方法的最佳方法是什么?

【问题讨论】:

标签: json scala reflection


【解决方案1】:

一个很好的解决方案可能是进行多态反序列化。这允许您向您的 json 添加一个字段(如“类型”)并允许Jackson(假设您使用像 Jackson 这样的出色 json 解析器)代表您找出正确的类型。看起来你可能没有使用 Jackson;我保证它值得使用。

post 很好地介绍了多态类型。它涵盖了许多有用的情况,包括您无法修改 3rd 方代码的情况(这里您添加了一个 Mixin 来注释类型层次结构)。

最简单的情况最终看起来像这样(所有这些都适用于 Scala 对象——jackson even has a great scala module):

object Test {
  @JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    include = JsonTypeInfo.As.PROPERTY,
    property = "type"
  )
  @JsonSubTypes(Array(
    new Type(value = classOf[Cat], name = "cat"),
    new Type(value = classOf[Dog], name = "dog")
  ))
  trait Animal

  case class Dog(name: String, breed: String, leash_color: String) extends Animal
  case class Cat(name: String, favorite_toy: String) extends Animal

  def main(args: Array[String]): Unit = {
    val objectMapper = new ObjectMapper with ScalaObjectMapper
    objectMapper.registerModule(DefaultScalaModule)

    val dogStr = """{"type": "dog", "name": "Spike", "breed": "mutt",  "leash_color": "red"}"""
    val catStr = """{"type": "cat", "name": "Fluffy", "favorite_toy": "spider ring"}"""

    val animal1 = objectMapper.readValue[Animal](dogStr)
    val animal2 = objectMapper.readValue[Animal](catStr)

    println(animal1)
    println(animal2)
  }
}

这会生成以下输出:

// Dog(Spike,mutt,red)
// Cat(Fluffy,spider ring)

您也可以避免列出子类型映射,但它要求 json "type" 字段有点复杂。尝试一下;你可能会喜欢它。像这样定义动物:

@JsonTypeInfo(
  use = JsonTypeInfo.Id.CLASS,
  include = JsonTypeInfo.As.PROPERTY,
  property = "type"
)
trait Animal

它会像这样产生(和消费)json:

/*
{
    "breed": "mutt",
    "leash_color": "red",
    "name": "Spike",
    "type": "classpath.to.Test$Dog"
}
{
    "favorite_toy": "spider ring",
    "name": "Fluffy",
    "type": "classpath.to.Test$Cat"
}
*/

【讨论】:

  • 这听起来很酷,但我正在使用包含一个非常强大的 JSON 库的 Play 框架,所以我不想包含对外部库的另一个依赖项。不确定我是否不能用 Play json 库做类似的事情。
  • *咳嗽*咳嗽;如果它不给你多态反序列化,显然不是那么强大。查看他们的 API,似乎无法从 classOf[T] 或类型标记中获取 Reads[T] 的实例。如果您不愿意使用 Jackson,我认为唯一的选择是 @lmm 建议的。
  • 我刚刚尝试了您的代码并得到以下异常:线程“主”com.fasterxml.jackson.databind.JsonMappingException 中的异常:无法构造 com.stratio.datavis 的实例.Test$Animal,问题:抽象类型要么需要映射到具体类型,要么需要自定义反序列化器,要么在 [Source: {"type": "dog", "name": "Spike", “品种”:“杂种”,“leash_color”:“红色”};行:1,列:1] 在 com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:164)
  • @ardlema 听起来您可能缺少动物特征上的 @JsonSubTypes 映射。您使用的是哪种动物特征?我只是将顶部块复制/粘贴到当前导入的项目中:com.fasterxml.jackson.annotation._com.fasterxml.jackson.annotation.JsonSubTypes.Type,它工作得很好。我正在使用杰克逊 2.4.3。
  • 酷!是的,这是我的错,我没有导入正确的类。它现在正在工作。我想我正在走这条路。非常感谢您的帮助!!
【解决方案2】:

您应该根据班级名称选择您的Reads[T]。不幸的是,这可能必须是手动模式匹配:

val r: Reads[_] = theCaseClassName match {
  case "com.ardlema.JDBCDataProviderProperties" => JDBCReads
  case ... => ...
}
val value = json.as(r).asInstanceOf[...]

另外,看看json.as的实现;在某些时候,它可能需要classTag,然后调用.runtimeClass。假设是这样,您可以做任何事情并在那里传递您自己的myCaseClass

【讨论】:

  • 听起来不错,但我想避免实现与所有选项匹配的模式(它会打破我的“动态”想法)。我将按照您的建议查看 json.as 实现。
猜你喜欢
  • 1970-01-01
  • 2014-11-15
  • 1970-01-01
  • 2021-06-25
  • 1970-01-01
  • 2012-12-23
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多