注:之前在segmentfault编辑的Lagom 官方文档,但是他的编辑页面实在太卡了,转战CSDN吧,Lagom的前面的会陆续搬到CSDN上来的
副标题:编写Lagom服务
1.Service Descriptors(服务描述符)
Lagom服务由接口描述,称为服务描述符。这个接口并不只是定义怎么取调用这个接口和实现它,它还描述接口如何映射到底层传输协议的元数据。一般来说,服务描述符,它的实现和消费应该对正在使用的传输保持不可知,无论他是一个REST还是webSocket,或者其他的传输方式。让我们来看看一个简单的描述符:
import com.lightbend.lagom.javadsl.api.*;
import static com.lightbend.lagom.javadsl.api.Service.*;
public interface HelloService extends Service {
ServiceCall<String, String> sayHello();
default Descriptor descriptor() {
return named("hello").withCalls(
call(this::sayHello)
);
}
}
这个描述符定义了一个带有一个叫做sayHello的方法调用的服务,sayHello是一个返回ServiceCall对象格式的方法,这是一个当消费服务时候可以被执行的的,由服务本身实现的调用的代表(这句话是真的很拗口,所以下面对源码的注释进行翻译一下,方便理解)。
那么在Lagom源码中,对于ServiceCall的定义是这样的:首先:
public interface ServiceCall<Request, Response>
这是ServiceCall的定义,他是一个函数式接口,函数式接口必然存在一个未实现的方法,它就是
该方法的功能是调用服务的回调(invoke service call)
CompletionStage<Response> invoke(Request request);
注意这里的一个重要的原因是,当执行sayHello方法的时候并不会真正的去执行这个call调用,他只是简单的返回了调用的句柄,这个句柄是可以被ServiceCall的invoke方法执行的。
同时,ServiceCall有两个类型参数,Request和Response,请求参数Request是传入请求消息的类型,响应参数Response是传出响应消息的类型。在上面的例子中,这两个都是String,所以当我们的服务调用时候仅仅只是简单的处理了一下文本罢了。
尽管sayHello()方法描述了调用如何以编程方式执行或实现的,它没有描述如何将这个调用映射到传输当中。而这个则是通过实现一个叫descriptor()的default方法来调用的,当然,这个默认的方法,是Service接口提供的。
在上述代码中,我们可以看到我们返回了一个叫“hello”的服务,我们描述了一个调用,叫sayHello调用。由于这种服务非常简单,因此在这种情况下,我们不需要做任何事情,只需简单地将我们需要调用的作为方法作为方法引用来传递给call方法(注意:这里的call方法是Lagom的Service接口的call方法,,该方法有多个版本的重载)。
1.0 描述符概述
描述了一个服务
描述符是服务提供的一组调用描述符,再加上关于服务及其调用如何服务的元数据。元数据可能包括版本控制和迁移、SLA、分片提示、断路器策略等。
1.1 调用标识符(就是SpringMVC中的RequestMapping)
每个服务调用需要有一个标识符。标识符用于为客户端和服务的实现提供路由信息,因此可以映射到适当的方法调用。标识符可以是静态名称或路径,也可以有动态组件,其中动态路径参数从路径中提取并传递给服务调用方法。
第一种,也是最简单的标识符类型是名称,默认情况下,该名称将与实现它的接口的方法名称相同。还可以提供自定义名称,通过将其传递给namedCall方法:
//named方法:为具有给定名称的服务创建描述符
//withCall(Call<?, ?>... calls)方法:将给定的服务调用添加到此服务。
// 该方法的入参是Call类型的,它定义一个“调用”,描述一个HTTP请求。例如,用于创建链接或填充重定向数据。
//namedCall():创建一个由给定名称标识的服务调用描述符。
default Descriptor descriptor() {
return named("hello").withCalls(
namedCall("hello", this::sayHello)
);
}
在这个例子中,我们起了一个‘hello’的服务名,替换了默认的名字‘sayHello’.当我们使用REST的时候,这就意味着我们有一个/hello的服务路径。
1.1.1基于路径标识符
第二种标识符是基于路径的标识符。这将使用URI路径和查询字符串来路由调用,并且从it动态路径参数可以选择提取出来。它们可以使用pathCall方法方法进行配置。
通过在路径中声明动态部分,从路径中提取动态路径参数。这些是用冒号来固定的,例如:/order/:id的路径有一个叫id的动态部分。Lagom将从路径中提取该参数,并将其传递给服务调用方法。为了将其转换为方法所接受的类型,Lagom将使用一个叫PathParamSerializer的东西。Lagom 包括许多 PathParamSerializer 它开箱即用,例如字符串、 长整数和布尔值。正如下面例子看到的,我们从路径中提取一个long类型的参数,并将它放到服务调用中
ServiceCall<NotUsed, Order> getOrder(long id);
default Descriptor descriptor() {
return named("orders").withCalls(
pathCall("/order/:id", this::getOrder)
);
}
可以将多个参数提取出来,这些参数将通过从URL中提取的顺序传递给服务调用方法:
ServiceCall<NotUsed, Item> getItem(long orderId, String itemId);
default Descriptor descriptor() {
return named("orders").withCalls(
pathCall("/order/:orderId/item/:itemId", this::getItem)
);
}
查询字符串参数也可以从路径中提取,在?开始的查询字符串中,使用一个个&分隔,一直到结尾。例如,下面的服务调用使用查询字符串参数来实现分页:
ServiceCall<NotUsed, PSequence<Item>> getItems(long orderId, int pageNo, int pageSize);
default Descriptor descriptor() {
return named("orders").withCalls(
pathCall("/order/:orderId/items?pageNo&pageSize", this::getItems)
);
}
当你使用cal,nameCall或者pathCall的时候,如果Lagom将其映射到REST,Lagom将尽最大努力以语义的方式将其映射到REST。这意味着如果有一个请求消息,它可能会使用POST,如果啥也没有,它可能会使用GET
1.1.2 RESR标识符
最后一种类型标识符是REST标识符。REST标识符旨在创建语义REST Api 时使用。他们用这两个路径,如基于路径标识符和请求方法,来识别它们。他们可以使用 restCall 方法来配置︰
ServiceCall<Item, NotUsed> addItem(long orderId);
ServiceCall<NotUsed, Item> getItem(long orderId, String itemId);
ServiceCall<NotUsed, NotUsed> deleteItem(long orderId, String itemId);
default Descriptor descriptor() {
return named("orders").withCalls(
restCall(Method.POST, "/order/:orderId/item", this::addItem),
restCall(Method.GET, "/order/:orderId/item/:itemId", this::getItem),
restCall(Method.DELETE, "/order/:orderId/item/:itemId", this::deleteItem)
);
}
1.2Messages 消息
在Lagom的每个服务调用都有一个请求消息类型和一个响应消息类型。当不使用请求或响应消息时,akka.NotUsed可以在他们的地方使用。请求和响应消息类型分为两类,strict的和stream的。
1.2.1Strict messages
一个strict消息是可以由一个简单的Java对象来表示的单个消息。消息将被缓冲到内存中,然后解析为JSON。当两个消息类型都是严格的时,调用被称为同步调用,即发送和接收请求,然后发送和接收响应。调用者和被调用者在他们的交互中保持同步。
到目前为止,我们看到的所有服务调用示例都使用了strict的消息,例如,上面的订单服务描述符接受和返回项目和订单。输入值直接传递给服务调用,并直接从服务调用返回,这些值在发送之前被序列化到内存中的JSON缓冲区,并在从JSON中反序列化之前完全读入内存
1.2.2 stremed message
stremed消息是akka.source类型的消息。akka.source是akka的api,该api允许异步的流动和处理消息。下面是streamed服务调用的例子:
ServiceCall<String, Source<String, ?>> tick(int interval);
default Descriptor descriptor() {
return named("clock").withCalls(
pathCall("/tick/:interval", this::tick)
);
}
上述的这个streamd服务调用,有一个strict的请求消息类型,和一个stremed的相应类型。该方法的实现,可能会返回一个akka.source对象,该对象在指定的时间间隔发送输入的消息字符串。
双向流调用可能是这样的:
ServiceCall<Source<String, ?>, Source<String, ?>> sayHello();
default Descriptor descriptor() {
return named("hello").withCalls(
call(this::sayHello)
);
}
在这个例子中,这个服务会将从名称以hello开头的请求的请求消息流中收取的消息进行转换,并返回一个akka.source对象。
Lagom将为流选择适当的传输,通常,这将是websocket。WebSockets支持双向流,因此是流媒体的一个良好通用选项。当只有一个请求或响应消息流时,Lagom将通过发送或接收一条消息来实现发送和接收strict的消息,然后将WebSocket打开直到另一个方向关闭。否则,当两个方向关闭时,Lagom将关闭WebSocket。
1.2.3 Message serialization消息序列化
默认情况下,Lagom将为请求和响应序列化和反序列化选择合适的序列化器。开箱即用的,Lagom将使用JSON进行通信,使用Jackson来序列化和反序列化消息。
有关消息序列化器的详细信息,包括如何编写和配置自定义消息序列化器,请参见消息序列化器。
2.实现服务
服务通过提供服务描述符接口的实现来实现,实现该描述符指定的每个调用。
例如,这里是HelloService描述符的实现:
import com.lightbend.lagom.javadsl.api.*;
import akka.NotUsed;
import static java.util.concurrent.CompletableFuture.completedFuture;
public class HelloServiceImpl implements HelloService {
public ServiceCall<String, String> sayHello() {
return name -> completedFuture("Hello " + name);
}
}
正如你所看到的,这个sayHello方法以lambda实现的。这里有一件重要的事情要实现,那就是调用sayHello()本身并没有执行调用,它只返回要执行的调用。这里的优势在于,当涉及到用其他横切关注点(例如身份验证)来组合调用时,可以很容易地使用基于函数的复合函数。
如果您以前使用过基于Java的web框架,您可能熟悉使用注释来组合横切关注点。注释有它们的局限性——它们不可以组合,也就是说,如果你有两个不同的注释,你想要应用到很多不同的方法上,那么简单地创建一个新的注释来组合它们是不直接的。相反,函数只是一些方法,如果你想把两种方法组合在一起,你就会创建一个新的方法来调用它们。另外,你完全控制了它们是如何组成的,你知道它们是由什么顺序组成的,而不是通过注解,通过反射来神奇地读取它们并从中获得意义。
现在我们再看一下ServiceCall接口:
interface ServiceCall<Request, Response> {
CompletionStage<Response> invoke(Request request);
}
ServiceCall是一个函数接口,它的这个唯一的未实现的方法,它会接受这个请求,返回响应作为一个CompletionStage(看2.0,我会简单介绍下这个对象,java8的)。如果你之前不知道CompletionStage,他是一个承诺。当一个API返回一个承诺时,这个值可能还不会被计算,但是API承诺在将来某个时候,它将会被计算。因为值还没有计算,所以不能立即与它交互。你可以做的,就是使用thenApply和thenCompose,把转换承诺的回调附属给一个新的值。带有thenApply和thenCompose的CompletionStage,是在Java中构建响应式应用程序的基本构建块,它们允许您的代码是异步的,而不是等待将要发生的事情,而是附加回调,对正在进行的计算进行响应。
当然,简单的hello world计算不是异步的,它需要做的就是将两个字符串连接起来,然后立即返回。在这种情况下,我们需要将其结果打包在一个CompletionStage中,这个可以通过调用CompletableFuture.completedFuture()来结束。CompletableFuture.completedFuture()返回一个包装了一个立即可用的CompletionStage 的子类。
有提供服务的实现,我们现在可以用Lagom 框架注册。Lagom是建立在play框架之上的,因此,使用Play的基于Guice的依赖注入支持来注册组件。要注册一个服务,您需要实现一个Guice模块。这可以通过在根包中创建一个名为Module的类来实现:
import com.google.inject.AbstractModule;
import com.lightbend.lagom.javadsl.server.ServiceGuiceSupport;
public class Module extends AbstractModule implements ServiceGuiceSupport {
protected void configure() {
bindService(HelloService.class, HelloServiceImpl.class);
}
}
正如你看到的,这个模块继承自Guice的AbstarctMoudle,还有Lagom的ServiceGuiceSupport(Lagom服务实现必须创建这个接口的一个实现,并使用它来绑定服务)。这个模块,你可以提供你喜欢的任何Guice绑定。在本例中,我们只是为HelloService提供绑定。bindService()只会被调用一次,因为这将绑定到一个用于路由Lagom服务调用的Play路由上,如果绑定多次,将会导致Guice配置错误。将使用描述符绑定中的名称来命名您的服务。当与其他微服务进行交互时,这个名称将被Lagom用作默认值来标识您的微服务。
按照惯例,Play将自动加载在根包下被称为Module的模块,然而如果你想给你的模块程序一个自定义的名字,或者将它纳入到根包下,您可以手动添加如下的application.conf,添加模块到Play的启用模块列表中:
play.modules.enabled += com.example.MyModule
2.0 概念介绍
2.0.1 Interface CompletionStage<\T>
他是异步计算的一个可能出现的阶段,当另一个完成阶段完成时,就再执行一个操作或计算一个值。一个阶段在结束其计算时完成,但这可能反过来触发其他依赖阶段。在这个接口中定义的功能只需要一些基本的表单,扩展到一组更大的方法来捕获一系列的使用样式:
一个阶段执行的计算可以用函数、消费者或Runnable来表示(独自的使用像apply、accept或run的名称的方法),这取决于它是否需要参数和/或产生结果。例如,stage.thenApply(x - >square(x)).thenAccept(x - > System.out.print(x)). thenrun(()- > System.out.println())。这是一种新的形式(组合)应用阶段本身的功能,而不是它们的结果。
一个阶段的执行可能是由一个阶段的完成,或者两个阶段,或者两个阶段中的两个阶段所触发。单个阶段的依赖项是用前缀的方法来安排的。由两个阶段的完成所触发的,可以使用相应命名的方法组合它们的结果或效果。由两个阶段触发的结果不能保证依赖阶段的计算中使用的结果或效果。
各个阶段之间的依赖性控制了计算的触发,但是不保证任何特定的顺序。此外,新阶段的计算的执行可能有三种方式:默认执行、默认异步执行(使用带有后台默认异步执行工具的后缀async的方法)或自定义(通过提供的执行程序)。默认和异步模式的执行属性是由CompletionStage实现指定的,而不是这个接口。带有显式执行器参数的方法可能具有任意的执行属性,甚至可能不支持并发执行,但以一种可容纳异步的方式来安排处理。
两种方法支持处理触发阶段是否完成正常或异常:whenComplete方法允许无论结果如何,都要注入行动,都允许注入操作,否则在完成时保留结果。另外,handle方法还允许这个stage(状态)进一步依赖其他的状态进一步的处理以计算一个替换的结果。在所有其他情况下,如果一个阶段的计算以(未检查的)异常或错误突然终止,那么所有依赖阶段都需要完成异常的完成,而一个CompletionException将异常作为其原因。如果一个阶段依赖于两个阶段,并且两个阶段都是异常的,那么CompletionException可能对应于这些异常中的任何一个。如果一个阶段依赖于其他两个阶段,并且只有其中一个完成异常,则不能保证依赖阶段是否完成正常或异常。在方法完成时,当所提供的操作本身遇到异常时,如果未完成异常,则该异常将异常完成。
所有方法都遵循上述触发、执行和异常完成规范(在单独的方法规范中没有重复)。此外,虽然用于传递完成结果的参数(即T类型的参数)用于接受它们的方法可能是null,但是传递一个空值的其他参数将导致抛出一个NullPointerException。
这个接口不定义最初创建的方法,强制完成正常或异常的,探测完成状态或结果,或者等待一个阶段的完成。完成阶段的实现可能提供了实现这些效果的方法。方法toCompletableFuture()可以通过提供一个公共转换类型实现这个接口的不同实现之间的互操作性。
2.0.2 Moudle AbstractModule
2.0.2.1Moudle
package com.google.inject
public interface Module {
void configure(Binder binder);
}
一个模块对象,它提供配置信息,典型的接口绑定,这将被用来创建一个Injector。基于Guice的应用程序最终由一组模块和一些自引导代码组成。
除了通过configure方法来配置绑定,还会为使用了@Provides(模块对象的创建提供者方法绑定的一个注释方法。该方法的返回 类型绑定到它的返回值。Guice将把依赖项传递给方法作为参数。)来注释了的方法进行绑定,使用scope和binding注解对这些方法来进行配置绑定。
configure方法为这个模块提供绑定和其他配置,这个方法的参数唯一的是一个Binder 对象
2.0.2.2 AbstractModule
减少重复和结果,使其更具可读性。简单地扩展这个类,并实现configure方法,并调用那些在Binder中发现的反应他们继承的方法。例如:
* public class MyModule extends AbstractModule {
* protected void configure() {
* bind(Service.class).to(ServiceImpl.class).in(Singleton.class);
* bind(CreditCardPaymentService.class);
* bind(PaymentService.class).to(CreditCardPaymentService.class);
* bindConstant().annotatedWith(Names.named(“port”)).to(8080);
* }
* }
2.0.2.3 Binder
收集配置信息(主要是从@Building注解中获取),用来传构建一个Injector对象。Guice将该对象提供给应用程序的Module实现,他们每一个都是可以贡献他们自己的绑定和其他注册。
Guice使用一个嵌入式领域特定语言,或EDSL,来帮助你简单地、易地创建绑定。这种方法对整体来说是很好的可用性,但这确实需要付出很小的代价——很难通过阅读方法级javadocs来了解如何使用绑定EDSL。替代的,你应该参考下在下面的例子,为了节省空间,这些例子省略了Binder方法开头的声明,正如您将会在继承扩展AbstractModule 时一样。
实例一:bind(ServiceImpl.class);
这个声明基本上没有任何作用,它绑定‘serviceImpl’类到他自己身上,但是并没有更改Guice的默认行为。如果你更喜欢你的Module类作为其提供的服务的显式显示,你可能还是想使用它。此外,在极少数情况下, Guice可能无法验证injector创建时间的绑定,除非它 明确给出
实例二:bind(Service.class).to(ServiceImpl.class);
指定对于Service实例的没有@Building绑定的请求,应该把它当作是对于一个ServiceImpl请求来对待。换句话说,就是Service声明方法,ServiceImpl提供实现,对于接口的访问,会调用实现的方法。它忽略了Sercive接口中任何被@ImplmentdBy和@ProvidedBy注解的方法的注解。因为Guice在它到达这个注解之前,早已移向serviceImpl,也就是根本没有打过照面。
实例三:bind(Service.class).toProvider(ServiceProvider.class);
在这个例子中,ServiceProvider必须实现或者继承自Provider<\Service>.(看下面的介绍) 这个绑定指定,Guice解决一个对于Service的一个没有注解注入的请求,应该首先以常规的方式来分解ServiceProvider这个实例,然后在结果提供程序实例上调用Provider.get()以获得Service实例。
你在这里使用的Provider不需要是一个工厂,那时因为的提供者经常创建它提供的所有实例。然而,这通常是一个很好的做法,你可以使用Guice的@Scope来指导什么时候创建。
实例四:bind(Service.class).annotatedWith(Red.class).to(ServiceImpl.class);
与上面的例子相似,但只适用于使用注解绑定了的请求,上述例子的是指针对@Red注解绑定的请求。如果你的同样包含针对特殊的值的以@Red注解绑定的,那么这个绑定就会成为一个“万能的”,他会接管所有的那些@Red没有精确绑定那些个值。
实例五:bind(ServiceImpl.class).in(Singleton.class);
bind(ServiceImpl.class).in(Scopes.SINGLETON);
上述这两个只能写一个
这些语句中的任何一个都将ServiceImpl类放入到单例范围内。Guice只会创建一个ServiceImpl实例,对于这种类型的所有注入请求,都要使用它。如果第二个绑定是按照前面的例子中的注释来限定的,仍然可以绑定 ServiceImpl 的另一个实例。Guice并不过分的关心你对于一个单利的对象创建了多个,如果你全告诉Guice那些全是你需要的,那么也只会让你有一个是可用的。
注意:以这种方式指定的范围覆盖了在ServiceImpl类中指定的任何作用域。
此外,出了Singleton和Scopes.SINGLETON,servlet特定的scopes也是可用的,您的模块也可以在这里贡献自己的定制范围。
实例六:bind(new TypeLiteral<PaymentService<CreditCard>>() {})
.to(CreditCardPaymentService.class);
不可否认,这种奇特的构造是绑定参数类型的方法,他会该诉Guice如何向类型元素的注入请求表示敬意。
Guice目前无法绑定或注入泛型类型,像Set<\E>,所有类型的参数都必须是完全指定。
实例七:bind(Service.class).toInstance(new ServiceImpl());
bind(Service.class).toInstance(SomeLegacyRegistry.getService());
二者取其一
在这个示例中,模块本身,而不是Guice,负责去获取ServiceImpl的实例,然后要求Guice对于Service的请求,都拿这个单一的ServiceImpl去处理。当Injector创建了后,它将会自动的为这个实例执行属性和方法的注入,但是在SercviceImpl上的任何可注入的构造函数完全不理会。注意这种在预加载情况下,不容易控制的。
实例八:bindConstant().annotatedWith(ServerHost.class).to(args[0]);
设置一个固定的绑定。固定的注射必须经常被标注。档一个固定绑定的值是一个字符串的时候,他是非常合适转换为所有的原始类型。例如转化为enum,可以使用Enum.valueOf(),其他类型的转换可以是配置使用convertToTypes(Matcher, TypeConverter)。
实例九:red = MyModule.class.getDeclaredField(“red”).getAnnotation(Color.class);
bind(Service.class).annotatedWith(red).to(RedService.class);
如果绑定注释有参数,则可以为你注释的不同的特定值而应用不同的绑定。获得正确的注释实例是一种痛苦——上面所展示的一种方法是将原型注释应用到模块类中的一个字段中,这样你就可以阅读这个注释实例,并将其交给Guice。
实例十: bind(Service.class)
.annotatedWith(Names.named(“blue”))
.to(BlueService.class);
区分名称是一个常见的用例我们提供了一个标准注解,@com.google.inject.name.Named。因为Guice的库函数的支持,通过名字绑定比任意注解绑定的要简单的许多。但是,请记住,这些名称将位于一个单一的平面名称空间中,并使用应用程序中使用的所有其他名称
实例十一:Constructor loneCtor = getLoneCtorFromServiceImplViaReflection();
bind(ServiceImpl.class)
.toConstructor(loneCtor);
在这个例子中,我们直接告诉Guice使用哪一个构造器来新创建一个类的实现。它以为着我们不需要对任何一个构造函数放置任何@Inject,Guice把所提供的构造函数看作是注释的。当你不能修改那些已经存在的类的时候,使用它会比使用Provider更方便一点。
上述例子的清单远远不够详尽(我去,这么多还不全),如果您可以考虑一个示例的概念如何与另一个示例的概念共存,那么您很可能将二者结合在一起。如果这两个概念对彼此毫无意义,那么你很可能就无法做到这一点。在一些情况下,Guice将会让一些虚假的事情发生,然后在运行时,当你试图创建你的Injector时,就会告诉你这些问题。
2.0.2.4 Provider
Provider是一个接口,只有一个方法叫get(),这个方法会返回一个T类型的对象。Provider通过Guice 以多种方式使用 :
当获取实例的默认方法时(一个可注入或无参数的构造函数)对于特定的绑定是不够的,模块可以指定一个自定义的Provider来代替,这个自定义的Provider要确切地控制Guice如何创建或获取绑定的实例。
实现类可能总是选择有一个Provider<\T>的实例被注入进来,而不是在注入进来一个T类型的实例。这可能会让您访问多个 实例,你希望安全的改变和丢弃,超出范围的实例(例如在@SessionScoped对象中使用@RequestScoped对象),将被延迟初始化的实例
一个自定义的Scope对象是被实现为作为Provider<\T>的装饰器,这决定 何时委托给后台提供程序,以及何时以其他方式提供实例。
Injector提供了到Provider<\T> 的访问,通过 Injector.getProvider()方法,它用来针对给定的键以满足请求
2.1 Working with streams
当请求和响应主体是strict的,与它们一起工作是非常简单的。然而,如果它们是stremed的,则需要使用Akka流来与它们协同工作。让我们来看看在服务描述符示例中如何实现一些流服务调用。
tick服务调用将返回在指定的时间间隔发送消息的akka.source。Akka流为这样的流提供了一个有用的构造函数:
public ServerServiceCall<String, Source<String, ?>> tick(int intervalMs) {
return tickMessage -> {
FiniteDuration interval = FiniteDuration.create(intervalMs, TimeUnit.MILLISECONDS);
return completedFuture(Source.tick(interval, interval, tickMessage));
};
}
前两个参数是发送消息之前的延迟,以及应该发送消息的间隔。第三个参数是应该在每个tick上发送的消息.调用这个 服务调用 的间隔为1000,并且tick方法的请求消息将导致返回的流,该流每秒钟发送一个tick的消息。
sayHello服务调用可以通过映射传入的 akka.source:
public ServerServiceCall<Source<String, ?>, Source<String, ?>> sayHello() {
return names -> completedFuture(names.map(name -> "Hello " + name));
}
当你映射一个akka.source,您可以返回一个新的akka.source,它将映射转换应用到传入的akka.source生成的每个消息。
这些处理流的例子很明显是微不足道的。发布-订阅(参考指南5.9)和持久化read端(参考指南5.4)的部分显示了在Lagom中使用流的真实示例(就在参考治指南五中)。
2.2 Handling headers
有时您可能需要处理请求头,或者向响应头添加信息。ServiceCall 提供了handleRequestHeader 和handleResponseHeader 方法来允许你这样做,但是,不建议您直接实现这一点,而是应该使用ServerServiceCall。
ServerServiceCall 是一个继承自ServiceCall 的接口,并提供了额外的方法–invokeWithHeaders。这与常规invoke方法不同,因为除了请求参数之外,它还接受一个RequestHeader参数。但不是返回一个CompletionStage<\Response>,他是返回一个CompletionStage<\Pair<\ResponseHeader, \Response>>。因此它可以允许你处理请求头,发送一个自定义的响应头。ServerServiceCall 实现handleRequestHeader 和handleResponseHeader 方法,因此,当Lagom调用invoke方法时,它将被委托给invokewithheader方法。
ServerServiceCall 是一个函数式接口,将原始invoke方法抽象出来,因此,当一个接口要求您通过或返回一个ServerServiceCall时,如果用lambda来实现它,则不必处理头。提供了一个额外的功能接口,HeaderServiceCall,它继承自ServerServiceCall ,将invokewithheader作为抽象方法。这可以用两种方式来处理带有lambda实现服务调用的头。
如果您直接执行服务调用,您可以简单地将返回类型改为HeaderServiceCall,例如:
public HeaderServiceCall<String, String> sayHello() {
return (requestHeader, name) -> {
String user = requestHeader.principal()
.map(Principal::getName).orElse("No one");
String response = user + " wants to say hello to " + name;
ResponseHeader responseHeader = ResponseHeader.OK
.withHeader("Server", "Hello service");
return completedFuture(Pair.create(responseHeader, response));
};
}
如果您需要通过或返回一个ServerServiceCall,您可以使用HeaderServiceCall.of方法,像这样:
public ServerServiceCall<String, String> sayHello() {
return HeaderServiceCall.of((requestHeader, name) -> {
String user = requestHeader.principal()
.map(Principal::getName).orElse("No one");
String response = user + " wants to say hello to " + name;
ResponseHeader responseHeader = ResponseHeader.OK
.withHeader("Server", "Hello service");
return completedFuture(Pair.create(responseHeader, response));
});
}
2.3 Service call composition
您可能会遇到一些情况,您希望通过横切关注点(如安全性或日志记录)来编写服务调用。在Lagom中,这是通过显式地组合服务调用完成的。下面是一个简单的日志服务调用:
public <Request, Response> ServerServiceCall<Request, Response> logged(
ServerServiceCall<Request, Response> serviceCall) {
return HeaderServiceCall.compose(requestHeader -> {
System.out.println("Received " + requestHeader.method() + " " + requestHeader.uri());
return serviceCall;
});
}
这将使用来自HeaderServiceCall的compose方法,该方法需要一个回调,它接受请求头,并返回一个服务调用。
如果我们要实现HelloService可以被日志记录,我们可以这样使用:
public ServerServiceCall<String, String> sayHello() {
return logged(
name -> completedFuture("Hello " + name)
);
}
另一个常见的横切关注点是身份验证。假设您有一个用户存储接口:
interface UserStorage {
CompletionStage<Optional<User>> lookupUser(String username);
}
您可以使用它来实现经过验证的服务调用:
public <Request, Response> ServerServiceCall<Request, Response> authenticated(
Function<User, ServerServiceCall<Request, Response>> serviceCall) {
return HeaderServiceCall.composeAsync(requestHeader -> {
// First lookup user
CompletionStage<Optional<User>> userLookup = requestHeader.principal()
.map(principal -> userStorage.lookupUser(principal.getName()))
.orElse(completedFuture(Optional.empty()));
// Then, if it exists, apply it to the service call
return userLookup.thenApply(maybeUser -> {
if (maybeUser.isPresent()) {
return serviceCall.apply(maybeUser.get());
} else {
throw new Forbidden("User must be authenticated to access this service call");
}
});
});
}
这次,因为对用户的查找是异步的,我们使用composeAsync异步方法,该方法允许异步的返回服务调用以处理服务。另外,我们不只是简单地接受服务调用,而是接受用户对服务调用的函数。这意味着服务调用可以访问用户:
public ServerServiceCall<String, String> sayHello() {
return authenticated( user ->
name -> completedFuture("Hello " + user)
);
}
请注意,与其他框架相比,用户对象可能通过使用线程本地或在非类型化映射中通过过滤器传递,因此用户对象被显式地通过。如果您的代码需要访问用户对象,在你忘记放置过滤器,他不会有配置错误的,代码根本不会编译。
通常,您希望一起组合多个服务调用。这就是基于功能的组合功能真正发挥作用的地方,与注释相反。由于服务调用只是常规方法,您可以简单地定义一个新方法来组合它们,例如:
public <Request, Response> ServerServiceCall<Request, Response> filter(
Function<User, ServerServiceCall<Request, Response>> serviceCall) {
return logged(authenticated(serviceCall));
}
在hello服务中使用这个
public ServerServiceCall<String, String> sayHello() {
return filter( user ->
name -> completedFuture("Hello " + user)
);
}
3.Service Metadata 服务元数据
服务元数据(也称为ServiceInfo)包括一个名称和一个acl集合。元数据在大多数情况下是自动计算的,您不必查看它,甚至不需要提供它。
有几种情况是由Lagom支持的:
1.当你创建一个Lagom服务的时候,你会使用bindSevice方法来绑定服务的时候,Lagom将把服务描述符的名称和acl绑定到ServiceInfo中。
2.当你创建一个Lagom服务,但是并不想绑定任何服务,你需要使用bindServiceInfo方法,并手动的提供元数据。
3.当你从任何已经使用Guice注入的app来消费Lagom服务,你只需要简单的绑定一个服务客户端(第四节讲)。在这种情况下,在这个场景中,Lagom没有将ServiceInfo捆绑在封面之下,您必须以编程方式提供。
4.最后的场景是客户端应用程序不使用Guice并通过Lagom客户端工厂连接到Lagom。在此场景中,Lagom还将为您创建元数据
3.1 Service Name and Service ACLs
服务彼此 之间进行交互。当作为另一种服务的客户机,这种相互作用需要每个服务来标识自己的身份。需要此标识时,会默认使用 ServiceInfo 的名称。比如 HelloService:
import com.lightbend.lagom.javadsl.api.*;
import static com.lightbend.lagom.javadsl.api.Service.*;
public interface HelloService extends Service {
ServiceCall<String, String> sayHello();
default Descriptor descriptor() {
return named("hello").withCalls(
call(this::sayHello)
);
}
}
如果greetings服务打包HelloService和Greetings 服务是调用I18n服务(而不是在代码片段中),那么这些调用将包括hello,因为这是HelloService服务的名称(参见代码named(“hello”))。
服务可以在服务网关中发布acl,以列出服务提供的端点。这些acl将允许您通过服务网关开发服务器端服务发现。
//AutoAcl:标识是否会自动生成一个ACL来为网关,使其可以路由调用到此服务。
//withASutoAcl方法:设置此Descriptor(描述符)中的服务调用是否默认为他们自动生成ACL
default Descriptor descriptor() {
return named("users")
.withCalls(
restCall(Method.POST, "/api/users/login", this::login)
)
.withAutoAcl(true);
在本例中,UsersService的开发人员将autoacl设置为true。这将指示Lagom从每个调用的路径模式中生成服务的acl。在本例中,将创建/api/users/login的服务ACL。部署时,您的工具应该遵守这些规范,并确保您的API网关设置正确。
3.2 这里解释一下ServiceInfo对象(这不是官方文档内容)
1.下面这是该类的定义,final的
它是此服务的公共信息。第三方注册可以使用此功能在服务注册中心注册服务
这个信息需要一个名称和一组locatable服务
public final class ServiceInfo
2.属性,也就是线程安全里说的状态
{服务名}
private final String serviceName;
{你所绑定进来的那些可定位的服务,ServiceAcl的命名组}
private final PMap<String, PSequence<ServiceAcl>> locatableServices;
3.下面引用一段官方注释的例子:
PSequence<ServiceAcl> helloAcls = TreePVector.from(Arrays.asList(
ServiceAcl.methodAndPath(Method.GET, "?/hello/.*"),
ServiceAcl.methodAndPath(Method.POST, "/login"))
);
PSequence<ServiceAcl> goodbyeAcls = TreePVector.singleton(
ServiceAcl.methodAndPath(Method.POST, "/logout/.*")
);
PMap<String, PSequence<ServiceAcl>> locatableServices =
HashTreePMap.<String, PSequence<ServiceAcl>>empty()
.plus("hello-service", helloAcls)
.plus("goodbye-service", goodbyeAcls);
new ServiceInfo("GreetingService", locatableServices);
4.那么ServiceAcl到底是个啥,其实就是个不能变得String,很像
----访问控制列表(Access Control List,ACL)
4.1定义
public final class ServiceAcl;也是final的
4.2属性
private final Optional<Method> method; GET/POST/PUT/DELETE
private final Optional<String> pathRegex; 路径
4.Consuming services 消费服务
我们已经了解了如何定义服务描述符以及如何实现它们,现在我们需要使用它们。服务描述符包含了所有需要知道如何调用服务的Lagom,因此,Lagom能够为您实现服务描述符接口。
4.1 绑定一个服务客户端
使用服务所需的第一件事是绑定它,因此Lagom可以为您的应用程序提供一个实现。这可以在ServiceClientGuiceSupport上使用bindClient方法完成。
import com.google.inject.AbstractModule;
import com.lightbend.lagom.javadsl.client.ServiceClientGuiceSupport;
import docs.services.HelloService;
import com.lightbend.lagom.javadsl.api.ServiceInfo;
public class Module extends AbstractModule implements ServiceClientGuiceSupport {
protected void configure() {
bindServiceInfo(ServiceInfo.of("hello-service"));
bindClient(HelloService.class);
}
}
在使用客户机时,Lagom将需要一个ServiceInfo实现并使用它来标识自己到远程服务。当您开发一个仅实现ServiceClientGuiceSupport来消费Lagom服务的应用程序时,您需要调用bindServiceInfo()并提供描述您的应用程序的ServiceInfo实例。
如果您已经使用ServiceGuiceSupport绑定了服务实现,这个接口扩展了ServiceClientGuiceSupport,因此您现有的模块可以用作:
import com.google.inject.AbstractModule;
import com.lightbend.lagom.javadsl.server.ServiceGuiceSupport;
public class ServiceModule extends AbstractModule implements ServiceGuiceSupport {
protected void configure() {
bindService(HelloService.class, HelloServiceImpl.class);
bindClient(EchoService.class);
}
}
注意,当你绑定到服务端使用bindService方法时候,这将会自动的为你的服务绑定一个客户端。在前面的例子中,当我们启动应用程序时,将会有一个服务(HelloService)和两个客户端(HelloService和EchoService)是可用的。
4.2使用一个服务客户端
绑定客户端之后,您现在可以使用@Inject注解将它注入到任何Lagom组件中。下面是一个消费服务的样例:
public class MyServiceImpl implements MyService {
private final HelloService helloService;
@Inject
public MyServiceImpl(HelloService helloService) {
this.helloService = helloService;
}
@Override
public ServiceCall<NotUsed, String> sayHelloLagom() {
return msg -> {
CompletionStage<String> response = helloService.sayHello().invoke("Lagom");
return response.thenApply(answer ->
"Hello service said: " + answer
);
};
}
}
4.3 短路器
断路器被用来提供稳定性和防止分布式系统中的级联故障 。这些应该与在服务之间的接口的明智的超时一起使用,以防止单个服务的失败导致其他服务的崩溃。
例如,我们有一个与第三方web服务交互的web应用程序。假设第三方已经超额消费了他们的能力,他们的数据库在负载下崩溃。假设数据库以这样的方式失败,需要很长时间才能将错误反馈给第三方web服务。这反过来会使调用在很长一段时间后失效。回到我们的web应用程序,用户已经注意到他们的表单提交的时间似乎更长。用户会使用刷新,向已运行的请求添加更多请
求。这最终会导致由于资源耗尽而导致web应用程序的失败。
在web服务调用中引入断路器会导致请求fail-fast,让用户知道有什么问题,并且不需要更新他们的请求.这也限制了只有那些使用依赖于第三方的功能的用户的失败行为,其他用户不再受影响,因为没有资源耗尽。断路器还可以允许有经验的开发人员标记使用不可用的功能的部分,或者在断路器打开时适当显示一些缓存内容。
断路器有三个状态:
在正常运行期间,断路器处于封闭状态:
异常或调用超过配置的call-timeout增加失败计数器
成功的话,将失败计数重置为零
当故障计数器到达max-failures的值时,断路器会被触发为打开状态
在打开状态:
所有的调用都会立即失败并处以CircuitBreakerOpenException异常
在配置的reset-timeout之后,断路器进入一个半开状态
当处于一个半开的状态:
第一个调用尝试是允许的,但不快速失败
所有其他调用都是失败的,只有在打开状态时例外
如果第一个调用成功,则将断路器重新设置为闭状态
如果第一个调用失败,则该断路器会再次进入一个打开状态。
所有使用Lagom服务客户端的服务都默认使用断路器。断路器在客户端使用和配置,但是粒度和配置标识符由服务提供者定义。默认情况下,一个断路器实例用于所有调用(方法)到另一个服务。可以为每个方法设置一个独特的断路器标识符,以便为每个方法使用单独的断路器实例。也可以通过在几个方法上使用相同的标识符来分组相关的方法。
@Override
default Descriptor descriptor() {
return named("hello").withCalls(
namedCall("hi", this::sayHi),
namedCall("hiAgain", this::hiAgain)
.withCircuitBreaker(CircuitBreaker.identifiedBy("hello2"))
);
}
在上面的例子中,默认标识符用于sayHi方法,因为没有特定的标识符。他的默认标识符与服务名相同。在这个例子中“hello”。hiAgain方法将使用另一个断路器实例,因为“hello2”被指定为断路器标识符。
在客户端,您可以配置断路器。默认的配置是:
# Circuit breakers for calls to other services are configured
# in this section. A child configuration section with the same
# name as the circuit breaker identifier will be used, with fallback
# to the `lagom.circuit-breaker.default` section.
lagom.circuit-breaker {
# Default configuration that is used if a configuration section
# with the circuit breaker identifier is not defined.
default {
# Possibility to disable a given circuit breaker.
enabled = on
# Number of failures before opening the circuit.
max-failures = 10
# Duration of time after which to consider a call a failure.
call-timeout = 10s
# Duration of time in open state after which to attempt to close
# the circuit, by first entering the half-open state.
reset-timeout = 15s
}
}
如果您自己不定义任何配置,那么将使用该配置。 通过上面的“hello”示例,我们可以通过在application.conf中定义属性来调整配置。配置如:
lagom.circuit-breaker {
# will be used by sayHi method
hello.max-failures = 5
# will be used by hiAgain method
hello2 {
max-failures = 7
reset-timeout = 30s
}
# Change the default call-timeout
# will be used for both sayHi and hiAgain methods
default.call-timeout = 5s
}
关于断路器状态的信息和吞吐量和延迟的一些度量标准是由在每个服务的下列路径中提供的度量服务提供的:
/_status/circuit-breaker/current 电路断路器的当前状态的快照
/_status/circuit-breaker/stream 断路器状态流
Lightbend Monitoring将为Lagom断路器提供指标,包括集群中所有节点的信息的聚合视图。
5.Testing Services 测试服务
5.1 运行测试
你可以在sbt构建下,使用如下的操作来运行测试:
运行test,来运行所有的测试
值运行一个测试class,使用testOnly my.namespace.MyTest
只运行失败的测试,那么使用testQuick
要持续运行测试,在前面运行一个带有波浪线的命令,就像~testQuick.
5.2 Junit
Lagom推荐的测试框架是Junit,需要添加的依赖是:
<dependency>
<groupId>com.lightbend.lagom</groupId>
<artifactId>lagom-javadsl-testkit_2.11</artifactId>
<version>${lagom.version}</version>
<scope>test</scope>
</dependency>
当使用Cassandra时,必须对测试进行分叉,这是通过在项目的构建中添加以下内容来实现的:
.settings(lagomForkedTestSettings: _*)
PS:测试部分暂时不翻译了,不是不重要。
6 Message Serializers消息序列化
立即可用的,Lagom使用jackson来序列化请求和响应消息。但是,您可以在每个服务调用基础上定义自定义序列化器,并为整个服务注册一个给定类型的序列化器,最后您还可以自定义在没有选择序列化器时使用的序列化工厂来完全改变序列化器的使用。
6.1 Lagom如何选择消息序列化器
Lagom使用三个步骤来选择一个服务调用的消息序列化器。
step1:每个服务调用消息序列化器
Lagom首先检查服务调用中是否定义了特定的消息序列化器。默认情况下,每个服务调用都将请求和响应消息序列化器的决定提升到下一个级别,但是使用withRequestSerializer或withResponseSerializer调用,可以为特定的服务调用请求或响应指定定制的序列化器。
当在描述符中定义服务调用时,这可以被覆盖:
default Descriptor descriptor() {
return named("orderservice").withCalls(
pathCall("/orders/:id", this::getOrder)
.withResponseSerializer(new MyOrderSerializer())
);
}
>step2: 每个类型消息序列化器
如果在服务调用级别没有提供任何消息序列化器,那么Lagom将检查一个序列化器是否在该类型的服务级别上注册。每个服务为该类型维护一个类型的映射,并且这些类型用于匹配映射中类型的服务调用.
在这个级别,Lagom提供了大量的序列化器,包括String和NotUsed的序列化器。用户也可以,也可以在描述符中用Descriptor.with方法,提供自定义类型级别的序列化器
default Descriptor descriptor() {
return named("orderservice").withCalls(
pathCall("/orders/:id", this::getOrder)
).withMessageSerializer(Order.class, new MyOrderSerializer());
}
>step3:序列化器工厂
如果没有找到一个服务调用或一个类型的消息序列化器,那么Lagom将最终请求它的序列化器工厂为类型的序列化器。当使用默认值时,这是Lagom通常为您的类型定位序列化器的方式。
Lagom提供了一个SerializerFactory接口,用于动态查找和创建类型的序列化器。Lagom提供的默认实现是Jackson serializer工厂,它序列化为JSON。,当然,你还是可以在当声明的签名中,使用Descriptor.with方法,来自定义自己的SerializerFactory。
default Descriptor descriptor() {
return named("orderservice").withCalls(
pathCall("/orders/:id", this::getOrder)
).withSerializerFactory(new MySerializerFactory());
}
6.2 自定义序列化器
当然,如果不能实现自定义序列化器,那么能够配置自定义序列化器是毫无意义的。Lagom提供了一个MessageSerializer接口,可以用来实现定制的序列化器。
正如我们已经看到的,在Lagom中有两种类型的消息strict的消息和stream的消息。对于这两种类型的消息,Lagom提供了MessageSerializer的两个子接口:StrictMessageSerializer和StreamedMessageSerializer.它们的主要区别在于它们序列化和反序列化的格式。strict的消息序列化器从ByteString进行序列化和反序列化,也就是说,它们严格地工作在内存中,而流式消息序列化器则使用流,即:Source
6.2.1 消息协议
Lagom有消息协议的概念。消息协议是使用MessageProtocol类型来表达的,它们有三个属性、一个内容类型、一个字符集和一个版本。所有这些属性都是可选的,并且可以或不可能被消息序列化器使用。
消息协议对HTTP的Content-Type和Accept标头进行了粗略的转换,如果一个mime类型方案编码了这个版本,或者可能从URL中提取了这个版本,这取决于服务是如何配置的,那么这个版本可能会从这些文件中提取
6.2.2 Content negotiation内容协商
Lagom消息序列化器能够使用内容协商来决定使用正确的协议来进行对话。这可以用于指定不同的有线格式,如JSON和XML,以及不同的版本。
Lagom的内容协商反映了与HTTP相同的功能。对于请求消息,客户端将选择它想要使用的任何协议,因此在那里不需要协商。然后,服务器使用客户机发送的消息协议来决定如何反序列化请求。
对于响应,客户端发送将接受的消息协议列表,并且服务器应该从该列表中选择一个协议来响应。然后客户端将读取所选择的协议,并使用该协议反序列化响应。
6.2.3 Negotiated serializers 协商序列化器
作为内容协商的结果,Lagom的MessageSerializer不会直接序列化和反序列化消息,相反,它提供了用于协商消息协议的方法,该协议返回一个NegotiatedSerializer或NegotiatedDeserializer。这些经过协商的类实际上负责进行序列化和反序列化。
让我们看一个内容协商的例子。假设我们想实现一个定制的字符串MessageSerializer,它可以序列化为纯文本,也可以序列化JSON,这取决于客户端的请求。如果您有一些客户将文本正文发送为JSON,而另一些客户将其发送为纯文本,那么这可能会很有用,也许其中的一个客户是一个传统的客户端,但是现在您想用新客户端来完成它。
首先,我们将为自定义的纯文本字符串的协商器去实现NegotiatedSerializer接口
public class PlainTextSerializer implements MessageSerializer.NegotiatedSerializer<String, ByteString> {
private final String charset;
public PlainTextSerializer(String charset) {
this.charset = charset;
}
@Override
public MessageProtocol protocol() {
return new MessageProtocol(Optional.of("text/plain"), Optional.of(charset), Optional.empty());
}
@Override
public ByteString serialize(String s) throws SerializationException {
return ByteString.fromString(s, charset);
}
}
协议方法返回这个序列化器序列化到的协议,您可以看到我们正在传递这个序列化器将在构造函数中使用的字符集。序列化方法是从字符串到ByteString的直接转换。 接下来我们将实现相同的内容,但要序列化为JSON:
public class JsonTextSerializer implements MessageSerializer.NegotiatedSerializer<String, ByteString> {
private final ObjectMapper mapper = new ObjectMapper();
@Override
public MessageProtocol protocol() {
return new MessageProtocol(Optional.of("application/json"), Optional.empty(), Optional.empty());
}
@Override
public ByteString serialize(String s) throws SerializationException {
try {
return ByteString.fromArray(mapper.writeValueAsBytes(s));
} catch (JsonProcessingException e) {
throw new SerializationException(e);
}
}
}
这里我们使用Jackson将字符串转换为JSON字符串。
现在我们来实现纯文本反序列化器:
public class PlainTextDeserializer implements MessageSerializer.NegotiatedDeserializer<String, ByteString> {
private final String charset;
public PlainTextDeserializer(String charset) {
this.charset = charset;
}
@Override
public String deserialize(ByteString bytes) throws DeserializationException {
return bytes.decodeString(charset);
}
}
同样,我们将charset作为构造函数参数,即入参,并且我们从ByteString到String有一个直接的转换。
同样,我们有一个JSON文本反序列化器
public class JsonTextDeserializer implements MessageSerializer.NegotiatedDeserializer<String, ByteString> {
private final ObjectMapper mapper = new ObjectMapper();
@Override
public String deserialize(ByteString bytes) throws DeserializationException {
try {
return mapper.readValue(bytes.iterator().asInputStream(), String.class);
} catch (IOException e) {
throw new DeserializationException(e);
}
}
}
现在我们已经实现了经过协商的序列化器和反序列化器,现在是时候实现MessageSerializer来执行实际的协议协商了。我们将继承自StrictMessageSerializer:
public class TextMessageSerializer implements StrictMessageSerializer<String> {
接下来我们要做的是定义我们接受的协议。客户端将使用此设置来设置Accept标头
@Override
public PSequence<MessageProtocol> acceptResponseProtocols() {
return TreePVector.from(Arrays.asList(
new MessageProtocol().withContentType("text/plain"),
new MessageProtocol().withContentType("application/json")
));
}
您可以看到这个序列化器支持文本和json协议。需要注意的一点是,我们没有在文本协议中设置charset,这是因为我们不需要对它进行具体的处理,我们可以接受服务器选择的任何字符集。
现在我们来实现serializerForRequest方法。客户端使用此方法来确定要为请求使用哪个序列化器。因为在这个阶段,服务器和客户端之间没有发生通信,所以不能进行协商,因此客户机只选择默认的序列化器,在本例中是utf - 8纯文本序列化器:
@Override
public NegotiatedSerializer<String, ByteString> serializerForRequest() {
return new PlainTextSerializer("utf-8");
}
接下来我们将实现deserializer方法。这是由服务器用于选择请求的反序列化器的,以及为响应选择deserializer的客户端。在MessageProtocol中传递的是发送请求或响应的内容类型,我们需要检查它是否为我们可以反序列化的内容类型,并返回适当的内容类型:`
@Override
public NegotiatedDeserializer<String, ByteString> deserializer(MessageProtocol protocol) throws UnsupportedMediaType {
if (protocol.contentType().isPresent()) {
if (protocol.contentType().get().equals("text/plain")) {
return new PlainTextDeserializer(protocol.charset().orElse("utf-8"));
} else if (protocol.contentType().get().equals("application/json")) {
return new JsonTextDeserializer();
} else {
throw new UnsupportedMediaType(protocol, new MessageProtocol().withContentType("text/plain"));
}
} else {
return new PlainTextDeserializer("utf-8");
}
}
注意,如果没有指定内容类型,我们将返回默认的反序列化器。我们也可以通过抛出异常来失败,但是不这样做是一个好主意,因为一些底层传输不允许通过消息传递内容类型。例如,如果用于WebSocket请求,web浏览器不允许为WebSocket请求设置内容类型。如果不设置内容类型,则返回默认值,确保最大可移植性。
接下来我们将实现serializerForResponse方法。这将接受客户端发送的已接受协议列表,并选择一个用于序列化响应的协议。如果它无法找到它支持的一个,它会抛出一个异常。请注意,任何属性的空值意味着客户愿意接受任何东西,同样,如果客户机没有指定任何接受协议。
@Override
public NegotiatedSerializer<String, ByteString> serializerForResponse(List<MessageProtocol> acceptedMessageProtocols) throws NotAcceptable {
if (acceptedMessageProtocols.isEmpty()) {
return new PlainTextSerializer("utf-8");
} else {
for (MessageProtocol protocol: acceptedMessageProtocols) {
if (protocol.contentType().isPresent()) {
String contentType = protocol.contentType().get();
if (contentType.equals("text/plain") || contentType.equals("text/*") || contentType.equals("*/*")) {
return new PlainTextSerializer(protocol.charset().orElse("utf-8"));
} else if (protocol.contentType().get().equals("application/json")) {
return new JsonTextSerializer();
}
} else {
return new PlainTextSerializer(protocol.charset().orElse("utf-8"));
}
}
throw new NotAcceptable(acceptedMessageProtocols, new MessageProtocol().withContentType("text/plain"));
}
}
6.3 自定义序列化程序工厂
正如已解释过的,默认情况下,Lagom提供了一个jackson序列化工厂,但是允许其重写它。序列化器工厂负责给定类型,如果可以找到该类型,则返回该类型的MessageSerializer。
下面的XML serializer示例展示了创建自定义序列化工厂的示例。
6.3.1 样例
6.3.1.1协议缓冲区序列化器
协议缓冲区是针对JSON的高性能语言中立替代品,对于服务之间的内部通信来说,这是一个很好的选择。这里有一个示例,说明如何为由protoc生成的Order类编写MessageSerializer:
public class ProtobufSerializer implements StrictMessageSerializer<Order> {
private final NegotiatedSerializer<Order, ByteString> serializer = new NegotiatedSerializer<Order, ByteString>() {
@Override
public MessageProtocol protocol() {
return new MessageProtocol().withContentType("application/octet-stream");
}
@Override
public ByteString serialize(Order order) throws SerializationException {
ByteStringBuilder builder = ByteString.createBuilder();
order.writeTo(builder.asOutputStream());
return builder.result();
}
};
private final NegotiatedDeserializer<Order, ByteString> deserializer =
bytes -> Order.parseFrom(bytes.iterator().asInputStream());
@Override
public NegotiatedSerializer<Order, ByteString> serializerForRequest() {
return serializer;
}
@Override
public NegotiatedDeserializer<Order, ByteString> deserializer(MessageProtocol protocol) throws UnsupportedMediaType {
return deserializer;
}
@Override
public NegotiatedSerializer<Order, ByteString> serializerForResponse(List<MessageProtocol> acceptedMessageProtocols) throws NotAcceptable {
return serializer;
}
}
注意,这个MessageSerializer不尝试进行任何内容协商。在很多情况下,内容协商是多余的,如果你不需要它,你就不需要实现它
6.3.1.2 XML序列化器
由于它的大小和缓慢的性能,所以不推荐XML,但是可能有可能需要使用它,例如在与遗留系统交互时。这里有一个JAXB序列化器工厂的示例: 公共类JaxbSerializerFactory实现SerializerFactory
public class JaxbSerializerFactory implements SerializerFactory {
private final Unmarshaller unmarshaller;
private final Marshaller marshaller;
public JaxbSerializerFactory() {
try {
JAXBContext context = JAXBContext.newInstance();
this.unmarshaller = context.createUnmarshaller();
this.marshaller = context.createMarshaller();
} catch (JAXBException e) {
throw new RuntimeException(e);
}
}
@Override
public <MessageEntity> MessageSerializer<MessageEntity, ?> messageSerializerFor(Type type) {
if (type instanceof Class) {
Class<MessageEntity> clazz = (Class<MessageEntity>) type;
return new StrictMessageSerializer<MessageEntity>() {
NegotiatedSerializer<MessageEntity, ByteString> serializer = new NegotiatedSerializer<MessageEntity, ByteString>() {
@Override
public MessageProtocol protocol() {
return new MessageProtocol().withContentType("application/xml");
}
@Override
public ByteString serialize(MessageEntity messageEntity) throws SerializationException {
ByteStringBuilder builder = ByteString.createBuilder();
try {
marshaller.marshal(messageEntity, builder.asOutputStream());
return builder.result();
} catch (JAXBException e) {
throw new SerializationException(e);
}
}
};
NegotiatedDeserializer<MessageEntity, ByteString> deserializer =
bytes -> {
try {
return unmarshaller.unmarshal(new StreamSource(bytes.iterator().asInputStream()),
clazz).getValue();
} catch (JAXBException e) {
throw new DeserializationException(e);
}
};
@Override
public NegotiatedSerializer<MessageEntity, ByteString> serializerForRequest() {
return serializer;
}
@Override
public NegotiatedDeserializer<MessageEntity, ByteString> deserializer(MessageProtocol protocol) throws UnsupportedMediaType {
return deserializer;
}
@Override
public NegotiatedSerializer<MessageEntity, ByteString> serializerForResponse(List<MessageProtocol> acceptedMessageProtocols) throws NotAcceptable {
return serializer;
}
};
} else {
throw new IllegalArgumentException("JAXB does not support deserializing generic types");
}
}
}
7. Header Filters 消息头过滤
在Lagom中,你可以向你的服务描述符中添加HeaderFilter。在头信息过滤器中,你通常处理协议协商或认证。
单个的HeaderFilter实现可以转换离开客户端或进入服务器的请求,以及离开服务器并进入客户机的响应。
public class UserAgentHeaderFilter implements HeaderFilter {
@Override
public RequestHeader transformClientRequest(RequestHeader request) {
if (request.principal().isPresent()) {
Principal principal = request.principal().get();
if (principal instanceof ServicePrincipal) {
String serviceName = ((ServicePrincipal) principal).serviceName();
return request.withHeader(Http.HeaderNames.USER_AGENT, serviceName);
} else {
return request;
}
} else {
return request;
}
}
@Override
public RequestHeader transformServerRequest(RequestHeader request) {
Optional<String> userAgent = request.getHeader(Http.HeaderNames.USER_AGENT);
if (userAgent.isPresent()) {
return request.withPrincipal(ServicePrincipal.forServiceNamed(userAgent.get()));
} else {
return request;
}
}
@Override
public ResponseHeader transformServerResponse(ResponseHeader response, RequestHeader request) {
return response;
}
@Override
public ResponseHeader transformClientResponse(ResponseHeader response, RequestHeader request) {
return response;
}
}
如果没有指定任何Lagom服务,上面样例中的UserAgentHeaderFilter,则会作为默认的HeaderFilter服务将使用。它使用了一个用服务名标识客户端的ServicePrincipal。这样,服务器可以通过用户代理识别调用的客户端。
在UserAgentHeaderFilter中,transformClientRequest方法中的代码将会在下面这种情况下被调用:即当ServicePrincipal是在请求中指定好了,并准备一个客户端调用来添加一个user-agent的header的时候。在服务器端,transformServerRequest 方法将会被用于去读取user-agent的头部信息,并将该值设置为请求的Principle。(还是用源码的注释来解释吧:transformClientRequest就是处理所有从客户端发出去的信息,而transformServerRequest 则是处理所有进入服务器的请求)
记住,头过滤器只应该用于处理横切协议的关注点,不要再多了。例如,您可能有一个头过滤器,它描述了当前经过身份验证的用户如何通过HTTP通信(例如,通过添加一个用户头)。跨领域的关注点,例如身份验证和验证,不应该在头过滤器中处理,而是应该使用服务调用组合来处理。
7.1 头过滤器组成
每一个服务描述符仅仅只能有一个头过滤器。为了使用几个过滤器,你可以使用HeaderFilter的compose方法来返回一个你所自己组合的HeaderFilter。组合的顺序是很重要的,他会按照你所组合的顺序,进行顺序过滤。当收到数据时,过滤器将被以相反的顺序使用。如果我们注册两个详细的过滤器Foo和Bar这样的:
default Descriptor descriptor() {
return named("echo").withCalls(
namedCall("echo", this::echo)
)
.withHeaderFilter(HeaderFilter.composite(new FooFilter(), new BarFilter()))
.withAutoAcl(true);
然后调用服务,然后我们将在服务器输出日志中获取以下信息:
[debug] Bar - transforming Server Request
[debug] Foo - transforming Server Request
[debug] Foo - transforming Server Response
[debug] Bar - transforming Server Response
8.Service error handling 服务的错误异常处理
Lagom提供了许多不同的机制来控制和定制错误在服务之间处理和报告的方式。
在Lagom设计的错误处理背后有一些原则:
在生产中,Lagom服务永远不应该给出它遇到的另一个服务的错误的细节,除非它知道这样做是安全的。出于安全原因,攻击者可以使用未经审查的错误消息获取关于如何实现服务的详细信息。在实践中,这意味着有一些在Lagom认为安全的例外情况下,它会返回一些细节,其余的则不返回任何内容.
在开发过程中,在线路上发送完整的错误消息是很有用的。当服务在开发中运行时,Lagom将尝试发送关于异常的有用信息。
如果可能的话,Lagom将尝试在服务端抛出客户端的错误。因此,如果服务器端抛出异常,表示它不能序列化某些东西,那么客户端代码应该接收到相同的异常。
如果可能,异常应该被映射到惯用的协议响应代码,如HTTP 4xx和5xx状态码和WebSocket错误关闭代码。
8.1 Exception serializers
Lagom提供了一个ExceptionSerializer接口,它允许你将异常序列化为你所想的类型,例如JSON等。它还允许你从从错误代码或者序列化的数据进行异常的再还原。
异常序列化器将异常转换为RawexceptionMessage。原始异常消息包含状态码,它将对应于HTTP状态码或WebSocket关闭代码、消息体和协议描述符,以说明消息的内容类型是什么——在HTTP中,这将转换为响应中的Content-type标头。
Lagom提供的默认的异常序列化器使用Jackson将异常序列化为JSON。这个异常序列化器实现了上面所述的指导原则,如果他是TransportException的一个子类,除非是在开发环境,否则它只返回异常的细节。在TransprtException异常的子类中有一些有用的构建,包括NotFound和PolicyViolation。Lagom通常能够将这些异常抛出给客户端。您也可以直接实例化TransportException并使用它,或者您可以定义TransportException的子类,但是注意,因为它不知道它,Lagom不会将子类放入客户端。