要启用JSON序列化,需要遵循三个步骤。
第一步是为要序列化的每个类定义Format,这可以使用自动映射或手动映射来完成。
implicit val format: Format[ItemAdded] = Json.format
最好的方式是将Format以隐式定义的方式在类伴生对象中声明,以便通过隐式解析找到它。
第二步是实现JsonSerializerRegistry,并从其serializers方法返回所有服务格式。
import com.lightbend.lagom.scaladsl.playjson.JsonSerializerimport com.lightbend.lagom.scaladsl.playjson.JsonSerializerRegistryobject MyRegistry extends JsonSerializerRegistry {override val serializers = Vector(JsonSerializer[ItemAdded],JsonSerializer[OrderAdded])}
完成后,您可以通过覆盖应用程序中的jsonSerializerRegistry组件方法来提供序列化程序注册表,例如:
import com.lightbend.lagom.scaladsl.server._import com.lightbend.lagom.scaladsl.cluster.ClusterComponentsabstract class MyApplication(context: LagomApplicationContext)extends LagomApplication(context)with ClusterComponents {override lazy val jsonSerializerRegistry = MyRegistry}
如果您需要在Lagom应用程序之外使用注册表,例如在测试中,可以通过自定义actor系统的创建来实现,例如:
import akka.actor.ActorSystemimport akka.actor.setup.ActorSystemSetupimport com.lightbend.lagom.scaladsl.playjson.JsonSerializerRegistryval system = ActorSystem("my-actor-system",ActorSystemSetup(JsonSerializerRegistry.serializationSetupFor(MyRegistry)))
压缩
如本文所述,压缩仅用于服务集群的持久事件、持久快照和远程消息。
JSON可能相当冗长,对于大型消息,启用压缩可能会很有好处。这是通过使用JsonSerializer.compressed[T]生成器方法,而不是JsonSerializer.apply[T](如上面的示例片段所示):
import com.lightbend.lagom.scaladsl.playjson.JsonSerializerimport com.lightbend.lagom.scaladsl.playjson.JsonSerializerRegistryobject 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 Fruitcase object Pear extends Fruitcase object Apple extends Fruitcase class Banana(ripe: Boolean) extends Fruitobject 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 deserializeval 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.serializersprivate 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.serializersval 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).getjson + ("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 schemaval 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).getval withoutOldAddress = json - "street" - "city" - "zipCode" - "country"// use existing formatter to write the address in the new schemawithoutOldAddress + ("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序列化逻辑将首先查找名称更改,然后使用更改后的名称去解决任何模式迁移的问题。
