gRPC is a modern open source high performance Remote Procedure Call (RPC) framework that can run in any environment.
Eang Sopheaktra
July 04 2024 10:19 am
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:
protoc
compiler is platform-dependent, making build-time stub generation complex.javax.annotation.Generated
annotation, necessitating an old Java EE Annotations dependency.RestClient
or WebClient
) can’t be directly configured for gRPC due to custom transport technologies used by gRPC1.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.
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:
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>
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>
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
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!");
}
}
}
Download the source code for the sample application with gRPC. After this tutorial you will learn such as: