gRPC with Spring Boot 3
gRPC is a modern open source high performance Remote Procedure Call (RPC) framework that can run in any environment. 0 commentsBy Sopheaktra Eang | June 27, 2024
gRPC with Spring Boot 3
Spring Boot doesn’t directly support gRPC. Only Protocol Buffers are supported, allowing us to implement protobuf-based REST services.
To use gRPC with Spring Boot, we need to address a few challenges:
- Platform-dependent compiler: The
protoc
compiler is platform-dependent, making build-time stub generation complex. - Dependencies: We require compatible dependencies within our Spring Boot application. Unfortunately, protoc for Java adds a
javax.annotation.Generated
annotation, necessitating an old Java EE Annotations dependency. - Server Runtime: gRPC service providers need to run within a server. We can use the shaded Netty provided by the gRPC for Java project or replace it with a Spring Boot-provided server.
- Message Transport: Spring Boot’s clients (e.g.,
RestClient
orWebClient
) can’t be directly configured for gRPC due to custom transport technologies used by gRPC1.
What's gRPC?
gRPC is a modern open-source high-performance Remote Procedure Call (RPC) framework. It can run in any environment and efficiently connects services across data centers.
Key Points
- Definition: gRPC allows client applications to directly call methods on a server application running on a different machine, as if it were a local object. This simplifies the creation of distributed applications and services.
- Service Definition: In gRPC, you define a service by specifying the methods that can be called remotely, along with their parameters and return types. The server implements this interface and runs a gRPC server to handle client calls.
- Client-Server Interaction: On the client side, there’s a stub (referred to as a client in some languages) that provides the same methods as the server. gRPC clients and servers can communicate in various environments and languages.
- Protocol Buffers: By default, gRPC uses Protocol Buffers, which are Google’s mature open-source mechanism for serializing structured data. Protocol buffers define the structure of data in a .proto file, and the protocol buffer compiler generates data access classes in your preferred language.
- Cross-Platform: gRPC is cross-platform and supports load balancing, tracing, health checking, and authentication.
Benefit to use it
- Performance: gRPC is designed for high performance. It uses HTTP/2 as the transport protocol, which allows multiplexing multiple requests over a single connection. This reduces latency and improves efficiency.
- Efficient Serialization: gRPC uses Protocol Buffers (protobufs) for data serialization. Protobufs are compact, efficient, and language-agnostic, making them ideal for communication between different services.
- Strongly Typed Contracts: With gRPC, you define service contracts using a .proto file. This contract specifies the methods, input parameters, and return types. The generated code enforces these contracts, ensuring type safety and consistency.
- Bidirectional Streaming: gRPC supports bidirectional streaming, allowing both clients and servers to send multiple messages in a single connection. This is useful for real-time communication and chat applications.
- Load Balancing and Service Discovery: gRPC integrates seamlessly with tools like Envoy and etcd for service discovery and load balancing. It simplifies managing distributed systems.
- Authentication and Security: gRPC provides built-in support for authentication, encryption, and authorization. You can use SSL/TLS for secure communication.
- Language Support: gRPC supports multiple programming languages, including Java, Python, Go, C++, and more. This flexibility allows you to build polyglot microservices.
- Interoperability: While gRPC is commonly associated with Google’s ecosystem, it’s not limited to it. You can use gRPC with non-Google technologies, making it suitable for heterogeneous environments.
What's kind of use case with gRPC?
- Distributed Systems: gRPC is ideal for building communication channels between distributed services. It simplifies interactions across different components of a system.
- Microservices Architecture: In microservices-based architectures, gRPC facilitates communication between microservices. It offers strong typing, efficient serialization, and bidirectional streaming.
- Hybrid Cloud Application Development: When building applications that span both on-premises and cloud environments, gRPC ensures seamless communication between services regardless of their location.
- Cross-Platform Mobile Apps: gRPC supports multiple programming languages, making it suitable for mobile app development. It allows efficient communication between mobile clients and backend services.
gRPC vs websocket
- Transport Protocol:
- WebSocket: Uses TCP directly.
- gRPC: Built on top of HTTP/2.
- Data Format:
- WebSocket: Can send text or binary data.
- gRPC: Uses Protocol Buffers (Protobuf) for serializing structured data.
- Streaming:
- Both support bidirectional streaming.
- gRPC provides a more structured approach with defined service contracts.
- Web Browser Support:
- WebSocket: Works well in browsers.
- gRPC: Works in the browser with gRPC-web, but request streaming and bidirectional streaming are not fully supported due to their dependence on HTTP/2.
Noted: if you need high-performance communication with streaming capabilities, gRPC is a great choice. For simpler continuous communication, WebSocket might be more suitable.
The fully fledged server uses the following:
- Spring Framework
- SpringBoot
- Spring Data Jpa
- H2 database
- Grpc Server
- io.grpc
- Lombok
- Mapstruct
There are a number of third-party plugins
<plugin>
<groupId>com.github.os72</groupId>
<artifactId>protoc-jar-maven-plugin</artifactId>
<version>3.11.4</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<includeMavenTypes>direct</includeMavenTypes>
<inputDirectories>
<include>src/main/resources</include>
</inputDirectories>
<outputTargets>
<outputTarget>
<type>java</type>
<addSources>none</addSources>
<cleanOutputFolder>true</cleanOutputFolder>
<outputDirectory>${project.basedir}/src/main/generated</outputDirectory>
</outputTarget>
<outputTarget>
<type>grpc-java</type>
<addSources>none</addSources>
<cleanOutputFolder>true</cleanOutputFolder>
<outputDirectory>${project.basedir}/src/main/generated</outputDirectory>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${io.grpc.version}</pluginArtifact>
</outputTarget>
</outputTargets>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>src/main/generated</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
Dependencies
There are a number of third-party dependencies used in the project. Browse the Maven pom.xml file for details of libraries and versions used.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-server-spring-boot-starter</artifactId>
<version>${grpc.server.starter.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>${io.grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>${io.grpc.version}</version>
</dependency>
<dependency> <!-- necessary for Java 9+ -->
<groupId>org.apache.tomcat</groupId>
<artifactId>annotations-api</artifactId>
<version>6.0.53</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Resource
Create package in resource with "proto/profile.proto"
syntax = "proto3";
package com.tra21.grpc;
option java_multiple_files = true;
option java_package = "com.tra21.grpc";
message CreateProfile {
string first_name = 1;
string last_name = 2;
bytes image_file = 3;
}
message UpdateProfile {
int32 id = 1;
string first_name = 2;
string last_name = 3;
bytes image_file = 4;
}
message ProfileDto {
int32 id = 1;
string first_name = 2;
string last_name = 3;
string image_path = 4;
}
message PaginationReq {
int32 page = 1;
int32 size = 2;
}
message PaginationRes {
int32 page = 1;
int32 size = 2;
int32 total_elements = 3;
int32 total_pages = 4;
}
message ProfilePage {
PaginationRes pagination_res = 1;
repeated ProfileDto data = 2;
}
message ProfileId {
int32 id = 1;
}
service ProfileService {
rpc GetProfiles(PaginationReq) returns (ProfilePage){}
rpc GetProfile(ProfileId) returns (ProfileDto){}
rpc CreateProfileProof(stream CreateProfile) returns (ProfileDto) {}
rpc UpdateProfileProof(stream UpdateProfile) returns (ProfileDto) {}
}
create "application.properties"
spring.application.name=grpc
server.port=8080
spring.profiles.active=dev
create "application-dev.yaml" for profile dev
spring:
h2:
console.enabled: true
datasource:
url: jdbc:h2:mem:testdb
driverClassName: org.h2.Driver
username: sa
password: password
jpa:
show-sql: true
format_sql: true
hibernate:
ddl-auto: update
properties:
hibernate:
dialect: org.hibernate.dialect.H2Dialect
ddl-update: update
jdbc:
lob:
non_contextual_creation: true
Code
Create service pakage with directory "services/grpc/ProfileGrpcServiceImpl.java"
package com.tra21.grpc.services.grpc;
import com.tra21.grpc.CreateProfile;
import com.tra21.grpc.PaginationReq;
import com.tra21.grpc.ProfileDto;
import com.tra21.grpc.ProfileId;
import com.tra21.grpc.ProfilePage;
import com.tra21.grpc.ProfileServiceGrpc.ProfileServiceImplBase;
import com.tra21.grpc.UpdateProfile;
import com.tra21.grpc.mappers.PageMapper;
import com.tra21.grpc.mappers.ProfileMapper;
import com.tra21.grpc.services.IProfileService;
import com.tra21.grpc.services.upload.interf.IUploadService;
import com.tra21.grpc.stream.CreateProfileProofObserver;
import com.tra21.grpc.stream.UpdateProfileProofObserver;
import io.grpc.stub.StreamObserver;
import lombok.RequiredArgsConstructor;
import net.devh.boot.grpc.server.service.GrpcService;
@GrpcService
@RequiredArgsConstructor
public class ProfileGrpcServiceImpl extends ProfileServiceImplBase {
private final IProfileService profileService;
private final ProfileMapper profileMapper;
private final PageMapper pageMapper;
private final IUploadService uploadService;
@Override
public void getProfiles(PaginationReq paginationReq, StreamObserver<ProfilePage> responseObserver){
ProfilePage response = pageMapper.mapPageGrpcList(
pageMapper.mapPageList(
profileService.pageOfProfiles(pageMapper.mapPage(paginationReq))
)
);
responseObserver.onNext(response);
responseObserver.onCompleted();
}
@Override
public void getProfile(ProfileId profileId, StreamObserver<ProfileDto> responseObserver) {
ProfileDto response = profileMapper.mapProfileGrpc(profileService.getProfile((long) profileId.getId()));
responseObserver.onNext(response);
responseObserver.onCompleted();
}
@Override
public StreamObserver<CreateProfile> createProfileProof(StreamObserver<ProfileDto> responseObserver){
return new CreateProfileProofObserver(responseObserver, profileService, uploadService, profileMapper);
}
@Override
public StreamObserver<UpdateProfile> updateProfileProof(StreamObserver<ProfileDto> responseObserver){
return new UpdateProfileProofObserver(responseObserver, profileService, uploadService, profileMapper);
}
}
In above services "ProfileGrpcServiceImpl" has upload so I created package with "services/upload/UploadService.java" and "services/upload/FileUploadService.java" implement from interface "services/upload/interf/IUploadService.java" and "services/upload/interf/IFileUploadService.java".
After that let give difinition to interface.
IUploadService interface's definition:
package com.tra21.grpc.services.upload.interf;
import com.tra21.grpc.dtos.global.requests.UploadImagesDto;
import com.tra21.grpc.dtos.global.responses.ImageDto;
public interface IUploadService {
ImageDto uploadImage(UploadImagesDto uploadImagesDto);
}
IFileUploadService interface's definition:
package com.tra21.grpc.services.upload.interf;
import org.springframework.core.io.Resource;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.stream.Stream;
public interface IFileUploadService {
public void init();
public void save(ByteArrayOutputStream byteArrayOutputStream, String filename);
public void save(ByteArrayOutputStream byteArrayOutputStream, String path, String fileName);
public String generateNewFilename(String oldName, String newName);
public Resource load(String filename);
public void deleteAll();
public Stream<Path> loadAll();
}
After interface definition so we need to give definition to our service that implement above interface.
UploadService's definition:
package com.tra21.grpc.services.upload;
import com.tra21.grpc.dtos.global.requests.UploadImagesDto;
import com.tra21.grpc.dtos.global.responses.ImageDto;
import com.tra21.grpc.services.upload.interf.IFileUploadService;
import com.tra21.grpc.services.upload.interf.IUploadService;
import lombok.RequiredArgsConstructor;
import org.slf4j.helpers.MessageFormatter;
import org.springframework.stereotype.Service;
import java.util.UUID;
@Service
@RequiredArgsConstructor
public class UploadService implements IUploadService {
private final IFileUploadService fileUploadService;
private final String pathUpload = "images";
@Override
public ImageDto uploadImage(UploadImagesDto uploadImagesDto) {
String newLogoFilename = fileUploadService.generateNewFilename("old_name." + uploadImagesDto.getFile().getFileType(), UUID.randomUUID().toString());
fileUploadService.save(
uploadImagesDto.getByteArrayOutputStream(),
pathUpload,
newLogoFilename
);
return ImageDto.builder()
.filename(newLogoFilename)
.fileType(uploadImagesDto.getFile().getFileType())
.pathFile(MessageFormatter.format("{}/{}", pathUpload, newLogoFilename).getMessage())
.build();
}
}
FileUploadService's definition:
package com.tra21.grpc.services.upload;
import com.tra21.grpc.services.upload.interf.IFileUploadService;
import jakarta.annotation.PostConstruct;
import lombok.extern.log4j.Log4j2;
import org.slf4j.helpers.MessageFormatter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.util.FileSystemUtils;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
@Service
@Log4j2
public class FileUploadService implements IFileUploadService {
@Value("${upload.folder.path:file:///grpc/uploads}")
private String uploadFolderPath;
@Autowired
private ResourceLoader resourceLoader;
private Path root; //= Paths.get("demo/src/main/resources/static/uploads");
@PostConstruct
private void FileUploadServicePostConstruct() {
try {
this.root = Paths.get(resourceLoader.getResource(uploadFolderPath).getURI());
} catch (IOException e) {
log.error("Error to get main path upload file", e);
}
}
@Override
public void init() {
try {
Files.createDirectories(root);
} catch (IOException e) {
throw new RuntimeException("Could not initialize folder for upload!");
}
}
@Override
public void save(ByteArrayOutputStream byteArrayOutputStream, String filename) {
try {
FileOutputStream fileOutputStream = new FileOutputStream(this.root.resolve(Objects.requireNonNull(filename)).toFile());
byteArrayOutputStream.writeTo(fileOutputStream);
fileOutputStream.close();
} catch (Exception e) {
if (e instanceof FileAlreadyExistsException) {
throw new RuntimeException("A file of that name already exists.");
}
throw new RuntimeException(e.getMessage());
}
}
@Override
public void save(ByteArrayOutputStream byteArrayOutputStream, String path, String fileName) {
try {
File pathFile = new File(path);
if(!pathFile.exists()){
boolean isCreate = pathFile.mkdirs();
}
String filePath = MessageFormatter.format("{}/{}", path, fileName).getMessage();
FileOutputStream fileOutputStream = new FileOutputStream(this.root.resolve(Paths.get(filePath)).toFile());
byteArrayOutputStream.writeTo(fileOutputStream);
fileOutputStream.close();
} catch (Exception e) {
log.error("file upload ", e);
if (e instanceof FileAlreadyExistsException) {
throw new RuntimeException("A file of that name already exists.");
}
throw new RuntimeException(e.getMessage());
}
}
@Override
public String generateNewFilename(String oldName, String newName) {
String[] exLogo = Optional.ofNullable(oldName).orElse("").split("\\.");
return MessageFormatter.format("{}.{}", newName, exLogo[exLogo.length - 1]).getMessage();
}
@Override
public Resource load(String filename) {
try {
Path file = root.resolve(filename);
Resource resource = new UrlResource(file.toUri());
if (resource.exists() || resource.isReadable()) {
return resource;
} else {
throw new RuntimeException("Could not read the file!");
}
} catch (MalformedURLException e) {
throw new RuntimeException("Error: " + e.getMessage());
}
}
@Override
public void deleteAll() {
FileSystemUtils.deleteRecursively(root.toFile());
}
@Override
public Stream<Path> loadAll() {
try {
return Files.walk(this.root, 1).filter(path -> !path.equals(this.root)).map(this.root::relativize);
} catch (IOException e) {
throw new RuntimeException("Could not load the files!");
}
}
}
Summary
Download the source code for the sample application with gRPC. After this tutorial you will learn such as:
- Spring Framework
- SpringBoot
- Spring Data Jpa
- H2 database
- gRPC
- Lombok
- Mapstruct