Lagom服务由一个接口来描述,称为服务描述符。该接口不仅定义了如何调用和实现服务,还定义了描述如何将接口映射到底层传输协议的元数据。一般来说,服务描述符的实现和使用应该与正在使用的传输协议(无论是REST、websockets还是其他传输)无关。让我们看一个简单的描述符:

  1. import com.lightbend.lagom.scaladsl.api._
  2. trait HelloService extends Service {
  3. def sayHello: ServiceCall[String, String]
  4. override def descriptor = {
  5. import Service._
  6. named("hello").withCalls(
  7. call(sayHello)
  8. )
  9. }
  10. }

这个描述符定义了一个带有一个调用的服务,即sayHello调用。sayHello是一个返回ServiceCall类型的方法,这个类型表示在使用服务时可以被调用触发,并由服务本身实现。接口定义如下:

  1. trait ServiceCall[Request, Response] {
  2. def invoke(request: Request): Future[Response]
  3. }

这里需要注意的一件重要的事情是,调用sayHello方法并不会实际触发该调用,它只是获得了该调用的句柄,然后可以使用invoke方法调用该句柄。
ServiceCall接受两个类型参数,RequestResponseRequest参数是传入请求消息的类型,Response参数是传出响应消息的类型。在上面的例子中,它们都是String,所以我们的服务调用只处理简单的文本消息。
虽然sayHello方法描述了如何以编程方式调用或实现该调用,但它没有描述如何将该调用映射到传输。这是通过提供[descriptor](https://www.lagomframework.com/documentation/1.6.x/scala/api/com/lightbend/lagom/scaladsl/api/Service.html#descriptor:Descriptor)调用的实现来完成的,该调用的接口由[Service](https://www.lagomframework.com/documentation/1.6.x/scala/api/com/lightbend/lagom/scaladsl/api/Service.html)描述。
可以看到我们返回了一个名为hello的服务,我们描述了一个调用,sayHello调用。因为这个服务非常简单,在这种情况下,我们只需要简单地将上述示例中定义的service调用sayHello作为对call方法的方法引用传递给service调用。

Call 标识符

每个服务调用都需要有一个标识符。标识符用于客户端和服务端之前提供路由信息,以便调用可以映射到适当的调用。标识符可以是静态名称或路径,也可以具有动态组件,其中动态路径参数从路径中提取并传递给服务调用方法。
最简单的标识符类型是名称,默认情况下,该名称被设置为与实现它的接口上的方法名称相同。在上面的例子中,我们使用call方法创建了一个名为sayHello的服务调用。也可以使用namedCall方法提供自定义名称:

  1. named("hello").withCalls(
  2. namedCall("hello", sayHello)
  3. )

在本例中,我们将它命名为hello,而不是默认的sayHello。当使用REST实现时,这将意味着该调用的路径为/hello

路径标识符

第二种类型的标识符是基于路径的标识符。使用一个URI路径和查询字符串来路由调用,并且可以从中选择性地提取动态路径参数。可以使用[pathCall](https://www.lagomframework.com/documentation/1.6.x/scala/api/com/lightbend/lagom/scaladsl/api/Service$.html#pathCall[Request,Response](String,ScalaMethodServiceCall[Request,Response])(MessageSerializer[Request,_],MessageSerializer[Response,_]):Call[Request,Response])方法进行配置。
通过在路径中声明动态部分,可以从路径中提取动态路径参数。它们以冒号作为前缀,例如,/order/:id的路径有一个称为id的动态部分。Lagom将从路径中提取该参数,并将其传递给服务调用方法。为了将其转换为该方法接受的类型,Lagom将使用隐式提供的[PathParamSerializer](https://www.lagomframework.com/documentation/1.6.x/scala/api/com/lightbend/lagom/scaladsl/api/deser/PathParamSerializer.html)。Lagom包括许多现成的PathParamSerializer,比如StringLongIntBooleanUUID。下面示例从路径中提取一个long参数并将其传递给服务调用:

  1. def getOrder(orderId: Long): ServiceCall[NotUsed, Order]
  2. override def descriptor = {
  3. import Service._
  4. named("orders").withCalls(
  5. pathCall("/order/:id", getOrder _)
  6. )
  7. }

注意,这里我们使用的是对该方法的eta-expanded扩展引用,因为该方法接受一个参数。
当然可以提取多个参数,这些参数将按照从URL中提取的顺序传递给你的服务调用方法:

  1. def getItem(orderId: Long, itemId: String): ServiceCall[NotUsed, Item]
  2. override def descriptor = {
  3. import Service._
  4. named("orders").withCalls(
  5. pathCall("/order/:orderId/item/:itemId", getItem _)
  6. )
  7. }

查询字符串参数也可以从路径中提取,在路径的最后,在?之后使用&分隔参数列表。例如,以下服务调用使用查询字符串参数来实现分页:

  1. def getItems(orderId: Long, pageNo: Int, pageSize: Int): ServiceCall[NotUsed, Seq[Item]]
  2. override def descriptor = {
  3. import Service._
  4. named("orders").withCalls(
  5. pathCall("/order/:orderId/items?pageNo&pageSize", getItems _)
  6. )
  7. }

当使用call, namedCallpathCall时,如果Lagom将其映射到REST, Lagom将尽最大努力以语义方式将其映射到REST。例如,如果有一个请求消息,它将使用POST方法,而如果没有,它将使用GET方法。

REST 标识符

最后一种标识符类型是REST标识符。REST标识符被设计为在创建语义REST API时使用。它们同时使用路径(如基于路径的标识符)和请求方法来标识它们。它们可以使用restCall(MessageSerializer[Request,],MessageSerializer[Response,]):Call[Request,Response])方法进行配置:

  1. def addItem(orderId: Long): ServiceCall[Item, NotUsed]
  2. def getItem(orderId: Long, itemId: String): ServiceCall[NotUsed, Item]
  3. def deleteItem(orderId: Long, itemId: String): ServiceCall[NotUsed, NotUsed]
  4. def descriptor = {
  5. import Service._
  6. import com.lightbend.lagom.scaladsl.api.transport.Method
  7. named("orders").withCalls(
  8. restCall(Method.POST, "/order/:orderId/item", addItem _),
  9. restCall(Method.GET, "/order/:orderId/item/:itemId", getItem _),
  10. restCall(Method.DELETE, "/order/:orderId/item/:itemId", deleteItem _)
  11. )
  12. }

消息

Lagom中的每个服务调用都有一个请求消息类型和一个响应消息类型。当请求或响应消息没有被使用时,akka.NotUsed可以用来替代。请求和响应消息类型分为两类,严格的和流的。

严格消息

严格消息是可以用一个简单的Scala对象表示的单个消息,通常是一个case类。消息将被缓冲到内存中,然后解析,例如JSON。当两种消息类型都是严格的时,调用称为同步调用,请求发送和接收,然后响应被发送和接收。调用者和被调用者在他们的通信中同步发生。
到目前为止,我们看到的所有服务调用示例都使用了严格的消息,例如,上面的订单服务描述符接受并返回项目和订单。输入值直接传递给服务调用,并直接从服务调用返回,这些值在发送之前序列化到内存中的JSON缓冲区,并在从JSON反序列化返回之前完全读入内存。

流式消息

流消息是类型为[Source](https://doc.akka.io/api/akka/2.6/akka/stream/scaladsl/Source.html?_ga=2.179079009.458939283.1621606685-1842420647.1617804183)的消息。Source是一个允许异步流和消息处理的Akka streams API。下面是一个流服务调用的例子:

  1. import akka.NotUsed
  2. import akka.stream.scaladsl.Source
  3. def tick(interval: Int): ServiceCall[String, Source[String, NotUsed]]
  4. def descriptor = {
  5. import Service._
  6. named("clock").withCalls(
  7. pathCall("/tick/:interval", tick _)
  8. )
  9. }

此服务调用具有严格的请求类型和流响应类型。该方法的实现可能会返回一个Source,该Source会在指定的时间间隔发送输入的消息String
一个双向流调用可能看起来像这样:

  1. import akka.NotUsed
  2. import akka.stream.scaladsl.Source
  3. def sayHello: ServiceCall[Source[String, NotUsed], Source[String, NotUsed]]
  4. def descriptor = {
  5. import Service._
  6. named("hello").withCalls(
  7. call(this.sayHello)
  8. )
  9. }

在这种情况下,服务器可能返回一个Source,该Source将请求流中接收到的每个消息转换为带有Hello前缀的消息。
Lagom会为流选择一个合适的传输方式,通常是WebSockets。WebSocket协议支持双向流,因此是一个很好的通用流选项。当只有一个请求或响应消息流时,Lagom将通过发送或接收单个消息来实现严格消息的发送和接收,然后让WebSocket打开,直到另一个方向关闭。否则,当任何一个方向关闭时,Lagom将关闭WebSocket。

消息序列化

请求和响应的消息序列化器使用类型类提供。每个调用,namedCall, pathCallrestCall方法都为每个请求和响应消息接受一个隐式MessageSerializer 。Lagom提供了一个用于String消息的序列化器,以及隐式地将Play JSON Format类型类转换为消息序列化器的序列化器。

使用 Play JSON

Play JSON提供了一个基于类的函数类型库,用于编写JSON格式化程序。有关如何使用这个库的详细文档,请参见Play文档。现在,我们将看看如何使用Play的JSON格式宏定义case类的JSON格式。
假设有一个这样的Usercase类:

  1. case class User(
  2. id: Long,
  3. name: String,
  4. email: Option[String]
  5. )

Play JSON格式可以在User伴生对象上定义如下:

  1. object User {
  2. import play.api.libs.json._
  3. implicit val format: Format[User] = Json.format[User]
  4. }

此格式将生成并解析JSON,格式如下:

  1. {
  2. "id": 12345,
  3. "name": "John Smith",
  4. "email": "john.smith@example.org"
  5. }

字段可以通过使其类型为Option而成为可选的,这意味着如果该属性不存在,该格式将不会解析JSON,当它生成JSON时,它将不会生成该属性。
通过在User同伴对象上定义格式,我们可以确保在需要时自动使用这种格式,因为Scala的隐式作用域规则。这意味着,除了声明格式外,不需要做进一步的工作来确保将这种格式用于MessageSerializer
请注意,如果您的case类引用另一个非原始类型,例如另一个case类,您还需要为该case类定义格式。

写自定义消息序列化器

可以编写自定义消息序列化器,例如,使用协议缓冲区或其他消息格式类型。有关更多信息,请参见消息序列化器文档。