snakeyaml 和 spark 导致无法构造对象
snakeyaml and spark results in an inability to construct objects
给定 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
解决方案
使用 spark-submit
而不是 spark-shell
创建 self-contained application 和 运行 它。
我已经为您创建了一个最小的项目 gist here。您需要做的就是将两个文件(build.sbt
和 Main.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 的 shell (REPL) transforms all classes you define in it by adding $iw
parameter to your constructors. I've 。SnakeYAML 期望 JavaBean-like classes 的零参数构造函数,但没有,所以它失败。
你可以自己试试:
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 中工作,你可以定义你自己的 class 实现 org.yaml.snakeyaml.constructor.BaseConstructor
并确保 $iw
被传递给构造函数, 但这是很多工作(前段时间出于安全原因,我实际上在 Scala 中编写了自己的 Constructor
,所以我对此有一些经验)。
您还可以定义自定义 Constructor
硬编码以实例化特定 class(在您的情况下为 EmailAccount
),类似于 DiceConstructor
shown in SnakeYAML's documentation.这要容易得多,但需要为每个要支持的 class 编写代码。
示例:
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 中加载它。
此解决方案的额外优势是它可以直接创建不可变案例 class 实例。不幸的是,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
)。
给定 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 shellscala -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
解决方案
使用 spark-submit
而不是 spark-shell
创建 self-contained application 和 运行 它。
我已经为您创建了一个最小的项目 gist here。您需要做的就是将两个文件(build.sbt
和 Main.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 的 shell (REPL) transforms all classes you define in it by adding $iw
parameter to your constructors. I've
你可以自己试试:
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 中工作,你可以定义你自己的 class 实现 org.yaml.snakeyaml.constructor.BaseConstructor
并确保 $iw
被传递给构造函数, 但这是很多工作(前段时间出于安全原因,我实际上在 Scala 中编写了自己的 Constructor
,所以我对此有一些经验)。
您还可以定义自定义 Constructor
硬编码以实例化特定 class(在您的情况下为 EmailAccount
),类似于 DiceConstructor
shown in SnakeYAML's documentation.这要容易得多,但需要为每个要支持的 class 编写代码。
示例:
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 中加载它。
此解决方案的额外优势是它可以直接创建不可变案例 class 实例。不幸的是,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
)。