gRPC with Spring Boot 3

gRPC is a modern open source high performance Remote Procedure Call (RPC) framework that can run in any environment.

Sopheaktra

Eang Sopheaktra

July 04 2024 10:19 am

0 214

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

Comments

Subscribe to our newsletter.

Get updates about new articles, contents, coding tips and tricks.

Weekly articles
Always new release articles every week or weekly released.
No spam
No marketing, share and spam you different notification.
© 2023-2025 Tra21, Inc. All rights reserved.