【问题标题】:snakeyaml and spark results in an inability to construct objectssnakeyaml 和 spark 导致无法构造对象
【发布时间】:2016-10-26 10:51:51
【问题描述】:

下面的代码在给定snakeyaml 1.17版的scala shell中执行良好

import org.yaml.snakeyaml.Yaml
import org.yaml.snakeyaml.constructor.Constructor
import scala.collection.mutable.ListBuffer
import scala.beans.BeanProperty

class EmailAccount {
  @scala.beans.BeanProperty var accountName: String = null

  override def toString: String = {
    return s"acct ($accountName)"
  }
}

val text = """accountName: Ymail Account"""

val yaml = new Yaml(new Constructor(classOf[EmailAccount]))
val e = yaml.load(text).asInstanceOf[EmailAccount]
println(e)

但是,在 spark(本例中为 2.0.0)中运行时,产生的错误是:

org.yaml.snakeyaml.constructor.ConstructorException: Can't construct a java object for tag:yaml.org,2002:EmailAccount; exception=java.lang.NoSuchMethodException: EmailAccount.<init>()
 in 'string', line 1, column 1:
    accountName: Ymail Account
    ^

  at org.yaml.snakeyaml.constructor.Constructor$ConstructYamlObject.construct(Constructor.java:350)
  at org.yaml.snakeyaml.constructor.BaseConstructor.constructObject(BaseConstructor.java:182)
  at org.yaml.snakeyaml.constructor.BaseConstructor.constructDocument(BaseConstructor.java:141)
  at org.yaml.snakeyaml.constructor.BaseConstructor.getSingleData(BaseConstructor.java:127)
  at org.yaml.snakeyaml.Yaml.loadFromReader(Yaml.java:450)
  at org.yaml.snakeyaml.Yaml.load(Yaml.java:369)
  ... 48 elided
Caused by: org.yaml.snakeyaml.error.YAMLException: java.lang.NoSuchMethodException: EmailAccount.<init>()
  at org.yaml.snakeyaml.constructor.Constructor$ConstructMapping.createEmptyJavaBean(Constructor.java:220)
  at org.yaml.snakeyaml.constructor.Constructor$ConstructMapping.construct(Constructor.java:190)
  at org.yaml.snakeyaml.constructor.Constructor$ConstructYamlObject.construct(Constructor.java:346)
  ... 53 more
Caused by: java.lang.NoSuchMethodException: EmailAccount.<init>()
  at java.lang.Class.getConstructor0(Class.java:2810)
  at java.lang.Class.getDeclaredConstructor(Class.java:2053)
  at org.yaml.snakeyaml.constructor.Constructor$ConstructMapping.createEmptyJavaBean(Constructor.java:216)
  ... 55 more

我用

启动了 scala shell
scala -classpath "/home/placey/snakeyaml-1.17.jar"

我用

启动了 spark shell
/home/placey/Downloads/spark-2.0.0-bin-hadoop2.7/bin/spark-shell --master local --jars /home/placey/snakeyaml-1.17.jar

【问题讨论】:

  • 它说找不到 Student 类,它是 yaml 类还是您的应用程序中的自定义类?你是从另一个 jar 导入它吗?
  • 那是应用程序中的自定义类。直接实例化它可以正常工作。
  • @placeybordeaux 我的回答解决了你的问题吗?
  • @PawełBartkiewicz 我还没有测试过,我会尽快分配赏金并接受。
  • @placeybordeaux 谢谢,我只是想知道我的回答是否有什么可以改进的地方。抱歉,如果感觉有点唠叨,等到赏金期结束绝对是个好主意。总有可能有人会想出更好的答案。

标签: scala apache-spark snakeyaml


【解决方案1】:

解决方案

创建一个self-contained application 并使用spark-submit 运行它,而不是使用spark-shell

我已经为您创建了一个最小项目,名称为 gist here。您需要做的就是将两个文件(build.sbtMain.scala)放在某个目录中,然后运行:

sbt package

