Spring boot starter for gRPC framework.
- 1. Features
- 2. Setup
- 3. Usage
- 4. Show case
- 4.1. Service implementation
- 4.2. Interceptors support
- 4.3. Distributed tracing support (Spring Cloud Sleuth integration)
- 4.4. GRPC server metrics (Micrometer.io integration)
- 4.5. Spring Boot Validation support
- 4.6. Spring security support
- 4.7. Transport Security (TLS)
- 4.8. Custom gRPC Server Configuration
- 5. Implementing message validation
- 6. Spring Security Integration
- 7. Consul Integration
- 8. Eureka Integration
- 9. License
Auto-configures and runs the embedded gRPC server with @GRpcService-enabled beans as part of spring-boot application.
|
❗
|
Starting from release 4.0.0 the starter is compiled and tested against Spring Boot 2.X.X only,1.5.X Spring boot version support is dropped to allow tight Spring Boot Security framework integration. |
repositories {
mavenCentral()
//maven { url "https://oss.sonatype.org/content/repositories/snapshots" } //for snashot builds
}
dependencies {
compile 'io.github.lognet:grpc-spring-boot-starter:4.4.5'
}If you are using Spring Boot Dependency Management plugin, it might pull not the same version as the version this started was compiled against, causing binary incompatibility issue.
In this case you’ll need to forcibly and explicitly set the grpc version to use (see version matrix here ):
configurations.all {
resolutionStrategy.eachDependency { details ->
if ("io.grpc".equalsIgnoreCase(details.requested.group)) {
details.useVersion "1.33.0"
}
}
}|
❗
|
Starting from release 3.0.0 the artifacts are published to maven central.
Pay attention that group has changed from org.lognet to io.github.lognet.
|
|
ℹ️
|
The release notes with compatibility matrix can be found here |
-
Start by generating stub and server interface(s) from your
.protofile(s). -
Annotate your server interface implementation(s) with
@org.lognet.springboot.grpc.GRpcService -
Optionally configure the server port in your
application.yml/properties. Default port is6565.
grpc:
port: 6565|
ℹ️
|
A random port can be defined by setting the port to 0.The actual port being used can then be retrieved by using @LocalRunningGrpcPort annotation on int field which will inject the running port (explicitly configured or randomly selected)
|
-
Optionally enable server reflection (see https://github.com/grpc/grpc-java/blob/master/documentation/server-reflection-tutorial.md)
grpc:
enableReflection: true-
Optionally set the startup phase order (defaults to
Integer.MAX_VALUE).
grpc:
start-up-phase: XXX-
Optionally set the number of seconds to wait for preexisting calls to finish during graceful server shutdown. New calls will be rejected during this time. A negative value is equivalent to an infinite grace period. Default value is
0(means don’t wait).
grpc:
shutdownGrace: 30-
Netty-specific server properties can be specified under
grpc.netty-serverprefix.
By configuring one of thegrpc.netty-server.xxxxvalues you are implicitly setting transport to be Netty-based.
grpc:
netty-server:
keep-alive-time: 30s (1)
max-inbound-message-size: 10MB (2)
primary-listen-address: 10.10.15.23:0 (3)
additional-listen-addresses:
- 192.168.0.100:6767 (4)-
Durationtype properties can be configured with string value format described here. -
DataSizetype properties can be configured with string value described here -
Exposed on external network IP with custom port.
SocketAddresstype properties string value format:-
host:port(ifportvalue is less than 1, uses random value) -
host:(uses default grpc port,6565)
-
-
Exposed on internal network IP as well with predefined port
6767.
The starter supports also the in-process server, which should be used for testing purposes :
grpc:
enabled: false (1)
inProcessServerName: myTestServer (2)-
Disables the default server (
NettyServer). -
Enables the
in-processserver.
|
ℹ️
|
If you enable both the NettyServer and in-process server, they will both share the same instance of HealthStatusManager and GRpcServerBuilderConfigurer (see Custom gRPC Server Configuration).
|
In the grpc-spring-boot-starter-demo project you can find fully functional examples with integration tests.
The service definition from .proto file looks like this :
service Greeter {
rpc SayHello ( HelloRequest) returns ( HelloReply) {}
}Note the generated io.grpc.examples.GreeterGrpc.GreeterImplBase class that extends io.grpc.BindableService.(The generated classes were intentionally committed for demo purposes).
All you need to do is to annotate your service implementation with @org.lognet.springboot.grpc.GRpcService
@GRpcService
public static class GreeterService extends GreeterGrpc.GreeterImplBase{
@Override
public void sayHello(GreeterOuterClass.HelloRequest request, StreamObserver<GreeterOuterClass.HelloReply> responseObserver) {
final GreeterOuterClass.HelloReply.Builder replyBuilder = GreeterOuterClass.HelloReply.newBuilder().setMessage("Hello " + request.getName());
responseObserver.onNext(replyBuilder.build());
responseObserver.onCompleted();
}
}The starter supports the registration of two kinds of interceptors: Global and Per Service.
In both cases the interceptor has to implement io.grpc.ServerInterceptor interface.
-
Per service
@GRpcService(interceptors = { LogInterceptor.class })
public class GreeterService extends GreeterGrpc.GreeterImplBase{
// ommited
}LogInterceptor will be instantiated via spring factory if there is bean of type LogInterceptor, or via no-args constructor otherwise.
-
Global
@GRpcGlobalInterceptor
public class MyInterceptor implements ServerInterceptor{
// ommited
}The annotation on java config factory method is also supported :
@Configuration
public class MyConfig{
@Bean
@GRpcGlobalInterceptor
public ServerInterceptor globalInterceptor(){
return new ServerInterceptor(){
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {
// your logic here
return next.startCall(call, headers);
}
};
}
}The particular service also has the opportunity to disable the global interceptors :
@GRpcService(applyGlobalInterceptors = false)
public class GreeterService extends GreeterGrpc.GreeterImplBase{
// ommited
}Global interceptors can be ordered using Spring’s @Ordered or @Priority annotations.
Following Spring’s ordering semantics, lower order values have higher priority and will be executed first in the interceptor chain.
@GRpcGlobalInterceptor
@Order(10)
public class A implements ServerInterceptor{
// will be called before B
}
@GRpcGlobalInterceptor
@Order(20)
public class B implements ServerInterceptor{
// will be called after A
}The starter uses built-in interceptors to implement Spring Security, Validation and Metrics integration.
Their order can also be controlled by below properties :
-
grpc.security.auth.interceptor-order( defaults toOrdered.HIGHEST_PRECEDENCE) -
grpc.validation.interceptor-order( defaults toOrdered.HIGHEST_PRECEDENCE+10) -
grpc.metrics.interceptor-order( defaults toOrdered.HIGHEST_PRECEDENCE+20)
This gives you the ability to setup the desired order of built-in and your custom interceptors.
Keep on reading !!! There is more
The way grpc interceptor works is that it intercepts the call and returns the server call listener, which in turn can intercept the request message as well, before forwarding it to the actual service call handler :
interceptor_1(interceptCall) → interceptor_2(interceptCall) → interceptor_3(interceptCall) →
interceptor_1(On_Message)→ interceptor_2(On_Message)→ interceptor_3(On_Message)→
actual service call
By setting grpc.security.auth.fail-fast property to false all downstream interceptors as well as all upstream interceptors (On_Message) will still be executed
Assuming interceptor_2 is securityInterceptor :
-
For failed authentication/authorization with
grpc.security.auth.fail-fast=true(default):interceptor_1(interceptCall)→securityInterceptor(interceptCall)- Call is Closed →interceptor_3(interceptCall)→
interceptor_1(On_Message)→securityInterceptor(On_Message)→`interceptor_3(On_Message)→
`actual service call -
For failed authentication/authorization with
grpc.security.auth.fail-fast=false:interceptor_1(interceptCall)→securityInterceptor(interceptCall)→interceptor_3(interceptCall)→interceptor_1(On_Message)→securityInterceptor(On_Message)- Call is Closed→interceptor_3(On_Message)→
actual service call
This started is natively supported by spring-cloud-sleuth project.
Please continue to sleuth grpc integration.
By including org.springframework.boot:spring-boot-starter-actuator dependency,
the starter will collect gRPC server metrics , broken down by
-
method- gRPC service method FQN (Fully Qualified Name) -
result- Response status code -
address- server local address (if you exposed additional listen addresses, withgrpc.netty-server.additional-listen-addressesproperty)
After configuring the exporter of your choice,
you should see the timer named grpc.server.calls.
By defining GRpcMetricsTagsContributor bean in your application context, you can add custom tags to the grpc.server.calls timer.
You can also use RequestAwareGRpcMetricsTagsContributor bean to tag unary calls.
Demo is here
|
💡
|
Keep the dispersion low not to blow up the cardinality of the metric. |
RequestAwareGRpcMetricsTagsContributor can be still executed for failed authentication if metric interceptor has higher precedence than security interceptor and grpc.security.auth.fail-fast set to false.
This case is covered by this test.
|
💡
|
Make sure to read Interceptors ordering chapter. |
The starter can be auto-configured to validate request/response gRPC service messages. Please continue to Implementing message validation for configuration details.
The starter provides built-in support for authenticating and authorizing users leveraging integration with Spring Security framework.
Please refer to the sections on Spring Security Integration for details on supported authentication providers and configuration options.
The transport security can be configured using root certificate together with its private key path:
grpc:
security:
cert-chain: classpath:cert/server-cert.pem
private-key: file:../grpc-spring-boot-starter-demo/src/test/resources/cert/server-key.pemThe value of both properties is in form supported by ResourceEditor.
The client side should be configured accordingly :
((NettyChannelBuilder)channelBuilder)
.useTransportSecurity()
.sslContext(GrpcSslContexts.forClient().trustManager(certChain).build());This starter will pull the io.netty:netty-tcnative-boringssl-static dependency by default to support SSL.
If you need another SSL/TLS support, please exclude this dependency and follow Security Guide.
|
ℹ️
|
If the more detailed tuning is needed for security setup, please use custom configurer described in Custom gRPC Server Configuration |
To intercept the io.grpc.ServerBuilder instance used to build the io.grpc.Server, you can add bean that inherits from org.lognet.springboot.grpc.GRpcServerBuilderConfigurer to your context and override the configure method.
By the time of invocation of configure method, all discovered services, including theirs interceptors, had been added to the passed builder.
In your implementation of configure method, you can add your custom configuration:
@Component
public class MyGRpcServerBuilderConfigurer extends GRpcServerBuilderConfigurer{
@Override
public void configure(ServerBuilder<?> serverBuilder){
serverBuilder
.executor(YOUR EXECUTOR INSTANCE)
.compressorRegistry(YOUR COMPRESSION REGISTRY)
.decompressorRegistry(YOUR DECOMPRESSION REGISTRY)
.useTransportSecurity(YOUR TRANSPORT SECURITY SETTINGS);
((NettyServerBuilder)serverBuilder)// cast to NettyServerBuilder (which is the default server) for further customization
.sslContext(GrpcSslContexts // security fine tuning
.forServer(...)
.trustManager(...)
.build())
.maxConnectionAge(...)
.maxConnectionAgeGrace(...);
}
};
}|
ℹ️
|
If you enable both NettyServer and in-process servers, the configure method will be invoked on the same instance of configurer.If you need to differentiate between the passed serverBuilder s, you can check the type.This is the current limitation. |
Thanks to Bean Validation configuration support via XML deployment descriptor , it’s possible to
provide the constraints for generated classes via XML instead of instrumenting the generated messages with custom protoc compiler.
-
Add
org.springframework.boot:spring-boot-starter-validationdependency to your project. -
Create
META-INF/validation.xmland constraints declarations file(s). (IntelliJ IDEA has great auto-complete support for authorizing bean validation constraints xml files )
See also samples fromHibernatevalidator documentation
You can find demo configuration and corresponding tests here
Note, that both request and response messages are being validated.
If your gRPC method uses the same request and response message type, you can use org.lognet.springboot.grpc.validation.group.RequestMessage and
org.lognet.springboot.grpc.validation.group.ResponseMessage validation groups to apply different validation logic :
...
<getter name="someField">
<!--should be empty for request message-->
<constraint annotation="javax.validation.constraints.Size">
<groups>
<value>org.lognet.springboot.grpc.validation.group.RequestMessage</value> (1)
</groups>
<element name="min">0</element>
<element name="max">0</element>
</constraint>
<!--should NOT be empty for response message-->
<constraint annotation="javax.validation.constraints.NotEmpty">
<groups>
<value>org.lognet.springboot.grpc.validation.group.ResponseMessage</value> (2)
</groups>
</constraint>
</getter>
...-
Apply this constraint only for
requestmessage -
Apply this constraint only for
responsemessage
Note also custom cross-field constraint and its usage :
<bean class="io.grpc.examples.GreeterOuterClass$Person">
<class>
<constraint annotation="org.lognet.springboot.grpc.demo.PersonConstraint"/>
</class>
...
</bean>As described in Interceptors ordering chapter, you can give validation interceptor the higher precedence than security interceptor and set grpc.security.auth.fail-fast property to false.
In this scenario, if call is both unauthenticated and invalid, the client will get Status.INVALID_ARGUMENT instead of Status.PERMISSION_DENIED/Status.UNAUTHENTICATED response status.
By adding GRpcErrorHandler bean to your application, you get a chance to send your custom response headers. The error handler will be called with Status.INVALID_ARGUMENT and incoming request message that is failed.
| Scheme | Dependencies |
|---|---|
Basic |
|
Bearer |
|
Custom |
|
GRPC security configuration follows the same principals and APIs as Spring WEB security configuration.
Defining bean with type GrpcSecurityConfigurerAdapter annotated with @EnableGrpcSecurity is sufficient to secure you GRPC services and/or methods :
@EnableGrpcSecurity
public class GrpcSecurityConfiguration extends GrpcSecurityConfigurerAdapter {
}This default configuration secures GRPC methods/services annotated with org.springframework.security.access.annotation.@Secured annotation.
Leaving value of the annotation empty (@Secured({})) means : authenticate only, no authorization will be performed.
If JwtDecoder bean exists in your context, it will also register JwtAuthenticationProvider to handle the validation of authentication claim.
Various configuration examples and test scenarios are here.
@EnableGrpcSecurity
public class GrpcSecurityConfiguration extends GrpcSecurityConfigurerAdapter {
@Autowired
private JwtDecoder jwtDecoder;
@Override
public void configure(GrpcSecurity builder) throws Exception {
builder.authorizeRequests()(1)
.methods(GreeterGrpc.getSayHelloMethod()).hasAnyAuthority("SCOPE_profile")(2)
.and()
.authenticationProvider(JwtAuthProviderFactory.withAuthorities(jwtDecoder));(3)
}
}-
Get hold of authorization configuration object
-
MethodDefinitionofsayHellomethod is allowed for authenticated users withSCOPE_profileauthority. -
Use
JwtAuthenticationProviderto validate user claim (BEARERtoken) against resource server configured withspring.security.oauth2.resourceserver.jwt.issuer-uriproperty.
One is possible to plug in your own bespoke authentication provider by implementing AuthenticationSchemeSelector interface.
@EnableGrpcSecurity
public class GrpcSecurityConfiguration extends GrpcSecurityConfigurerAdapter {
@Override
public void configure(GrpcSecurity builder) throws Exception {
builder.authorizeRequests()
.anyMethod().authenticated()(1)
.and()
.authenticationSchemeSelector(new AuthenticationSchemeSelector() { (2)
@Override
public Optional<Authentication> getAuthScheme(CharSequence authorization) {
return new MyAuthenticationObject(); (3)
}
})
.authenticationProvider(new AuthenticationProvider() { (4)
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
MyAuthenticationObject myAuth= (MyAuthenticationObject)authentication;
//validate myAuth
return MyValidatedAuthenticationObject(withAuthorities);(5)
}
@Override
public boolean supports(Class<?> authentication) {
return MyAuthenticationObject.class.isInstance(authentication);
}
});
}
}-
Secure all services methods.
-
Register your own
AuthenticationSchemeSelector. -
Based on provided authorization header - return
Authenticationobject as a claim (not authenticated yet) -
Register your own
AuthenticationProviderthat supports validation ofMyAuthenticationObject -
Validate provided
authenticationand return validated and authenticatedAuthenticationobject
Client side configuration support section explains how to pass custom authorization scheme and claim from GRPC client.
To obtain Authentication object in the implementation of secured method, please use below snippet
final Authentication auth = GrpcSecurity.AUTHENTICATION_CONTEXT_KEY.get();By adding GRpcErrorHandler bean to your application, you get a chance to provide your custom response headers. The error handler will be called with Status.PERMISSION_DENIED/Status.UNAUTHENTICATED (and incoming request message , if you set grpc.security.auth.fail-fast property to false).
The demo is here
By adding io.github.lognet:grpc-client-spring-boot-starter dependency to your java grpc client application you can easily configure per-channel or per-call credentials :
- Per-channel
-
class MyClient{ public void doWork(){ final AuthClientInterceptor clientInterceptor = new AuthClientInterceptor((1) AuthHeader.builder() .bearer() .binaryFormat(true)(3) .tokenSupplier(this::generateToken)(4) ); Channel authenticatedChannel = ClientInterceptors.intercept( ManagedChannelBuilder.forAddress("host", 6565), clientInterceptor (2) ); // use authenticatedChannel to invoke GRPC service } private ByteBuffer generateToken(){ (4) // generate bearer token against your resource server } }
-
Create client interceptor
-
Intercept channel
-
Turn the binary format on/off:
-
When
true, the authentication header is sent withAuthentication-binkey using binary marshaller. -
When
false, the authentication header is sent withAuthenticationkey using ASCII marshaller.
-
-
-
Provide token generator function (Please refer to for example.)
- Per-call
-
class MyClient{ public void doWork(){ AuthCallCredentials callCredentials = new AuthCallCredentials( (1) AuthHeader.builder().basic("user","pwd".getBytes()) ); final SecuredGreeterGrpc.SecuredGreeterBlockingStub securedFutureStub = SecuredGreeterGrpc.newBlockingStub(ManagedChannelBuilder.forAddress("host", 6565));(2) final String reply = securedFutureStub .withCallCredentials(callCredentials)(3) .sayAuthHello(Empty.getDefaultInstance()).getMessage(); } }
-
Create call credentials with basic scheme
-
Create service stub
-
Attach call credentials to the call
AuthHeadercould also be built with bespoke authorization scheme :AuthHeader .builder() .authScheme("myCustomAuthScheme") .tokenSupplier(()->generateMyCustomToken())
Starting from version 3.3.0, the starter will auto-register the running grpc server in Consul registry if org.springframework.cloud:spring-cloud-starter-consul-discovery is in classpath and
spring.cloud.service-registry.auto-registration.enabled is NOT set to false.
The registered service name will be prefixed with grpc- ,i.e. grpc-${spring.application.name} to not interfere with standard registered web-service name if you choose to run both embedded Grpc and Web servers.
Setting spring.cloud.consul.discovery.register-health-check to true will register GRPC health check service in Consul.
Tags could be set by defining spring.cloud.consul.discovery.tags property.
You can find the test that demonstrates the feature here.
When building production-ready services, the advise is to have separate project for your service(s) gRPC API that holds only proto-generated classes both for server and client side usage.
You will then add this project as compile dependency to your gRPC client and gRPC server projects.
To integrate Eureka simply follow the great guide from Spring.
Below are the essential parts of configurations for both server and client projects.
-
Add eureka starter as dependency of your server project together with generated classes from
protofiles:
dependencies {
compile('org.springframework.cloud:spring-cloud-starter-eureka')
compile project(":yourProject-api")
}-
Configure gRPC server to register itself with Eureka.
bootstrap.yamlspring: application: name: my-service-name (1)
-
Eureka’s
ServiceIdby default is the spring application name, provide it before the service registers itself with Eureka.application.yamlgrpc: port: 6565 (1) eureka: instance: nonSecurePort: ${grpc.port} (2) client: serviceUrl: defaultZone: http://${eureka.host:localhost}:${eureka.port:8761}/eureka/ (3)
-
Specify the port number the gRPC is listening on.
-
Register the eureka service port to be the same as
grpc.portso client will know where to send the requests to. -
Specify the registry URL, so the service will register itself with.
-
-
Expose the gRPC service as part of Spring Boot Application.
EurekaGrpcServiceApp.java@SpringBootApplication @EnableEurekaClient public class EurekaGrpcServiceApp { @GRpcService public static class GreeterService extends GreeterGrpc.GreeterImplBase { @Override public void sayHello(GreeterOuterClass.HelloRequest request, StreamObserver<GreeterOuterClass.HelloReply> responseObserver) { } } public static void main(String[] args) { SpringApplication.run(DemoApp.class,args); } }
-
Add eureka starter as dependency of your client project together with generated classes from
protofiles:
dependencies {
compile('org.springframework.cloud:spring-cloud-starter-eureka')
compile project(":yourProject-api")
}-
Configure client to find the eureka service registry:
eureka:
client:
register-with-eureka: false (1)
service-url:
defaultZone: http://${eureka.host:localhost}:${eureka.port:8761}/eureka/ (2)-
falseif this project is not meant to act as a service to another client. -
Specify the registry URL, so this client will know where to look up the required service.
@EnableEurekaClient
@SpringBootApplication
public class GreeterServiceConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(GreeterServiceConsumerApplication.class, args);
}
}-
Use EurekaClient to get the coordinates of gRPC service instance from Eureka and consume the service :
@EnableEurekaClient
@Component
public class GreeterServiceConsumer {
@Autowired
private EurekaClient client;
public void greet(String name) {
final InstanceInfo instanceInfo = client.getNextServerFromEureka("my-service-name", false);(1)
final ManagedChannel channel = ManagedChannelBuilder.forAddress(instanceInfo.getIPAddr(), instanceInfo.getPort())
.usePlaintext()
.build(); (2)
final GreeterServiceGrpc.GreeterServiceFutureStub stub = GreeterServiceGrpc.newFutureStub(channel); (3)
stub.greet(name); (4)
}
}-
Get the information about the
my-service-nameinstance. -
Build
channelaccordingly. -
Create stub using the
channel. -
Invoke the service.