译者|李锐审稿人|SunShujuan使用Guice和gRPC-specificscopes在gRPC服务端和客户端应用中进行依赖注入,需要了解grpc-scopeslib提供domains有哪些功能,以及何时以及如何使用它们。1.gRPCgRPC是一种用于通过HTTP/2进行远程过程调用的高性能协议。它主要用于微服务之间的通信,也用于终端用户使用浏览器或移动设备(如REST或GraphQL)发出的请求。gRPC由Google设计,其开源实现库可用于多种平台和编程语言,包括Java。gRPC的一个独特功能是流式传输请求和响应:在定义gRPC过程时,可以指示客户端将发送请求消息流,而不仅仅是一个请求消息。同样,可以指示服务器将使用响应消息流进行响应:ProtoBuf1serviceMyService{2rpcunary(Request)returns(Response){}3rpcstreamingClient(streamRequest)returns(Response){}4rpcstreamingServer(Request)returns(streamResponse){}5rpcbiDiStreaming(streamRequest)returns(streamResponse){}6}请求流和响应流完全独立:响应消息不需要与特定的请求消息相关联,服务器也不需要等待其客户端的流完成,以启动响应流。2.GuiceGuice是Google开发的Java轻量级依赖注入框架。它遵循“做好一件事”的Unix原则。它是依赖注入的,可用于多种环境:Servlet应用程序、自定义服务器应用程序(如gRPC服务器)、独立桌面应用程序等。依赖注入框架最重要的特性之一是作用域:当代码需要注入对象时,框架可以重用与给定场景关联的实例。很多人可能熟悉Servlet范围的概念:Guice中的@RequestScoped和@SessionScoped,Spring中的@RequestScope和@SessionScope。例如,当需要注入EntityManager或DB事务时,这通常必须是与当前正在处理的HttpServletRequest关联的实例。(注意:在Guice中,Servlet范围不是核心框架的一部分:它们作为扩展提供,因为它们在非Servlet应用程序中没有意义)。本文将描述grpc-scopeslib提供了哪些范围,并解释何时以及如何使用它们。3.范围到底是什么?通常,范围是一个对象,它知道在哪里查看以及在哪里存储与给定场景相关的对象。例如,在从数据源请求新的JDBC连接之前,@RequestScope可能首先检查连接是否已存储在当前处理的HttpServletRequest的某些属性中:如果是,则注入此存储的连接。否则,从DataSource请求一个新连接,然后将其存储在给定属性下以供将来注入,最后根据请求注入。更正式地说,在Guice中,Scope定义如下:Java1publicinterfaceScope{2publicProviderscope(Keykey,Providerunscoped);34//javadocs和其他样板方法omitted5}因此,例如,请求范围的scope(...)方法的简化实现可能如下所示:Java1publicProviderscope(Keykey,Providerunscoped){2return()->{3HttpServletRequestrequest=getCurrentRequest();4Tinstance=(T)request.getAttribute(key.toString());5if(instance==null){6instance=unscoped.get();7request.setAttribute(key.toString(),instance);8}9returninstance;10};11}例如,getCurrentRequest()可以与一些将新传入请求存储在静态ThreadLocal变量中的过滤器结合使用。不过需要注意的是,为了简化作用域概念演示,上述实现有几个问题这里没有解决。4.gRPC服务中哪些范围可能有用?RPC服务器公开多个过程,每个过程都可以由多个客户端调用。每个客户端可以同时发出多个RPC调用(对多个或单个进程)。自然地,服务器在单个RPC调用的上下文中限制注入是有意义的。在grpc-scopeslib中,这个作用域简称为rpcScope。对于大多数无状态RPC系统,仅rpcScope就足够了。然而,gRPC流式传输让事情变得相当复杂:流式调用可以持续很长时间:稳定的微服务必须流式传输持续数小时的RPC调用并不少见。此外,流中的后续消息之间可能会有几分钟的停顿。一般来说,这意味着rpcScope不适合在非活动使用时短暂或不保留的对象的作用域注入。例如,一个事务的持续时间通常应该少于一秒,将对象保留在池中(例如JDBC连接)可能会显着降低服务器性能。这种情况的自然解决方案是引入另一个范围,该范围将跨越请求流中单个消息的处理。JavagRPC实现异步处理流:每次新消息到达时用户代码都会收到回调,因此新范围可以跨越每个此类回调调用。然而,消息到达并不是用户服务代码在RPC调用的生命周期中可能收到的唯一回调:当处理来自对等方的流时,服务器和客户端代码都需要提供StreamObserver接口的实现以接收流事件回调:Java1publicinterfaceStreamObserver{2voidonNext(Vvalue);//下一条消息到达3voidonError(Throwablet);//发生错误(在服务器端这可能只是取消)4voidonCompleted();//对方表示他们的stream56结束//省略了javadoc,为了本文的目的添加了方法注释7}在服务器端,您还可以选择通过ServerCallStreamObserver注册以接收额外的回调:Java1publicabstractclassServerCallStreamObserverextendsCallStreamObserver{2publicabstractvoidsetOnCancelHandler(RunnableonCancelHandler);3publicabstractvoidsetOnReadyHandler(RunnableonReadyHandler);4publicvoidsetOnCloseHandler(RunnableonCloseHandler){...}56//省略了javadoc和其他方法7}"onCancel(...)大致是onError(...)的重复调用“onReady(...)”来表示另一方在暂时阻塞后准备好接收更多消息(对于双向进程),最后当服务器成功刷新给定调用中的所有响应消息,并在底层HTTP/2流关闭后调用“onClose(...)”。服务器可能需要对每个此类事件做出不同的反应:例如,它们可能需要在“onClose()”中提交事务并在“onCancel(...)”中将其回滚。为了能够执行此类操作,相应的服务代码通常需要注入类似于处理传入消息的对象。因此,在grpc-scopesliblistenerEventScopes的场景中,注入每个单独的事件回调(来自StreamObserver和ServerCallStreamObserver)。(名称的listener部分来自与关联的Listenerobject每个调用所有这些回调的RPC)5.如果客户端被告知还需要作用域怎么办?在双向流方法的情况下,客户端和服务器之间的区别变得非常模糊:一旦发起调用,服务器就做不需要等待来自客户端流的任何消息,可以立即开始发送消息。客户端实际上可以等待他的流,直到来自服务器的第一条消息到达,并且n开始发送实际上是向服务器发送一条消息,用于响应一条消息。例如,工作人员可以作为gRPC客户端连接到充当gRPC服务器的管理器,以注册并开始接收要执行的任务,然后发回结果。为了处理来自服务器(管理器)的异步消息,客户端(工作者)可能需要从服务器(管理器)注入一个作用域为给定任务消息场景的对象。另一种更常见的情况是当服务器响应另一个流媒体服务器进行gRPC调用时。例如,第一台服务器可以是第二台服务器前面的一种代理。同样,为了处理来自第二个服务器的异步响应,第一个服务器可能需要为给定的响应注入作用域消息上下文的对象。因此,上面描述的listenerEventScope和rpcScope在客户端也是可用的:客户端可能接收到的每个回调都会有一个单独的事件上下文,所有与给定客户端RPC调用相关的回调都会有一个将共享同一个RPC场景。6、如何判断这两个scope哪个适合注入?粗略地说,如果在Servlet应用程序中,@RequestScope用于定义对象的范围,那么在gRPC应用程序中,通常应该使用listenerEventScope来定义其范围。这是因为请求范围的内容通常需要短期或短期保留,如前面描述的示例所示。但是,由于性能原因,没有这样的要求Request作用域的东西可能会随着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中,每个作用域都是作用域类的一个实例,可以在模块中使用它来定义作用域绑定。例如:Java1bind(EntityManager.class)2toProvider(entityManagerFactory::createEntityManager)3.in(grpcModule.listenerEventScope);那么,具有gRPC作用域的静态变量和GuiceServlet扩展中的类似静态变量在哪里?grpc-scopes不鼓励使用静态场景,因为它会导致很多问题。相反,在应用程序的main方法中,可以创建GrpcModule的本地实例,在其公共字段上提供两个范围。但是,如果没有静态范围变量,则只需创建GrpcModule的静态实例并复制这两个字段:Java1publicclassMyGrpcServer{2publicstaticfinalGrpcModuleGRPC_MODULE=newGrpcModule();3publicstaticfinalScopeRPC_SCOPE=GRPC_MODULE。rpcScope;4publicstaticfinalScopeEVENT_SCOPE=GRPC_MODULE.listenerEventScope;56publicstaticvoidmain(String[]args){/*...*/}78//这里有更多代码...9}8.如何让它发挥作用?(1)如上所述创建GrpcModule实例。(2)如前所述,创建可以在其绑定中使用来自GrpcModule的范围的其他模块。(3)通过传递上述模块(GrpcModule)创建一个GuiceInjector。(4)向上面的Injector请求gRPC服务类和/或客户端响应观察者类的实例。(5)在GrpcModule中使用拦截器,如下:Java1grpcServer=ServerBuilder2.forPort(port)3.addService(ServerInterceptors.intercept(4myService,grpcModule.contextInterceptor/*moreinterceptorshere...*/))5//更多服务和其他东西在这里...6.build();对于在服务器应用程序中工作的范围,使用GrpcModule.serverInterceptor在将服务添加到gRPC服务器时拦截它们。Java1finalvarmanagedChannel=ManagedChannelBuilder2.forTarget(TARGET)3.usePlaintext()4.build();5finalvarchannel=ClientInterceptors.intercept(6managedChannel,grpcModule.clientInterceptor);7finalvarstub=MyServiceGrpcnnel.newStub(;到使范围在客户端应用程序中工作,在创建存根之前使用GrpcModule.clientInterceptor拦截Channel实例(例如ManagedChannel)。就是这样,查看项目的自述文件。之后,可能会看到一个完整的示例应用程序,它正确地使用这些范围注入JPAEntityManager实例原文链接:https://dzone.com/articles/combining-grpc-with-guice