开箱即用,Lagom使用Play JSON
序列化请求和响应消息。您也可以定义自定义的序列化器用于自己的类型,使用任何可以织入的协议,从JSON到protobufs,再到XML。
Lagom如何选择消息序列化器
当您声明一个服务描述时,call
, namedCall
, pathCall
, restCall
和 topic
方法全都使用**MessageSerializer**
隐式参数来处理服务调用的消息。在Scala中全部使用隐式参数是可能的,您可以让Scala编译器处理这些隐式参数,或者您也可以明确显式地使用这些参数。
下面的例子展示了如何显式地使用Lagom默认的String
序列化器。
import com.lightbend.lagom.scaladsl.api.deser.MessageSerializer
trait HelloService extends Service {
def sayHello: ServiceCall[String, String]
override def descriptor = {
import Service._
named("hello").withCalls(
call(sayHello)(MessageSerializer.StringMessageSerializer, MessageSerializer.StringMessageSerializer)
)
}
}
在service descriptors 文档中我们已经了解到,在一个样例类的伴生对象中如何声明一个隐式的Play JSON Format ,Lagom将使用Format处理消息类型。这种方式可以生效,是因为Lagom提供了一种隐式的MessageSerializer,其中包装了一个Play JSONFormat
。MessageSerializer
伴生对象中的jsValueFormatMessageSerializer
方法实现了这一点。MessageSerializer
伴生对象中也提供了其他可能用到的非JSON格式的隐式方法。例如, NotUsed
, Done
或者 String
这些请求响应类型将使用默认的序列化器。Lagom还附带了对字节串序列化器(akka noop)的支持,因此可以直接访问装配对象。
JSON消息序列化器格式化对象也可以被显式地使用。我们假设您有一个带有id
属性的消息对象,当您想要发起一次服务调用,由Play JSON指令集提供的默认格式化对象来完成格式化工作,但是当在JSON中id
属性被命名为identifier
时,您可能想要额外不同的格式化方式,这种情况您需要提供两种不同的格式化方式。
import play.api.libs.json._
import play.api.libs.functional.syntax._
case class MyMessage(id: String)
object MyMessage {
implicit val format: Format[MyMessage] = Json.format
val alternateFormat: Format[MyMessage] = {
(__ \ "identifier")
.format[String]
.inmap(MyMessage.apply, _.id)
}
}
您可以看到我们已经把其中一个设定为隐式的,所以如果我们让隐式处理生效,它就会被选中。然后,非隐式调用可以在服务调用描述符中显式传递:
trait MyService extends Service {
def getMessage: ServiceCall[NotUsed, MyMessage]
def getMessageAlternate: ServiceCall[NotUsed, MyMessage]
override def descriptor = {
import Service._
named("my-service").withCalls(
call(getMessage),
call(getMessageAlternate)(
implicitly[MessageSerializer[NotUsed, ByteString]],
MessageSerializer.jsValueFormatMessageSerializer(
implicitly[MessageSerializer[JsValue, ByteString]],
MyMessage.alternateFormat
)
)
)
}
}
自定义序列化器
JSON可能不是您想要使用的唯一一种织入格式。可以使用LagomsMessageSerializer
特性来实现定制的序列化器。
正如我们已经看到的那样,Lagom中有两种类型的消息,严格消息和流消息。对于这两种类型的消息,Lagom提供了MessageSerializer
的两个子接口,StrictMessageSerializer
和StreamedMessageSerializer
,它们主要不同于序列化和反序列化的装配格式。严格的消息序列化器序列化和反序列化使用ByteString
,也就是说,它们严格地在内存中工作,而流消息序列化器使用流,即Source[ByteString, _]
。
在研究如何实现序列化程序之前,需要介绍几个基本概念。
消息协议
Lagom有一个消息协议的概念。消息协议使用**MessageProtocol**
类型表示,它们有三个属性:内容类型、字符集和版本。所有这些属性都是可选的,消息序列化程序可能使用也可能不使用。
消息协议大致转换为HTTP的 Content-Type
和 Accept
头信息,如果使用编码版本的mime类型方案,则可能从中提取版本,或者也可能从URL中提取版本,这取决于服务的配置方式。
内容协商
Lagom消息序列化器能够使用内容协商来决定要使用的正确协议来相互通信。这可以用来指定不同的装配格式,比如JSON和XML,以及不同的版本。
Lagom的内容协商功能与HTTP相同。对于请求消息,客户端将选择它想要使用的任何协议,因此不需要进行协商。然后,服务器使用客户端发送的消息协议来决定如何反序列化请求。
对于响应,客户机发送一个它将接受的消息协议列表,服务器应该从该列表中选择一个协议来响应。然后,客户端将读取服务器选择的协议,并使用该协议反序列化响应。
协商序列化器
作为内容协商的结果,Lagom的MessageSerializer
并不直接序列化和反序列化消息,而是提供了协商消息协议的方法,返回**NegotiatedSerializer**
或 **NegotiatedDeserializer**
。实际上,正是这些协商好的类负责执行序列化和反序列化。
让我们看一个内容协商的示例。假设我们想实现一个自定义字符串MessageSerializer
,它可以序列化为纯文本,也可以序列化为JSON,这取决于客户端请求的内容。如果您有一些客户端以JSON的形式发送文本主体,而另一些客户端以纯文本的形式发送文本主体,这可能会很有用,也许其中一个客户端是用一种方式完成工作的传统客户端,但现在您想用新客户端来完成另一种方式。
首先,我们将实现NegotiatedSerializer
序列化为纯文本字符串:
import akka.util.ByteString
import com.lightbend.lagom.scaladsl.api.deser.MessageSerializer.NegotiatedSerializer
import com.lightbend.lagom.scaladsl.api.transport.DeserializationException
import com.lightbend.lagom.scaladsl.api.transport.MessageProtocol
import com.lightbend.lagom.scaladsl.api.transport.NotAcceptable
import com.lightbend.lagom.scaladsl.api.transport.UnsupportedMediaType
class PlainTextSerializer(val charset: String) extends NegotiatedSerializer[String, ByteString] {
override val protocol = MessageProtocol(Some("text/plain"), Some(charset))
def serialize(s: String) = ByteString.fromString(s, charset)
}
protocol
方法返回这个序列化器序列化到的协议,您可以看到,我们正在传递这个序列化器将在构造函数中使用的字符集。serialize
方法是从String
到ByteString
的直接转换。
接下来,我们将实现同样的事情,这次将序列化为JSON:
import play.api.libs.json.Json
import play.api.libs.json.JsString
class JsonTextSerializer extends NegotiatedSerializer[String, ByteString] {
override val protocol = MessageProtocol(Some("application/json"))
def serialize(s: String) =
ByteString.fromString(Json.stringify(JsString(s)))
}
这里我们使用Play JSON将String
转换为JSON字符串。
现在让我们实现纯文本反序列化器:
import com.lightbend.lagom.scaladsl.api.deser.MessageSerializer.NegotiatedDeserializer
class PlainTextDeserializer(val charset: String) extends NegotiatedDeserializer[String, ByteString] {
def deserialize(bytes: ByteString) =
bytes.decodeString(charset)
}
同样,我们将字符集作为构造函数参数,并直接从ByteString
转换为String
。
类似地,我们有一个JSON文本反序列化器:
import scala.util.control.NonFatal
class JsonTextDeserializer extends NegotiatedDeserializer[String, ByteString] {
def deserialize(bytes: ByteString) = {
try {
Json.parse(bytes.iterator.asInputStream).as[String]
} catch {
case NonFatal(e) => throw DeserializationException(e)
}
}
}
现在我们已经实现了协商的序列化器和反序列化器,现在是实现MessageSerializer
来进行实际的协议协商的时候了。我们的类将继承StrictMessageSerializer
:
import com.lightbend.lagom.scaladsl.api.deser.StrictMessageSerializer
class TextMessageSerializer extends StrictMessageSerializer[String] {
我们需要做的下一件事是定义我们能接受的协议。这将被客户端用来设置Accept
头文件:
override def acceptResponseProtocols = List(
MessageProtocol(Some("text/plain")),
MessageProtocol(Some("application/json"))
)
您可以看到,这个序列化器既支持文本协议,也支持json协议。需要注意的一点是,我们没有在文本协议中设置字符集,这是因为我们不需要对它进行特定的设置,我们可以接受服务器选择的任何字符集。
现在让我们实现serializerForRequest
方法。客户端使用它来确定对请求使用哪个序列化器。因为在这个阶段,服务器和客户端之间没有通信,所以不能进行协商,所以客户端只是选择一个默认的序列化器,在这种情况下,一个utf-8纯文本序列化器如下:
def serializerForRequest = new PlainTextSerializer("utf-8")
接下来我们将实现deserializer
方法。这可以由服务器用于为请求选择反序列化器,也可以由客户机用于为响应选择反序列化器。在MessageProtocol
中传递的是与请求或响应一起发送的内容类型,我们需要检查它,看看它是否是可以反序列化的内容类型,并返回适当的内容类型:
def deserializer(protocol: MessageProtocol) = {
protocol.contentType match {
case Some("text/plain") | None =>
new PlainTextDeserializer(protocol.charset.getOrElse("utf-8"))
case Some("application/json") =>
new JsonTextDeserializer
case _ =>
throw UnsupportedMediaType(protocol, MessageProtocol(Some("text/plain")))
}
}
注意,如果没有指定内容类型,我们将返回一个默认的反序列化器。在这里,我们也可能通过抛出异常而快速失败,但最好不要这样做,因为一些底层传输不允许将内容类型与消息一起传递。例如,如果这用于WebSocket请求,web浏览器不允许你为WebSocket请求设置内容类型。如果没有设置内容类型,则返回默认值,这样可以确保最大的可移植性。
接下来,我们将实现serializerForResponse
方法。它接受客户端发送的已接受协议的列表,并选择一个用于序列化响应。如果它找不到它支持的对象,它就会抛出异常。请注意,任何属性的空值都意味着客户端将接受任何内容,就像客户端没有指定任何可接受的协议一样。
import scala.collection.immutable
def serializerForResponse(accepted: immutable.Seq[MessageProtocol]) = {
accepted match {
case Nil => new PlainTextSerializer("utf-8")
case protocols =>
protocols
.collectFirst {
case MessageProtocol(Some("text/plain" | "text/*" | "*/*" | "*"), charset, _) =>
new PlainTextSerializer(charset.getOrElse("utf-8"))
case MessageProtocol(Some("application/json"), _, _) =>
new JsonTextSerializer
}
.getOrElse {
throw NotAcceptable(accepted, MessageProtocol(Some("text/plain")))
}
}
}
示例
Protocol buffer 序列化器
Protocol buffers是一种与JSON无关的高性能语言替代方案,对于服务之间的内部通信来说,它是一个非常好的选择。下面是一个如何为protoc
生成的Order
类编写MessageSerializer
的例子:
import akka.util.ByteString
import com.lightbend.lagom.scaladsl.api.deser.MessageSerializer.NegotiatedDeserializer
import com.lightbend.lagom.scaladsl.api.deser.MessageSerializer.NegotiatedSerializer
import com.lightbend.lagom.scaladsl.api.deser.StrictMessageSerializer
import com.lightbend.lagom.scaladsl.api.transport.MessageProtocol
import scala.collection.immutable
class ProtobufSerializer extends StrictMessageSerializer[Order] {
private final val serializer = {
new NegotiatedSerializer[Order, ByteString]() {
override def protocol: MessageProtocol =
MessageProtocol(Some("application/octet-stream"))
def serialize(order: Order) = {
val builder = ByteString.createBuilder
order.writeTo(builder.asOutputStream)
builder.result
}
}
}
private final val deserializer = {
new NegotiatedDeserializer[Order, ByteString] {
override def deserialize(bytes: ByteString) =
Order.parseFrom(bytes.iterator.asInputStream)
}
}
override def serializerForRequest =
serializer
override def deserializer(protocol: MessageProtocol) =
deserializer
override def serializerForResponse(
acceptedMessageProtocols: immutable.Seq[MessageProtocol]
) = serializer
}
注意,这个MessageSerializer
不会做任何内容协商。许多情况下,内容协商是一种过分的手段,如果你不需要它,不必去实现它。