From caf3cd79fb96439d78406481c278f4603742fd8a Mon Sep 17 00:00:00 2001 From: Oleg Zhurakousky Date: Mon, 11 Oct 2021 20:46:43 +0200 Subject: [PATCH] GH-754 Add initial support for specifying grpc service The newly added support allows one to actually specify which groc service to bootstrap in the event multiple services are available in the classpath Resolves #754 --- .../pom.xml | 17 --------- spring-cloud-function-grpc/README.md | 38 +++++++++++++++++++ .../function/grpc/FunctionGrpcProperties.java | 26 +++++++++++++ .../function/grpc/GrpcAutoConfiguration.java | 10 +++++ .../cloud/function/grpc/GrpcServer.java | 4 ++ .../grpc/demo/DemoGrpcApplication.java | 3 +- 6 files changed, 80 insertions(+), 18 deletions(-) diff --git a/spring-cloud-function-adapters/spring-cloud-function-grpc-cloudevent-ext/pom.xml b/spring-cloud-function-adapters/spring-cloud-function-grpc-cloudevent-ext/pom.xml index c7becacbd..f56b83214 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-grpc-cloudevent-ext/pom.xml +++ b/spring-cloud-function-adapters/spring-cloud-function-grpc-cloudevent-ext/pom.xml @@ -21,23 +21,6 @@ org.springframework.boot spring-boot-starter - - - io.grpc - grpc-netty - 1.16.1 - - - io.grpc - grpc-protobuf - 1.16.1 - - - io.grpc - grpc-stub - 1.16.1 - - org.springframework.cloud spring-cloud-function-grpc diff --git a/spring-cloud-function-grpc/README.md b/spring-cloud-function-grpc/README.md index f56068acb..0f13b5dd2 100644 --- a/spring-cloud-function-grpc/README.md +++ b/spring-cloud-function-grpc/README.md @@ -34,6 +34,7 @@ message GrpcMessage { As you can see it is a very generic structure which can support any type of data amd metadata you wish to exchange. It alos defines a `MessagingService` allowing you to generate required stubs to support true plolyglot nature of gRPC. + ``` service MessagingService { rpc biStream(stream GrpcMessage) returns (stream GrpcMessage); @@ -73,6 +74,7 @@ public static class SampleConfiguration { ``` After identifying this function via `spring.cloud.function.definition` property (see example [here](https://github.com/spring-cloud/spring-cloud-function/blob/ded02fec0a6d3d66b8ec00f99f28be2a4bbec668/spring-cloud-function-grpc/src/test/java/org/springframework/cloud/function/grpc/GrpcInteractionTests.java)), you can invoke it using utility method(s) provided in `GrpcUtils` class + ```java Message message = MessageBuilder.withPayload("\"hello gRPC\"".getBytes()) .setHeader("foo", "bar") @@ -81,6 +83,7 @@ Message reply = GrpcUtils.requestReply(message); ``` You can also provide `spring.cloud.function.definition` property via `Message` headers, to support more dynamic cases. + ```java Message message = MessageBuilder.withPayload("\"hello gRPC\"".getBytes()) .setHeader("foo", "bar") @@ -102,6 +105,7 @@ public static class SampleConfiguration { ``` After identifying this function via `spring.cloud.function.definition` property (see example [here](https://github.com/spring-cloud/spring-cloud-function/blob/ded02fec0a6d3d66b8ec00f99f28be2a4bbec668/spring-cloud-function-grpc/src/test/java/org/springframework/cloud/function/grpc/GrpcInteractionTests.java)), you can invoke it using utility method(s) provided in `GrpcUtils` class + ```java Message message = MessageBuilder.withPayload("\"hello gRPC\"".getBytes()).setHeader("foo", "bar").build(); @@ -114,6 +118,7 @@ List> results = reply.collectList().block(Duration.ofSeconds(5)) You can see that gRPC stream is mapped to instance of `Flux` from [project reactor](https://projectreactor.io/) Similarly to the _request/reply_ you can also provide `spring.cloud.function.definition` property via `Message` headers, to support more dynamic cases. + ```java Message message = MessageBuilder.withPayload("\"hello gRPC\"".getBytes()) .setHeader("foo", "bar") @@ -143,6 +148,7 @@ public static class SampleConfiguration { ``` After identifying this function via `spring.cloud.function.definition` property (see example [here](https://github.com/spring-cloud/spring-cloud-function/blob/ded02fec0a6d3d66b8ec00f99f28be2a4bbec668/spring-cloud-function-grpc/src/test/java/org/springframework/cloud/function/grpc/GrpcInteractionTests.java)), you can invoke it using utility method(s) provided in `GrpcUtils` class + ```java List> messages = new ArrayList<>(); messages.add(MessageBuilder.withPayload("\"Ricky\"".getBytes()).setHeader("foo", "bar") @@ -175,6 +181,7 @@ public static class SampleConfiguration { ``` After identifying this function via `spring.cloud.function.definition` property (see example [here](https://github.com/spring-cloud/spring-cloud-function/blob/ded02fec0a6d3d66b8ec00f99f28be2a4bbec668/spring-cloud-function-grpc/src/test/java/org/springframework/cloud/function/grpc/GrpcInteractionTests.java)), you can invoke it using utility method(s) provided in `GrpcUtils` class + ```java List> messages = new ArrayList<>(); messages.add(MessageBuilder.withPayload("\"Ricky\"".getBytes()).setHeader("foo", "bar") @@ -193,3 +200,34 @@ List> results = clientResponseObserver.collectList().block(Durat You can see that gRPC stream is mapped to instance of `Flux` from [project reactor](https://projectreactor.io/) Unlike the _request/reply_ and _server-side streaming_, you can ONLY pass function definition via property or environment variable. + +#### Pluggable protobuf extension + +While the core data object and its corresponding schema <> are modeled after Spring Message and can represent +virtually any object, there are times when you may want to plug-in your own protobuf services. + +Spring Cloud Function provides such support by allowing you to develop extensions, which once exist could be enabled by simply +including its dependency in the POM. Such extensions are just another spring-boot project that has dependency on `spring-cloud-function-grpc` + +```xml + + org.springframework.cloud + spring-cloud-function-grpc + +``` + +It must also contain 3 classes; 1) Its configuration class, 2) Type converter for the actual protobuf 'message'and 3) Service handler +where you would normally implement your handling functionality. However instead of implementing full functionality you can model your service +after MessagingService provided by us and if you do you can rely on the existing implementation of the core interaction models provided by gRPC + +In fact Spring Cloud Function provides one of such extensions to support [Cloud Events](https://github.com/spring-cloud/spring-cloud-function/tree/main/spring-cloud-function-adapters/spring-cloud-function-grpc-cloudevent-ext) proto, so you can model yours after it. + +#### Multiple services on classpath + +With the protobuf extension mentioned in the previous section you may very well end up with several services on the classpath. +By default each available service will be enabled. However, if your intention is to only use one, you can specify which one by providing +its class name via `spring.cloud.function.grpc.service-class-name` property: + +``` +--spring.cloud.function.grpc.service-class-name=org.springframework.cloud.function.grpc.ce.CloudEventHandler +``` \ No newline at end of file diff --git a/spring-cloud-function-grpc/src/main/java/org/springframework/cloud/function/grpc/FunctionGrpcProperties.java b/spring-cloud-function-grpc/src/main/java/org/springframework/cloud/function/grpc/FunctionGrpcProperties.java index 526bbed6c..dd1427688 100644 --- a/spring-cloud-function-grpc/src/main/java/org/springframework/cloud/function/grpc/FunctionGrpcProperties.java +++ b/spring-cloud-function-grpc/src/main/java/org/springframework/cloud/function/grpc/FunctionGrpcProperties.java @@ -28,13 +28,29 @@ import org.springframework.cloud.function.context.FunctionProperties; @ConfigurationProperties(prefix = FunctionProperties.PREFIX + ".grpc") public class FunctionGrpcProperties { + private final static String GRPC_PREFIX = FunctionProperties.PREFIX + ".grpc"; + /** + * The name of function definition property. + */ + public final static String SERVICE_CLASS_NAME = GRPC_PREFIX + ".service-class-name"; + /** * Default gRPC port. */ public final static int GRPC_PORT = 6048; + /** + * gRPC port server will bind to. Default 6048; + */ private int port = GRPC_PORT; + /** + * The fully qualified name of the service you wish to enable/expose. + * Setting this property ensures that only a single service is enabled/exposed, + * regardless of how many services are available on the classpath. + */ + private String serviceClassName; + /** * Grpc Server port. */ @@ -45,4 +61,14 @@ public class FunctionGrpcProperties { public void setPort(int port) { this.port = port; } + + + public String getServiceClassName() { + return serviceClassName; + } + + + public void setServiceClassName(String serviceClassName) { + this.serviceClassName = serviceClassName; + } } diff --git a/spring-cloud-function-grpc/src/main/java/org/springframework/cloud/function/grpc/GrpcAutoConfiguration.java b/spring-cloud-function-grpc/src/main/java/org/springframework/cloud/function/grpc/GrpcAutoConfiguration.java index 902cfdb4a..bf5ebcf30 100644 --- a/spring-cloud-function-grpc/src/main/java/org/springframework/cloud/function/grpc/GrpcAutoConfiguration.java +++ b/spring-cloud-function-grpc/src/main/java/org/springframework/cloud/function/grpc/GrpcAutoConfiguration.java @@ -25,6 +25,7 @@ import org.springframework.cloud.function.context.FunctionProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; import com.google.protobuf.GeneratedMessageV3; @@ -43,15 +44,24 @@ class GrpcAutoConfiguration { @Bean public GrpcServer grpcServer(FunctionGrpcProperties grpcProperties, BindableService[] grpcMessagingServices) { Assert.notEmpty(grpcMessagingServices, "'grpcMessagingServices' must not be null or empty"); + if (StringUtils.hasText(grpcProperties.getServiceClassName())) { + for (BindableService bindableService : grpcMessagingServices) { + if (bindableService.getClass().getName().equals(grpcProperties.getServiceClassName())) { + return new GrpcServer(grpcProperties, new BindableService[] {bindableService}); + } + } + } return new GrpcServer(grpcProperties, grpcMessagingServices); } + @SuppressWarnings({ "unchecked", "rawtypes" }) @Bean public BindableService grpcSpringMessageHandler(MessageHandlingHelper helper) { return new GrpcServerMessageHandler(helper); } + @SuppressWarnings({ "rawtypes", "unchecked" }) @Bean public MessageHandlingHelper grpcMessageHandlingHelper(List> grpcConverters, FunctionProperties funcProperties, FunctionCatalog functionCatalog) { diff --git a/spring-cloud-function-grpc/src/main/java/org/springframework/cloud/function/grpc/GrpcServer.java b/spring-cloud-function-grpc/src/main/java/org/springframework/cloud/function/grpc/GrpcServer.java index f5924ef9e..dd97e8cb5 100644 --- a/spring-cloud-function-grpc/src/main/java/org/springframework/cloud/function/grpc/GrpcServer.java +++ b/spring-cloud-function-grpc/src/main/java/org/springframework/cloud/function/grpc/GrpcServer.java @@ -32,6 +32,9 @@ import io.grpc.protobuf.services.ProtoReflectionService; /** * * @author Oleg Zhurakousky + * @author Dave Syer + * + * @since 3.2 * */ class GrpcServer implements SmartLifecycle { @@ -70,6 +73,7 @@ class GrpcServer implements SmartLifecycle { logger.info("gRPC server is listening on port " + this.grpcProperties.getPort()); } catch (Exception e) { + stop(); throw new IllegalStateException(e); } }); diff --git a/spring-cloud-function-samples/function-sample-grpc-cloudevent/src/main/java/com/example/grpc/demo/DemoGrpcApplication.java b/spring-cloud-function-samples/function-sample-grpc-cloudevent/src/main/java/com/example/grpc/demo/DemoGrpcApplication.java index 77c79c9af..83d7bcc33 100644 --- a/spring-cloud-function-samples/function-sample-grpc-cloudevent/src/main/java/com/example/grpc/demo/DemoGrpcApplication.java +++ b/spring-cloud-function-samples/function-sample-grpc-cloudevent/src/main/java/com/example/grpc/demo/DemoGrpcApplication.java @@ -30,7 +30,8 @@ public class DemoGrpcApplication { public static void main(String[] args) throws Exception { - SpringApplication.run(DemoGrpcApplication.class, args); + SpringApplication.run(DemoGrpcApplication.class, + "--spring.cloud.function.grpc.service-class-name=org.springframework.cloud.function.grpc.ce.CloudEventHandler"); CloudEvent cloudEvent = CloudEvent.newBuilder() .setTextData("{\"event_name\":\"SCF supports CloudEvent gRPC\"}")