要启用JSON序列化,需要遵循三个步骤。
第一步是为要序列化的每个类定义Format,这可以使用自动映射或手动映射来完成。

  1. implicit val format: Format[ItemAdded] = Json.format

最好的方式是将Format以隐式定义的方式在类伴生对象中声明,以便通过隐式解析找到它。
第二步是实现JsonSerializerRegistry,并从其serializers方法返回所有服务格式。

  1. import com.lightbend.lagom.scaladsl.playjson.JsonSerializer
  2. import com.lightbend.lagom.scaladsl.playjson.JsonSerializerRegistry
  3. object MyRegistry extends JsonSerializerRegistry {
  4. override val serializers = Vector(
  5. JsonSerializer[ItemAdded],
  6. JsonSerializer[OrderAdded]
  7. )
  8. }

完成后,您可以通过覆盖应用程序中的jsonSerializerRegistry组件方法来提供序列化程序注册表,例如:

  1. import com.lightbend.lagom.scaladsl.server._
  2. import com.lightbend.lagom.scaladsl.cluster.ClusterComponents
  3. abstract class MyApplication(context: LagomApplicationContext)
  4. extends LagomApplication(context)
  5. with ClusterComponents {
  6. override lazy val jsonSerializerRegistry = MyRegistry
  7. }

如果您需要在Lagom应用程序之外使用注册表,例如在测试中,可以通过自定义actor系统的创建来实现,例如:

  1. import akka.actor.ActorSystem
  2. import akka.actor.setup.ActorSystemSetup
  3. import com.lightbend.lagom.scaladsl.playjson.JsonSerializerRegistry
  4. val system = ActorSystem(
  5. "my-actor-system",
  6. ActorSystemSetup(
  7. JsonSerializerRegistry.serializationSetupFor(MyRegistry)
  8. )
  9. )

压缩

如本文所述,压缩仅用于服务集群的持久事件、持久快照和远程消息。
JSON可能相当冗长,对于大型消息,启用压缩可能会很有好处。这是通过使用JsonSerializer.compressed[T]生成器方法,而不是JsonSerializer.apply[T](如上面的示例片段所示):

  1. import com.lightbend.lagom.scaladsl.playjson.JsonSerializer
  2. import com.lightbend.lagom.scaladsl.playjson.JsonSerializerRegistry
  3. object RegistryWithCompression extends JsonSerializerRegistry {
  4. override val serializers = Vector(
  5. // 'ItemAdded' uses `apply[T]()` .
  6. JsonSerializer[ItemAdded],
  7. // The OrderAdded message is usually rather big, so we want it compressed
  8. // when it's too large.
  9. JsonSerializer.compressed[OrderAdded]
  10. )
  11. }

默认情况下,序列化程序只压缩大于32Kb的消息。可以使用配置属性更改此阈值:

  1. lagom.serialization.json {
  2. # The serializer will compress the payload if the message class
  3. # was registered using JsonSerializer.compressed and the payload
  4. # is larger than this value. Only used for remote messages within
  5. # the cluster of the service.
  6. compress-larger-than = 32 KiB
  7. }

自动映射

Json.format[MyClass] 宏将检查case class包含哪些字段,并生成一种Format,使用JSON中的字段名和类的类型。
宏允许根据类的确切结构定义格式,这很方便,避免了在显式定义格式上花费开发时间,另一方面,它将JSON的结构与类的结构紧密结合,因此类的重构意外地导致格式无法读取更改前序列化的JSON。有适当的工具来处理这个问题(参见模式演化),但必须小心。
如果类包含复杂类型的字段,它会从范围中的implicit标记Format中提取这些字段。这意味着在调用宏之前,必须为类中使用的所有复杂类型提供此类隐式格式。

  1. case class UserMetadata(twitterHandle: String)
  2. object UserMetadata {
  3. implicit val format: Format[UserMetadata] = Json.format
  4. }
  5. case class AddComment(userId: String, comment: String, userMetadata: UserMetadata)
  6. object AddComment {
  7. implicit val format: Format[AddComment] = Json.format
  8. }

手动映射

使用Play JSON API可以通过多种方式定义Format,可以使用JSON组合器,也可以手动实现将JsValue转换为JsSuccess(T)JsFailure()的函数。

  1. case class OrderAdded(productId: String, quantity: Int)
  2. import play.api.libs.functional.syntax._
  3. import play.api.libs.json._
  4. object OrderAdded {
  5. implicit val format: Format[OrderAdded] =
  6. (JsPath \ "product_id")
  7. .format[String]
  8. .and((JsPath \ "quantity").format[Int])
  9. .apply(OrderAdded.apply, unlift(OrderAdded.unapply))
  10. }

