1

如何将gRPC与Guice相结合

 1 year ago
source link: https://server.51cto.com/article/720092.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

如何将gRPC与Guice相结合

译文
作者: 李睿 2022-10-08 00:35:48
使用Guice和gRPC特定的作用域在 gRPC 服务器和客户端应用程序中进行依赖注入,需要了解grpc-scopeslib提供了哪些作用域,以及何时和如何使用它们。

译者 | 李睿

审校 | 孙淑娟

使用Guice和gRPC特定的作用域在 gRPC 服务器和客户端应用程序中进行依赖注入,需要了解grpc-scopeslib提供了哪些作用域,以及何时和如何使用它们。  

1、gRPC

gRPC是通过HTTP/2进行远程过程调用的高性能协议。它主要用于微服务之间的通信,也可以用于使用浏览器或移动设备(如REST或GraphQL)的最终用户的请求。gRPC由谷歌公司设计,它的开源实现库可用于多种平台和编程语言,其中包括Java。 

gRPC的一个独特特性是流式请求和响应:在定义gRPC过程时,可以指出客户端将发送请求消息流,而不是仅仅一个请求消息。同样,可以指示服务器将使用响应消息流进行响应: 

ProtoBuf
1 service MyService {
2rpc unary(Request) returns (Response) {}
3rpc streamingClient(stream Request) returns (Response) {}
4rpc streamingServer(Request) returns (stream Response) {}
5 rpc biDiStreaming(stream Request) returns (stream Response) {}
6 }

请求流和响应流彼此完全独立:响应消息不需要与特定的请求消息相关联,服务器也不需要等待其客户端的流完成后,才能启动响应流。

2、Guice

Guice是由谷歌公司开发的Java轻量级依赖注入框架。它遵循“做好一件事”的Unix原则。它是依赖注入,可以在多种环境中使用:Servlet应用程序、自定义服务器应用程序(例如gRPC服务器)、独立桌面应用程序等等。

依赖注入框架最重要的特性之一是作用域:当代码需要注入对象时,框架可以重用与给定场景关联的实例。很多人可能对Servlet作用域的概念很熟悉:Guice中的@RequestScoped和@SessionScoped,Spring中的@RequestScope和@SessionScope。例如,当需要注入EntityManager或DB事务时,这通常必须是与当前处理的HttpServletRequest关联的实例。(注:在Guice中,Servlet范围不是核心框架的一部分:它们作为扩展提供,因为它们在非Servlet应用程序中没有意义)。 

本文将描述grpc-scopeslib提供了哪些作用域,并解释何时以及如何使用它们。 

3、究竟什么是作用域?

一般来说,作用域是一个对象,它知道在哪里寻找以及在哪里存储与某个给定场景相关的对象。例如,在从DataSource请求新的JDBC连接之前,@RequestScope可能首先检查当前正在处理的HttpServletRequest的某些属性中是否已经存储了一个连接:如果是,则只需注入这个存储的连接。否则,从DataSource请求一个新的连接,然后将其存储在给定属性下以供将来注入,最后按请求注入。更正式地说,在Guice中,Scope定义如下: 

Java
1 public interface Scope {
2public <T> Provider<T> scope(Key<T> key, Provider<T> unscoped);
3
4 // javadocs and other boilerplate methods omitted
5 }

因此,例如,请求范围的scope(...)方法的简化实现可能如下所示: 

Java
1 public <T> Provider<T> scope(Key<T> key, Provider<T> unscoped) {
2 return () -> {
3 HttpServletRequest request = getCurrentRequest();
4 T instance = (T) request.getAttribute(key.toString());
5 if (instance == null) {
6 instance = unscoped.get();
7 request.setAttribute(key.toString(), instance);
8}
9 return instance;
10 };
11 }

例如,getCurrentRequest()可以与一些过滤器结合使用,这些过滤器将新传入的请求存储在一些静态ThreadLocal变量上。然而需要注意,为了简化作用域概念演示,上面的实现有几个问题在这里没有解决。

4、在gRPC服务中哪些作用域可能有用?

RPC服务器公开了几个过程,每个过程可能被多个客户端调用。每个客户端可以同时发出多个RPC调用(对多个或单个过程)。自然地,服务器在单个RPC调用的场景中限定注入是有意义的。在grpc-scopeslib中,该作用域简称为rpcScope。

