Skip to content

Commit e76174d

Browse files
committed
Add support for in-process outside of tests
This commit allows the usage of inprocess server and channel factories outside of tests. Resolves #144 Signed-off-by: Chris Bono <chris.bono@gmail.com>
1 parent fcd70ee commit e76174d

File tree

30 files changed

+1204
-177
lines changed

30 files changed

+1204
-177
lines changed

samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
import io.grpc.reflection.v1.ServerReflectionResponse;
3030
import io.grpc.stub.StreamObserver;
3131

32-
@SpringBootTest(properties = { "debug=true", "spring.grpc.server.port=0",
32+
@SpringBootTest(properties = { "spring.grpc.server.port=0",
3333
"spring.grpc.client.default-channel.address=static://0.0.0.0:${local.grpc.port}" })
3434
@DirtiesContext
3535
public class GrpcServerApplicationTests {

spring-grpc-core/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@
7171
<artifactId>netty-transport-native-epoll</artifactId>
7272
<optional>true</optional>
7373
</dependency>
74+
<dependency>
75+
<groupId>io.grpc</groupId>
76+
<artifactId>grpc-inprocess</artifactId>
77+
<optional>true</optional>
78+
</dependency>
7479
<dependency>
7580
<groupId>io.grpc</groupId>
7681
<artifactId>grpc-protobuf</artifactId>
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.grpc.client;
18+
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
22+
import org.springframework.util.Assert;
23+
24+
import io.grpc.ManagedChannel;
25+
26+
/**
27+
* A composite {@link GrpcChannelFactory} that combines a list of channel factories.
28+
* <p>
29+
* The composite delegates channel creation to the first composed factory that supports
30+
* the given target string.
31+
*
32+
* @author Chris Bono
33+
*/
34+
public class CompositeGrpcChannelFactory implements GrpcChannelFactory {
35+
36+
private List<GrpcChannelFactory> channelFactories = new ArrayList<>();
37+
38+
/**
39+
* Creates a new CompositeGrpcChannelFactory with the given factories.
40+
* @param channelFactories the channel factories
41+
*/
42+
public CompositeGrpcChannelFactory(List<GrpcChannelFactory> channelFactories) {
43+
Assert.notEmpty(channelFactories, "composite channel factory requires at least one channel factory");
44+
this.channelFactories.addAll(channelFactories);
45+
}
46+
47+
@Override
48+
public boolean supports(String target) {
49+
return this.channelFactories.stream().anyMatch((cf) -> cf.supports(target));
50+
}
51+
52+
@Override
53+
public ManagedChannel createChannel(final String target, ChannelBuilderOptions options) {
54+
return this.channelFactories.stream()
55+
.filter((cf) -> cf.supports(target))
56+
.findFirst()
57+
.orElseThrow(
58+
() -> new IllegalStateException("No grpc channel factory found that supports target : " + target))
59+
.createChannel(target, options);
60+
}
61+
62+
}

spring-grpc-core/src/main/java/org/springframework/grpc/client/DefaultGrpcChannelFactory.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,18 @@ public DefaultGrpcChannelFactory(List<GrpcChannelBuilderCustomizer<T>> globalCus
7070
this.interceptorsConfigurer = interceptorsConfigurer;
7171
}
7272

73+
/**
74+
* Whether this factory supports the given target string. The target can be either a
75+
* valid nameresolver-compliant URI, an authority string as described in
76+
* {@link Grpc#newChannelBuilder(String, ChannelCredentials)}.
77+
* @param target the target string as described in method javadocs
78+
* @return true unless the target begins with 'in-process:'
79+
*/
80+
@Override
81+
public boolean supports(String target) {
82+
return !target.startsWith("in-process:");
83+
}
84+
7385
public void setVirtualTargets(VirtualTargets targets) {
7486
this.targets = targets;
7587
}

spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcChannelFactory.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@
2929
*/
3030
public interface GrpcChannelFactory {
3131

32+
/**
33+
* Whether this factory supports the given target string. The target can be either a
34+
* valid nameresolver-compliant URI, an authority string as described in
35+
* {@link Grpc#newChannelBuilder(String, ChannelCredentials)}.
36+
* @param target the target string as described in method javadocs
37+
* @return whether this factory supports the given target string
38+
*/
39+
boolean supports(String target);
40+
3241
/**
3342
* Creates a {@link ManagedChannel} for the given target string. The target can be
3443
* either a valid nameresolver-compliant URI, an authority string as described in
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright 2024-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.grpc.client;
17+
18+
import java.util.List;
19+
20+
import io.grpc.ChannelCredentials;
21+
import io.grpc.Grpc;
22+
import io.grpc.inprocess.InProcessChannelBuilder;
23+
24+
/**
25+
* {@link GrpcChannelFactory} that creates in-process gRPC channels.
26+
*
27+
* @author Chris Bono
28+
*/
29+
public class InProcessGrpcChannelFactory extends DefaultGrpcChannelFactory<InProcessChannelBuilder> {
30+
31+
/**
32+
* Construct an in-process channel factory instance and sets the
33+
* {@link #setVirtualTargets virtualTargets} to the identity function so that the
34+
* exact passed in target string is used as the target of the channel factory.
35+
* @param globalCustomizers the global customizers to apply to all created channels
36+
* @param interceptorsConfigurer configures the client interceptors on the created
37+
* channels
38+
*/
39+
public InProcessGrpcChannelFactory(List<GrpcChannelBuilderCustomizer<InProcessChannelBuilder>> globalCustomizers,
40+
ClientInterceptorsConfigurer interceptorsConfigurer) {
41+
super(globalCustomizers, interceptorsConfigurer);
42+
setVirtualTargets((p) -> p);
43+
}
44+
45+
/**
46+
* Whether this factory supports the given target string. The target can be either a
47+
* valid nameresolver-compliant URI, an authority string as described in
48+
* {@link Grpc#newChannelBuilder(String, ChannelCredentials)}.
49+
* @param target the target string as described in method javadocs
50+
* @return true if the target begins with 'in-process:'
51+
*/
52+
@Override
53+
public boolean supports(String target) {
54+
return target.startsWith("in-process:");
55+
}
56+
57+
@Override
58+
protected InProcessChannelBuilder newChannelBuilder(String target, ChannelCredentials creds) {
59+
return InProcessChannelBuilder.forName(target.substring(11));
60+
}
61+
62+
}

spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessGrpcServerFactory.java renamed to spring-grpc-core/src/main/java/org/springframework/grpc/server/InProcessGrpcServerFactory.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2024-2024 the original author or authors.
2+
* Copyright 2024-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -13,15 +13,17 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package org.springframework.grpc.test;
16+
package org.springframework.grpc.server;
1717

1818
import java.util.List;
1919

20-
import org.springframework.grpc.server.DefaultGrpcServerFactory;
21-
import org.springframework.grpc.server.ServerBuilderCustomizer;
22-
2320
import io.grpc.inprocess.InProcessServerBuilder;
2421

22+
/**
23+
* {@link GrpcServerFactory} that can be used to create an in-process gRPC server.
24+
*
25+
* @author Chris Bono
26+
*/
2527
public class InProcessGrpcServerFactory extends DefaultGrpcServerFactory<InProcessServerBuilder> {
2628

2729
public InProcessGrpcServerFactory(String address,
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright 2023-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.grpc.client;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
20+
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
21+
import static org.mockito.Mockito.mock;
22+
23+
import java.util.List;
24+
25+
import org.junit.jupiter.api.BeforeEach;
26+
import org.junit.jupiter.api.Test;
27+
28+
import io.grpc.ManagedChannel;
29+
30+
/**
31+
* Unit tests for the {@link CompositeGrpcChannelFactory}.
32+
*/
33+
@SuppressWarnings({ "unchecked", "rawtypes" })
34+
class CompositeGrpcChannelFactoryTests {
35+
36+
private TestChannelFactory fooChannelFactory;
37+
38+
private TestChannelFactory barChannelFactory;
39+
40+
private CompositeGrpcChannelFactory compositeChannelFactory;
41+
42+
@BeforeEach
43+
void prepareFactories() {
44+
this.fooChannelFactory = new TestChannelFactory("foo");
45+
this.barChannelFactory = new TestChannelFactory("bar");
46+
this.compositeChannelFactory = new CompositeGrpcChannelFactory(List.of(fooChannelFactory, barChannelFactory));
47+
}
48+
49+
@Test
50+
void atLeastOneChannelFactoryRequired() {
51+
assertThatIllegalArgumentException().isThrownBy(() -> new CompositeGrpcChannelFactory(null))
52+
.withMessage("composite channel factory requires at least one channel factory");
53+
assertThatIllegalArgumentException().isThrownBy(() -> new CompositeGrpcChannelFactory(List.of()))
54+
.withMessage("composite channel factory requires at least one channel factory");
55+
}
56+
57+
@Test
58+
void supportsDependsOnSupportsOfComposedFactories() {
59+
assertThat(compositeChannelFactory.supports("foo")).isTrue();
60+
assertThat(compositeChannelFactory.supports("bar")).isTrue();
61+
assertThat(compositeChannelFactory.supports("zaa")).isFalse();
62+
}
63+
64+
@Test
65+
void firstComposedChannelFactorySupportsTarget() {
66+
assertThat(compositeChannelFactory.createChannel("foo")).isNotNull();
67+
assertThat(fooChannelFactory.getActualTarget()).isEqualTo("foo");
68+
assertThat(barChannelFactory.getActualTarget()).isNull();
69+
}
70+
71+
@Test
72+
void secondComposedChannelFactorySupportsTarget() {
73+
assertThat(compositeChannelFactory.createChannel("bar")).isNotNull();
74+
assertThat(fooChannelFactory.getActualTarget()).isNull();
75+
assertThat(barChannelFactory.getActualTarget()).isEqualTo("bar");
76+
}
77+
78+
@Test
79+
void noComposedChannelFactorySupportsTarget() {
80+
assertThatIllegalStateException().isThrownBy(() -> compositeChannelFactory.createChannel("zaa"))
81+
.withMessage("No grpc channel factory found that supports target : zaa");
82+
assertThat(fooChannelFactory.getActualTarget()).isNull();
83+
assertThat(barChannelFactory.getActualTarget()).isNull();
84+
}
85+
86+
static class TestChannelFactory implements GrpcChannelFactory {
87+
88+
private String expectedTarget;
89+
90+
private String actualTarget;
91+
92+
TestChannelFactory(String expectedTarget) {
93+
this.expectedTarget = expectedTarget;
94+
}
95+
96+
public boolean supports(String target) {
97+
return target.equals(this.expectedTarget);
98+
}
99+
100+
@Override
101+
public ManagedChannel createChannel(String target, ChannelBuilderOptions options) {
102+
this.actualTarget = target;
103+
return mock();
104+
}
105+
106+
String getActualTarget() {
107+
return this.actualTarget;
108+
}
109+
110+
}
111+
112+
}

spring-grpc-core/src/test/java/org/springframework/grpc/client/GrpcChannelFactoryTests.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import io.grpc.ClientInterceptor;
3535
import io.grpc.ManagedChannel;
3636
import io.grpc.ManagedChannelBuilder;
37+
import io.grpc.inprocess.InProcessChannelBuilder;
3738
import io.grpc.netty.NettyChannelBuilder;
3839

3940
/**
@@ -159,6 +160,60 @@ void shadedNettyChannelFactoryUsesShadedNettyChannelBuilder() {
159160
.isInstanceOf(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class)));
160161
}
161162

163+
@Test
164+
void inProcessChannelFactoryUsesInProcessChannelBuilder() {
165+
var channelName = "in-process:foo";
166+
var customizer1 = mock(GrpcChannelBuilderCustomizer.class);
167+
var channelFactory = new InProcessGrpcChannelFactory(List.of(), mock());
168+
channel = channelFactory.createChannel(channelName,
169+
ChannelBuilderOptions.defaults().withCustomizer(customizer1));
170+
assertThat(channel).isNotNull();
171+
verify(customizer1).customize(anyString(),
172+
ArgumentMatchers
173+
.assertArg((builder) -> assertThat(builder).isInstanceOf(InProcessChannelBuilder.class)
174+
.extracting("managedChannelImplBuilder.target")
175+
.isEqualTo("directaddress:///foo")));
176+
// NOTE: the impl target ending in foo proves the original target was stripped
177+
// of 'in-process:' prefix
178+
}
179+
180+
}
181+
182+
@Nested
183+
class SupportsApiTests {
184+
185+
@Test
186+
void defaultSupportsEverythingExceptInProcess() {
187+
var channelFactory = new DefaultGrpcChannelFactory(List.of(), mock());
188+
assertThat(channelFactory.supports("foo")).isTrue();
189+
assertThat(channelFactory.supports("static:127.0.0.1")).isTrue();
190+
assertThat(channelFactory.supports("in-process:foo")).isFalse();
191+
}
192+
193+
@Test
194+
void nettySupportsEverythingExceptInProcess() {
195+
var channelFactory = new NettyGrpcChannelFactory(List.of(), mock());
196+
assertThat(channelFactory.supports("foo")).isTrue();
197+
assertThat(channelFactory.supports("static:127.0.0.1")).isTrue();
198+
assertThat(channelFactory.supports("in-process:foo")).isFalse();
199+
}
200+
201+
@Test
202+
void shadedNettySupportsEverythingExceptInProcess() {
203+
var channelFactory = new ShadedNettyGrpcChannelFactory(List.of(), mock());
204+
assertThat(channelFactory.supports("foo")).isTrue();
205+
assertThat(channelFactory.supports("static:127.0.0.1")).isTrue();
206+
assertThat(channelFactory.supports("in-process:foo")).isFalse();
207+
}
208+
209+
@Test
210+
void inProcessSupportsOnlyInProcess() {
211+
var channelFactory = new InProcessGrpcChannelFactory(List.of(), mock());
212+
assertThat(channelFactory.supports("foo")).isFalse();
213+
assertThat(channelFactory.supports("static:127.0.0.1")).isFalse();
214+
assertThat(channelFactory.supports("in-process:foo")).isTrue();
215+
}
216+
162217
}
163218

164219
}

0 commit comments

Comments
 (0)