Skip to content

Allow in-process server and channel outside of test module #187

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import io.grpc.reflection.v1.ServerReflectionResponse;
import io.grpc.stub.StreamObserver;

@SpringBootTest(properties = { "debug=true", "spring.grpc.server.port=0",
@SpringBootTest(properties = { "spring.grpc.server.port=0",
"spring.grpc.client.default-channel.address=static://0.0.0.0:${local.grpc.port}" })
@DirtiesContext
public class GrpcServerApplicationTests {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,23 @@ void clientChannelWithSsl(@Autowired GrpcChannelFactory channels) {

}

@Nested
@SpringBootTest(properties = { "spring.grpc.server.inprocess.name=foo", "spring.grpc.server.host=0.0.0.0",
"spring.grpc.server.port=0" })
class ServerWithRegularAndInProcessChannelsAndFactories {

@Test
void servesResponseToNonInProcessClient(@Autowired GrpcChannelFactory channels, @LocalGrpcPort int port) {
assertThatResponseIsServedToChannel(channels.createChannel("0.0.0.0:" + port));
}

@Test
void servesResponseToInProcessClient(@Autowired GrpcChannelFactory channels) {
assertThatResponseIsServedToChannel(channels.createChannel("in-process:foo"));
}

}

private void assertThatResponseIsServedToChannel(ManagedChannel clientChannel) {
SimpleGrpc.SimpleBlockingStub client = SimpleGrpc.newBlockingStub(clientChannel);
HelloReply response = client.sayHello(HelloRequest.newBuilder().setName("Alien").build());
Expand Down
5 changes: 5 additions & 0 deletions spring-grpc-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@
<artifactId>netty-transport-native-epoll</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-inprocess</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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<GrpcChannelFactory> channelFactories = new ArrayList<>();

/**
* Creates a new CompositeGrpcChannelFactory with the given factories.
* @param channelFactories the channel factories
*/
public CompositeGrpcChannelFactory(List<GrpcChannelFactory> 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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ public DefaultGrpcChannelFactory(List<GrpcChannelBuilderCustomizer<T>> globalCus
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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@
*/
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<InProcessChannelBuilder> {

/**
* 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<GrpcChannelBuilderCustomizer<InProcessChannelBuilder>> 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));
}

}
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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<InProcessServerBuilder> {

public InProcessGrpcServerFactory(String address,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,14 @@ public int getPort() {
return this.server == null ? 0 : this.server.getPort();
}

/**
* Gets the server factory used to create the server.
* @return the server factory to create the server
*/
public GrpcServerFactory getFactory() {
return this.factory;
}

/**
* Creates and starts the grpc server.
* @throws IOException If the server is unable to bind the port.
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}

}

}
Loading