特别映射情况注意事项

映射选项

自动映射将处理Option字段,用于手动映射可使用(JsPath \ "optionalField").formatNullable[A]。这将把缺少的字段视为None,允许添加新字段,而不提供显式的模式迁移步骤。

映射单例

对于顶级单例(Scala object),可以使用com.lightbend.lagom.scaladsl.playjson.Serializers.emptySingletonFormat 以获取输出空JSON的Format(因为该类型也与数据一起编码)。

  1. case object GetOrders {
  2. implicit val format: Format[GetOrders.type] =
  3. JsonSerializer.emptySingletonFormat(GetOrders)
  4. }

映射层次结构

在映射类型层次结构时,例如ADT、trait或抽象类,需要为超类型提供一种Format,该格式基于JSON中的一些信息来决定反序列化哪个子类型。

  1. import play.api.libs.json._
  2. sealed trait Fruit
  3. case object Pear extends Fruit
  4. case object Apple extends Fruit
  5. case class Banana(ripe: Boolean) extends Fruit
  6. object Banana {
  7. implicit val format: Format[Banana] = Json.format
  8. }
  9. object Fruit {
  10. implicit val format = Format[Fruit](
  11. Reads { js =>
  12. // use the fruitType field to determine how to deserialize
  13. val fruitType = (JsPath \ "fruitType").read[String].reads(js)
  14. fruitType.fold(
  15. errors => JsError("fruitType undefined or incorrect"), {
  16. case "pear" => JsSuccess(Pear)
  17. case "apple" => JsSuccess(Apple)
  18. case "banana" => (JsPath \ "data").read[Banana].reads(js)
  19. }
  20. )
  21. },
  22. Writes {
  23. case Pear => JsObject(Seq("fruitType" -> JsString("pear")))
  24. case Apple => JsObject(Seq("fruitType" -> JsString("apple")))
  25. case b: Banana =>
  26. JsObject(
  27. Seq(
  28. "fruitType" -> JsString("banana"),
  29. "data" -> Banana.format.writes(b)
  30. )
  31. )
  32. }
  33. )
  34. }

模式演化

在使用Akka Persistence Typed、Lagom Persistence (classic)或任何类型的事件源进行长时间运行的项目时,模式演化成为开发应用程序的一个重要方面。随着时间的推移,这些要求以及我们对业务领域的理解可能(也将)发生变化。
Lagom提供了一种在反序列化期间执行JSON树模型转换的方法。要进行这些转换,可以强制修改json,也可以使用Play JSON transformers
我们将一起分析一下这样的场景是如何演化的。

移除属性

移除一个属性可以在没有任何迁移代码的情况下完成。手动映射和自动映射都将忽略在类中不存在的属性。
删除字段无需任何迁移代码。手动和自动映射都会忽略类中不存在的属性。

增加属性

如果使用了自动映射或手动映射,并且您已确保丢失的字段按格式读取为None,则可以在不使用任何迁移代码的情况下添加可选字段。
之前的类:

  1. case class ItemAdded(shoppingCartId: String, productId: String, quantity: Int)

带有一个可选属性discount的新类:

  1. case class ItemAdded(shoppingCartId: String, productId: String, quantity: Int, discount: Option[BigDecimal])

假设我们想要一个没有默认值的给定的discount属性:

  1. case class ItemAdded(shoppingCartId: String, productId: String, quantity: Int, discount: Double)

要添加一个新的必填字段,我们必须使用JSON迁移向JSON添加一个默认值。这就是使用命令式代码添加discount字段的迁移逻辑:

  1. class ShopSerializerRegistry extends JsonSerializerRegistry {
  2. import play.api.libs.json._
  3. override val serializers = ShopCommands.serializers ++ ShopEvents.serializers
  4. private val itemAddedMigration = new JsonMigration(2) {
  5. override def transform(fromVersion: Int, json: JsObject): JsObject = {
  6. if (fromVersion < 2) {
  7. json + ("discount" -> JsNumber(0.0d))
  8. } else {
  9. json
  10. }
  11. }
  12. }
  13. override def migrations = Map[String, JsonMigration](
  14. classOf[ItemAdded].getName -> itemAddedMigration
  15. )
  16. }

