gRPC servers without Dagger
To implement a gRPC service, you create a class that extends a base service implementation generated from the Protocol Buffer definition.
service HelloService {
rpc SayHello(HelloRequest) returns (HelloResponse);
}
The service definition above generates a class HelloServiceGrpc
that contains
a base implementation HelloServiceImplBase
:
public class HelloServiceGrpc {
public static class HelloServiceImplBase implements BindableService {
public void sayHello(
HelloRequest request, StreamObserver<HelloResponse> responseObserver);
}
}
You then extend that class:
class Hello extends HelloServiceImplBase {
@Override
public void sayHello(
HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
// ...
}
}
Typically, you create the server by adding an instance of Hello
to a
[ServerBuilder
]:
Hello helloService = new Hello(...);
serverBuilder.add(helloService);
Server server = serverBuilder.build();
But because you have to create your service instance before you create your
server, if you’re using Dagger to create the service, Hello
can’t inject
anything that isn’t unscoped or at @Singleton
scope.
Configuring a service using Dagger-gRPC
You can use Dagger-gRPC to instantiate your service implementation once for each call. To do so, follow these steps:
-
Annotate your service implementation with
@GrpcService
, setting thegrpcClass
parameter to the top-level class that contains the base implementation it extends, and give it an@Inject
constructor.@GrpcService(grpcClass = HelloServiceGrpc.class) class Hello extends HelloServiceImplBase { @Inject Hello(...) {...} ... }
Make sure the Dagger-gRPC server annotation processor runs over your source code.
That will generate an interface
HelloServiceDefinition
and a moduleHelloUnscopedGrpcServiceModule
. -
Create a
@Singleton
component that implementsHelloServiceDefinition
, and installHelloUnscopedGrpcServiceModule
into that component. In another module you install on that component, bindHelloServiceDefinition
to your component type.Also make sure to bind a list of
ServerInterceptor
s for the service, annotated with@ForGrpcService(serviceClass)
. The list can be empty.@Singleton @Component(modules = {HelloUnscopedGrpcServiceModule.class, MyModule.class}) interface MyComponent extends HelloServiceDefinition {} @Module abstract class MyModule { @Binds abstract HelloServiceDefinition helloComponent(MyComponent myComponent); @Provides @ForGrpcService(HelloServiceGrpc.class) static List<? extends ServerInterceptor> helloInterceptors( AuthInterceptor authInterceptor, LoggingInterceptor loggingInterceptor) { return Arrays.asList(authInterceptor, loggingInterceptor); } }
-
Install
NettyServerModule
(or [InProcessServerModule
] for testing) into your@Singleton
component. That component will provide the gRPCServer
object.@Singleton @Component(modules = { NettyServerModule.class, HelloUnscopedGrpcServiceModule.class, MyModule.class, }) interface MyComponent { Server server(); }
Now you can start your server, and a new instance of Hello
will be injected to
handle each call.
int port = ...;
MyComponent component =
DaggerMyComponent.builder()
.nettyServerModule(NettyServerModule.bindingToPort(port))
.build();
Server server = component.server();
server.start();
Binding in call scope <a name=call-scope></a>
If you want to bind objects in a scope whose lifetime is the same as one gRPC
ServerCall
, Dagger-gRPC supports that too.
-
Instead of making your
@Singleton
component implementHelloServiceDefinition
, create a subcomponent type that implementsHelloServiceDefinition
and is in@CallScoped
. InstallHelloGrpcServiceModule
into it. If you want bindings in that component to be able to depend on the gRPC call metadata, then also installGrpcCallMetadataModule
.@CallScope @Subcomponent(modules = { HelloGrpcServiceModule.class, GrpcCallMetadataModule.class, ...}) interface ServiceComponent extends HelloServiceDefinition {}
-
Instead of
HelloUnscopedGrpcServiceModule
, installHelloGrpcProxyModule
into your@Singleton
component. In your other module, instead of providingHelloServiceDefinition
, provide an instance ofHelloServiceDefinition.Factory
that calls the proper subcomponent factory and/or builder method.@Singleton @Component(modules = {HelloGrpcProxyModule.class, MyModule.class}) interface MyComponent { ... ServiceComponent serviceComponent(GrpcCallMetadataModule metadataModule); } @Module class MyModule { @Provide static HelloServiceDefinition.Factory helloComponent( final MyComponent myComponent) { return new HelloServiceDefinition.Factory() { @Override public HelloServiceDefinition grpcService( GrpcCallMetadataModule metadataModule) { return myComponent.serviceComponent(metadataModule); } }; } }
-
Make sure that a module installed into your
HelloServiceDefinition
subcomponent or any ancestor component binds the list of interceptors for that service, just like above:@Module class MyModuleInstalledInSubcomponentOrAncestor { @Provides @ForGrpcService(HelloServiceGrpc.class) static List<? extends ServerInterceptor> helloInterceptors( AuthInterceptor authInterceptor, LoggingInterceptor loggingInterceptor) { return Arrays.asList(authInterceptor, loggingInterceptor); } }
Now any binding annotated with @CallScoped
will be used only once per call,
with the bound object shared among all its dependent bindings.
If you’ve installed GrpcCallMetadataModule
onto the subcomponent, then
you’ll also be able to inject the headers from the call into any
binding in the subcomponent.
You create and start the server in the same way as above.