如何将gRPC与Guice相结合

译文
服务器 服务器产品
使用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

责任编辑:武晓燕 来源: 51CTO技术栈
相关推荐

2011-05-04 09:32:16

2017-08-22 15:35:12

2019-12-17 16:14:24

机器学习设计算法

2018-06-23 13:55:15

Apache SparPython数据

2022-04-23 10:55:51

存储AI/ML对象锁定

2013-10-14 09:29:20

RESTJSONJava

2016-06-12 16:54:58

物联网智能制造PTC

2021-12-13 19:40:53

物联网数据分析IOT

2023-12-29 07:04:28

Go项目Docker编写

2012-04-11 13:44:45

ibmdw

2021-02-26 10:24:49

机器学习人工智能程序

2022-05-23 14:45:49

元宇宙AR

2021-01-06 10:58:45

物联网技术物联网IOT

2021-01-14 15:14:58

人工智能AI

2009-12-28 14:00:46

2021-12-24 14:23:22

首席信息安全官CISO网络安全

2024-01-18 08:15:05

AIGC知识图谱大模型

2022-08-19 08:00:00

OPA集成Kubernetes集群

2017-12-19 15:01:53

AI大数据现代教育

2024-01-12 10:24:07

NVIDIA
点赞
收藏

51CTO技术栈公众号