*/
- @Retention(RUNTIME)
- @Target(TYPE)
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.TYPE)
@Documented
@interface GrpcInterceptor {
}
@@ -173,8 +169,8 @@ public interface Grpc {
* Specifies that an annotation type is a gRPC interceptor binding type. A gRPC Interceptor binding is
* used to specify the binding of a gRPC client or server interceptor to target gRPC service and methods.
*/
- @Target(ANNOTATION_TYPE)
- @Retention(RUNTIME)
+ @Target(ElementType.ANNOTATION_TYPE)
+ @Retention(RetentionPolicy.RUNTIME)
@Documented
@interface GrpcInterceptorBinding {
}
@@ -198,8 +194,8 @@ public interface Grpc {
* public void updateOrder(Order order) { ... }
*
*/
- @Target({TYPE, ElementType.METHOD})
- @Retention(RUNTIME)
+ @Target({ElementType.TYPE, ElementType.METHOD})
+ @Retention(RetentionPolicy.RUNTIME)
@interface GrpcInterceptors {
/**
* An ordered list of interceptors.
@@ -213,8 +209,8 @@ public interface Grpc {
* An annotation used to annotate a type or method to specify the
* named marshaller supplier to use for rpc method calls.
*/
- @Target({TYPE, ElementType.METHOD})
- @Retention(RUNTIME)
+ @Target({ElementType.TYPE, ElementType.METHOD})
+ @Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@interface GrpcMarshaller {
@@ -240,8 +236,8 @@ public interface Grpc {
* An annotation to mark a class as representing a gRPC service
* or a method as a gRPC service method.
*/
- @Target({ElementType.METHOD, ANNOTATION_TYPE})
- @Retention(RUNTIME)
+ @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
+ @Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@interface GrpcMethod {
@@ -265,8 +261,8 @@ public interface Grpc {
/**
* An annotation to indicate the request type of gRPC method.
*/
- @Target({ElementType.METHOD})
- @Retention(RUNTIME)
+ @Target(ElementType.METHOD)
+ @Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@interface RequestType {
@@ -281,8 +277,8 @@ public interface Grpc {
/**
* An annotation to indicate the response type of a gRPC method.
*/
- @Target({ElementType.METHOD})
- @Retention(RUNTIME)
+ @Target(ElementType.METHOD)
+ @Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@interface ResponseType {
@@ -293,4 +289,27 @@ public interface Grpc {
*/
Class> value();
}
+
+ /**
+ * An annotation that can be used to specify the name of a configured gRPC channel.
+ */
+ @Target({ElementType.TYPE, ElementType.METHOD})
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface GrpcChannel {
+
+ /**
+ * The name of the configured channel.
+ *
+ * @return name of the channel
+ */
+ String value();
+ }
+
+ /**
+ * An annotation used to mark an injection point for a gRPC service client proxy.
+ */
+ @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface GrpcProxy {
+ }
}
diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/package-info.java b/grpc/core/src/main/java/io/helidon/grpc/core/package-info.java
index a10d014bfed..4c5124c1aa9 100644
--- a/grpc/core/src/main/java/io/helidon/grpc/core/package-info.java
+++ b/grpc/core/src/main/java/io/helidon/grpc/core/package-info.java
@@ -15,6 +15,6 @@
*/
/**
- * Core classes used by both the gRPC server API and gRPC client API.
+ * Core classes used by both the gRPC server and gRPC client.
*/
package io.helidon.grpc.core;
diff --git a/microprofile/grpc/client/pom.xml b/microprofile/grpc/client/pom.xml
new file mode 100644
index 00000000000..f2b2c481fba
--- /dev/null
+++ b/microprofile/grpc/client/pom.xml
@@ -0,0 +1,179 @@
+
+
+
+
+ 4.0.0
+
+ io.helidon.microprofile.grpc
+ helidon-microprofile-grpc-project
+ 4.1.0-SNAPSHOT
+
+
+ helidon-microprofile-grpc-client
+ Helidon Microprofile gRPC Client
+
+
+
+ io.helidon.grpc
+ helidon-grpc-core
+
+
+ io.helidon.grpc
+ helidon-grpc-api
+
+
+ io.helidon.microprofile.grpc
+ helidon-microprofile-grpc-core
+
+
+ io.helidon.config
+ helidon-config
+
+
+ io.helidon.webclient
+ helidon-webclient-grpc
+
+
+ io.helidon.common
+ helidon-common-tls
+
+
+ jakarta.inject
+ jakarta.inject-api
+
+
+ jakarta.enterprise
+ jakarta.enterprise.cdi-api
+
+
+ io.helidon.webserver
+ helidon-webserver-grpc
+ test
+
+
+ io.helidon.microprofile.grpc
+ helidon-microprofile-grpc-server
+ test
+
+
+ io.helidon.config
+ helidon-config-yaml
+ test
+
+
+ io.helidon.microprofile.testing
+ helidon-microprofile-testing-junit5
+ test
+
+
+ io.helidon.webserver.testing.junit5
+ helidon-webserver-testing-junit5-grpc
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ org.hamcrest
+ hamcrest-all
+ test
+
+
+
+ javax.annotation
+ javax.annotation-api
+ true
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+
+
+
+ kr.motd.maven
+ os-maven-plugin
+ ${version.plugin.os}
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ io.helidon.codegen
+ helidon-codegen-apt
+ ${helidon.version}
+
+
+ io.helidon.builder
+ helidon-builder-codegen
+ ${helidon.version}
+
+
+ io.helidon.codegen
+ helidon-codegen-helidon-copyright
+ ${helidon.version}
+
+
+
+
+
+ io.helidon.codegen
+ helidon-codegen-apt
+ ${helidon.version}
+
+
+ io.helidon.builder
+ helidon-builder-codegen
+ ${helidon.version}
+
+
+ io.helidon.codegen
+ helidon-codegen-helidon-copyright
+ ${helidon.version}
+
+
+
+
+ org.xolstice.maven.plugins
+ protobuf-maven-plugin
+
+
+
+ test-compile
+ test-compile-custom
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ChannelProducer.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ChannelProducer.java
new file mode 100644
index 00000000000..9a1cb1f3c5f
--- /dev/null
+++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ChannelProducer.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2019, 2024 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.microprofile.grpc.client;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.locks.ReentrantLock;
+
+import io.helidon.config.Config;
+import io.helidon.grpc.api.Grpc;
+
+import io.grpc.Channel;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.inject.Produces;
+import jakarta.enterprise.inject.spi.InjectionPoint;
+import jakarta.inject.Inject;
+
+/**
+ * A producer of gRPC {@link io.grpc.Channel Channels}.
+ */
+@ApplicationScoped
+public class ChannelProducer {
+
+ private final GrpcChannelsProvider provider;
+ private final ReentrantLock lock = new ReentrantLock();
+ private final Map channelMap = new HashMap<>();
+
+ /**
+ * Create a {@link ChannelProducer}.
+ *
+ * @param config the {@link io.helidon.config.Config} to use to configure
+ * the provided {@link io.grpc.Channel}s
+ */
+ @Inject
+ ChannelProducer(Config config) {
+ provider = GrpcChannelsProvider.create(config.get("grpc.client"));
+ }
+
+ /**
+ * Produces a gRPC {@link io.grpc.Channel}.
+ *
+ * @param injectionPoint the injection point
+ * @return a gRPC {@link io.grpc.Channel}
+ */
+ @Produces
+ @Grpc.GrpcChannel(GrpcChannelsProvider.DEFAULT_CHANNEL_NAME)
+ public Channel get(InjectionPoint injectionPoint) {
+ Grpc.GrpcChannel qualifier = injectionPoint.getQualifiers()
+ .stream()
+ .filter(q -> q.annotationType().equals(Grpc.GrpcChannel.class))
+ .map(q -> (Grpc.GrpcChannel) q)
+ .findFirst()
+ .orElse(null);
+
+ String name = (qualifier == null) ? GrpcChannelsProvider.DEFAULT_CHANNEL_NAME : qualifier.value();
+ return findChannel(name);
+ }
+
+ /**
+ * Produces the default gRPC {@link io.grpc.Channel}.
+ *
+ * @return the default gRPC {@link io.grpc.Channel}
+ */
+ @Produces
+ public Channel getDefaultChannel() {
+ return findChannel(GrpcChannelsProvider.DEFAULT_CHANNEL_NAME);
+ }
+
+ /**
+ * Obtain the named {@link io.grpc.Channel}.
+ *
+ * @param name the channel name
+ * @return the named {@link io.grpc.Channel}
+ */
+ public Channel findChannel(String name) {
+ try {
+ lock.lock();
+ return channelMap.computeIfAbsent(name, provider::channel);
+ } finally {
+ lock.unlock();
+ }
+ }
+}
diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientMethodDescriptor.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientMethodDescriptor.java
new file mode 100644
index 00000000000..01a1260ac71
--- /dev/null
+++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientMethodDescriptor.java
@@ -0,0 +1,414 @@
+/*
+ * Copyright (c) 2019, 2024 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.microprofile.grpc.client;
+
+import java.util.Arrays;
+
+import io.helidon.grpc.core.InterceptorWeights;
+import io.helidon.grpc.core.MarshallerSupplier;
+import io.helidon.grpc.core.MethodHandler;
+import io.helidon.grpc.core.WeightedBag;
+
+import io.grpc.CallCredentials;
+import io.grpc.ClientInterceptor;
+import io.grpc.MethodDescriptor;
+
+/**
+ * Encapsulates all metadata necessary to define a gRPC method. In addition to wrapping a {@link MethodDescriptor},
+ * this class also holds the request and response types of the gRPC method. A
+ * {@link ClientServiceDescriptor} can contain zero or more {@link MethodDescriptor}.
+ *
+ * An instance of ClientMethodDescriptor can be created either from an existing {@link MethodDescriptor} or
+ * from one of the factory methods {@link #bidirectional(String, String)}, {@link #clientStreaming(String, String)},
+ * {@link #serverStreaming(String, String)} or {@link #unary(String, String)}.
+ */
+public final class ClientMethodDescriptor {
+
+ /**
+ * The simple name of the method.
+ */
+ private final String name;
+
+ /**
+ * The {@link MethodDescriptor} for this method. This is usually obtained from protocol buffer
+ * method getDescriptor (from service getDescriptor).
+ */
+ private final MethodDescriptor, ?> descriptor;
+
+ /**
+ * The list of client interceptors for this method.
+ */
+ private final WeightedBag interceptors;
+
+ /**
+ * The {@link CallCredentials} for this method.
+ */
+ private final CallCredentials callCredentials;
+
+ /**
+ * The method handler for this method.
+ */
+ private final MethodHandler, ?> methodHandler;
+
+ private ClientMethodDescriptor(String name,
+ MethodDescriptor, ?> descriptor,
+ WeightedBag interceptors,
+ CallCredentials callCredentials,
+ MethodHandler, ?> methodHandler) {
+ this.name = name;
+ this.descriptor = descriptor;
+ this.interceptors = interceptors;
+ this.callCredentials = callCredentials;
+ this.methodHandler = methodHandler;
+ }
+
+ /**
+ * Creates a new {@link Builder} with the specified name and {@link MethodDescriptor}.
+ *
+ * @param serviceName the name of the owning gRPC service
+ * @param name the simple method name
+ * @param descriptor the underlying gRPC {@link MethodDescriptor.Builder}
+ * @return A new instance of a {@link Builder}
+ */
+ static Builder builder(String serviceName,
+ String name,
+ MethodDescriptor.Builder, ?> descriptor) {
+ return new Builder(serviceName, name, descriptor);
+ }
+
+ /**
+ * Creates a new {@link Builder} with the specified
+ * name and {@link MethodDescriptor}.
+ *
+ * @param serviceName the name of the owning gRPC service
+ * @param name the simple method name
+ * @param descriptor the underlying gRPC {@link MethodDescriptor.Builder}
+ * @return a new instance of a {@link Builder}
+ */
+ static ClientMethodDescriptor create(String serviceName,
+ String name,
+ MethodDescriptor.Builder, ?> descriptor) {
+ return builder(serviceName, name, descriptor).build();
+ }
+
+ /**
+ * Creates a new unary {@link Builder} with
+ * the specified name.
+ *
+ * @param serviceName the name of the owning gRPC service
+ * @param name the method name
+ * @return a new instance of a {@link Builder}
+ */
+ static Builder unary(String serviceName, String name) {
+ return builder(serviceName, name, MethodDescriptor.MethodType.UNARY);
+ }
+
+ /**
+ * Creates a new client Streaming {@link Builder} with
+ * the specified name.
+ *
+ * @param serviceName the name of the owning gRPC service
+ * @param name the method name
+ * @return a new instance of a {@link Builder}
+ */
+ static Builder clientStreaming(String serviceName, String name) {
+ return builder(serviceName, name, MethodDescriptor.MethodType.CLIENT_STREAMING);
+ }
+
+ /**
+ * Creates a new server streaming {@link Builder} with
+ * the specified name.
+ *
+ * @param serviceName the name of the owning gRPC service
+ * @param name the method name
+ * @return a new instance of a {@link Builder}
+ */
+ static Builder serverStreaming(String serviceName, String name) {
+ return builder(serviceName, name, MethodDescriptor.MethodType.SERVER_STREAMING);
+ }
+
+ /**
+ * Creates a new bidirectional {@link Builder} with
+ * the specified name.
+ *
+ * @param serviceName the name of the owning gRPC service
+ * @param name the method name
+ * @return a new instance of a {@link Builder}
+ */
+ static Builder bidirectional(String serviceName, String name) {
+ return builder(serviceName, name, MethodDescriptor.MethodType.BIDI_STREAMING);
+ }
+
+ /**
+ * Return the {@link CallCredentials} set on this service.
+ *
+ * @return the {@link CallCredentials} set on this service
+ */
+ public CallCredentials callCredentials() {
+ return this.callCredentials;
+ }
+
+ /**
+ * Creates a new {@link Builder} with the specified name.
+ *
+ * @param serviceName the name of the owning gRPC service
+ * @param name the method name
+ * @param methodType the gRPC method type
+ * @return a new instance of a {@link Builder}
+ */
+ static Builder builder(String serviceName,
+ String name,
+ MethodDescriptor.MethodType methodType) {
+
+ MethodDescriptor.Builder, ?> builder = MethodDescriptor.newBuilder()
+ .setFullMethodName(serviceName + "/" + name)
+ .setType(methodType);
+
+ return new Builder(serviceName, name, builder)
+ .requestType(Object.class)
+ .responseType(Object.class);
+ }
+
+ /**
+ * Returns the simple name of the method.
+ *
+ * @return The simple name of the method.
+ */
+ public String name() {
+ return name;
+ }
+
+ /**
+ * Returns the {@link MethodDescriptor} of this method.
+ *
+ * @param the request type
+ * @param the response type
+ * @return The {@link MethodDescriptor} of this method.
+ */
+ @SuppressWarnings("unchecked")
+ public MethodDescriptor descriptor() {
+ return (MethodDescriptor) descriptor;
+ }
+
+ /**
+ * Obtain the {@link ClientInterceptor}s to use for this method.
+ *
+ * @return the {@link ClientInterceptor}s to use for this method
+ */
+ WeightedBag interceptors() {
+ return interceptors.readOnly();
+ }
+
+ /**
+ * Obtain the {@link MethodHandler} to use to make client calls.
+ *
+ * @return the {@link MethodHandler} to use to make client calls
+ */
+ public MethodHandler, ?> methodHandler() {
+ return methodHandler;
+ }
+
+ /**
+ * ClientMethod configuration API.
+ */
+ public interface Rules {
+
+ /**
+ * Sets the type of parameter of this method.
+ *
+ * @param type The type of parameter of this method.
+ * @return this {@link Rules} instance for fluent call chaining
+ */
+ Rules requestType(Class> type);
+
+ /**
+ * Sets the type of parameter of this method.
+ *
+ * @param type The type of parameter of this method.
+ * @return this {@link Rules} instance for fluent call chaining
+ */
+ Rules responseType(Class> type);
+
+ /**
+ * Register one or more {@link ClientInterceptor interceptors} for the method.
+ *
+ * @param interceptors the interceptor(s) to register
+ * @return this {@link Rules} instance for fluent call chaining
+ */
+ Rules intercept(ClientInterceptor... interceptors);
+
+ /**
+ * Register one or more {@link ClientInterceptor interceptors} for the method.
+ *
+ * The added interceptors will be applied using the specified priority.
+ *
+ * @param priority the priority to assign to the interceptors
+ * @param interceptors one or more {@link ClientInterceptor}s to register
+ * @return this {@link Rules} to allow fluent method chaining
+ */
+ Rules intercept(int priority, ClientInterceptor... interceptors);
+
+ /**
+ * Register the {@link MarshallerSupplier} for the method.
+ *
+ * If not set the default {@link MarshallerSupplier} from the service will be used.
+ *
+ * @param marshallerSupplier the {@link MarshallerSupplier} for the service
+ * @return this {@link Rules} instance for fluent call chaining
+ */
+ Rules marshallerSupplier(MarshallerSupplier marshallerSupplier);
+
+ /**
+ * Register the specified {@link CallCredentials} to be used for this method. This overrides
+ * any {@link CallCredentials} set on the {@link ClientServiceDescriptor}.
+ *
+ * @param callCredentials the {@link CallCredentials} to set.
+ * @return this {@link Rules} instance for fluent call chaining
+ */
+ Rules callCredentials(CallCredentials callCredentials);
+
+ /**
+ * Set the {@link MethodHandler} that can be used to invoke the method.
+ *
+ * @param methodHandler the {@link MethodHandler} to use
+ * @return this {@link Rules} instance for fluent call chaining
+ */
+ Rules methodHandler(MethodHandler, ?> methodHandler);
+ }
+
+ /**
+ * {@link MethodDescriptor} builder implementation.
+ */
+ public static class Builder
+ implements Rules, io.helidon.common.Builder {
+
+ private String name;
+ private final MethodDescriptor.Builder, ?> descriptor;
+ private Class> requestType;
+ private Class> responseType;
+ private final WeightedBag interceptors = WeightedBag.create(InterceptorWeights.USER);
+ private MarshallerSupplier defaultMarshallerSupplier = MarshallerSupplier.create();
+ private MarshallerSupplier marshallerSupplier;
+ private CallCredentials callCredentials;
+ private MethodHandler, ?> methodHandler;
+
+ /**
+ * Constructs a new Builder instance.
+ *
+ * @param serviceName The name of the service ths method belongs to
+ * @param name the name of this method
+ * @param descriptor The gRPC method descriptor builder
+ */
+ Builder(String serviceName, String name, MethodDescriptor.Builder, ?> descriptor) {
+ this.name = name;
+ this.descriptor = descriptor.setFullMethodName(serviceName + "/" + name);
+ }
+
+ @Override
+ public Builder requestType(Class> type) {
+ this.requestType = type;
+ return this;
+ }
+
+ @Override
+ public Builder responseType(Class> type) {
+ this.responseType = type;
+ return this;
+ }
+
+ @Override
+ public Builder intercept(ClientInterceptor... interceptors) {
+ this.interceptors.addAll(Arrays.asList(interceptors));
+ return this;
+ }
+
+ @Override
+ public Builder intercept(int priority, ClientInterceptor... interceptors) {
+ this.interceptors.addAll(Arrays.asList(interceptors), priority);
+ return this;
+ }
+
+ @Override
+ public Builder marshallerSupplier(MarshallerSupplier supplier) {
+ this.marshallerSupplier = supplier;
+ return this;
+ }
+
+ Builder defaultMarshallerSupplier(MarshallerSupplier supplier) {
+ if (supplier == null) {
+ this.defaultMarshallerSupplier = MarshallerSupplier.create();
+ } else {
+ this.defaultMarshallerSupplier = supplier;
+ }
+ return this;
+ }
+
+ @Override
+ public Builder methodHandler(MethodHandler, ?> methodHandler) {
+ this.methodHandler = methodHandler;
+ return this;
+ }
+
+ /**
+ * Sets the full name of this Method.
+ *
+ * @param fullName the full name of the method
+ * @return this builder instance for fluent API
+ */
+ Builder fullName(String fullName) {
+ descriptor.setFullMethodName(fullName);
+ this.name = fullName.substring(fullName.lastIndexOf('/') + 1);
+ return this;
+ }
+
+ @Override
+ public Rules callCredentials(CallCredentials callCredentials) {
+ this.callCredentials = callCredentials;
+ return this;
+ }
+
+ /**
+ * Builds and returns a new instance of {@link ClientMethodDescriptor}.
+ *
+ * @return a new instance of {@link ClientMethodDescriptor}
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public ClientMethodDescriptor build() {
+ MarshallerSupplier supplier = this.marshallerSupplier;
+
+ if (supplier == null) {
+ supplier = defaultMarshallerSupplier;
+ }
+
+ if (requestType != null) {
+ descriptor.setRequestMarshaller((MethodDescriptor.Marshaller) supplier.get(requestType));
+ }
+
+ if (responseType != null) {
+ descriptor.setResponseMarshaller((MethodDescriptor.Marshaller) supplier.get(responseType));
+ }
+
+ return new ClientMethodDescriptor(name,
+ descriptor.build(),
+ interceptors,
+ callCredentials,
+ methodHandler);
+ }
+
+ }
+}
diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientProxy.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientProxy.java
new file mode 100644
index 00000000000..18883921d95
--- /dev/null
+++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientProxy.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2019, 2024 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.microprofile.grpc.client;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.util.Map;
+
+/**
+ * A dynamic proxy that forwards methods to gRPC call handlers.
+ */
+class ClientProxy implements InvocationHandler {
+
+ private final GrpcServiceClient client;
+
+ /**
+ * A map of Java method name to gRPR method name.
+ */
+ private final Map names;
+
+ /**
+ * Create a {@link ClientProxy}.
+ *
+ * @param client the {@link GrpcServiceClient} to use
+ * @param names a map of Java method names to gRPC method names
+ */
+ private ClientProxy(GrpcServiceClient client, Map names) {
+ this.client = client;
+ this.names = names;
+ }
+
+ /**
+ * Create a {@link ClientProxy} instance.
+ *
+ * @param client the {@link GrpcServiceClient} to use
+ * @param names a map of Java method names to gRPC method names
+ * @return a {@link ClientProxy} instance for the specified service client
+ */
+ static ClientProxy create(GrpcServiceClient client, Map names) {
+ return new ClientProxy(client, names);
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) {
+ return client.invoke(names.get(method.getName()), args);
+ }
+
+ /**
+ * Obtain the underlying {@link GrpcServiceClient}.
+ *
+ * @return the underlying {@link GrpcServiceClient}
+ */
+ public GrpcServiceClient getClient() {
+ return client;
+ }
+}
diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientRequestAttribute.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientRequestAttribute.java
new file mode 100644
index 00000000000..4e47dfd6b34
--- /dev/null
+++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientRequestAttribute.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2019, 2024 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.microprofile.grpc.client;
+
+/**
+ * An enum of possible gRPC client call attributes to attach to
+ * call tracing spans.
+ */
+public enum ClientRequestAttribute {
+ /**
+ * Add the method type to the tracing span.
+ */
+ METHOD_TYPE,
+
+ /**
+ * Add the method name to the tracing span.
+ */
+ METHOD_NAME,
+
+ /**
+ * Add the call deadline to the tracing span.
+ */
+ DEADLINE,
+
+ /**
+ * Add the compressor type to the tracing span.
+ */
+ COMPRESSOR,
+
+ /**
+ * Add the security authority to the tracing span.
+ */
+ AUTHORITY,
+
+ /**
+ * Add the method call options to the tracing span.
+ */
+ ALL_CALL_OPTIONS,
+
+ /**
+ * Add the method call headers to the tracing span.
+ */
+ HEADERS
+}
diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientServiceDescriptor.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientServiceDescriptor.java
new file mode 100644
index 00000000000..f31ed4a9754
--- /dev/null
+++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientServiceDescriptor.java
@@ -0,0 +1,655 @@
+/*
+ * Copyright (c) 2019, 2024 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.microprofile.grpc.client;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import io.helidon.grpc.core.InterceptorWeights;
+import io.helidon.grpc.core.MarshallerSupplier;
+import io.helidon.grpc.core.WeightedBag;
+
+import com.google.protobuf.DescriptorProtos;
+import com.google.protobuf.Descriptors;
+import io.grpc.BindableService;
+import io.grpc.CallCredentials;
+import io.grpc.ClientInterceptor;
+import io.grpc.MethodDescriptor.MethodType;
+import io.grpc.ServiceDescriptor;
+
+import static io.helidon.grpc.core.GrpcHelper.extractMethodName;
+
+/**
+ * Encapsulates all details about a client side gRPC service.
+ */
+public class ClientServiceDescriptor {
+
+ private final String serviceName;
+ private final Map methods;
+ private final WeightedBag interceptors;
+ private final CallCredentials callCredentials;
+
+ private ClientServiceDescriptor(String serviceName,
+ Map methods,
+ WeightedBag interceptors,
+ CallCredentials callCredentials) {
+ this.serviceName = serviceName;
+ this.methods = methods;
+ this.interceptors = interceptors;
+ this.callCredentials = callCredentials;
+ }
+
+ /**
+ * Create a {@link ClientServiceDescriptor} from a {@link ServiceDescriptor}.
+ *
+ * @param descriptor the {@link ServiceDescriptor}
+ * @return a {@link ClientServiceDescriptor}
+ */
+ public static ClientServiceDescriptor create(ServiceDescriptor descriptor) {
+ return builder(descriptor).build();
+ }
+
+ /**
+ * Create a {@link ClientServiceDescriptor} from a {@link BindableService}.
+ *
+ * @param service the BindableService
+ * @return a {@link ClientServiceDescriptor}
+ */
+ public static ClientServiceDescriptor create(BindableService service) {
+ return builder(service).build();
+ }
+
+ /**
+ * Create a {@link Builder} from a {@link ServiceDescriptor}.
+ *
+ * @param service the {@link ServiceDescriptor}
+ * @return a {@link Builder}
+ */
+ public static Builder builder(ServiceDescriptor service) {
+ return new Builder(service);
+ }
+
+ /**
+ * Create a {@link Builder} from a {@link BindableService}.
+ *
+ * @param service the {@link BindableService}
+ * @return a {@link Builder}
+ */
+ public static Builder builder(BindableService service) {
+ return new Builder(service);
+ }
+
+ /**
+ * Create a {@link Builder} form a name and type.
+ *
+ * The {@link Class#getSimpleName() class simple name} will be used for the service name.
+ *
+ * @param serviceClass the service class
+ * @return a {@link Builder}
+ */
+ public static Builder builder(Class> serviceClass) {
+ try {
+ Method method = serviceClass.getMethod("getServiceDescriptor");
+ if (method.getReturnType() == ServiceDescriptor.class) {
+ ServiceDescriptor svcDesc = (ServiceDescriptor) method.invoke(null);
+ return builder(svcDesc);
+ }
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException itEx) {
+ // Ignored.
+ }
+ return builder(serviceClass.getSimpleName(), serviceClass);
+ }
+
+ /**
+ * Create a {@link Builder} form a name and type.
+ *
+ * @param serviceName the getName of the service to use to initialise the builder
+ * @param serviceClass the service class
+ * @return a {@link Builder}
+ */
+ public static Builder builder(String serviceName, Class> serviceClass) {
+ return new Builder(serviceName, serviceClass);
+ }
+
+ /**
+ * Obtain the service name.
+ *
+ * @return the service name
+ */
+ public String name() {
+ return serviceName;
+ }
+
+ /**
+ * Return {@link ClientMethodDescriptor} for a specified method getName.
+ *
+ * @param name method getName
+ * @return method getDescriptor for the specified getName
+ */
+ public ClientMethodDescriptor method(String name) {
+ return methods.get(name);
+ }
+
+ /**
+ * Return the collections of methods that make up this service.
+ *
+ * @return service methods
+ */
+ public Collection methods() {
+ return Collections.unmodifiableCollection(methods.values());
+ }
+
+ /**
+ * Return service interceptors.
+ *
+ * @return service interceptors
+ */
+ public WeightedBag interceptors() {
+ return interceptors.readOnly();
+ }
+
+ /**
+ * Return the {@link CallCredentials} set on this service.
+ *
+ * @return the {@link CallCredentials} set on this service
+ */
+ public CallCredentials callCredentials() {
+ return this.callCredentials;
+ }
+
+ @Override
+ public String toString() {
+ return "ClientServiceDescriptor(name='" + serviceName + "')";
+ }
+
+ // ---- inner interface: Rules -----------------------------------------
+
+ /**
+ * Fluent configuration interface for the {@link ClientServiceDescriptor}.
+ */
+ public interface Rules {
+ /**
+ * Obtain the name fo the service this configuration configures.
+ *
+ * @return the name fo the service this configuration configures
+ */
+ String name();
+
+ /**
+ * Set the name for the service.
+ *
+ * @param name the name of service
+ * @return this {@link Rules} instance for fluent call chaining
+ * @throws NullPointerException if the getName is null
+ * @throws IllegalArgumentException if the getName is a blank String
+ */
+ Rules name(String name);
+
+ /**
+ * Register the proto file for the service.
+ *
+ * @param proto the service proto
+ * @return this {@link Rules} instance for fluent call chaining
+ */
+ Rules proto(Descriptors.FileDescriptor proto);
+
+ /**
+ * Register the {@link MarshallerSupplier} for the service.
+ *
+ * @param marshallerSupplier the {@link MarshallerSupplier} for the service
+ * @return this {@link Rules} instance for fluent call chaining
+ */
+ Rules marshallerSupplier(MarshallerSupplier marshallerSupplier);
+
+ /**
+ * Register one or more {@link ClientInterceptor interceptors} for the service.
+ *
+ * @param interceptors the interceptor(s) to register
+ * @return this {@link Rules} instance for fluent call chaining
+ */
+ Rules intercept(ClientInterceptor... interceptors);
+
+ /**
+ * Add one or more {@link ClientInterceptor} instances that will intercept calls
+ * to this service.
+ *
+ * The added interceptors will be applied using the specified priority.
+ *
+ * @param priority the priority to assign to the interceptors
+ * @param interceptors one or more {@link ClientInterceptor}s to add
+ * @return this builder to allow fluent method chaining
+ */
+ Rules intercept(int priority, ClientInterceptor... interceptors);
+
+ /**
+ * Register one or more {@link ClientInterceptor interceptors} for a named method of the service.
+ *
+ * @param methodName the name of the method to intercept
+ * @param interceptors the interceptor(s) to register
+ * @return this {@link Rules} instance for fluent call chaining
+ * @throws IllegalArgumentException if no method exists for the specified getName
+ */
+ Rules intercept(String methodName, ClientInterceptor... interceptors);
+
+ /**
+ * Register one or more {@link ClientInterceptor interceptors} for a named method of the service.
+ *
+ * The added interceptors will be applied using the specified priority.
+ *
+ * @param methodName the name of the method to intercept
+ * @param priority the priority to assign to the interceptors
+ * @param interceptors the interceptor(s) to register
+ * @return this {@link Rules} instance for fluent call chaining
+ *
+ * @throws IllegalArgumentException if no method exists for the specified name
+ */
+ Rules intercept(String methodName, int priority, ClientInterceptor... interceptors);
+
+ /**
+ * Register unary method for the service.
+ *
+ * @param name The getName of the method
+ * @return this {@link Rules} instance for fluent call chaining
+ */
+ Rules unary(String name);
+
+ /**
+ * Register unary method for the service.
+ *
+ * @param name the name of the method
+ * @param configurer the method configurer
+ * @return this {@link Rules} instance for fluent call chaining
+ */
+ Rules unary(String name, Consumer configurer);
+
+ /**
+ * Register server streaming method for the service.
+ *
+ * @param name The name of the method
+ * @return this {@link Rules} instance for fluent call chaining
+ */
+ Rules serverStreaming(String name);
+
+ /**
+ * Register server streaming method for the service.
+ *
+ * @param name the name of the method
+ * @param configurer the method configurer
+ * @return this {@link Rules} instance for fluent call chaining
+ */
+ Rules serverStreaming(String name, Consumer configurer);
+
+ /**
+ * Register client streaming method for the service.
+ *
+ * @param name The name of the method
+ * @return this {@link Rules} instance for fluent call chaining
+ */
+ Rules clientStreaming(String name);
+
+ /**
+ * Register client streaming method for the service.
+ *
+ * @param name the name of the method
+ * @param configurer the method configurer
+ * @return this {@link Rules} instance for fluent call chaining
+ */
+ Rules clientStreaming(String name, Consumer configurer);
+
+ /**
+ * Register bi-directional streaming method for the service.
+ *
+ * @param name The name of the method
+ * @return this {@link Rules} instance for fluent call chaining
+ */
+ Rules bidirectional(String name);
+
+ /**
+ * Register bi-directional streaming method for the service.
+ *
+ * @param name the name of the method
+ * @param configurer the method configurer
+ * @return this {@link Rules} instance for fluent call chaining
+ */
+ Rules bidirectional(String name, Consumer configurer);
+
+ /**
+ * Register the {@link CallCredentials} to be used for this service.
+ *
+ * @param callCredentials the {@link CallCredentials} to set.
+ * @return this {@link Rules} instance for fluent call chaining
+ */
+ Rules callCredentials(CallCredentials callCredentials);
+
+ /**
+ * Register the {@link CallCredentials} to be used for the specified method in this service. This overrides
+ * any {@link CallCredentials} set on this {@link ClientServiceDescriptor}
+ *
+ * @param name the method name
+ * @param callCredentials the {@link CallCredentials} to set.
+ * @return this {@link Rules} instance for fluent call chaining
+ */
+ Rules callCredentials(String name, CallCredentials callCredentials);
+
+ }
+
+ // ---- inner class: BaseBuilder --------------------------------------------
+
+ /**
+ * A {@link ClientServiceDescriptor} builder.
+ */
+ public static final class Builder implements Rules, io.helidon.common.Builder {
+
+ private String name;
+ private final WeightedBag interceptors = WeightedBag.create(InterceptorWeights.USER);
+ private final Class> serviceClass;
+ private Descriptors.FileDescriptor proto;
+ private MarshallerSupplier marshallerSupplier = MarshallerSupplier.create();
+ private CallCredentials callCredentials;
+
+ private final Map methodBuilders = new HashMap<>();
+
+ /**
+ * Builds the ClientService from a {@link BindableService}.
+ *
+ * @param service the {@link BindableService} to use to initialize the builder
+ */
+ private Builder(BindableService service) {
+ this(service.bindService().getServiceDescriptor());
+ }
+
+ /**
+ * Builds the ClientService from a {@link BindableService}.
+ *
+ * @param serviceDescriptor the {@link ServiceDescriptor} to use to initialize the builder
+ */
+ private Builder(ServiceDescriptor serviceDescriptor) {
+ this.name = serviceDescriptor.getName();
+ this.serviceClass = serviceDescriptor.getClass();
+
+ for (io.grpc.MethodDescriptor, ?> md : serviceDescriptor.getMethods()) {
+ String methodName = extractMethodName(md.getFullMethodName());
+
+ methodBuilders.put(methodName, ClientMethodDescriptor.builder(this.name, methodName, md.toBuilder()));
+ }
+ }
+
+ /**
+ * Create a new {@link Builder}.
+ *
+ * @param name the service name
+ * @param serviceClass the service class
+ */
+ private Builder(String name, Class> serviceClass) {
+ this.name = name;
+ this.serviceClass = serviceClass;
+ }
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public Builder name(String serviceName) {
+ if (serviceName == null) {
+ throw new NullPointerException("Service getName cannot be null");
+ }
+
+ if (serviceName.trim().isEmpty()) {
+ throw new IllegalArgumentException("Service getName cannot be blank");
+ }
+
+ this.name = serviceName.trim();
+ for (Map.Entry e : methodBuilders.entrySet()) {
+ e.getValue().fullName(io.grpc.MethodDescriptor.generateFullMethodName(this.name, e.getKey()));
+ }
+ return this;
+ }
+
+ @Override
+ public Builder proto(Descriptors.FileDescriptor proto) {
+ this.proto = proto;
+ return this;
+ }
+
+ @Override
+ public Builder marshallerSupplier(MarshallerSupplier marshallerSupplier) {
+ this.marshallerSupplier = marshallerSupplier;
+ return this;
+ }
+
+ @Override
+ public Builder unary(String name) {
+ return unary(name, null);
+ }
+
+ @Override
+ public Builder unary(String name, Consumer configurer) {
+ methodBuilders.put(name, createMethodDescriptor(name, MethodType.UNARY, configurer));
+ return this;
+ }
+
+ @Override
+ public Builder serverStreaming(String name) {
+ return serverStreaming(name, null);
+ }
+
+ @Override
+ public Builder serverStreaming(String name,
+ Consumer configurer) {
+ methodBuilders.put(name, createMethodDescriptor(name, MethodType.SERVER_STREAMING, configurer));
+ return this;
+ }
+
+ @Override
+ public Builder clientStreaming(String name) {
+ return clientStreaming(name, null);
+ }
+
+ @Override
+ public Builder clientStreaming(String name,
+ Consumer configurer) {
+ methodBuilders.put(name, createMethodDescriptor(name, MethodType.CLIENT_STREAMING, configurer));
+ return this;
+ }
+
+ @Override
+ public Builder bidirectional(String name) {
+ return bidirectional(name, null);
+ }
+
+ @Override
+ public Builder bidirectional(String name,
+ Consumer configurer) {
+ methodBuilders.put(name, createMethodDescriptor(name, MethodType.BIDI_STREAMING, configurer));
+ return this;
+ }
+
+ @Override
+ public Builder intercept(ClientInterceptor... interceptors) {
+ this.interceptors.addAll(Arrays.asList(interceptors));
+ return this;
+ }
+
+ @Override
+ public Rules intercept(int priority, ClientInterceptor... interceptors) {
+ this.interceptors.addAll(Arrays.asList(interceptors), priority);
+ return this;
+ }
+
+ @Override
+ public Builder intercept(String methodName, ClientInterceptor... interceptors) {
+ ClientMethodDescriptor.Builder method = methodBuilders.get(methodName);
+
+ if (method == null) {
+ throw new IllegalArgumentException("No method exists with getName '" + methodName + "'");
+ }
+
+ method.intercept(interceptors);
+
+ return this;
+ }
+
+ @Override
+ public Builder intercept(String methodName, int priority, ClientInterceptor... interceptors) {
+ ClientMethodDescriptor.Builder method = methodBuilders.get(methodName);
+
+ if (method == null) {
+ throw new IllegalArgumentException("No method exists with getName '" + methodName + "'");
+ }
+
+ method.intercept(priority, interceptors);
+
+ return this;
+ }
+
+ @Override
+ public Builder callCredentials(CallCredentials callCredentials) {
+ this.callCredentials = callCredentials;
+ return this;
+ }
+
+ @Override
+ public Builder callCredentials(String methodName, CallCredentials callCredentials) {
+ ClientMethodDescriptor.Builder method = methodBuilders.get(methodName);
+
+ if (method == null) {
+ throw new IllegalArgumentException("No method exists with getName '" + methodName + "'");
+ }
+
+ method.callCredentials(callCredentials);
+ return this;
+ }
+
+ @Override
+ public ClientServiceDescriptor build() {
+ Map methods = new LinkedHashMap<>();
+ for (Map.Entry entry : methodBuilders.entrySet()) {
+ methods.put(entry.getKey(), entry.getValue().build());
+ }
+
+ return new ClientServiceDescriptor(name, methods, interceptors, callCredentials);
+ }
+
+ // ---- helpers -----------------------------------------------------
+
+ private ClientMethodDescriptor.Builder createMethodDescriptor(
+ String methodName,
+ MethodType methodType,
+ Consumer configurer) {
+
+ io.grpc.MethodDescriptor.Builder, ?> grpcDesc = io.grpc.MethodDescriptor.newBuilder()
+ .setFullMethodName(io.grpc.MethodDescriptor.generateFullMethodName(this.name, methodName))
+ .setType(methodType)
+ .setSampledToLocalTracing(true);
+
+ Class> requestType = getTypeFromMethodDescriptor(methodName, true);
+ Class> responseType = getTypeFromMethodDescriptor(methodName, false);
+
+ ClientMethodDescriptor.Builder builder = ClientMethodDescriptor.builder(this.name, methodName, grpcDesc)
+ .defaultMarshallerSupplier(this.marshallerSupplier)
+ .requestType(requestType)
+ .responseType(responseType);
+
+ if (configurer != null) {
+ configurer.accept(builder);
+ }
+
+ return builder;
+ }
+
+ private Class> getTypeFromMethodDescriptor(String methodName, boolean fInput) {
+
+ // if the proto is not present, assume that we are not using
+ // protobuf for marshalling and that whichever marshaller is used
+ // doesn't need type information (basically, that the serialized
+ // stream is self-describing)
+ if (proto == null) {
+ return Object.class;
+ }
+
+ // todo: add error handling here, and fail fast with a more
+ // todo: meaningful exception (and message) than a NPE
+ // todo: if the service or the method cannot be found
+ Descriptors.ServiceDescriptor svc = proto.findServiceByName(name);
+ Descriptors.MethodDescriptor mtd = svc.findMethodByName(methodName);
+ Descriptors.Descriptor type = fInput ? mtd.getInputType() : mtd.getOutputType();
+
+ String pkg = getPackageName();
+ String outerClass = getOuterClassName();
+
+ // make sure that any nested protobuf class names are converted
+ // into a proper Java binary class getName
+ String className = pkg + "." + outerClass + type.getFullName().replace('.', '$');
+
+ // the assumption here is that the protobuf generated classes can always
+ // be loaded by the same class loader that loaded the service class,
+ // as the service implementation is bound to depend on them
+ try {
+ return serviceClass != null
+ ? serviceClass.getClassLoader().loadClass(className)
+ : this.getClass().getClassLoader().loadClass(className);
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private String getPackageName() {
+ String pkg = proto.getOptions().getJavaPackage();
+ return "".equals(pkg) ? proto.getPackage() : pkg;
+ }
+
+ private String getOuterClassName() {
+ DescriptorProtos.FileOptions options = proto.getOptions();
+ if (options.getJavaMultipleFiles()) {
+ // there is no outer class -- each message will have its own top-level class
+ return "";
+ }
+
+ String outerClass = options.getJavaOuterClassname();
+ if ("".equals(outerClass)) {
+ outerClass = getOuterClassFromFileName(proto.getName());
+ }
+
+ // append $ in order to timed a proper binary getName for the nested message class
+ return outerClass + "$";
+ }
+
+ private String getOuterClassFromFileName(String name) {
+ // strip .proto extension
+ name = name.substring(0, name.lastIndexOf(".proto"));
+
+ String[] words = name.split("_");
+ StringBuilder sb = new StringBuilder(name.length());
+
+ for (String word : words) {
+ sb.append(Character.toUpperCase(word.charAt(0)))
+ .append(word.substring(1));
+ }
+
+ return sb.toString();
+ }
+ }
+}
diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/DelegatingBeanAttributes.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/DelegatingBeanAttributes.java
new file mode 100644
index 00000000000..04333d9fd81
--- /dev/null
+++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/DelegatingBeanAttributes.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2019, 2024 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.microprofile.grpc.client;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Set;
+
+import jakarta.enterprise.inject.spi.BeanAttributes;
+
+/**
+ * A {@link jakarta.enterprise.inject.spi.BeanAttributes} implementation.
+ *
+ * @param the class of the bean instance
+ */
+class DelegatingBeanAttributes implements BeanAttributes {
+
+ private final BeanAttributes> delegate;
+ private final Set types;
+
+ /**
+ * Create a {@link DelegatingBeanAttributes}.
+ *
+ * @param delegate the {@link jakarta.enterprise.inject.spi.BeanAttributes} to delegate to
+ * @param types the {@link java.lang.reflect.Type}s for this bean
+ */
+ private DelegatingBeanAttributes(BeanAttributes> delegate, Set types) {
+ super();
+ Objects.requireNonNull(delegate);
+ this.delegate = delegate;
+ this.types = Collections.unmodifiableSet(types);
+ }
+
+ /**
+ * Create a {@link DelegatingBeanAttributes}.
+ *
+ * @param delegate the {@link jakarta.enterprise.inject.spi.BeanAttributes} to delegate to
+ * @param types the {@link java.lang.reflect.Type}s for this bean
+ */
+ static DelegatingBeanAttributes create(BeanAttributes> delegate, Set types) {
+ return new DelegatingBeanAttributes<>(delegate, types);
+ }
+
+ @Override
+ public String getName() {
+ return this.delegate.getName();
+ }
+
+ @Override
+ public Set getQualifiers() {
+ return this.delegate.getQualifiers();
+ }
+
+ @Override
+ public Class extends Annotation> getScope() {
+ return this.delegate.getScope();
+ }
+
+ @Override
+ public Set> getStereotypes() {
+ return this.delegate.getStereotypes();
+ }
+
+ @Override
+ public Set getTypes() {
+ if (types == null || types.isEmpty()) {
+ return this.delegate.getTypes();
+ } else {
+ return types;
+ }
+ }
+
+ @Override
+ public boolean isAlternative() {
+ return this.delegate.isAlternative();
+ }
+
+ @Override
+ public String toString() {
+ return this.delegate.toString();
+ }
+}
diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelDescriptorBlueprint.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelDescriptorBlueprint.java
new file mode 100644
index 00000000000..54e7b8da1a9
--- /dev/null
+++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelDescriptorBlueprint.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.microprofile.grpc.client;
+
+import java.util.Optional;
+
+import io.helidon.builder.api.Option;
+import io.helidon.builder.api.Prototype;
+import io.helidon.common.tls.Tls;
+
+@Prototype.Blueprint
+@Prototype.Configured
+interface GrpcChannelDescriptorBlueprint {
+
+ /**
+ * The name of this channel.
+ *
+ * @return channel name
+ */
+ @Option.Configured
+ String name();
+
+ /**
+ * The host to connect to.
+ *
+ * @return the host
+ */
+ @Option.Configured
+ @Option.Default(GrpcChannelsProvider.DEFAULT_HOST)
+ String host();
+
+ /**
+ * The port to connect to.
+ *
+ * @return the port
+ */
+ @Option.Configured
+ @Option.DefaultInt(GrpcChannelsProvider.DEFAULT_PORT)
+ int port();
+
+ /**
+ * The target URI.
+ *
+ * @return the URI
+ */
+ @Option.Configured
+ Optional target();
+
+ /**
+ * TLS configuration for the connection.
+ *
+ * @return the TLS config
+ */
+ @Option.Configured
+ Optional tls();
+}
diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsDescriptorBlueprint.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsDescriptorBlueprint.java
new file mode 100644
index 00000000000..c6932921197
--- /dev/null
+++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsDescriptorBlueprint.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.microprofile.grpc.client;
+
+import java.util.List;
+
+import io.helidon.builder.api.Option;
+import io.helidon.builder.api.Prototype;
+
+@Prototype.Blueprint
+@Prototype.Configured
+interface GrpcChannelsDescriptorBlueprint {
+
+ /**
+ * List of channel descriptors.
+ *
+ * @return channel descriptors
+ */
+ @Option.Configured
+ List channels();
+}
diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsProvider.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsProvider.java
new file mode 100644
index 00000000000..aacbc1f25d6
--- /dev/null
+++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsProvider.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (c) 2019, 2024 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.microprofile.grpc.client;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.helidon.common.tls.Tls;
+import io.helidon.config.Config;
+import io.helidon.webclient.grpc.GrpcClient;
+
+import io.grpc.Channel;
+
+/**
+ * GrpcChannelsProvider is a factory for pre-configured gRPC Channel instances.
+ */
+public class GrpcChannelsProvider {
+
+ /**
+ * A constant for holding the default channel configuration name (which is "default").
+ */
+ public static final String DEFAULT_CHANNEL_NAME = "default";
+
+ /**
+ * A constant for holding the default host name (which is "localhost").
+ */
+ public static final String DEFAULT_HOST = "localhost";
+
+ /**
+ * A constant for holding the default port (which is "1408").
+ */
+ public static final int DEFAULT_PORT = 1408;
+
+ private final Map channelConfigs;
+
+ private GrpcChannelsProvider(Map channelDescriptors) {
+ this.channelConfigs = new HashMap<>(channelDescriptors);
+ }
+
+ /**
+ * Builds a new instance of {@link GrpcChannelsProvider} using default configuration. The
+ * default configuration connects to "localhost:1408" without TLS.
+ *
+ * @return a new instance of {@link GrpcChannelsProvider}
+ */
+ public static GrpcChannelsProvider create() {
+ return GrpcChannelsProvider.builder().build();
+ }
+
+ /**
+ * Creates a {@link GrpcChannelsProvider} using the specified configuration.
+ *
+ * @param config The externalized configuration.
+ * @return a new instance of {@link GrpcChannelsProvider}
+ */
+ public static GrpcChannelsProvider create(Config config) {
+ return new Builder(config).build();
+ }
+
+ /**
+ * Create a new {@link Builder}.
+ *
+ * @return a new {@link Builder}
+ */
+ public static Builder builder() {
+ return builder(null);
+ }
+
+ /**
+ * Create a new {@link Builder}.
+ *
+ * @param config the {@link Config} to bootstrap from
+ * @return a new {@link Builder}
+ */
+ public static Builder builder(Config config) {
+ return new Builder(config);
+ }
+
+ /**
+ * Returns a {@link Channel} for the specified channel or host name.
+ *
+ * If the specified channel name does not exist in the configuration, we will assume
+ * that it represents the name of the gRPC host to connect to and will create a plain text
+ * channel to the host with the specified {@code name}, on a default port (1408).
+ *
+ * @param name the name of the channel configuration as specified in the configuration file,
+ * or the name of the host to connect to
+ * @return a new instance of {@link Channel}
+ * @throws NullPointerException if name is null
+ * @throws IllegalArgumentException if name is empty
+ */
+ public Channel channel(String name) {
+ if (name == null) {
+ throw new NullPointerException("name cannot be null.");
+ }
+ if (name.trim().isEmpty()) {
+ throw new IllegalArgumentException("name cannot be empty or blank.");
+ }
+ GrpcChannelDescriptor chCfg = channelConfigs.computeIfAbsent(name, hostName ->
+ GrpcChannelDescriptor.builder().name(name).host(name).build());
+ return createChannel(name, chCfg);
+ }
+
+ /**
+ * Creates a channel from a channel descriptor.
+ *
+ * @param name the channel name
+ * @param descriptor the channel descriptor
+ * @return the channel
+ */
+ private Channel createChannel(String name, GrpcChannelDescriptor descriptor) {
+ Tls clientTls = descriptor.tls().orElse(null);
+ if (clientTls == null) {
+ throw new IllegalArgumentException("Client TLS must be configured for gRPC proxy client");
+ }
+ int port = descriptor.port();
+ GrpcClient grpcClient = GrpcClient.builder()
+ .tls(clientTls)
+ .baseUri("https://" + descriptor.host() + ":" + port)
+ .build();
+ return grpcClient.channel();
+ }
+
+ /**
+ * Builder builds an instance of {@link GrpcChannelsProvider}.
+ */
+ public static class Builder implements io.helidon.common.Builder {
+
+ private final Map channelConfigs = new HashMap<>();
+
+ private Builder(Config config) {
+ // Add the default channel (which can be overridden in the config)
+ channel(DEFAULT_CHANNEL_NAME, GrpcChannelDescriptor.builder().name(DEFAULT_CHANNEL_NAME).build());
+
+ if (config == null) {
+ return;
+ }
+
+ GrpcChannelsDescriptor channelsDescriptor = GrpcChannelsDescriptor.builder().config(config).build();
+ channelsDescriptor.channels().forEach(ch -> channelConfigs.put(ch.name(), ch));
+ }
+
+ /**
+ * Add or replace the specified {@link GrpcChannelDescriptor}.
+ *
+ * @param name the name of the configuration
+ * @param descriptor the {@link GrpcChannelDescriptor} to be added
+ * @return this Builder instance
+ */
+ public Builder channel(String name, GrpcChannelDescriptor descriptor) {
+ channelConfigs.put(name, descriptor);
+ return this;
+ }
+
+ /**
+ * Create a new instance of {@link GrpcChannelsProvider} from this Builder.
+ *
+ * @return a new instance of {@link GrpcChannelsProvider}
+ */
+ public GrpcChannelsProvider build() {
+ return new GrpcChannelsProvider(channelConfigs);
+ }
+ }
+}
diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcClientBuilder.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcClientBuilder.java
new file mode 100644
index 00000000000..b391de0116a
--- /dev/null
+++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcClientBuilder.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (c) 2019, 2024 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.microprofile.grpc.client;
+
+import java.util.Objects;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import io.helidon.common.Builder;
+import io.helidon.grpc.api.Grpc;
+import io.helidon.grpc.core.MethodHandler;
+import io.helidon.microprofile.grpc.core.AbstractServiceBuilder;
+import io.helidon.microprofile.grpc.core.AnnotatedMethod;
+import io.helidon.microprofile.grpc.core.AnnotatedMethodList;
+import io.helidon.microprofile.grpc.core.InstanceSupplier;
+import io.helidon.microprofile.grpc.core.ModelHelper;
+
+import static java.lang.System.Logger.Level;
+
+/**
+ * A builder for constructing a {@link io.helidon.microprofile.grpc.client.ClientServiceDescriptor} instances
+ * from an annotated POJO.
+ */
+class GrpcClientBuilder extends AbstractServiceBuilder
+ implements Builder {
+
+ private static final System.Logger LOGGER = System.getLogger(GrpcClientBuilder.class.getName());
+
+ /**
+ * Create a {@link GrpcClientBuilder} for a given gRPC service class.
+ *
+ * @param serviceClass gRPC service (handler) class.
+ * @param instance the target instance to call gRPC handler methods on
+ * @throws NullPointerException if the service or instance parameters are null
+ */
+ private GrpcClientBuilder(Class> serviceClass, Supplier> instance) {
+ super(serviceClass, instance);
+ }
+
+ /**
+ * Create a {@link GrpcClientBuilder} for a given gRPC service.
+ *
+ * @param service the service to call gRPC handler methods on
+ * @throws NullPointerException if the service is null
+ * @return a {@link GrpcClientBuilder}
+ */
+ static GrpcClientBuilder create(Object service) {
+ return new GrpcClientBuilder(service.getClass(), InstanceSupplier.singleton(service));
+ }
+
+ /**
+ * Create a {@link GrpcClientBuilder} for a given gRPC service class.
+ *
+ * @param serviceClass gRPC service (handler) class.
+ * @throws NullPointerException if the service class is null
+ * @return a {@link GrpcClientBuilder}
+ */
+ static GrpcClientBuilder create(Class> serviceClass) {
+ return new GrpcClientBuilder(Objects.requireNonNull(serviceClass), createInstanceSupplier(serviceClass));
+ }
+
+ /**
+ * Create a new resource model builder for the introspected class.
+ *
+ * The model returned is filled with the introspected data.
+ *
+ *
+ * @return new resource model builder for the introspected class.
+ */
+ @Override
+ public ClientServiceDescriptor.Builder build() {
+ checkForNonPublicMethodIssues();
+
+ Class> annotatedServiceClass = annotatedServiceClass();
+ AnnotatedMethodList methodList = AnnotatedMethodList.create(annotatedServiceClass);
+ String name = determineServiceName(annotatedServiceClass);
+
+ ClientServiceDescriptor.Builder builder = ClientServiceDescriptor.builder(serviceClass())
+ .name(name)
+ .marshallerSupplier(getMarshallerSupplier());
+
+ addServiceMethods(builder, methodList);
+
+ LOGGER.log(Level.DEBUG, () -> String.format("A new gRPC service was created by ServiceModeller: %s", builder));
+
+ return builder;
+ }
+
+ /**
+ * Add methods to the {@link io.helidon.microprofile.grpc.client.ClientServiceDescriptor.Builder}.
+ *
+ * @param builder the {@link io.helidon.microprofile.grpc.client.ClientServiceDescriptor.Builder} to add the method to
+ * @param methodList the list of methods to add
+ */
+ private void addServiceMethods(ClientServiceDescriptor.Builder builder, AnnotatedMethodList methodList) {
+ for (AnnotatedMethod am : methodList.withAnnotation(Grpc.GrpcMethod.class)) {
+ addServiceMethod(builder, am);
+ }
+ for (AnnotatedMethod am : methodList.withMetaAnnotation(Grpc.GrpcMethod.class)) {
+ addServiceMethod(builder, am);
+ }
+ }
+
+ /**
+ * Add a method to the {@link io.helidon.microprofile.grpc.client.ClientServiceDescriptor.Builder}.
+ *
+ * The method configuration will be determined by the annotations present on the
+ * method and the method signature.
+ *
+ * @param builder the {@link io.helidon.microprofile.grpc.client.ClientServiceDescriptor.Builder} to add the method to
+ * @param method the {@link io.helidon.microprofile.grpc.core.AnnotatedMethod} representing the method to add
+ */
+ private void addServiceMethod(ClientServiceDescriptor.Builder builder, AnnotatedMethod method) {
+ Grpc.GrpcMethod annotation = method.firstAnnotationOrMetaAnnotation(Grpc.GrpcMethod.class);
+ String name = determineMethodName(method, annotation);
+
+ MethodHandler handler = handlerSuppliers().stream()
+ .filter(supplier -> supplier.supplies(method))
+ .findFirst()
+ .map(supplier -> supplier.get(name, method, instanceSupplier()))
+ .orElseThrow(() -> new IllegalArgumentException("Cannot locate a method handler supplier for method " + method));
+
+ Class> requestType = handler.getRequestType();
+ Class> responseType = handler.getResponseType();
+ AnnotatedMethodConfigurer configurer = new AnnotatedMethodConfigurer(method, requestType, responseType, handler);
+
+ switch (annotation.value()) {
+ case UNARY:
+ builder.unary(name, configurer);
+ break;
+ case CLIENT_STREAMING:
+ builder.clientStreaming(name, configurer);
+ break;
+ case SERVER_STREAMING:
+ builder.serverStreaming(name, configurer);
+ break;
+ case BIDI_STREAMING:
+ builder.bidirectional(name, configurer);
+ break;
+ case UNKNOWN:
+ default:
+ LOGGER.log(Level.ERROR, () -> "Unrecognized method type " + annotation.value());
+ }
+ }
+
+ /**
+ * A {@link java.util.function.Consumer} of {@link io.helidon.microprofile.grpc.client.ClientMethodDescriptor.Rules}
+ * that applies configuration changes based on annotations present on the gRPC
+ * method.
+ */
+ private static class AnnotatedMethodConfigurer
+ implements Consumer {
+
+ private final AnnotatedMethod method;
+ private final Class> requestType;
+ private final Class> responseType;
+ private final MethodHandler methodHandler;
+
+ private AnnotatedMethodConfigurer(AnnotatedMethod method,
+ Class> requestType,
+ Class> responseType,
+ MethodHandler methodHandler) {
+ this.method = method;
+ this.requestType = requestType;
+ this.responseType = responseType;
+ this.methodHandler = methodHandler;
+ }
+
+ @Override
+ public void accept(ClientMethodDescriptor.Rules config) {
+ config.requestType(requestType)
+ .responseType(responseType)
+ .methodHandler(methodHandler);
+
+ if (method.isAnnotationPresent(Grpc.GrpcMarshaller.class)) {
+ config.marshallerSupplier(ModelHelper.getMarshallerSupplier(
+ method.getAnnotation(Grpc.GrpcMarshaller.class)));
+ }
+ }
+ }
+}
diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcClientCdiExtension.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcClientCdiExtension.java
new file mode 100644
index 00000000000..d0ec1033a71
--- /dev/null
+++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcClientCdiExtension.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2019, 2024 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.microprofile.grpc.client;
+
+import java.lang.reflect.Type;
+import java.util.HashSet;
+import java.util.Set;
+
+import io.helidon.grpc.api.Grpc;
+
+import jakarta.enterprise.event.Observes;
+import jakarta.enterprise.inject.spi.AfterBeanDiscovery;
+import jakarta.enterprise.inject.spi.Annotated;
+import jakarta.enterprise.inject.spi.AnnotatedMethod;
+import jakarta.enterprise.inject.spi.AnnotatedType;
+import jakarta.enterprise.inject.spi.BeanAttributes;
+import jakarta.enterprise.inject.spi.BeanManager;
+import jakarta.enterprise.inject.spi.BeforeBeanDiscovery;
+import jakarta.enterprise.inject.spi.Extension;
+import jakarta.enterprise.inject.spi.ProcessInjectionPoint;
+import jakarta.enterprise.inject.spi.ProducerFactory;
+
+/**
+ * A CDI extension to add gRPC client functionality.
+ */
+public class GrpcClientCdiExtension implements Extension {
+
+ private final Set proxyTypes = new HashSet<>();
+
+ /**
+ * Adds beans to the bean manager.
+ *
+ * @param event before bean discovery event
+ */
+ public void addBeans(@Observes BeforeBeanDiscovery event) {
+ event.addAnnotatedType(ChannelProducer.class, ChannelProducer.class.getName());
+ }
+
+ /**
+ * Process injection points.
+ *
+ * In this method injection points that have the {@link io.helidon.grpc.api.Grpc.GrpcProxy} are processed
+ * and their types are stored so that in the {@link #afterBean(
+ *jakarta.enterprise.inject.spi.AfterBeanDiscovery, jakarta.enterprise.inject.spi.BeanManager)}
+ * we can manually create a producer for the correct service proxy type.
+ *
+ * @param pip the injection point
+ * @param the declared type of the injection point.
+ * @param the bean class of the bean that declares the injection point
+ */
+ public void gatherApplications(@Observes ProcessInjectionPoint pip) {
+ Annotated annotated = pip.getInjectionPoint().getAnnotated();
+ if (annotated.isAnnotationPresent(Grpc.GrpcProxy.class)) {
+ Type type = pip.getInjectionPoint().getType();
+ proxyTypes.add(type);
+ }
+ }
+
+ /**
+ * Process the previously captured {@link io.helidon.grpc.api.Grpc.GrpcProxy} injection points.
+ *
+ * For each {@link io.helidon.grpc.api.Grpc.GrpcProxy} injection point we create a producer bean
+ * for the required type.
+ *
+ * @param event the {@link jakarta.enterprise.inject.spi.AfterBeanDiscovery} event
+ * @param beanManager the CDI bean manager
+ */
+ public void afterBean(@Observes AfterBeanDiscovery event, BeanManager beanManager) {
+ AnnotatedType producerType = beanManager.createAnnotatedType(GrpcProxyProducer.class);
+ AnnotatedMethod super GrpcProxyProducer> producerMethod = producerType.getMethods()
+ .stream()
+ .filter(m -> m.isAnnotationPresent(Grpc.GrpcProxy.class))
+ .filter(m -> m.isAnnotationPresent(Grpc.GrpcChannel.class))
+ .findFirst()
+ .orElse(null);
+ if (producerMethod != null) {
+ for (Type type : proxyTypes) {
+ addProducerBean(event, beanManager, producerMethod, type);
+ }
+ }
+ }
+
+ private void addProducerBean(AfterBeanDiscovery event,
+ BeanManager beanManager,
+ AnnotatedMethod super GrpcProxyProducer> producerMethod,
+ Type type) {
+ BeanAttributes> producerAttributes = beanManager.createBeanAttributes(producerMethod);
+ ProducerFactory factory = beanManager.getProducerFactory(producerMethod, null);
+ Set types = Set.of(Object.class, type);
+ BeanAttributes> beanAttributes = DelegatingBeanAttributes.create(producerAttributes, types);
+ event.addBean(beanManager.createBean(beanAttributes, GrpcProxyProducer.class, factory));
+ }
+}
diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcConfigurablePort.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcConfigurablePort.java
new file mode 100644
index 00000000000..9742daf7bac
--- /dev/null
+++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcConfigurablePort.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.helidon.microprofile.grpc.client;
+
+/**
+ * Interface implemented by all gRPC client proxies. The method {@link #channelPort} can be
+ * called at runtime to override the client URI port from config. Typically used for testing.
+ */
+public interface GrpcConfigurablePort {
+
+ /**
+ * Name of single setter method on this interface.
+ */
+ String CHANNEL_PORT = "channelPort";
+
+ /**
+ * Overrides client URI port.
+ *
+ * @param value the new port value
+ */
+ void channelPort(int value);
+}
diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyBuilder.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyBuilder.java
new file mode 100644
index 00000000000..82d9bea7f00
--- /dev/null
+++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyBuilder.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2019, 2024 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.microprofile.grpc.client;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import io.helidon.common.Builder;
+
+import io.grpc.Channel;
+
+/**
+ * A builder for gRPC clients dynamic proxies.
+ *
+ * @param the type of the interface to be proxied
+ */
+public class GrpcProxyBuilder implements Builder, T> {
+
+ private static final Map, ClientServiceDescriptor> DESCRIPTORS = new ConcurrentHashMap<>();
+
+ private final GrpcServiceClient client;
+
+ private final Class type;
+
+ private GrpcProxyBuilder(GrpcServiceClient client, Class type) {
+ this.client = client;
+ this.type = type;
+ }
+
+ /**
+ * Create a {@code GrpcProxyBuilder} that can build gRPC dynamic proxies
+ * for a given gRPC service interface.
+ *
+ * The class passed to this method should be properly annotated with
+ * {@link io.helidon.grpc.api.Grpc.GrpcService} and
+ * {@link io.helidon.grpc.api.Grpc.GrpcMethod} annotations
+ * so that the proxy can properly route calls to the server.
+ *
+ * @param channel the {@link io.grpc.Channel} to connect to the server
+ * @param type the service type
+ * @param the service type
+ * @return a {@link io.helidon.microprofile.grpc.client.GrpcProxyBuilder} that can build dynamic proxies
+ * for the gRPC service
+ */
+ public static GrpcProxyBuilder create(Channel channel, Class type) {
+ ClientServiceDescriptor descriptor = DESCRIPTORS.computeIfAbsent(type, GrpcProxyBuilder::createDescriptor);
+ return new GrpcProxyBuilder<>(GrpcServiceClient.builder(channel, descriptor).build(), type);
+ }
+
+ /**
+ * Build a gRPC client dynamic proxy of the required type.
+ *
+ * @return a gRPC client dynamic proxy
+ */
+ @Override
+ public T build() {
+ return client.proxy(type, GrpcConfigurablePort.class);
+ }
+
+ private static ClientServiceDescriptor createDescriptor(Class> type) {
+ GrpcClientBuilder builder = GrpcClientBuilder.create(type);
+ ClientServiceDescriptor.Builder descriptorBuilder = builder.build();
+ return descriptorBuilder.build();
+ }
+}
diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyProducer.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyProducer.java
new file mode 100644
index 00000000000..20fc3f9d5fa
--- /dev/null
+++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyProducer.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2019, 2024 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.microprofile.grpc.client;
+
+import io.helidon.grpc.api.Grpc;
+import io.helidon.microprofile.grpc.core.ModelHelper;
+
+import io.grpc.Channel;
+import jakarta.enterprise.inject.spi.InjectionPoint;
+
+/**
+ * A utility class of gRPC CDI producer stubs.
+ *
+ * The methods in this class are not real CDI producer methods,
+ * they act as templates that the {@link io.helidon.microprofile.grpc.client.GrpcClientCdiExtension}
+ * will use to create producers on the fly as injection points
+ * are observed.
+ */
+class GrpcProxyProducer {
+
+ private GrpcProxyProducer() {
+ }
+
+ /**
+ * A CDI producer method that produces a client proxy for a gRPC service that
+ * will connect to the server using the channel specified via
+ * {@link io.helidon.grpc.api.Grpc.GrpcChannel} annotation on the proxy interface
+ * or injection point, or the default {@link io.grpc.Channel}.
+ *
+ * This is not a real producer method but is used as a stub by the gRPC client
+ * CDI extension to create real producers as injection points are discovered.
+ *
+ * @param injectionPoint the injection point where the client proxy is to be injected
+ * @return a gRPC client proxy
+ */
+ @Grpc.GrpcProxy
+ @Grpc.GrpcChannel(GrpcChannelsProvider.DEFAULT_CHANNEL_NAME)
+ static Object proxyUsingNamedChannel(InjectionPoint injectionPoint, ChannelProducer producer) {
+ Class> type = ModelHelper.getGenericType(injectionPoint.getType());
+
+ String channelName;
+ if (injectionPoint.getAnnotated().isAnnotationPresent(Grpc.GrpcChannel.class)) {
+ channelName = injectionPoint.getAnnotated().getAnnotation(Grpc.GrpcChannel.class).value();
+ } else {
+ channelName = type.isAnnotationPresent(Grpc.GrpcChannel.class)
+ ? type.getAnnotation(Grpc.GrpcChannel.class).value()
+ : GrpcChannelsProvider.DEFAULT_CHANNEL_NAME;
+ }
+
+ Channel channel = producer.findChannel(channelName);
+ GrpcProxyBuilder> builder = GrpcProxyBuilder.create(channel, type);
+
+ return builder.build();
+ }
+}
diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcServiceClient.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcServiceClient.java
new file mode 100644
index 00000000000..022a2293a36
--- /dev/null
+++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcServiceClient.java
@@ -0,0 +1,483 @@
+/*
+ * Copyright (c) 2019, 2024 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.microprofile.grpc.client;
+
+import java.lang.reflect.Proxy;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+import io.helidon.grpc.core.InterceptorWeights;
+import io.helidon.grpc.core.MethodHandler;
+import io.helidon.grpc.core.WeightedBag;
+import io.helidon.webclient.api.ClientUri;
+
+import io.grpc.CallOptions;
+import io.grpc.Channel;
+import io.grpc.ClientInterceptor;
+import io.grpc.MethodDescriptor.MethodType;
+import io.grpc.Status;
+import io.grpc.stub.AbstractStub;
+import io.grpc.stub.ClientCalls;
+import io.grpc.stub.StreamObserver;
+
+/**
+ * A gRPC Client for a specific gRPC service.
+ */
+public class GrpcServiceClient {
+
+ private final Channel channel;
+
+ private final HashMap> methodStubs;
+
+ private final ClientServiceDescriptor clientServiceDescriptor;
+
+ /**
+ * Creates a {@link Builder}.
+ *
+ * @param channel the {@link Channel} to use to connect to the server
+ * @param descriptor the {@link ClientServiceDescriptor} describing the gRPC service
+ *
+ * @return a new instance of {@link Builder}
+ */
+ public static Builder builder(Channel channel, ClientServiceDescriptor descriptor) {
+ return new Builder(channel, descriptor);
+ }
+
+ /**
+ * Creates a {@link GrpcServiceClient}.
+ *
+ * @param channel the {@link Channel} to use to connect to the server
+ * @param descriptor the {@link ClientServiceDescriptor} describing the gRPC service
+ *
+ * @return a new instance of {@link Builder}
+ */
+ public static GrpcServiceClient create(Channel channel, ClientServiceDescriptor descriptor) {
+ return builder(channel, descriptor).build();
+ }
+
+ private GrpcServiceClient(Channel channel,
+ CallOptions callOptions,
+ ClientServiceDescriptor clientServiceDescriptor) {
+ this.channel = channel;
+ this.clientServiceDescriptor = clientServiceDescriptor;
+ this.methodStubs = new HashMap<>();
+
+ // Merge Interceptors specified in Channel, ClientServiceDescriptor and ClientMethodDescriptor.
+ // Add the merged interceptor list to the AbstractStub which will be be used for the invocation
+ // of the method.
+ for (ClientMethodDescriptor methodDescriptor : clientServiceDescriptor.methods()) {
+ GrpcMethodStub, ?> methodStub = new GrpcMethodStub<>(channel, callOptions, methodDescriptor);
+
+ WeightedBag priorityInterceptors = WeightedBag.create(InterceptorWeights.USER);
+ priorityInterceptors.addAll(clientServiceDescriptor.interceptors());
+ priorityInterceptors.addAll(methodDescriptor.interceptors());
+ List interceptors = priorityInterceptors.stream().toList();
+
+ if (!interceptors.isEmpty()) {
+ LinkedHashSet uniqueInterceptors = new LinkedHashSet<>(interceptors.size());
+
+ // iterate the interceptors in reverse order so that the interceptor chain is in the correct order
+ for (int i = interceptors.size() - 1; i >= 0; i--) {
+ ClientInterceptor interceptor = interceptors.get(i);
+ if (!uniqueInterceptors.contains(interceptor)) {
+ uniqueInterceptors.add(interceptor);
+ }
+ }
+
+ for (ClientInterceptor interceptor : uniqueInterceptors) {
+ methodStub = methodStub.withInterceptors(interceptor);
+ }
+ }
+
+ if (methodDescriptor.callCredentials() != null) {
+ // Method level CallCredentials take precedence over service level CallCredentials.
+ methodStub = methodStub.withCallCredentials(methodDescriptor.callCredentials());
+ } else if (clientServiceDescriptor.callCredentials() != null) {
+ methodStub = methodStub.withCallCredentials(clientServiceDescriptor.callCredentials());
+ }
+
+ methodStubs.put(methodDescriptor.name(), methodStub);
+ }
+ }
+
+ /**
+ * Obtain the service name.
+ *
+ * @return The name of the service
+ */
+ public String serviceName() {
+ return clientServiceDescriptor.name();
+ }
+
+ /**
+ * Invoke the specified method using the method's
+ * {@link MethodHandler}.
+ *
+ * @param name the name of the method to invoke
+ * @param args the method arguments
+ * @return the method response
+ */
+ Object invoke(String name, Object[] args) {
+ GrpcMethodStub, ?> stub = methodStubs.get(name);
+ if (stub == null) {
+ // may be used during testing to override a channel's port
+ if (name.equals(GrpcConfigurablePort.CHANNEL_PORT)) {
+ io.helidon.webclient.grpc.GrpcChannel grpcChannel = (io.helidon.webclient.grpc.GrpcChannel) channel;
+ ClientUri uri = grpcChannel.grpcClient().clientConfig().baseUri().orElseThrow();
+ uri.port((int) args[0]);
+ return null;
+ }
+ throw Status.INTERNAL.withDescription("gRPC method '" + name + "' does not exist").asRuntimeException();
+ }
+ ClientMethodDescriptor descriptor = stub.descriptor();
+ MethodHandler, ?> methodHandler = descriptor.methodHandler();
+
+ return switch (descriptor.descriptor().getType()) {
+ case UNARY -> methodHandler.unary(args, this::unary);
+ case CLIENT_STREAMING -> methodHandler.clientStreaming(args, this::clientStreaming);
+ case SERVER_STREAMING -> methodHandler.serverStreaming(args, this::serverStreaming);
+ case BIDI_STREAMING -> methodHandler.bidirectional(args, this::bidiStreaming);
+ default -> throw Status.INTERNAL.withDescription("Unknown or unsupported method type for method " + name)
+ .asRuntimeException();
+ };
+ }
+
+ /**
+ * Create a dynamic proxy for the specified interface that proxies
+ * calls to the wrapped gRPC service.
+ *
+ * @param type the interface to create a proxy for
+ * @param extraTypes extra types for the proxy to implement
+ * @param the type of the returned proxy
+ * @return a dynamic proxy that calls methods on this gRPC service
+ */
+ @SuppressWarnings("unchecked")
+ public T proxy(Class type, Class>... extraTypes) {
+ Map names = new HashMap<>();
+ names.put(GrpcConfigurablePort.CHANNEL_PORT, GrpcConfigurablePort.CHANNEL_PORT); // for testing
+ for (ClientMethodDescriptor methodDescriptor : clientServiceDescriptor.methods()) {
+ MethodHandler, ?> methodHandler = methodDescriptor.methodHandler();
+ if (methodHandler != null) {
+ names.put(methodHandler.javaMethodName(), methodDescriptor.name());
+ }
+ }
+
+ Class>[] proxyTypes;
+ if (extraTypes == null || extraTypes.length == 0) {
+ proxyTypes = new Class>[] {type};
+ } else {
+ proxyTypes = new Class>[extraTypes.length + 1];
+ proxyTypes[0] = type;
+ System.arraycopy(extraTypes, 0, proxyTypes, 1, extraTypes.length);
+ }
+ return (T) Proxy.newProxyInstance(type.getClassLoader(), proxyTypes, ClientProxy.create(this, names));
+ }
+
+ /**
+ * Invoke the specified unary method with the specified request object.
+ *
+ * @param methodName the method name to be invoked
+ * @param request the request parameter
+ * @param the request type
+ * @param the response type
+ *
+ * @return The result of this invocation
+ */
+ public RespT blockingUnary(String methodName, ReqT request) {
+ GrpcMethodStub stub = ensureMethod(methodName, MethodType.UNARY);
+ return ClientCalls.blockingUnaryCall(
+ stub.getChannel(), stub.descriptor().descriptor(), stub.getCallOptions(), request);
+ }
+
+ /**
+ * Asynchronously invoke the specified unary method.
+ *
+ * @param methodName the method name to be invoked
+ * @param request the request parameter
+ * @param the request type
+ * @param the response type
+ *
+ * @return A {@link CompletionStage} that will complete with the result of the unary method call
+ */
+ public CompletionStage unary(String methodName, ReqT request) {
+ SingleValueStreamObserver observer = new SingleValueStreamObserver<>();
+
+ GrpcMethodStub stub = ensureMethod(methodName, MethodType.UNARY);
+ ClientCalls.asyncUnaryCall(
+ stub.getChannel().newCall(stub.descriptor().descriptor(), stub.getCallOptions()),
+ request,
+ observer);
+
+ return observer.completionStage();
+ }
+
+ /**
+ * Invoke the specified unary method.
+ *
+ * @param methodName the method name to be invoked
+ * @param request the request parameter
+ * @param observer a {@link StreamObserver} to receive the result
+ * @param the request type
+ * @param the response type
+ */
+ public void unary(String methodName, ReqT request, StreamObserver observer) {
+ GrpcMethodStub stub = ensureMethod(methodName, MethodType.UNARY);
+ ClientCalls.asyncUnaryCall(
+ stub.getChannel().newCall(stub.descriptor().descriptor(), stub.getCallOptions()),
+ request,
+ observer);
+ }
+
+ /**
+ * Invoke the specified server streaming method.
+ *
+ * @param methodName the method name to be invoked
+ * @param request the request parameter
+ * @param the request type
+ * @param the response type
+ *
+ * @return an {@link Iterator} to obtain the streamed results
+ */
+ public Iterator blockingServerStreaming(String methodName, ReqT request) {
+ GrpcMethodStub stub = ensureMethod(methodName, MethodType.SERVER_STREAMING);
+ return ClientCalls.blockingServerStreamingCall(
+ stub.getChannel().newCall(stub.descriptor().descriptor(), stub.getCallOptions()),
+ request);
+ }
+
+ /**
+ * Invoke the specified server streaming method.
+ *
+ * @param methodName the method name to be invoked
+ * @param request the request parameter
+ * @param observer a {@link StreamObserver} to receive the results
+ * @param the request type
+ * @param the response type
+ */
+ public void serverStreaming(String methodName, ReqT request, StreamObserver observer) {
+ GrpcMethodStub stub = ensureMethod(methodName, MethodType.SERVER_STREAMING);
+ ClientCalls.asyncServerStreamingCall(
+ stub.getChannel().newCall(stub.descriptor().descriptor(), stub.getCallOptions()),
+ request,
+ observer);
+ }
+
+ /**
+ * Invoke the specified client streaming method.
+ *
+ * @param methodName the method name to be invoked
+ * @param items an {@link Iterable} of items to be streamed to the server
+ * @param the request type
+ * @param the response type
+ * @return A {@link StreamObserver} to retrieve the method call result
+ */
+ public CompletionStage clientStreaming(String methodName, Iterable items) {
+ return clientStreaming(methodName, StreamSupport.stream(items.spliterator(), false));
+ }
+
+ /**
+ * Invoke the specified client streaming method.
+ *
+ * @param methodName the method name to be invoked
+ * @param items a {@link Stream} of items to be streamed to the server
+ * @param the request type
+ * @param the response type
+ * @return A {@link StreamObserver} to retrieve the method call result
+ */
+ public CompletionStage clientStreaming(String methodName, Stream items) {
+ SingleValueStreamObserver obsv = new SingleValueStreamObserver<>();
+ GrpcMethodStub stub = ensureMethod(methodName, MethodType.CLIENT_STREAMING);
+ StreamObserver reqStream = ClientCalls.asyncClientStreamingCall(
+ stub.getChannel().newCall(stub.descriptor().descriptor(), stub.getCallOptions()),
+ obsv);
+
+ items.forEach(reqStream::onNext);
+ reqStream.onCompleted();
+
+ return obsv.completionStage();
+ }
+
+ /**
+ * Invoke the specified client streaming method.
+ *
+ * @param methodName the method name to be invoked
+ * @param observer a {@link StreamObserver} to receive the result
+ * @param the request type
+ * @param the response type
+ * @return a {@link StreamObserver} to use to stream requests to the server
+ */
+ public StreamObserver clientStreaming(String methodName, StreamObserver observer) {
+ GrpcMethodStub stub = ensureMethod(methodName, MethodType.CLIENT_STREAMING);
+ return ClientCalls.asyncClientStreamingCall(
+ stub.getChannel().newCall(stub.descriptor().descriptor(), stub.getCallOptions()),
+ observer);
+ }
+
+ /**
+ * Invoke the specified bidirectional streaming method.
+ *
+ * @param methodName the method name to be invoked.
+ * @param observer a {@link StreamObserver} to receive the result
+ * @param the request type
+ * @param the response type
+ * @return A {@link StreamObserver} to use to stream requests to the server
+ */
+ public StreamObserver bidiStreaming(String methodName, StreamObserver observer) {
+ GrpcMethodStub stub = ensureMethod(methodName, MethodType.BIDI_STREAMING);
+ return ClientCalls.asyncBidiStreamingCall(
+ stub.getChannel().newCall(stub.descriptor().descriptor(), stub.getCallOptions()),
+ observer);
+ }
+
+ @SuppressWarnings("unchecked")
+ private GrpcMethodStub ensureMethod(String methodName, MethodType methodType) {
+ GrpcMethodStub stub = (GrpcMethodStub) methodStubs.get(methodName);
+ if (stub == null) {
+ throw new IllegalArgumentException("No method named " + methodName + " registered with this service");
+ }
+ ClientMethodDescriptor cmd = stub.descriptor();
+ if (cmd.descriptor().getType() != methodType) {
+ throw new IllegalArgumentException("Method (" + methodName + ") already registered with a different method type.");
+ }
+
+ return stub;
+ }
+
+ /**
+ * GrpcMethodStub can be used to configure method specific Interceptors, Metrics, Tracing, Deadlines, etc.
+ */
+ private static class GrpcMethodStub
+ extends AbstractStub> {
+
+ private final ClientMethodDescriptor cmd;
+
+ GrpcMethodStub(Channel channel, CallOptions callOptions, ClientMethodDescriptor cmd) {
+ super(channel, callOptions);
+ this.cmd = cmd;
+ }
+
+ @Override
+ protected GrpcMethodStub build(Channel channel, CallOptions callOptions) {
+ return new GrpcMethodStub<>(channel, callOptions, cmd);
+ }
+
+ public ClientMethodDescriptor descriptor() {
+ return cmd;
+ }
+ }
+
+ /**
+ * Builder to build an instance of {@link GrpcServiceClient}.
+ */
+ public static class Builder {
+
+ private final Channel channel;
+
+ private CallOptions callOptions = CallOptions.DEFAULT;
+
+ private final ClientServiceDescriptor clientServiceDescriptor;
+
+ private Builder(Channel channel, ClientServiceDescriptor descriptor) {
+ this.channel = channel;
+ this.clientServiceDescriptor = descriptor;
+ }
+
+ /**
+ * Set the {@link CallOptions} to use.
+ *
+ * @param callOptions the {@link CallOptions} to use
+ * @return This {@link Builder} for fluent method chaining
+ */
+ public Builder callOptions(CallOptions callOptions) {
+ this.callOptions = callOptions;
+ return this;
+ }
+
+ /**
+ * Build an instance of {@link GrpcServiceClient}.
+ *
+ * @return an new instance of a {@link GrpcServiceClient}
+ */
+ public GrpcServiceClient build() {
+ return new GrpcServiceClient(channel, callOptions, clientServiceDescriptor);
+ }
+ }
+
+ /**
+ * A simple {@link StreamObserver} adapter class that completes
+ * a {@link CompletableFuture} when the observer is completed.
+ *
+ * This observer uses the value passed to its {@link #onNext(Object)} method to complete
+ * the {@link CompletableFuture}.
+ *
+ * This observer should only be used in cases where a single result is expected. If more
+ * that one call is made to {@link #onNext(Object)} then future will be completed with
+ * an exception.
+ *
+ * @param The type of objects received in this stream.
+ */
+ public static class SingleValueStreamObserver implements StreamObserver {
+
+ private int count;
+
+ private T result;
+
+ private final CompletableFuture resultFuture = new CompletableFuture<>();
+
+ /**
+ * Create a SingleValueStreamObserver.
+ */
+ public SingleValueStreamObserver() {
+ }
+
+ /**
+ * Obtain the {@link CompletableFuture} that will be completed
+ * when the {@link StreamObserver} completes.
+ *
+ * @return The CompletableFuture
+ */
+ public CompletionStage completionStage() {
+ return resultFuture;
+ }
+
+ @Override
+ public void onNext(T value) {
+ if (count++ == 0) {
+ result = value;
+ } else {
+ resultFuture.completeExceptionally(new IllegalStateException("More than one result received."));
+ }
+ }
+
+ @Override
+ public void onError(Throwable t) {
+ resultFuture.completeExceptionally(t);
+ }
+
+ @Override
+ public void onCompleted() {
+ resultFuture.complete(result);
+ }
+ }
+}
diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/package-info.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/package-info.java
new file mode 100644
index 00000000000..a3232b59001
--- /dev/null
+++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Client gRPC microprofile classes.
+ */
+package io.helidon.microprofile.grpc.client;
diff --git a/microprofile/grpc/client/src/main/java/module-info.java b/microprofile/grpc/client/src/main/java/module-info.java
new file mode 100644
index 00000000000..b4fa484d1e3
--- /dev/null
+++ b/microprofile/grpc/client/src/main/java/module-info.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * gRPC microprofile client module.
+ */
+module io.helidon.microprofile.grpc.client {
+
+ requires io.helidon.common;
+ requires io.helidon.common.tls;
+ requires io.helidon.config;
+ requires io.helidon.webclient.grpc;
+ requires io.helidon.grpc.api;
+ requires io.helidon.microprofile.grpc.core;
+
+ requires io.grpc;
+ requires jakarta.cdi;
+ requires jakarta.inject;
+
+ requires transitive io.helidon.grpc.core;
+
+ exports io.helidon.microprofile.grpc.client;
+
+ provides jakarta.enterprise.inject.spi.Extension with
+ io.helidon.microprofile.grpc.client.GrpcClientCdiExtension;
+
+}
\ No newline at end of file
diff --git a/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/ClientMethodDescriptorTest.java b/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/ClientMethodDescriptorTest.java
new file mode 100644
index 00000000000..fc4b49190d8
--- /dev/null
+++ b/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/ClientMethodDescriptorTest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2019, 2024 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.microprofile.grpc.client;
+
+import io.helidon.microprofile.grpc.client.test.Echo;
+import io.helidon.microprofile.grpc.client.test.EchoServiceGrpc;
+
+import io.grpc.MethodDescriptor;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.collection.IsEmptyIterable.emptyIterable;
+
+public class ClientMethodDescriptorTest {
+
+ private MethodDescriptor.Builder, ?> grpcDescriptor;
+
+ @BeforeEach
+ public void setup() {
+ grpcDescriptor = EchoServiceGrpc.getServiceDescriptor()
+ .getMethods()
+ .stream()
+ .filter(md -> md.getFullMethodName().equals("EchoService/Echo"))
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("Could not find echo method"))
+ .toBuilder();
+ }
+
+ @Test
+ public void shouldCreateMethodDescriptorFromGrpcDescriptor() {
+ ClientMethodDescriptor descriptor = ClientMethodDescriptor.create("FooService",
+ "foo",
+ grpcDescriptor);
+
+ assertThat(descriptor, is(notNullValue()));
+ assertThat(descriptor.name(), is("foo"));
+ assertThat(descriptor.interceptors(), is(emptyIterable()));
+
+ MethodDescriptor, ?> expected = grpcDescriptor.build();
+ MethodDescriptor, ?> methodDescriptor = descriptor.descriptor();
+ assertThat(methodDescriptor.getFullMethodName(), is("FooService/foo"));
+ assertThat(methodDescriptor.getType(), is(expected.getType()));
+ assertThat(methodDescriptor.getRequestMarshaller(), is(expected.getRequestMarshaller()));
+ assertThat(methodDescriptor.getResponseMarshaller(), is(expected.getResponseMarshaller()));
+ }
+
+ @Test
+ public void shouldCreateBidirectionalMethod() {
+ ClientMethodDescriptor descriptor = ClientMethodDescriptor.bidirectional("FooService", "foo")
+ .defaultMarshallerSupplier(new JavaMarshaller.Supplier())
+ .build();
+ assertThat(descriptor, is(notNullValue()));
+ assertThat(descriptor.name(), is("foo"));
+ MethodDescriptor, ?> methodDescriptor = descriptor.descriptor();
+ assertThat(methodDescriptor.getFullMethodName(), is("FooService/foo"));
+ assertThat(methodDescriptor.getType(), is(MethodDescriptor.MethodType.BIDI_STREAMING));
+ }
+
+ @Test
+ public void shouldCreateClientStreamingMethod() {
+ ClientMethodDescriptor descriptor = ClientMethodDescriptor.clientStreaming("FooService", "foo")
+ .defaultMarshallerSupplier(new JavaMarshaller.Supplier())
+ .build();
+ assertThat(descriptor, is(notNullValue()));
+ assertThat(descriptor.name(), is("foo"));
+ MethodDescriptor, ?> methodDescriptor = descriptor.descriptor();
+ assertThat(methodDescriptor.getFullMethodName(), is("FooService/foo"));
+ assertThat(methodDescriptor.getType(), is(MethodDescriptor.MethodType.CLIENT_STREAMING));
+ }
+
+ @Test
+ public void shouldCreateServerStreamingMethod() {
+ ClientMethodDescriptor descriptor = ClientMethodDescriptor.serverStreaming("FooService", "foo")
+ .defaultMarshallerSupplier(new JavaMarshaller.Supplier())
+ .build();
+ assertThat(descriptor, is(notNullValue()));
+ assertThat(descriptor.name(), is("foo"));
+ MethodDescriptor, ?> methodDescriptor = descriptor.descriptor();
+ assertThat(methodDescriptor.getFullMethodName(), is("FooService/foo"));
+ assertThat(methodDescriptor.getType(), is(MethodDescriptor.MethodType.SERVER_STREAMING));
+ }
+
+ @Test
+ public void shouldCreateUnaryMethod() {
+ ClientMethodDescriptor descriptor = ClientMethodDescriptor.unary("FooService", "foo")
+ .defaultMarshallerSupplier(new JavaMarshaller.Supplier())
+ .build();
+ assertThat(descriptor, is(notNullValue()));
+ assertThat(descriptor.name(), is("foo"));
+ MethodDescriptor, ?> methodDescriptor = descriptor.descriptor();
+ assertThat(methodDescriptor.getFullMethodName(), is("FooService/foo"));
+ assertThat(methodDescriptor.getType(), is(MethodDescriptor.MethodType.UNARY));
+ }
+
+ @Test
+ public void shouldSetName() {
+ ClientMethodDescriptor.Builder builder = ClientMethodDescriptor
+ .unary("FooService", "foo")
+ .defaultMarshallerSupplier(new JavaMarshaller.Supplier());
+
+ builder.fullName("Foo/Bar");
+
+ ClientMethodDescriptor descriptor = builder.build();
+
+ assertThat(descriptor.name(), is("Bar"));
+ assertThat(descriptor.descriptor().getFullMethodName(), is("Foo/Bar"));
+ }
+
+ @Test
+ public void testMarshallerTypesForProtoBuilder() {
+ ClientMethodDescriptor descriptor = ClientMethodDescriptor
+ .unary("EchoService", "Echo")
+ .requestType(Echo.EchoRequest.class)
+ .responseType(Echo.EchoResponse.class)
+ .build();
+
+ MethodDescriptor, ?> methodDescriptor = descriptor.descriptor();
+ assertThat(methodDescriptor.getRequestMarshaller(), instanceOf(MethodDescriptor.PrototypeMarshaller.class));
+ assertThat(methodDescriptor.getResponseMarshaller(), instanceOf(MethodDescriptor.PrototypeMarshaller.class));
+ }
+}
diff --git a/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/ClientServiceDescriptorTest.java b/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/ClientServiceDescriptorTest.java
new file mode 100644
index 00000000000..7d945d45fd3
--- /dev/null
+++ b/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/ClientServiceDescriptorTest.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright (c) 2019, 2024 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.microprofile.grpc.client;
+
+import java.util.Collection;
+
+import io.helidon.microprofile.grpc.client.test.StringServiceGrpc;
+
+import io.grpc.ClientInterceptor;
+import io.grpc.MethodDescriptor;
+import io.grpc.ServerMethodDefinition;
+import io.grpc.ServiceDescriptor;
+import org.junit.jupiter.api.Test;
+import services.TreeMapService;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.collection.IsEmptyIterable.emptyIterable;
+import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+
+public class ClientServiceDescriptorTest {
+
+ @Test
+ public void shouldCreateDescriptorFromGrpcServiceDescriptor() {
+ ServiceDescriptor grpcDescriptor = StringServiceGrpc.getServiceDescriptor();
+ ClientServiceDescriptor descriptor = ClientServiceDescriptor.create(grpcDescriptor);
+ String serviceName = "StringService";
+ assertThat(descriptor.name(), is(serviceName));
+ assertThat(descriptor.interceptors(), is(emptyIterable()));
+
+ Collection> expectedMethods = grpcDescriptor.getMethods();
+ Collection actualMethods = descriptor.methods();
+ assertThat(actualMethods.size(), is(expectedMethods.size()));
+
+ for (MethodDescriptor, ?> methodDescriptor : expectedMethods) {
+ String name = methodDescriptor.getFullMethodName().substring(serviceName.length() + 1);
+ ClientMethodDescriptor method = descriptor.method(name);
+ assertThat(method.name(), is(name));
+ assertThat(method.interceptors(), is(emptyIterable()));
+ MethodDescriptor