io.grpc
grpc-protobuf
diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/CompositeGrpcChannelFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/CompositeGrpcChannelFactory.java
new file mode 100644
index 0000000..aaa8a6c
--- /dev/null
+++ b/spring-grpc-core/src/main/java/org/springframework/grpc/client/CompositeGrpcChannelFactory.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2025-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.ArrayList;
+import java.util.List;
+
+import org.springframework.util.Assert;
+
+import io.grpc.ManagedChannel;
+
+/**
+ * A composite {@link GrpcChannelFactory} that combines a list of channel factories.
+ *
+ * The composite delegates channel creation to the first composed factory that supports
+ * the given target string.
+ *
+ * @author Chris Bono
+ */
+public class CompositeGrpcChannelFactory implements GrpcChannelFactory {
+
+ private List channelFactories = new ArrayList<>();
+
+ /**
+ * Creates a new CompositeGrpcChannelFactory with the given factories.
+ * @param channelFactories the channel factories
+ */
+ public CompositeGrpcChannelFactory(List channelFactories) {
+ Assert.notEmpty(channelFactories, "composite channel factory requires at least one channel factory");
+ this.channelFactories.addAll(channelFactories);
+ }
+
+ @Override
+ public boolean supports(String target) {
+ return this.channelFactories.stream().anyMatch((cf) -> cf.supports(target));
+ }
+
+ @Override
+ public ManagedChannel createChannel(final String target, ChannelBuilderOptions options) {
+ return this.channelFactories.stream()
+ .filter((cf) -> cf.supports(target))
+ .findFirst()
+ .orElseThrow(
+ () -> new IllegalStateException("No grpc channel factory found that supports target : " + target))
+ .createChannel(target, options);
+ }
+
+}
diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/DefaultGrpcChannelFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/DefaultGrpcChannelFactory.java
index 502f0ba..0ed19c0 100644
--- a/spring-grpc-core/src/main/java/org/springframework/grpc/client/DefaultGrpcChannelFactory.java
+++ b/spring-grpc-core/src/main/java/org/springframework/grpc/client/DefaultGrpcChannelFactory.java
@@ -70,6 +70,18 @@ public class DefaultGrpcChannelFactory>
this.interceptorsConfigurer = interceptorsConfigurer;
}
+ /**
+ * Whether this factory supports the given target string. The target can be either a
+ * valid nameresolver-compliant URI, an authority string as described in
+ * {@link Grpc#newChannelBuilder(String, ChannelCredentials)}.
+ * @param target the target string as described in method javadocs
+ * @return true unless the target begins with 'in-process:'
+ */
+ @Override
+ public boolean supports(String target) {
+ return !target.startsWith("in-process:");
+ }
+
public void setVirtualTargets(VirtualTargets targets) {
this.targets = targets;
}
diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcChannelFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcChannelFactory.java
index f2702d5..7f44bdb 100644
--- a/spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcChannelFactory.java
+++ b/spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcChannelFactory.java
@@ -29,6 +29,15 @@ import io.grpc.ManagedChannel;
*/
public interface GrpcChannelFactory {
+ /**
+ * Whether this factory supports the given target string. The target can be either a
+ * valid nameresolver-compliant URI, an authority string as described in
+ * {@link Grpc#newChannelBuilder(String, ChannelCredentials)}.
+ * @param target the target string as described in method javadocs
+ * @return whether this factory supports the given target string
+ */
+ boolean supports(String target);
+
/**
* Creates a {@link ManagedChannel} for the given target string. The target can be
* either a valid nameresolver-compliant URI, an authority string as described in
diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/InProcessGrpcChannelFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/InProcessGrpcChannelFactory.java
new file mode 100644
index 0000000..715d35e
--- /dev/null
+++ b/spring-grpc-core/src/main/java/org/springframework/grpc/client/InProcessGrpcChannelFactory.java
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ * 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.List;
+
+import io.grpc.ChannelCredentials;
+import io.grpc.Grpc;
+import io.grpc.inprocess.InProcessChannelBuilder;
+
+/**
+ * {@link GrpcChannelFactory} that creates in-process gRPC channels.
+ *
+ * @author Chris Bono
+ */
+public class InProcessGrpcChannelFactory extends DefaultGrpcChannelFactory {
+
+ /**
+ * Construct an in-process channel factory instance and sets the
+ * {@link #setVirtualTargets virtualTargets} to the identity function so that the
+ * exact passed in target string is used as the target of the channel factory.
+ * @param globalCustomizers the global customizers to apply to all created channels
+ * @param interceptorsConfigurer configures the client interceptors on the created
+ * channels
+ */
+ public InProcessGrpcChannelFactory(List> globalCustomizers,
+ ClientInterceptorsConfigurer interceptorsConfigurer) {
+ super(globalCustomizers, interceptorsConfigurer);
+ setVirtualTargets((p) -> p);
+ }
+
+ /**
+ * Whether this factory supports the given target string. The target can be either a
+ * valid nameresolver-compliant URI, an authority string as described in
+ * {@link Grpc#newChannelBuilder(String, ChannelCredentials)}.
+ * @param target the target string as described in method javadocs
+ * @return true if the target begins with 'in-process:'
+ */
+ @Override
+ public boolean supports(String target) {
+ return target.startsWith("in-process:");
+ }
+
+ @Override
+ protected InProcessChannelBuilder newChannelBuilder(String target, ChannelCredentials creds) {
+ return InProcessChannelBuilder.forName(target.substring(11));
+ }
+
+}
diff --git a/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessGrpcServerFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/InProcessGrpcServerFactory.java
similarity index 82%
rename from spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessGrpcServerFactory.java
rename to spring-grpc-core/src/main/java/org/springframework/grpc/server/InProcessGrpcServerFactory.java
index 71bf8e6..cdbc4ca 100644
--- a/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessGrpcServerFactory.java
+++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/InProcessGrpcServerFactory.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.
@@ -13,15 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.springframework.grpc.test;
+package org.springframework.grpc.server;
import java.util.List;
-import org.springframework.grpc.server.DefaultGrpcServerFactory;
-import org.springframework.grpc.server.ServerBuilderCustomizer;
-
import io.grpc.inprocess.InProcessServerBuilder;
+/**
+ * {@link GrpcServerFactory} that can be used to create an in-process gRPC server.
+ *
+ * @author Chris Bono
+ */
public class InProcessGrpcServerFactory extends DefaultGrpcServerFactory {
public InProcessGrpcServerFactory(String address,
diff --git a/spring-grpc-core/src/test/java/org/springframework/grpc/client/CompositeGrpcChannelFactoryTests.java b/spring-grpc-core/src/test/java/org/springframework/grpc/client/CompositeGrpcChannelFactoryTests.java
new file mode 100644
index 0000000..dc69e03
--- /dev/null
+++ b/spring-grpc-core/src/test/java/org/springframework/grpc/client/CompositeGrpcChannelFactoryTests.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2023-2024 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 static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
+import static org.mockito.Mockito.mock;
+
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import io.grpc.ManagedChannel;
+
+/**
+ * Unit tests for the {@link CompositeGrpcChannelFactory}.
+ */
+@SuppressWarnings({ "unchecked", "rawtypes" })
+class CompositeGrpcChannelFactoryTests {
+
+ private TestChannelFactory fooChannelFactory;
+
+ private TestChannelFactory barChannelFactory;
+
+ private CompositeGrpcChannelFactory compositeChannelFactory;
+
+ @BeforeEach
+ void prepareFactories() {
+ this.fooChannelFactory = new TestChannelFactory("foo");
+ this.barChannelFactory = new TestChannelFactory("bar");
+ this.compositeChannelFactory = new CompositeGrpcChannelFactory(List.of(fooChannelFactory, barChannelFactory));
+ }
+
+ @Test
+ void atLeastOneChannelFactoryRequired() {
+ assertThatIllegalArgumentException().isThrownBy(() -> new CompositeGrpcChannelFactory(null))
+ .withMessage("composite channel factory requires at least one channel factory");
+ assertThatIllegalArgumentException().isThrownBy(() -> new CompositeGrpcChannelFactory(List.of()))
+ .withMessage("composite channel factory requires at least one channel factory");
+ }
+
+ @Test
+ void supportsDependsOnSupportsOfComposedFactories() {
+ assertThat(compositeChannelFactory.supports("foo")).isTrue();
+ assertThat(compositeChannelFactory.supports("bar")).isTrue();
+ assertThat(compositeChannelFactory.supports("zaa")).isFalse();
+ }
+
+ @Test
+ void firstComposedChannelFactorySupportsTarget() {
+ assertThat(compositeChannelFactory.createChannel("foo")).isNotNull();
+ assertThat(fooChannelFactory.getActualTarget()).isEqualTo("foo");
+ assertThat(barChannelFactory.getActualTarget()).isNull();
+ }
+
+ @Test
+ void secondComposedChannelFactorySupportsTarget() {
+ assertThat(compositeChannelFactory.createChannel("bar")).isNotNull();
+ assertThat(fooChannelFactory.getActualTarget()).isNull();
+ assertThat(barChannelFactory.getActualTarget()).isEqualTo("bar");
+ }
+
+ @Test
+ void noComposedChannelFactorySupportsTarget() {
+ assertThatIllegalStateException().isThrownBy(() -> compositeChannelFactory.createChannel("zaa"))
+ .withMessage("No grpc channel factory found that supports target : zaa");
+ assertThat(fooChannelFactory.getActualTarget()).isNull();
+ assertThat(barChannelFactory.getActualTarget()).isNull();
+ }
+
+ static class TestChannelFactory implements GrpcChannelFactory {
+
+ private String expectedTarget;
+
+ private String actualTarget;
+
+ TestChannelFactory(String expectedTarget) {
+ this.expectedTarget = expectedTarget;
+ }
+
+ public boolean supports(String target) {
+ return target.equals(this.expectedTarget);
+ }
+
+ @Override
+ public ManagedChannel createChannel(String target, ChannelBuilderOptions options) {
+ this.actualTarget = target;
+ return mock();
+ }
+
+ String getActualTarget() {
+ return this.actualTarget;
+ }
+
+ }
+
+}
diff --git a/spring-grpc-core/src/test/java/org/springframework/grpc/client/GrpcChannelFactoryTests.java b/spring-grpc-core/src/test/java/org/springframework/grpc/client/GrpcChannelFactoryTests.java
index 718a935..6e5dc5d 100644
--- a/spring-grpc-core/src/test/java/org/springframework/grpc/client/GrpcChannelFactoryTests.java
+++ b/spring-grpc-core/src/test/java/org/springframework/grpc/client/GrpcChannelFactoryTests.java
@@ -34,6 +34,7 @@ import org.mockito.ArgumentMatchers;
import io.grpc.ClientInterceptor;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
+import io.grpc.inprocess.InProcessChannelBuilder;
import io.grpc.netty.NettyChannelBuilder;
/**
@@ -159,6 +160,60 @@ class GrpcChannelFactoryTests {
.isInstanceOf(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class)));
}
+ @Test
+ void inProcessChannelFactoryUsesInProcessChannelBuilder() {
+ var channelName = "in-process:foo";
+ var customizer1 = mock(GrpcChannelBuilderCustomizer.class);
+ var channelFactory = new InProcessGrpcChannelFactory(List.of(), mock());
+ channel = channelFactory.createChannel(channelName,
+ ChannelBuilderOptions.defaults().withCustomizer(customizer1));
+ assertThat(channel).isNotNull();
+ verify(customizer1).customize(anyString(),
+ ArgumentMatchers
+ .assertArg((builder) -> assertThat(builder).isInstanceOf(InProcessChannelBuilder.class)
+ .extracting("managedChannelImplBuilder.target")
+ .isEqualTo("directaddress:///foo")));
+ // NOTE: the impl target ending in foo proves the original target was stripped
+ // of 'in-process:' prefix
+ }
+
+ }
+
+ @Nested
+ class SupportsApiTests {
+
+ @Test
+ void defaultSupportsEverythingExceptInProcess() {
+ var channelFactory = new DefaultGrpcChannelFactory(List.of(), mock());
+ assertThat(channelFactory.supports("foo")).isTrue();
+ assertThat(channelFactory.supports("static:127.0.0.1")).isTrue();
+ assertThat(channelFactory.supports("in-process:foo")).isFalse();
+ }
+
+ @Test
+ void nettySupportsEverythingExceptInProcess() {
+ var channelFactory = new NettyGrpcChannelFactory(List.of(), mock());
+ assertThat(channelFactory.supports("foo")).isTrue();
+ assertThat(channelFactory.supports("static:127.0.0.1")).isTrue();
+ assertThat(channelFactory.supports("in-process:foo")).isFalse();
+ }
+
+ @Test
+ void shadedNettySupportsEverythingExceptInProcess() {
+ var channelFactory = new ShadedNettyGrpcChannelFactory(List.of(), mock());
+ assertThat(channelFactory.supports("foo")).isTrue();
+ assertThat(channelFactory.supports("static:127.0.0.1")).isTrue();
+ assertThat(channelFactory.supports("in-process:foo")).isFalse();
+ }
+
+ @Test
+ void inProcessSupportsOnlyInProcess() {
+ var channelFactory = new InProcessGrpcChannelFactory(List.of(), mock());
+ assertThat(channelFactory.supports("foo")).isFalse();
+ assertThat(channelFactory.supports("static:127.0.0.1")).isFalse();
+ assertThat(channelFactory.supports("in-process:foo")).isTrue();
+ }
+
}
}
diff --git a/spring-grpc-spring-boot-autoconfigure/pom.xml b/spring-grpc-spring-boot-autoconfigure/pom.xml
index 1eecb68..53c02da 100644
--- a/spring-grpc-spring-boot-autoconfigure/pom.xml
+++ b/spring-grpc-spring-boot-autoconfigure/pom.xml
@@ -93,6 +93,11 @@
netty-transport-native-epoll
true
+