如何 Json 编码从 Play 中的特征继承的案例 class?

How to Json encode a case class that inherits from a trait in Play?

我有一个 trait,它是某些情况下的基础 class classes:

trait BaseHeaders {
  val timestamp: LocalDateTime
  val host: String
  val os: String
  val method: String
  val userAgent: String
}

object BaseHeaders {
  implicit val traitWrites = new Writes[BaseHeaders] {
    def writes(baseHeaders: BaseHeaders) = JsString(BaseHeaders.toString)
  }
}

我的一个案例 classes 与以下案例类似:

case class PaymentHeader(
                          timestamp: LocalDateTime,
                          host: String,
                          os: String,
                          method: String,
                          path: String,
                          userAgent: String,
                          paymentStatus: String,
                        ) extends BaseHeaders {
  val kafkaEventPublisher = new KafkaEventPublisher("payment-topic")

  def publish(): Unit = kafkaEventPublisher.publishLog(this)

}

在我的 companion object 中,我构建了所需的案例 class:

object PaymentFunnel {

  def from(req: HttpRequest, status: String): PaymentHeader = {
    val (host, userAgent, channel, os) = HeaderExtractor.getHeaders(req)

    PaymentHeader(
      LocalDateTime.now(),
      host,
      os,
      req.method.value,
      req.uri.path.toString,
      userAgent,
      channel,
      status,
    )
  }

  implicit val format: Format[PaymentHeader] = JsonNaming.snakecase(Json.format[PaymentHeader])

}

现在我的最终方法是publishLog,定义如下:

def publishLog(message: BaseHeaders) = {
      Source.fromIterator(() => List(message).iterator)
        .map(baseHeader => {
          println("publishing headers data to kafka ::::: " + Json.toJson(baseHeader).toString())
          new ProducerRecord[String, String](topicName, Json.toJson(baseHeader).toString())
        })
        .runWith(Producer.plainSink(producerSettings))
    }

现在我在 Kafka 主题上得到的是 case class:

的字符串版本
`PaymentHeader(some data here for fields....)`

一个字符串版本的大小写class!我在 Kafka 端想要的是 json 序列化 PaymentHeader.

注意: 我还有许多其他案例 class 扩展 BaseHeadersPaymentHeaders.

我如何 json 在 Scala/Play 中对我的案例 class 进行编码?

Json.toJson 根据参数的 编译时 类型转换 class,而不是 run-time 类型,因此它正在转换它作为一个 BaseHeaders 对象。 BaseHeaders 的作者只是将 case class 转换为字符串,所以这就是您在 JSON.

中得到的内容

您需要模板化 publishLog 方法,以便 Json.toJson 看到消息的实际类型。

def publishLog[T <: BaseHeaders](message: T)

您还需要确保消息类型保留在调用函数中,并且 Play 知道如何将实际类型转换为 JSON。

你目前正在做的是调用 toString 方法,它基本上 returns 字符串表示 obj Scala 中的对象,JsString 只是将其假定为字符串,并在其周围放置 double-quotes。我可以提出 3 种方法,我会解释每种方法的优缺点,你可以决定使用哪一种:

方法 #1:进行模式匹配


在序列化特征时,在这种情况下重要的是 sub-type,所以想象一下:

sealed trait A { val name: String }
case class B(name: String) extends A
case class C(name: String, id: Int) extends A
/// and so on

您可以为每个 classes 定义 writer,然后在 trait 的伴生对象中:

object A {
  implicit val traitWriter: Writes[A] = {
    case b: B => Json.toJson(b)
    case c: C => Json.toJson(c)
  }
}

这种方法非常容易使用,但需要注意的是,当创建一个扩展特征的新 class 时,您需要更新特征的伴生对象中的编写器,否则您将面临匹配错误。 这样做的好处是您的代码非常简单 easy-to-read。 缺点是编译时不能确定进程(匹配错误问题)

方法 #2:消费者方法中的类型约束和上下文绑定


publishLog方法中,将方法签名更改为:

// original:
def publishLog(message: BaseHeaders) = ...
// to =>
def publishLog[T <: BaseHeaders : Writes](message: T) = ... // the body will be the same

并且只需从 trait 的伴生对象中删除 writer。这意味着,您是否期望某种类型“T”实际上是一个 BaseHeaders,并且还绑定了自己的 Writer,因此不会出现序列化问题。 这种方法的优点,(1) 是您的代码仍然保持简单并且 easy-to-read,(2) 您将摆脱定义一个在所有情况下都适用于特征的编写器的麻烦,(3) 您'确定编译时的一切。 这种方法的缺点是,无论何时你想使用 BaseHeaders 值,你都必须为你的方法定义相同的签名(如果你需要序列化),但这对我来说似乎很公平。

方法 #3:使用自身类型 instance-level 约束(不推荐)


trait BaseHeaders[T <: BaseHeaders[T]] { self: T => 
  val timestamp: LocalDateTime
  // ... other fields
  implicit def instanceWriter: Writes[T]
}

case class PaymentHeader(...) extends BaseHeaders[PaymentHeader] {
  implicit def instanceWriter: Writes[PaymentHeader] = implicitly
}

这样做的一个好处是您可以确定编译时的序列化 虽然它有 1 个主要缺点,但代码实际上变得比应有的难得多,这一点也不好。另一个是您需要在需要序列化时调用 instanceWriter