要启用JSON序列化,需要遵循三个步骤。
第一步是为要序列化的每个类定义Format,这可以使用自动映射或手动映射来完成。
implicit val format: Format[ItemAdded] = Json.format
最好的方式是将Format以隐式定义的方式在类伴生对象中声明,以便通过隐式解析找到它。
第二步是实现JsonSerializerRegistry,并从其serializers
方法返回所有服务格式。
import com.lightbend.lagom.scaladsl.playjson.JsonSerializer
import com.lightbend.lagom.scaladsl.playjson.JsonSerializerRegistry
object MyRegistry extends JsonSerializerRegistry {
override val serializers = Vector(
JsonSerializer[ItemAdded],
JsonSerializer[OrderAdded]
)
}
完成后,您可以通过覆盖应用程序中的jsonSerializerRegistry
组件方法来提供序列化程序注册表,例如:
import com.lightbend.lagom.scaladsl.server._
import com.lightbend.lagom.scaladsl.cluster.ClusterComponents
abstract class MyApplication(context: LagomApplicationContext)
extends LagomApplication(context)
with ClusterComponents {
override lazy val jsonSerializerRegistry = MyRegistry
}
如果您需要在Lagom应用程序之外使用注册表,例如在测试中,可以通过自定义actor系统的创建来实现,例如:
import akka.actor.ActorSystem
import akka.actor.setup.ActorSystemSetup
import com.lightbend.lagom.scaladsl.playjson.JsonSerializerRegistry
val system = ActorSystem(
"my-actor-system",
ActorSystemSetup(
JsonSerializerRegistry.serializationSetupFor(MyRegistry)
)
)
压缩
如本文所述,压缩仅用于服务集群的持久事件、持久快照和远程消息。
JSON可能相当冗长,对于大型消息,启用压缩可能会很有好处。这是通过使用JsonSerializer.compressed[T]生成器方法,而不是JsonSerializer.apply[T]
(如上面的示例片段所示):
import com.lightbend.lagom.scaladsl.playjson.JsonSerializer
import com.lightbend.lagom.scaladsl.playjson.JsonSerializerRegistry
object RegistryWithCompression extends JsonSerializerRegistry {
override val serializers = Vector(
// 'ItemAdded' uses `apply[T]()` .
JsonSerializer[ItemAdded],
// The OrderAdded message is usually rather big, so we want it compressed
// when it's too large.
JsonSerializer.compressed[OrderAdded]
)
}
默认情况下,序列化程序只压缩大于32Kb的消息。可以使用配置属性更改此阈值:
lagom.serialization.json {
# The serializer will compress the payload if the message class
# was registered using JsonSerializer.compressed and the payload
# is larger than this value. Only used for remote messages within
# the cluster of the service.
compress-larger-than = 32 KiB
}
自动映射
Json.format[MyClass] 宏将检查case class
包含哪些字段,并生成一种Format
,使用JSON中的字段名和类的类型。
宏允许根据类的确切结构定义格式,这很方便,避免了在显式定义格式上花费开发时间,另一方面,它将JSON的结构与类的结构紧密结合,因此类的重构意外地导致格式无法读取更改前序列化的JSON。有适当的工具来处理这个问题(参见模式演化),但必须小心。
如果类包含复杂类型的字段,它会从范围中的implicit
标记Format
中提取这些字段。这意味着在调用宏之前,必须为类中使用的所有复杂类型提供此类隐式格式。
case class UserMetadata(twitterHandle: String)
object UserMetadata {
implicit val format: Format[UserMetadata] = Json.format
}
case class AddComment(userId: String, comment: String, userMetadata: UserMetadata)
object AddComment {
implicit val format: Format[AddComment] = Json.format
}
手动映射
使用Play JSON API可以通过多种方式定义Format
,可以使用JSON组合器,也可以手动实现将JsValue
转换为JsSuccess(T)
或JsFailure()
的函数。
case class OrderAdded(productId: String, quantity: Int)
import play.api.libs.functional.syntax._
import play.api.libs.json._
object OrderAdded {
implicit val format: Format[OrderAdded] =
(JsPath \ "product_id")
.format[String]
.and((JsPath \ "quantity").format[Int])
.apply(OrderAdded.apply, unlift(OrderAdded.unapply))
}
特别映射情况注意事项
映射选项
自动映射将处理Option
字段,用于手动映射可使用(JsPath \ "optionalField").formatNullable[A]
。这将把缺少的字段视为None
,允许添加新字段,而不提供显式的模式迁移步骤。
映射单例
对于顶级单例(Scala object
),可以使用com.lightbend.lagom.scaladsl.playjson.Serializers.emptySingletonFormat 以获取输出空JSON的Format
(因为该类型也与数据一起编码)。
case object GetOrders {
implicit val format: Format[GetOrders.type] =
JsonSerializer.emptySingletonFormat(GetOrders)
}
映射层次结构
在映射类型层次结构时,例如ADT、trait或抽象类,需要为超类型提供一种Format
,该格式基于JSON中的一些信息来决定反序列化哪个子类型。
import play.api.libs.json._
sealed trait Fruit
case object Pear extends Fruit
case object Apple extends Fruit
case class Banana(ripe: Boolean) extends Fruit
object Banana {
implicit val format: Format[Banana] = Json.format
}
object Fruit {
implicit val format = Format[Fruit](
Reads { js =>
// use the fruitType field to determine how to deserialize
val fruitType = (JsPath \ "fruitType").read[String].reads(js)
fruitType.fold(
errors => JsError("fruitType undefined or incorrect"), {
case "pear" => JsSuccess(Pear)
case "apple" => JsSuccess(Apple)
case "banana" => (JsPath \ "data").read[Banana].reads(js)
}
)
},
Writes {
case Pear => JsObject(Seq("fruitType" -> JsString("pear")))
case Apple => JsObject(Seq("fruitType" -> JsString("apple")))
case b: Banana =>
JsObject(
Seq(
"fruitType" -> JsString("banana"),
"data" -> Banana.format.writes(b)
)
)
}
)
}
模式演化
在使用Akka Persistence Typed、Lagom Persistence (classic)或任何类型的事件源进行长时间运行的项目时,模式演化成为开发应用程序的一个重要方面。随着时间的推移,这些要求以及我们对业务领域的理解可能(也将)发生变化。
Lagom提供了一种在反序列化期间执行JSON树模型转换的方法。要进行这些转换,可以强制修改json,也可以使用Play JSON transformers
我们将一起分析一下这样的场景是如何演化的。
移除属性
移除一个属性可以在没有任何迁移代码的情况下完成。手动映射和自动映射都将忽略在类中不存在的属性。
删除字段无需任何迁移代码。手动和自动映射都会忽略类中不存在的属性。
增加属性
如果使用了自动映射或手动映射,并且您已确保丢失的字段按格式读取为None
,则可以在不使用任何迁移代码的情况下添加可选字段。
之前的类:
case class ItemAdded(shoppingCartId: String, productId: String, quantity: Int)
带有一个可选属性discount
的新类:
case class ItemAdded(shoppingCartId: String, productId: String, quantity: Int, discount: Option[BigDecimal])
假设我们想要一个没有默认值的给定的discount
属性:
case class ItemAdded(shoppingCartId: String, productId: String, quantity: Int, discount: Double)
要添加一个新的必填字段,我们必须使用JSON迁移向JSON添加一个默认值。这就是使用命令式代码添加discount
字段的迁移逻辑:
class ShopSerializerRegistry extends JsonSerializerRegistry {
import play.api.libs.json._
override val serializers = ShopCommands.serializers ++ ShopEvents.serializers
private val itemAddedMigration = new JsonMigration(2) {
override def transform(fromVersion: Int, json: JsObject): JsObject = {
if (fromVersion < 2) {
json + ("discount" -> JsNumber(0.0d))
} else {
json
}
}
}
override def migrations = Map[String, JsonMigration](
classOf[ItemAdded].getName -> itemAddedMigration
)
}
创建JsonMigration的一个具体子类,将模式的当前版本作为参数传递给它,然后当之前的fromVersion
被传入时,在transform
方法中的JsObject
上实现转换逻辑。
然后在JsonSerializerRegistry
的migrations
映射中提供JsonMigration
以及它迁移的类的类名。或者,您可以使用Play JSON transformers API,它更简洁但有更高的学习门槛。
class ShopSerializerRegistry extends JsonSerializerRegistry {
import play.api.libs.json._
override val serializers = ShopCommands.serializers ++ ShopEvents.serializers
val addDefaultDiscount = JsPath.json.update((JsPath \ "discount").json.put(JsNumber(0.0d)))
override def migrations = Map[String, JsonMigration](
JsonMigrations.transform[ItemAdded](
immutable.SortedMap(
1 -> addDefaultDiscount
)
)
)
}
在本例中,我们给出了JsonMigrations.transform
方法的类型是一个有序map,在当前模式版本之前就已经发生了转换操作。
重命名属性
假设我们想在上一个示例中将productId
字段重命名为itemId
。
case class ItemAdded(shoppingCartId: String, itemId: String, quantity: Int)
必要的迁移代码如下所示:
private val itemAddedMigration = new JsonMigration(2) {
override def transform(fromVersion: Int, json: JsObject): JsObject = {
if (fromVersion < 2) {
val productId = (JsPath \ "productId").read[JsString].reads(json).get
json + ("itemId" -> productId) - "productId"
} else {
json
}
}
}
override def migrations = Map[String, JsonMigration](
classOf[ItemAdded].getName -> itemAddedMigration
)
或者是基于转换器的迁移:
val productIdToItemId =
JsPath.json
.update(
(JsPath \ "itemId").json.copyFrom((JsPath \ "productId").json.pick)
)
.andThen((JsPath \ "productId").json.prune)
override def migrations = Map[String, JsonMigration](
JsonMigrations.transform[ItemAdded](
immutable.SortedMap(
1 -> productIdToItemId
)
)
)
结构改变
以类似的方式,我们可以任意改变结构。
case class Customer(name: String, street: String, city: String, zipCode: String, country: String)
case class Address(street: String, city: String, zipCode: String, country: String)
case class Customer(name: String, address: Address, shippingAddress: Option[Address])
迁移代码如下所示:
import play.api.libs.json._
import play.api.libs.functional.syntax._
val customerMigration = new JsonMigration(2) {
// use arbitrary logic to parse an Address
// out of the old schema
val readOldAddress: Reads[Address] = {
(JsPath \ "street")
.read[String]
.and(
(JsPath \ "city").read[String])
.and(
(JsPath \ "zipCode").read[String])
.and(
(JsPath \ "country").read[String])(Address)
}
override def transform(fromVersion: Int, json: JsObject): JsObject = {
if (fromVersion < 2) {
val address = readOldAddress.reads(json).get
val withoutOldAddress = json - "street" - "city" - "zipCode" - "country"
// use existing formatter to write the address in the new schema
withoutOldAddress + ("address" -> Customer.addressFormat.writes(address))
} else {
json
}
}
}
override def migrations: Map[String, JsonMigration] = Map(
classOf[Customer].getName -> customerMigration
)
重命名类
同样可以重命名类。例如,让我们将OrderAdded
重命名为OrderPlaced
。
之前的类定义:
case class OrderAdded(shoppingCartId: String)
新类定义:
case class OrderPlaced(shoppingCartId: String)
迁移代码如下所示:
override def migrations: Map[String, JsonMigration] = Map(
JsonMigrations
.renamed(fromClassName = "com.lightbend.lagom.shop.OrderAdded", inVersion = 2, toClass = classOf[OrderPlaced])
)
当一个类同时被重命名,并且随着时间的推移发生了其他更改时,将单独添加名称更改,如示例所示,并在迁移映射中为新类名定义转换。Lagom序列化逻辑将首先查找名称更改,然后使用更改后的名称去解决任何模式迁移的问题。