为了创建一个 JAR。 JAR 将位于target/scala-2.11/sparksnakeyamltest_2.11-1.0.jar 或类似位置。如果您还没有使用它,您可以get SBT from here。最后就可以运行项目了:

/home/placey/Downloads/spark-2.0.0-bin-hadoop2.7/bin/spark-submit --class "Main" --master local --jars /home/placey/snakeyaml-1.17.jar target/scala-2.11/sparksnakeyamltest_2.11-1.0.jar

输出应该是:

[many lines of Spark's log)]
acct (Ymail Account)
[more lines of Spark's log)]

说明

Spark 的外壳 (REPL) 通过将 $iw 参数添加到您的构造函数来转换您在其中定义的所有类。我有explained it here。 SnakeYAML 期望类 JavaBean 类的零参数构造函数,但没有,因此失败。

你可以自己试试:

scala> class Foo() {}
defined class Foo

scala> classOf[Foo].getConstructors()
res0: Array[java.lang.reflect.Constructor[_]] = Array(public Foo($iw))

scala> classOf[Foo].getConstructors()(0).getParameterCount
res1: Int = 1

如您所见,Spark 通过添加$iw 类型的参数来转换构造函数。

替代解决方案

定义你自己的Constructor

如果你真的需要让它在 shell 中工作,你可以定义你自己的类来实现 org.yaml.snakeyaml.constructor.BaseConstructor 并确保 $iw 被传递给构造函数,但这是很多工作(我实际上是自己写的Constructor 前段时间出于安全原因在 Scala 中使用,所以我对此有一些经验。

您还可以定义一个自定义的Constructor 硬编码来实例化特定类(在您的情况下为EmailAccount),类似于DiceConstructor shown in SnakeYAML's documentation。这要容易得多,但需要为您想要支持的每个类编写代码。

例子:

case class EmailAccount(accountName: String)

class EmailAccountConstructor extends org.yaml.snakeyaml.constructor.Constructor {

  val emailAccountTag = new org.yaml.snakeyaml.nodes.Tag("!emailAccount")
  this.rootTag = emailAccountTag
  this.yamlConstructors.put(emailAccountTag, new ConstructEmailAccount)

  private class ConstructEmailAccount extends org.yaml.snakeyaml.constructor.AbstractConstruct {
    def construct(node: org.yaml.snakeyaml.nodes.Node): Object = {
      // TODO: This is fine for quick prototyping in a REPL, but in a real
      //       application you should probably add type checks.
      val mnode = node.asInstanceOf[org.yaml.snakeyaml.nodes.MappingNode]
      val mapping = constructMapping(mnode)
      val name = mapping.get("accountName").asInstanceOf[String]
      new EmailAccount(name)
    }
  }

}

您可以将其保存为文件并使用 :load filename.scala 将其加载到 REPL 中。

此解决方案的额外优势是它可以直接创建不可变的案例类实例。不幸的是,Scala REPL 似乎在导入方面存在问题,所以我使用了完全限定名称。

不要使用 JavaBeans

您也可以将 YAML 文档解析为简单的 Java 映射:

scala> val yaml2 = new Yaml()
yaml2: org.yaml.snakeyaml.Yaml = Yaml:1141996301

scala> val e2 = yaml2.load(text)
e2: Object = {accountName=Ymail Account}

scala> val map = e2.asInstanceOf[java.util.Map[String, Any]]
map: java.util.Map[String,Any] = {accountName=Ymail Account}

scala> map.get("accountName")
res4: Any = Ymail Account

这样 SnakeYAML 就不需要使用反射了。

但是,由于您使用的是 Scala,我建议您尝试 MoultingYAML,它是 SnakeYAML 的 Scala 包装器。它将 YAML 文档解析为简单的 Java 类型,然后将它们映射到 Scala 类型(甚至是您自己的类型,如 EmailAccount)。

【讨论】:

猜你喜欢
  • 2011-06-26
  • 2018-03-03
  • 2012-05-06
  • 2016-10-18
  • 1970-01-01
  • 2013-08-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多