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\"}")