在大多数无状态RPC系统的情况下,单独使用rpcScope就足够了。然而gRPC流式传输使事情变得相当复杂:流式调用可能会持续很长时间:稳定的微服务必须流式传输持续数小时的RPC调用并不罕见。

此外,流中的后续消息之间可能会有几分钟的停顿。总的来说,这意味着rpcScope不适合用于对象的作用域注入,这些对象的寿命很短,或者在不活跃使用时不会被保留。例如,事务的持续时间通常应低于一秒,而保留池中的对象(如JDBC连接)可能会显著地降低服务器性能。这种情况的一个自然解决方案是引入另一个作用域,该作用域将跨越来自请求流的单个消息的处理。

JavagRPC实现以异步方式处理流:每次新消息到达时,用户代码都会收到一个回调,因此新的作用域可以跨越每个这样的回调调用。然而,消息到达并不是用户服务代码在RPC调用的生命周期中可能收到的唯一回调:在处理来自对等点的流时,服务器和客户端代码都需要提供StreamObserver接口的实现来接收流事件回调:

Java
1 public interface StreamObserver<V> {
2void onNext(V value);      // next message arrived
3void onError(Throwable t); // error occurred (on server side this may only be cancellation)
4void onCompleted();        // the other side indicated end of their stream
5
6 // javadocs omitted, method comments added for the purpose of this article
7 }

在服务器端,还可以选择通过ServerCallStreamObserver注册以接收额外的回调:

Java
1 public abstract class ServerCallStreamObserver<RespT> extends CallStreamObserver<RespT> {
2 public abstract void setOnCancelHandler(Runnable onCancelHandler);
3public abstract void setOnReadyHandler(Runnable onReadyHandler);
4 public void setOnCloseHandler(Runnable onCloseHandler) {...}
5
6 // javadocs and other methods omitted
7 }

“onCancel(…)”大致上是onError(…)的重复,调用“onReady(…)”,表示另一方在暂时阻塞后准备接收更多消息(对于bi-di过程),最后在服务器成功刷新给定调用中的所有响应消息并关闭底层HTTP/2流后调用“onClose(…)”。

服务器可能需要以不同的方式对每个此类事件做出反应:例如,它们可能需要在“onClose()”中提交事务,并在“onCancel(…)中回滚它。为了能够执行此类操作,相应的服务代码通常需要注入与处理到达的消息类似的对象。因此,在grpc-scopes lib listenerEventScopescopes注入到每个单个事件回调的场景中(来自StreamObserver和ServerCallStreamObserver)。(名称的侦听器部分来自与调用所有这些回调的每个RPC相关联的Listenerobject)

5、如果告诉客户也需要作用域,怎么办?

在双向流方法的情况下,客户端和服务器端之间的区别变得非常模糊:一旦发起调用,服务器不需要等待来自客户端流的任何消息,并且可以立即开始发送消息。客户端实际上可以等待他的流,直到来自服务器的第一条消息到达,然后开始发送实际上是对服务器消息的响应的消息。例如,工作人员可以作为gRPC客户端连接到作为gRPC服务器的管理器,以注册并开始接收要执行的任务,然后发回结果。为了处理来自服务器(管理者)的异步消息,客户端(工作人员)可能需要注入范围为来自服务器(管理者)的给定任务消息的场景的对象。

另一种更常见的情况是,作为处理来自客户端的消息的一部分,服务器对另一个流服务器进行gRPC调用。例如,第一个服务器可以是第二个服务器前面的一种代理。同样,为了处理来自第二个服务器的异步响应,第一个服务器可能需要注入作用域为给定响应消息的场景的对象。 

因此,以上描述的listenerEventScope和rpcScope在客户端也可用:客户端可能接收的每个回调都将具有单独的事件场景,与某个给定客户端RPC调用相关的所有回调都将共享相同的RPC场景。

6、如何确定这两个作用域的哪一个适合注入?

粗略地说,如果在Servlet应用程序中,要用@RequestScope来定义某个对象的作用域,那么在gRPC应用程序中,通常应该用listenerEventScope来定义它的作用域。这是因为请求范围的内容通常需要是短期的或短期保留的,如前面描述的示例所示。然而,由于性能原因,没有这一要求的请求作用域的内容可能会更好地与rpcScope一起逐步提高,因为这减少了创建/获取此类内容的频率。

