Scala flink解析映射中的JSON:InvalidProgrameException:Task不可序列化
我正在处理一个Flink项目,希望将源JSON字符串数据解析为JSON对象。我使用的是JSON解析。然而,我在Flink API中使用JSON解析器时遇到了一些问题(例如,Scala flink解析映射中的JSON:InvalidProgrameException:Task不可序列化,scala,serialization,jackson,apache-flink,flink-streaming,Scala,Serialization,Jackson,Apache Flink,Flink Streaming,我正在处理一个Flink项目,希望将源JSON字符串数据解析为JSON对象。我使用的是JSON解析。然而,我在Flink API中使用JSON解析器时遇到了一些问题(例如,map) 下面是一些代码示例,我无法理解为什么它会这样做 情景1: 在这种情况下,我所做的是: 创建新的ObjectMapper 注册DefaultScalaModule DefaultScalaModule是一个Scala对象,它包括对当前支持的所有Scala数据类型的支持 调用readValue以将JSON解析为Map
map
)
下面是一些代码示例,我无法理解为什么它会这样做
情景1:
在这种情况下,我所做的是:
ObjectMapper
DefaultScalaModule
DefaultScalaModule
是一个Scala对象,它包括对当前支持的所有Scala数据类型的支持
readValue
以将JSON解析为Map
org.apache.flink.api.common.invalidProgrameException:任务不可序列化
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")
}
}
情景2:
然后我在谷歌上搜索了一下,并提出了以下解决方案,其中registerModule
被移动到map
函数中
val mapper = new ObjectMapper
val counts = text.map(l => {
mapper.registerModule(DefaultScalaModule)
mapper.readValue(l, classOf[Map[String, String]])
})
然而,我无法理解的是:为什么使用外部定义的对象映射器的调用方法会起作用?
是因为对象映射器本身是可序列化的吗
现在,JSON解析工作正常,但每次我都必须调用mapper.registerModule(DefaultScalaModule)
,我认为这可能会导致一些性能问题(真的吗?)。我还尝试了另一种解决方案,如下所示
情景3:
我创建了一个新的案例类Jsen
,并将其用作相应的解析类,注册Scala模块。而且它也工作得很好
然而,如果您的输入JSON经常变化,那么这就不是那么灵活了。管理类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]))
...
它也工作得很好
我的主要问题是:在注册模块(DefaultScalaModule)
的保护下,任务不可序列化的实际原因是什么?
如何确定您的代码在编码过程中是否可能导致这种不可序列化的问题?问题在于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]])
})
这里重要的一点是从外部上下文捕获映射器
,并将其存储为Function1
对象的一部分,该对象必须可序列化。这意味着映射器必须是可序列化的。Jackson library的设计者认识到了这种需求,并且由于映射器中没有任何基本上不可序列化的东西,他们将其对象映射器
和默认的模块
序列化。不幸的是,Scala-Jackson模块的设计者忽略了这一点,通过使所有子类都不可序列化,使他们的DefaultScalaModule
完全不可序列化。这就是为什么第二个代码可以工作而第一个代码不能工作的原因:“raw”ObjectMapper
是可序列化的,而带有预注册的DefaultScalaModule
的ObjectMapper
不是
有一些可能的解决办法。可能最简单的方法是包装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
的实例不可序列化,但创建DefaultScalaModule
实例的函数是可序列化的
更新:@transient怎么样?
如果我添加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)
}
其中,lazy val
上的@transient
会影响两个备份字段。现在,您可以了解为什么lazy val
技巧有效:
它在本地工作,因为它延迟了mapperValue
字段的初始化,直到第一次访问mapper
方法,所以在执行序列化检查时,该字段是安全的null
它可以远程工作,因为MapperWrapper
是完全可序列化的,并且应该如何初始化lazy val
的逻辑被放入同一类的方法中(请参见def mapper
)
但是请注意,AFAIK编译lazy val
的方式是当前Scala编译器的一个实现细节,而不是Scala规范的一部分。如果在稍后某个时候,类似于.Net的类将被添加到Java标准库中,Scala编译器可能会开始生成不同的代码。这一点很重要,因为它为@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
,这不是问题,因为该字段根本不会被序列化
@transient
的一个潜在缺点是,如果Scala更改