创建JsonMigration的一个具体子类,将模式的当前版本作为参数传递给它,然后当之前的fromVersion被传入时,在transform方法中的JsObject上实现转换逻辑。
然后在JsonSerializerRegistrymigrations映射中提供JsonMigration以及它迁移的类的类名。或者,您可以使用Play JSON transformers API,它更简洁但有更高的学习门槛。

  1. class ShopSerializerRegistry extends JsonSerializerRegistry {
  2. import play.api.libs.json._
  3. override val serializers = ShopCommands.serializers ++ ShopEvents.serializers
  4. val addDefaultDiscount = JsPath.json.update((JsPath \ "discount").json.put(JsNumber(0.0d)))
  5. override def migrations = Map[String, JsonMigration](
  6. JsonMigrations.transform[ItemAdded](
  7. immutable.SortedMap(
  8. 1 -> addDefaultDiscount
  9. )
  10. )
  11. )
  12. }

在本例中,我们给出了JsonMigrations.transform方法的类型是一个有序map,在当前模式版本之前就已经发生了转换操作。

重命名属性

假设我们想在上一个示例中将productId字段重命名为itemId

  1. case class ItemAdded(shoppingCartId: String, itemId: String, quantity: Int)

必要的迁移代码如下所示:

  1. private val itemAddedMigration = new JsonMigration(2) {
  2. override def transform(fromVersion: Int, json: JsObject): JsObject = {
  3. if (fromVersion < 2) {
  4. val productId = (JsPath \ "productId").read[JsString].reads(json).get
  5. json + ("itemId" -> productId) - "productId"
  6. } else {
  7. json
  8. }
  9. }
  10. }
  11. override def migrations = Map[String, JsonMigration](
  12. classOf[ItemAdded].getName -> itemAddedMigration
  13. )

或者是基于转换器的迁移:

  1. val productIdToItemId =
  2. JsPath.json
  3. .update(
  4. (JsPath \ "itemId").json.copyFrom((JsPath \ "productId").json.pick)
  5. )
  6. .andThen((JsPath \ "productId").json.prune)
  7. override def migrations = Map[String, JsonMigration](
  8. JsonMigrations.transform[ItemAdded](
  9. immutable.SortedMap(
  10. 1 -> productIdToItemId
  11. )
  12. )
  13. )

结构改变

以类似的方式,我们可以任意改变结构。

  1. case class Customer(name: String, street: String, city: String, zipCode: String, country: String)
  1. case class Address(street: String, city: String, zipCode: String, country: String)
  2. case class Customer(name: String, address: Address, shippingAddress: Option[Address])

迁移代码如下所示:

  1. import play.api.libs.json._
  2. import play.api.libs.functional.syntax._
  3. val customerMigration = new JsonMigration(2) {
  4. // use arbitrary logic to parse an Address
  5. // out of the old schema
  6. val readOldAddress: Reads[Address] = {
  7. (JsPath \ "street")
  8. .read[String]
  9. .and(
  10. (JsPath \ "city").read[String])
  11. .and(
  12. (JsPath \ "zipCode").read[String])
  13. .and(
  14. (JsPath \ "country").read[String])(Address)
  15. }
  16. override def transform(fromVersion: Int, json: JsObject): JsObject = {
  17. if (fromVersion < 2) {
  18. val address = readOldAddress.reads(json).get
  19. val withoutOldAddress = json - "street" - "city" - "zipCode" - "country"
  20. // use existing formatter to write the address in the new schema
  21. withoutOldAddress + ("address" -> Customer.addressFormat.writes(address))
  22. } else {
  23. json
  24. }
  25. }
  26. }
  27. override def migrations: Map[String, JsonMigration] = Map(
  28. classOf[Customer].getName -> customerMigration
  29. )

重命名类

同样可以重命名类。例如,让我们将OrderAdded重命名为OrderPlaced
之前的类定义:

  1. case class OrderAdded(shoppingCartId: String)

新类定义:

  1. case class OrderPlaced(shoppingCartId: String)

迁移代码如下所示:

  1. override def migrations: Map[String, JsonMigration] = Map(
  2. JsonMigrations
  3. .renamed(fromClassName = "com.lightbend.lagom.shop.OrderAdded", inVersion = 2, toClass = classOf[OrderPlaced])
  4. )

当一个类同时被重命名,并且随着时间的推移发生了其他更改时,将单独添加名称更改,如示例所示,并在迁移映射中为新类名定义转换。Lagom序列化逻辑将首先查找名称更改,然后使用更改后的名称去解决任何模式迁移的问题。