diff --git a/spring-grpc-core/pom.xml b/spring-grpc-core/pom.xml
index 332016d..352192f 100644
--- a/spring-grpc-core/pom.xml
+++ b/spring-grpc-core/pom.xml
@@ -93,6 +93,11 @@
com.google.api.grpc
proto-google-common-protos
+
+ io.grpc
+ grpc-kotlin-stub
+ true
+
com.salesforce.servicelibs
reactor-grpc-stub
diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/CoroutineStubFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/CoroutineStubFactory.java
new file mode 100644
index 0000000..4d53b6a
--- /dev/null
+++ b/spring-grpc-core/src/main/java/org/springframework/grpc/client/CoroutineStubFactory.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * 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
+ *
+ * https://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 org.springframework.grpc.client;
+
+import java.util.function.Supplier;
+
+import io.grpc.CallOptions;
+import io.grpc.Channel;
+import io.grpc.ManagedChannel;
+import io.grpc.kotlin.AbstractCoroutineStub;
+
+public class CoroutineStubFactory implements StubFactory> {
+
+ @Override
+ public AbstractCoroutineStub> create(Supplier channel,
+ Class extends AbstractCoroutineStub>> type) {
+ try {
+ return type.getConstructor(Channel.class, CallOptions.class)
+ .newInstance(channel.get(), CallOptions.DEFAULT);
+
+ }
+ catch (Exception e) {
+ throw new IllegalStateException("Failed to create stub", e);
+ }
+ }
+
+ protected static boolean supports(Class> type) {
+ return AbstractCoroutineStub.class.isAssignableFrom(type);
+ }
+
+}
diff --git a/spring-grpc-core/src/test/java/org/springframework/grpc/client/GrpcClientFactoryTests.java b/spring-grpc-core/src/test/java/org/springframework/grpc/client/GrpcClientFactoryTests.java
index 64ca83f..4488e4c 100644
--- a/spring-grpc-core/src/test/java/org/springframework/grpc/client/GrpcClientFactoryTests.java
+++ b/spring-grpc-core/src/test/java/org/springframework/grpc/client/GrpcClientFactoryTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024-2024 the original author or authors.
+ * Copyright 2024-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -33,6 +33,7 @@ import org.springframework.grpc.client.GrpcClientFactoryTests.MyProto.MyStub;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ManagedChannel;
+import io.grpc.kotlin.AbstractCoroutineStub;
import io.grpc.stub.AbstractStub;
public class GrpcClientFactoryTests {
@@ -88,6 +89,16 @@ public class GrpcClientFactoryTests {
assertThat(factory.getClient("local", OtherStub.class, null)).isNotNull();
}
+ @Test
+ void testCoroutineStubFactory() {
+ context.registerBean(CoroutineStubFactory.class, CoroutineStubFactory::new);
+ GrpcClientFactory.register(context,
+ GrpcClientRegistrationSpec.of("local")
+ .factory(CoroutineStubFactory.class)
+ .types(MyCoroutineStub.class));
+ assertThat(factory.getClient("local", MyCoroutineStub.class, null)).isNotNull();
+ }
+
static class OtherStubFactory implements StubFactory {
@Override
@@ -135,6 +146,19 @@ public class GrpcClientFactoryTests {
}
+ public static class MyCoroutineStub extends AbstractCoroutineStub {
+
+ public MyCoroutineStub(Channel channel, CallOptions callOptions) {
+ super(channel, callOptions);
+ }
+
+ @Override
+ protected MyCoroutineStub build(Channel channel, CallOptions callOptions) {
+ return new MyCoroutineStub(channel, callOptions);
+ }
+
+ }
+
@Configuration(proxyBeanMethods = false)
static class MyConfiguration {
diff --git a/spring-grpc-dependencies/pom.xml b/spring-grpc-dependencies/pom.xml
index ddc4750..f35a32c 100644
--- a/spring-grpc-dependencies/pom.xml
+++ b/spring-grpc-dependencies/pom.xml
@@ -50,6 +50,7 @@
1.72.0
+ 1.4.3
4.30.2
2.54.1
1.13.6
@@ -101,6 +102,11 @@
pom
import
+
+ io.grpc
+ grpc-kotlin-stub
+ ${grpc-kotlin.version}
+
com.google.protobuf
protobuf-bom
diff --git a/spring-grpc-spring-boot-autoconfigure/pom.xml b/spring-grpc-spring-boot-autoconfigure/pom.xml
index e9d1488..1eecb68 100644
--- a/spring-grpc-spring-boot-autoconfigure/pom.xml
+++ b/spring-grpc-spring-boot-autoconfigure/pom.xml
@@ -141,6 +141,11 @@
spring-security-web
true
+
+ io.grpc
+ grpc-kotlin-stub
+ true
+
io.projectreactor
reactor-core
diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcClientAutoConfiguration.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcClientAutoConfiguration.java
index a4ec08c..dc8595f 100644
--- a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcClientAutoConfiguration.java
+++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcClientAutoConfiguration.java
@@ -18,13 +18,16 @@ package org.springframework.grpc.autoconfigure.client;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.grpc.autoconfigure.common.codec.GrpcCodecConfiguration;
import org.springframework.grpc.client.ChannelCredentialsProvider;
+import org.springframework.grpc.client.CoroutineStubFactory;
import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
import io.grpc.CompressorRegistry;
@@ -71,4 +74,16 @@ public class GrpcClientAutoConfiguration {
return new ChannelBuilderCustomizers(customizers.orderedStream().toList());
}
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnClass(name = "io.grpc.kotlin.AbstractCoroutineStub")
+ static class GrpcClientCoroutineStubConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ CoroutineStubFactory coroutineStubFactory() {
+ return new CoroutineStubFactory();
+ }
+
+ }
+
}
diff --git a/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/client/GrpcClientAutoConfigurationTests.java b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/client/GrpcClientAutoConfigurationTests.java
index 7afe8c6..4660022 100644
--- a/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/client/GrpcClientAutoConfigurationTests.java
+++ b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/client/GrpcClientAutoConfigurationTests.java
@@ -50,6 +50,7 @@ import io.grpc.Codec;
import io.grpc.CompressorRegistry;
import io.grpc.DecompressorRegistry;
import io.grpc.ManagedChannelBuilder;
+import io.grpc.kotlin.AbstractCoroutineStub;
import io.grpc.netty.NettyChannelBuilder;
import io.grpc.stub.AbstractStub;
@@ -73,6 +74,14 @@ class GrpcClientAutoConfigurationTests {
.run((context) -> assertThat(context).doesNotHaveBean(GrpcClientAutoConfiguration.class));
}
+ @Test
+ void whenGrpcKotlinIsNotOnClasspathThenAutoConfigurationIsSkipped() {
+ this.contextRunner()
+ .withClassLoader(new FilteredClassLoader(AbstractCoroutineStub.class))
+ .run((context) -> assertThat(context)
+ .doesNotHaveBean(GrpcClientAutoConfiguration.GrpcClientCoroutineStubConfiguration.class));
+ }
+
@Test
void whenClientEnabledPropertySetFalseThenAutoConfigurationIsSkipped() {
this.contextRunner()