Lagom服务由一个接口来描述,称为服务描述符。该接口不仅定义了如何调用和实现服务,还定义了描述如何将接口映射到底层传输协议的元数据。一般来说,服务描述符的实现和使用应该与正在使用的传输协议(无论是REST、websockets还是其他传输)无关。让我们看一个简单的描述符:
import com.lightbend.lagom.scaladsl.api._
trait HelloService extends Service {
def sayHello: ServiceCall[String, String]
override def descriptor = {
import Service._
named("hello").withCalls(
call(sayHello)
)
}
}
这个描述符定义了一个带有一个调用的服务,即sayHello
调用。sayHello
是一个返回ServiceCall
类型的方法,这个类型表示在使用服务时可以被调用触发,并由服务本身实现。接口定义如下:
trait ServiceCall[Request, Response] {
def invoke(request: Request): Future[Response]
}
这里需要注意的一件重要的事情是,调用sayHello
方法并不会实际触发该调用,它只是获得了该调用的句柄,然后可以使用invoke
方法调用该句柄。ServiceCall
接受两个类型参数,Request
和Response
。Request
参数是传入请求消息的类型,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
方法提供自定义名称:
named("hello").withCalls(
namedCall("hello", sayHello)
)
在本例中,我们将它命名为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,比如String
、Long
、Int
、Boolean
和UUID
。下面示例从路径中提取一个long
参数并将其传递给服务调用:
def getOrder(orderId: Long): ServiceCall[NotUsed, Order]
override def descriptor = {
import Service._
named("orders").withCalls(
pathCall("/order/:id", getOrder _)
)
}
注意,这里我们使用的是对该方法的eta-expanded扩展引用,因为该方法接受一个参数。
当然可以提取多个参数,这些参数将按照从URL中提取的顺序传递给你的服务调用方法:
def getItem(orderId: Long, itemId: String): ServiceCall[NotUsed, Item]
override def descriptor = {
import Service._
named("orders").withCalls(
pathCall("/order/:orderId/item/:itemId", getItem _)
)
}
查询字符串参数也可以从路径中提取,在路径的最后,在?
之后使用&
分隔参数列表。例如,以下服务调用使用查询字符串参数来实现分页:
def getItems(orderId: Long, pageNo: Int, pageSize: Int): ServiceCall[NotUsed, Seq[Item]]
override def descriptor = {
import Service._
named("orders").withCalls(
pathCall("/order/:orderId/items?pageNo&pageSize", getItems _)
)
}
当使用call
, namedCall
或pathCall
时,如果Lagom将其映射到REST, Lagom将尽最大努力以语义方式将其映射到REST
。例如,如果有一个请求消息,它将使用POST
方法,而如果没有,它将使用GET
方法。
REST 标识符
最后一种标识符类型是REST标识符。REST标识符被设计为在创建语义REST API时使用。它们同时使用路径(如基于路径的标识符)和请求方法来标识它们。它们可以使用restCall(MessageSerializer[Request,],MessageSerializer[Response,]):Call[Request,Response])方法进行配置:
def addItem(orderId: Long): ServiceCall[Item, NotUsed]
def getItem(orderId: Long, itemId: String): ServiceCall[NotUsed, Item]
def deleteItem(orderId: Long, itemId: String): ServiceCall[NotUsed, NotUsed]
def descriptor = {
import Service._
import com.lightbend.lagom.scaladsl.api.transport.Method
named("orders").withCalls(
restCall(Method.POST, "/order/:orderId/item", addItem _),
restCall(Method.GET, "/order/:orderId/item/:itemId", getItem _),
restCall(Method.DELETE, "/order/:orderId/item/:itemId", deleteItem _)
)
}
消息
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。下面是一个流服务调用的例子:
import akka.NotUsed
import akka.stream.scaladsl.Source
def tick(interval: Int): ServiceCall[String, Source[String, NotUsed]]
def descriptor = {
import Service._
named("clock").withCalls(
pathCall("/tick/:interval", tick _)
)
}
此服务调用具有严格的请求类型和流响应类型。该方法的实现可能会返回一个Source
,该Source会在指定的时间间隔发送输入的消息String
。
一个双向流调用可能看起来像这样:
import akka.NotUsed
import akka.stream.scaladsl.Source
def sayHello: ServiceCall[Source[String, NotUsed], Source[String, NotUsed]]
def descriptor = {
import Service._
named("hello").withCalls(
call(this.sayHello)
)
}
在这种情况下,服务器可能返回一个Source
,该Source将请求流中接收到的每个消息转换为带有Hello
前缀的消息。
Lagom会为流选择一个合适的传输方式,通常是WebSockets。WebSocket协议支持双向流,因此是一个很好的通用流选项。当只有一个请求或响应消息流时,Lagom将通过发送或接收单个消息来实现严格消息的发送和接收,然后让WebSocket打开,直到另一个方向关闭。否则,当任何一个方向关闭时,Lagom将关闭WebSocket。
消息序列化
请求和响应的消息序列化器使用类型类提供。每个调用,namedCall,
pathCall
和restCall
方法都为每个请求和响应消息接受一个隐式MessageSerializer 。Lagom提供了一个用于String
消息的序列化器,以及隐式地将Play JSON Format类型类转换为消息序列化器的序列化器。
使用 Play JSON
Play JSON提供了一个基于类的函数类型库,用于编写JSON格式化程序。有关如何使用这个库的详细文档,请参见Play文档。现在,我们将看看如何使用Play的JSON格式宏定义case类的JSON格式。
假设有一个这样的User
case类:
case class User(
id: Long,
name: String,
email: Option[String]
)
Play JSON格式可以在User
伴生对象上定义如下:
object User {
import play.api.libs.json._
implicit val format: Format[User] = Json.format[User]
}
此格式将生成并解析JSON,格式如下:
{
"id": 12345,
"name": "John Smith",
"email": "john.smith@example.org"
}
字段可以通过使其类型为Option
而成为可选的,这意味着如果该属性不存在,该格式将不会解析JSON,当它生成JSON时,它将不会生成该属性。
通过在User同伴对象上定义格式,我们可以确保在需要时自动使用这种格式,因为Scala的隐式作用域规则。这意味着,除了声明格式外,不需要做进一步的工作来确保将这种格式用于MessageSerializer
。
请注意,如果您的case类引用另一个非原始类型,例如另一个case类,您还需要为该case类定义格式。
写自定义消息序列化器
可以编写自定义消息序列化器,例如,使用协议缓冲区或其他消息格式类型。有关更多信息,请参见消息序列化器文档。