由于gRPC服务器默认是无状态的(没有内置机制来维护单独的RPC之间的客户端状态),因此在gRPC应用程序中,使用@SessionScope作用域的内容通常最终使用rpcScope作用域。如果基于Servlet的REST服务需要移植到gRPC,并且维护HttpSession对其功能至关重要,那么一个潜在的解决方法是将REST调用转换为双向流调用,其中一条响应消息对应于一条特定的请求消息。然而,这需要客户端长时间保持与服务器的连接,这在客户端是最终用户的情况下是不可行的,尤其是在用户使用移动设备的情况下。在这种情况下,gRPC可能基本上不是一个合适的解决方案。 

7、@RpcScoped和@EventScoped注解在哪里? 

grpc-scopes不鼓励过度使用注释,因为它们会污染代码并产生难以追踪的影响,而是提倡使用Guice模块对象使原有Java代码定义注入绑定。此外,作用域注释破坏了依赖注入的主要目的,即将组件逻辑代码与应用程序连接解耦。更糟糕的是,使用特定于平台的注释来注释类会限制可移植性:例如,要在gRPC应用程序中重用这些组件,这些组件原本独立于Servlet或Spring,但使用@RequestScoped/@SessionScoped/@RequestScope/@SessionScope之一进行了注释,需要包含除了提供这些注释之外没有任何其他目的的依赖项,这些在gRPC场景中毫无意义且令人困惑。如上所述,在Guice中,每个作用域都是作用域类的实例,可在模块中用于定义作用域绑定。例如:

Java
1bind(EntityManager.class)
2toProvider(entityManagerFactory::createEntityManager)
3 .in(grpcModule.listenerEventScope);

那么,具有gRPC作用域的静态变量与GuiceServlet扩展中类似的静态变量在哪里呢? 

grpc-scopes不鼓励使用静态场景,因为它会导致许多问题。与其相反,在应用程序的main方法中,可以创建GrpcModule的本地实例,该实例在其公共字段上提供两个作用域。然而,如果没有静态作用域变量,那么只需创建GrpcModule的静态实例并复制这两个字段:

Java
1 public class MyGrpcServer {
2 public static final GrpcModule GRPC_MODULE = new GrpcModule();
3 public static final Scope RPC_SCOPE = GRPC_MODULE.rpcScope;
4 public static final Scope EVENT_SCOPE = GRPC_MODULE.listenerEventScope;
5
6public static void main(String[] args) {/* ...  */}
7
8 // more code here...
9 }

8、如何让它发挥作用?

(1)如上所述创建GrpcModule的实例。 

(2)创建其他模块,这些模块可能在其绑定中使用来自GrpcModule的范围,如前所述。 

(3)通过传递上述模块(GrpcModule)创建一个GuiceInjector。 

(4)向上述Injector询问gRPC服务类和/或客户端响应观察者类的实例。 

(5)使用GrpcModule中的拦截器,如下所示: 

Java
1 grpcServer = ServerBuilder
2 .forPort(port)
3 .addService(ServerInterceptors.intercept(
4 myService, grpcModule.contextInterceptor /* more interceptors here... */))
5 // more services and other stuff here...
6.build();

对于在服务器应用程序中工作的作用域,在将服务添加到gRPC服务器时使用GrpcModule.serverInterceptor拦截服务。 

Java
1  final var managedChannel = ManagedChannelBuilder
2 .forTarget(TARGET)
3.usePlaintext()
4.build();
5 final var channel = ClientInterceptors.intercept(
6 managedChannel, grpcModule.clientInterceptor);
7 final var stub = MyServiceGrpc.newStub(channel);

为了使作用域在客户端应用程序中工作,在创建存根之前,使用GrpcModule.clientInterceptor拦截Channel实例(如ManagedChannel)。 

就是这样,可以查看项目的自述文件。之后,可能会看到一个完整的示例应用程序,它使用这些作用域来正确注入JPA EntityManager实例。

原文链接:https://dzone.com/articles/combining-grpc-with-guice


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK