Scala flink解析映射中的JSON:InvalidProgrameException:Task不可序列化

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

我正在处理一个Flink项目,希望将源JSON字符串数据解析为JSON对象。我使用的是JSON解析。然而,我在Flink API中使用JSON解析器时遇到了一些问题(例如,
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更改