Skip to content

Commit 6f44200

Browse files
committed
Add gRPC client sample with Spring Integration
Implement a new basic sample demonstrating Spring Integration's gRPC client capabilities. This sample shows how to integrate gRPC with Spring Boot and Spring Integration for both single-response and streaming response patterns. This example is based on the proto file that exists in the Spring gRPC project, to provide user familiarity
1 parent e809475 commit 6f44200

8 files changed

Lines changed: 413 additions & 0 deletions

File tree

basic/grpc-client/README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
gRPC Client Sample
2+
==================
3+
4+
This example demonstrates the use of Spring Integration's **gRPC Outbound Gateway** for client-side communication.
5+
6+
It uses Java configuration and the Spring Integration DSL to configure the gRPC client adapters.
7+
8+
The sample demonstrates two gRPC communication patterns:
9+
10+
1. **Single Response** - Simple request/reply pattern where the client sends a request and receives a single response
11+
2. **Streaming Response** - The client sends a request and receives a stream of responses
12+
13+
## Prerequisites
14+
15+
This sample requires a gRPC server to be running. You can use any gRPC server that implements the `Simple` service defined in `hello.proto`, or run the test which uses an in-process gRPC server.
16+
17+
## Running the sample
18+
19+
### Command Line Using Gradle
20+
21+
To run the sample using Gradle, execute:
22+
23+
$ gradlew :grpc-client:bootRun
24+
25+
This will start the Spring Boot application using the [Spring Boot Gradle Plugin](https://docs.spring.io/spring-boot/docs/current/gradle-plugin/reference/html/).
26+
27+
## Configuration
28+
29+
The application expects a gRPC server to be available. By default, it connects to `localhost:9090`. You can configure the server address in `application.properties`:
30+
31+
```properties
32+
spring.grpc.client.channels.local.address=static://localhost:9090
33+
```
34+
35+
## Output
36+
37+
The application demonstrates two patterns:
38+
39+
### Single Response Pattern
40+
41+
The client sends a `HelloRequest` with name "Jack" and receives a single `HelloReply`:
42+
43+
Single response reply: message: Hello Jack
44+
45+
### Streaming Response Pattern
46+
47+
The client sends a `HelloRequest` with name "Jane" and receives multiple `HelloReply` messages:
48+
49+
Stream received reply: Hello Jane
50+
Stream received reply: Hello Again
51+
52+
## Running the Tests
53+
54+
The test uses an in-process gRPC server and does not require an external server:
55+
56+
$ gradlew :grpc-client:test
57+
58+
The test validates both single-response and streaming-response patterns.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2026-present 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.integration.samples;
18+
19+
import org.springframework.boot.SpringApplication;
20+
import org.springframework.boot.autoconfigure.SpringBootApplication;
21+
22+
/**
23+
* Run the gRPC client sample application.
24+
* <p>
25+
* Demonstrates how to use Spring Integration with gRPC for client-side communication.
26+
*
27+
* @author Glenn Renfro
28+
*/
29+
@SpringBootApplication
30+
public class Application {
31+
32+
/**
33+
* Run the Spring Boot application.
34+
* @param args the command line arguments
35+
*/
36+
public static void main(String[] args) {
37+
SpringApplication.run(Application.class, args);
38+
}
39+
40+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Copyright 2026-present 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.integration.samples;
18+
19+
import io.grpc.ManagedChannel;
20+
import org.apache.commons.logging.Log;
21+
import org.apache.commons.logging.LogFactory;
22+
import reactor.core.publisher.Flux;
23+
24+
import org.springframework.beans.factory.annotation.Autowired;
25+
import org.springframework.beans.factory.annotation.Qualifier;
26+
import org.springframework.boot.ApplicationRunner;
27+
import org.springframework.context.annotation.Bean;
28+
import org.springframework.context.annotation.Configuration;
29+
import org.springframework.grpc.client.GrpcChannelFactory;
30+
import org.springframework.integration.channel.FluxMessageChannel;
31+
import org.springframework.integration.core.MessagingTemplate;
32+
import org.springframework.integration.dsl.IntegrationFlow;
33+
import org.springframework.integration.grpc.dsl.Grpc;
34+
import org.springframework.integration.support.MessageBuilder;
35+
import org.springframework.messaging.Message;
36+
import org.springframework.messaging.MessageChannel;
37+
38+
/**
39+
* Service for demonstrating gRPC client functionality with Spring Integration.
40+
*
41+
* @author Glenn Renfro
42+
*/
43+
@Configuration(proxyBeanMethods = false)
44+
public class GrpcClientConfiguration {
45+
46+
private static final Log LOGGER = LogFactory.getLog(GrpcClientConfiguration.class);
47+
48+
@Bean
49+
ManagedChannel managedChannel(GrpcChannelFactory factory) {
50+
return factory.createChannel("local");
51+
}
52+
53+
/**
54+
* Create an integration flow for outbound gRPC requests with single responses.
55+
* @param managedChannel the gRPC managed channel
56+
* @return the integration flow
57+
*/
58+
@Bean
59+
IntegrationFlow grpcOutboundFlowSingleResponse(ManagedChannel managedChannel) {
60+
return flow -> flow
61+
.handle(Grpc.outboundGateway(managedChannel, SimpleGrpc.class)
62+
.methodName("SayHello"));
63+
}
64+
65+
/**
66+
* Create an integration flow for outbound gRPC requests with streaming responses.
67+
* @param managedChannel the gRPC managed channel
68+
* @return the integration flow
69+
*/
70+
@Bean
71+
IntegrationFlow grpcOutboundFlowStreamResponse(ManagedChannel managedChannel) {
72+
return flow -> flow
73+
.handle(Grpc.outboundGateway(managedChannel, SimpleGrpc.class)
74+
.methodName("StreamHello"));
75+
}
76+
77+
/**
78+
* Create an application runner that sends a single gRPC request and receives a single response.
79+
* @param grpcInputChannelSingleResponse the message channel for single response requests
80+
* @return the application runner
81+
*/
82+
@Bean
83+
ApplicationRunner grpcClientSingleResponse(
84+
@Autowired @Qualifier("grpcOutboundFlowSingleResponse.input") MessageChannel grpcInputChannelSingleResponse) {
85+
86+
return args -> {
87+
HelloReply reply = new MessagingTemplate().convertSendAndReceive(grpcInputChannelSingleResponse,
88+
HelloRequest.newBuilder().setName("Jack").build(),
89+
HelloReply.class);
90+
91+
LOGGER.info("Single response reply: " + (reply != null ? reply.getMessage() : "No reply received"));
92+
};
93+
}
94+
95+
/**
96+
* Create an application runner that sends a gRPC request and receives a stream of responses.
97+
* @param grpcInputChannel the message channel for streaming response requests
98+
* @return the application runner
99+
*/
100+
@Bean
101+
ApplicationRunner grpcClientStreamResponse(
102+
@Autowired @Qualifier("grpcOutboundFlowStreamResponse.input") MessageChannel grpcInputChannel) {
103+
104+
return args -> {
105+
FluxMessageChannel grpcStreamOutputChannel = new FluxMessageChannel();
106+
Flux.from(grpcStreamOutputChannel)
107+
.map(message -> (HelloReply) message.getPayload())
108+
.map(HelloReply::getMessage)
109+
.doOnNext(msg -> LOGGER.info("Stream received reply: " + msg))
110+
.doOnError(error -> {
111+
LOGGER.error("Error in stream: " + error.getMessage(), error);
112+
})
113+
.subscribe();
114+
115+
Message<?> requestMessage = MessageBuilder
116+
.withPayload(HelloRequest.newBuilder().setName("Jane").build())
117+
.setReplyChannel(grpcStreamOutputChannel)
118+
.build();
119+
grpcInputChannel.send(requestMessage);
120+
};
121+
}
122+
123+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
syntax = "proto3";
2+
3+
option java_multiple_files = true;
4+
option java_package = "org.springframework.integration.samples";
5+
option java_outer_classname = "HelloWorldProto";
6+
7+
// The greeting service definition.
8+
service Simple {
9+
// Sends a greeting
10+
rpc SayHello (HelloRequest) returns (HelloReply) {
11+
}
12+
rpc StreamHello(HelloRequest) returns (stream HelloReply) {}
13+
}
14+
15+
// The request message containing the user's name.
16+
message HelloRequest {
17+
string name = 1;
18+
}
19+
20+
// The response message containing the greetings
21+
message HelloReply {
22+
string message = 1;
23+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# gRPC Client Configuration
2+
spring.grpc.client.channels.local.address=static://localhost:9090
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright 2026-present 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.integration.samples;
18+
19+
import java.time.Duration;
20+
21+
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.api.extension.ExtendWith;
23+
24+
import org.springframework.boot.test.context.SpringBootTest;
25+
import org.springframework.boot.test.context.TestConfiguration;
26+
import org.springframework.boot.test.system.CapturedOutput;
27+
import org.springframework.boot.test.system.OutputCaptureExtension;
28+
import org.springframework.context.annotation.Bean;
29+
import org.springframework.test.annotation.DirtiesContext;
30+
31+
import static org.assertj.core.api.Assertions.assertThat;
32+
import static org.awaitility.Awaitility.await;
33+
34+
/**
35+
* Integration test for gRPC client functionality.
36+
*
37+
* @author Glenn Renfro
38+
*/
39+
@SpringBootTest(properties = {
40+
"spring.grpc.server.inprocess.name=test",
41+
"spring.grpc.client.channels.local.address=in-process:test"
42+
})
43+
@DirtiesContext
44+
@ExtendWith(OutputCaptureExtension.class)
45+
public class GrpcClientTests {
46+
47+
/**
48+
* Verify the gRPC service returns the expected greeting message.
49+
* @param output the captured console output
50+
*/
51+
@Test
52+
void verifyMessage(CapturedOutput output) {
53+
await()
54+
.atMost(Duration.ofSeconds(5))
55+
.untilAsserted(() -> {
56+
assertThat(output.getAll())
57+
.contains("Single response reply: Hello Jack")
58+
.contains("Stream received reply: Hello Jane")
59+
.contains("Stream received reply: Hello Again");
60+
});
61+
}
62+
63+
@TestConfiguration(proxyBeanMethods = false)
64+
static class TestGrpcServerConfig {
65+
66+
/**
67+
* Create a test gRPC service implementation for the Simple service.
68+
* @return the gRPC service implementation
69+
*/
70+
@Bean
71+
SimpleGrpc.SimpleImplBase simpleService() {
72+
return new SimpleGrpc.SimpleImplBase() {
73+
74+
@Override
75+
public void sayHello(HelloRequest request, io.grpc.stub.StreamObserver<HelloReply> responseObserver) {
76+
HelloReply reply = HelloReply.newBuilder()
77+
.setMessage("Hello " + request.getName())
78+
.build();
79+
responseObserver.onNext(reply);
80+
responseObserver.onCompleted();
81+
}
82+
83+
@Override
84+
public void streamHello(HelloRequest request,
85+
io.grpc.stub.StreamObserver<HelloReply> responseObserver) {
86+
87+
HelloReply reply = HelloReply.newBuilder()
88+
.setMessage("Hello " + request.getName())
89+
.build();
90+
responseObserver.onNext(reply);
91+
reply = HelloReply.newBuilder()
92+
.setMessage("Hello Again")
93+
.build();
94+
responseObserver.onNext(reply);
95+
responseObserver.onCompleted();
96+
}
97+
};
98+
}
99+
100+
}
101+
102+
}

0 commit comments

Comments
 (0)