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 comments
By 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 or WebClient) 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. 

Requirements

The fully fledged server uses the following:

  • Spring Framework
  • SpringBoot
  • Spring Data Jpa
  • H2 database
  • Grpc Server
  • io.grpc
  • Lombok
  • Mapstruct

Plugin

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