如何 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 扩展 BaseHeaders
像 PaymentHeaders
.
我如何 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
。
我有一个 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 扩展 BaseHeaders
像 PaymentHeaders
.
我如何 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
。