我正在研究Flink项目,并希望将源JSON字符串数据解析为Json Object。我正在使用jackson-module-scala进行JSON解析。但是,我在Flink API(例如map
)中使用JSON解析器时遇到了一些问题。
以下是代码的一些示例,我无法理解其背后的原因,为什么它会像这样。
在这种情况下,我正在做jackson-module-scala's official exmaple code told me to do:
ObjectMapper
DefaultScalaModule
DefaultScalaModule
是一个Scala对象,包含对所有当前支持的Scala数据类型的支持。
readValue
以将JSON解析为Map
我收到的错误是:org.apache.flink.api.common.InvalidProgramException:
Task not serializable
。
object JsonProcessing {
def main(args: Array[String]) {
// set up the execution environment
val env = StreamExecutionEnvironment.getExecutionEnvironment
// get input data
val text = env.readTextFile("xxx")
val mapper = new ObjectMapper
mapper.registerModule(DefaultScalaModule)
val counts = text.map(mapper.readValue(_, classOf[Map[String, String]]))
// execute and print result
counts.print()
env.execute("JsonProcessing")
}
}
然后我做了一些Google,并提出了以下解决方案,其中registerModule
被移入map
函数。
val mapper = new ObjectMapper
val counts = text.map(l => {
mapper.registerModule(DefaultScalaModule)
mapper.readValue(l, classOf[Map[String, String]])
})
然而,我无法理解的是:为什么这会起作用,使用外部定义的对象mapper
的调用方法?是因为{{1} }本身是Serializable,如此处所述ObjectMapper.java#L114?
现在,JSON解析工作正常,但每次都要调用ObjectMapper
,我认为这可能会导致一些性能问题( 是吗? )。我还尝试了另外一种解决方案。
我创建了一个新的mapper.registerModule(DefaultScalaModule)
,并将其用作相应的解析类,注册Scala模块。它也工作正常。
但是,如果您的输入JSON经常变化,则这不是那么灵活。管理班级case class Jsen
是不可维护的。
Jsen
此外,我还尝试使用case class Jsen(
@JsonProperty("a") a: String,
@JsonProperty("c") c: String,
@JsonProperty("e") e: String
)
object JsonProcessing {
def main(args: Array[String]) {
...
val mapper = new ObjectMapper
val counts = text.map(mapper.readValue(_, classOf[Jsen]))
...
}
而未调用JsonNode
,如下所示:
registerModule
它的工作正常。
我的主要问题是:实际导致 ...
val mapper = new ObjectMapper
val counts = text.map(mapper.readValue(_, classOf[JsonNode]))
...
引发任务不可序列化问题的原因是什么?
如何确定您的代码是否可能在编码过程中导致此不可序列化的问题?
答案 0 :(得分:3)
问题在于Apache Flink旨在分发。这意味着它需要能够远程运行您的代码。因此,这意味着您的所有处理功能都应该是可序列化的。在当前实现中,即使您不在任何分布式模式下运行此流程,也可以在构建流式流程时尽早确保。这是一种权衡,显而易见的好处是可以向您提供反馈,直至违反此合同的行(通过异常堆栈跟踪)。
所以当你写
val counts = text.map(mapper.readValue(_, classOf[Map[String, String]]))
你实际写的是
val counts = text.map(new Function1[String, Map[String, String]] {
val capturedMapper = mapper
override def apply(param: String) = capturedMapper.readValue(param, classOf[Map[String, String]])
})
这里重要的是你从外部上下文中捕获mapper
并将其存储为必须可序列化的Function1
对象的一部分。这意味着mapper
必须是可序列化的。杰克逊图书馆的设计师认识到了这种需求,因为在映射器中没有任何根本不可分辨的东西,他们使ObjectMapper
和默认的Module
可序列化。不幸的是,Scala Jackson Module的设计师错过了这一点,并通过使ScalaTypeModifier
和所有子类不可序列化而使DefaultScalaModule
深度非串行化。这就是为什么你的第二个代码有效,而第一个代码没有:" raw" ObjectMapper
可序列化,而预先注册ObjectMapper
的{{1}}则不可。
有一些可能的解决方法。可能最简单的就是包裹DefaultScalaModule
ObjectMapper
然后将其用作
object MapperWrapper extends java.io.Serializable {
// this lazy is the important trick here
// @transient adds some safety in current Scala (see also Update section)
@transient lazy val mapper = {
val mapper = new ObjectMapper
mapper.registerModule(DefaultScalaModule)
mapper
}
def readValue[T](content: String, valueType: Class[T]): T = mapper.readValue(content, valueType)
}
此val counts = text.map(MapperWrapper.readValue(_, classOf[Map[String, String]]))
技巧有效,因为虽然lazy
的实例不可序列化,但创建DefaultScalaModule
实例的函数是。
更新:@transient怎么样?
如果我添加
DefaultScalaModule
与lazy val
相比,这里有什么不同?
这实际上是一个棘手的问题。编译@transient lazy val
的内容实际上是这样的:
lazy val
object MapperWrapper extends java.io.Serializable {
// @transient is set or not set for both fields depending on its presence at "lazy val"
[@transient] private var mapperValue: ObjectMapper = null
[@transient] @volatile private var mapperInitialized = false
def mapper: ObjectMapper = {
if (!mapperInitialized) {
this.synchronized {
val mapper = new ObjectMapper
mapper.registerModule(DefaultScalaModule)
mapperValue = mapper
mapperInitialized = true
}
}
mapperValue
}
def readValue[T](content: String, valueType: Class[T]): T = mapper.readValue(content, valueType)
}
上的@transient
影响两个支持字段。所以现在你可以看到为什么lazy val
技巧起作用了:
在本地工作,因为它会延迟lazy val
字段的初始化,直到第一次访问mapperValue
方法,因此在执行序列化检查时字段是安全的mapper
远程工作,因为null
是完全可序列化的,并且应该将MapperWrapper
如何初始化的逻辑放入同一类的方法中(参见lazy val
)。
但是请注意,AFAIK编译def mapper
的这种行为是当前Scala编译器的实现细节,而不是Scala规范的一部分。如果稍后将类似于.Net Lazy
的类添加到Java标准库中,则Scala编译器可能会开始生成不同的代码。这很重要,因为它为lazy val
提供了一种权衡。现在添加@transient
的好处是它可以确保像这样的代码也能正常工作:
@transient
如果没有val someJson:String = "..."
val something:Something = MapperWrapper.readValue(someJson:String, ...)
val counts = text.map(MapperWrapper.readValue(_, classOf[Map[String, String]]))
,上面的代码将失败,因为我们强制初始化@transient
支持字段,现在它包含一个不可序列化的值。使用lazy
这不是问题,因为该字段根本不会被序列化。
@transient
的一个潜在缺点是,如果Scala更改了生成@transient
的代码的方式,并且该字段标记为lazy val
,则实际上可能未对其进行反序列化远程工作场景。
还有@transient
的技巧,因为对于object
,Scala编译器生成自定义反序列化逻辑(覆盖readResolve
)以返回相同的单例对象。这意味着包含object
的对象并未真正反序列化,并且使用了lazy val
本身的值。这意味着object
内的@transient lazy val
比远程方案中的object
更能面向未来。