gRPC Protobuf

How to build a Spring + Hibernate + gRPC application

1. Overview

This tutorial will provide an introduction to Spring Data JPA having a Spring project using Java 17, Hibernate 6, lombok, and gRPC , it will cover configuring the persistence layer. For a step-by-step introduction to setting up the Spring context using Java-based configuration and gRPC service definition plus support classes generation, you can checkout the projection configuration and sources from our repository

2. gRPC

gRPC (Google Remote Procedure Calls) is a cross-platform open source high performance remote procedure call (RPC) framework. gRPC was initially created by Google, which has used a single general-purpose RPC infrastructure called Stubby to connect the large number of microservices running within and across its data centers for over a decade

3. JPA (Jakarta Persistence API)

Jakarta Persistence API (JPA; formerly Java Persistence API) is a Jakarta EE application programming interface specification that describes the management of relational data in enterprise Java applications, is the most well know Java object/relational mapping API, that uses Java Annotations and has been used in the industry for many years

4. Entity Class

Creating the JPA's entities, is one of the most important tasks on our implementation since this where the ORM (object/relational mapping) is happening, the class will map the tables and fields that exists in our database, lately since the transition from Java EE to Jakarta EE we have to use the new package "jakarta.persistence" instead of "java.persistence" or else entities will not be found by the normal spring entity scan (@EntityScan)

in the below example we use @Entity to define that en class will be an entity and by default this will be the name of the table and it can be overwritten by @Table, in order the map the fields of the table we use @Column, in this example (Only for Java) we use Lombok to avoid Java's boilerplate code of getter/setters which are a requirement for JPA

                

import jakarta.persistence.*;
import lombok.*;

@Entity
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "UserData")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name="identifier")
    private Long id;
    @Column(name="fullName", length = 40, nullable = false)
    private String fullName;
    @Column(name="email", length = 50, unique = true, nullable = false)
    private String email;
}                                        
                
            

User.java

                

import jakarta.persistence.*

@Entity
@Table(name = "UserData")
class User (
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "identifier")
    var id: Long? = null,
    @Column(name="fullName", length = 40, nullable = false)
    var fullName: String,
    @Column(name="email", length = 50, unique = true, nullable = false)
    var email: String
)

                    
                
            

User.kt

                

import jakarta.persistence.{Column, Entity, GeneratedValue, Id, Table}
import scala.beans.BeanProperty
@Entity
@Table(name = "UserData")
class User (@BeanProperty
            @Column(name = "fullName", length = 40, nullable = false)
            var fullName: String,
            @Column(name = "email", length = 50, nullable = false, unique = true)
            @BeanProperty var email: String){
  @Id
  @GeneratedValue
  @Column(name = "identifier")
  var id: Long = _
  def this() = this(null,null)
}

                    
                
            

User.scala

5. Repository

The main advantage of Spring Data is avoiding the boilerplate code from JPA, specially the generating JPQL queries

by implementing one of the Repository interfaces, the repository will already have some basic CRUD methods (and queries) defined and implemented. To define more specific access methods, Spring Data JPA supports quite the following options:

  • Simply define a new method in the interface
  • Provide the actual JPQL query by using the @Query annotation
  • Use the more advanced Specification and Querydsl support in Spring Data
  • Define custom queries via JPA Named Queries
                

import com.the.sample.app.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository {
    Optional findByEmail(String email);
}                                     
                
            

UserRepository.java

                

import com.the.sample.app.model.User
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import java.util.*

@Repository
interface UserRepository : JpaRepository{
    fun findByEmail(email: String?): Optional
}

                    
                
            

UserRepository.kt

                

import com.the.sample.app.model.User
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Repository
trait UserRepository extends JpaRepository[User,Long]{
  def findByEmail(email: String): User
}

                    
                
            

UserRepository.scala

6. Service

The service layer defines any business logic that needs to be implemented as well the transactional layer by using the @Transactional, any data access outside this layer is not permitted, ex a common issue is trying to access a collection that depends on a JPA releations on the integration layer like Rest or gRPC, this will trigger a JPA session error.

                

import com.the.sample.app.model.User;
import com.the.sample.app.repository.UserRepository;
import lombok.AllArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;
@Service
@Transactional
@AllArgsConstructor
public class UserServiceImpl implements UserService{
    private final UserRepository userRepository;

    @Override
    public List findAll(int page, int pageSize) {
        return userRepository.findAll(PageRequest.of(page,pageSize)).toList();
    }

    @Override
    public Optional findById(Long id) {
        return userRepository.findById(id);
    }

    @Override
    public Optional findByEmail(String email) {
        return userRepository.findByEmail(email);
    }

    @Override
    public void save(User user) {
        userRepository.save(user);
    }

    @Override
    public void deleteById(Long id) {
        userRepository.deleteById(id);
    }
}                                  
                
            

UserServiceImpl.java

                

import com.the.sample.app.model.User
import com.the.sample.app.repository.UserRepository
import org.springframework.data.domain.PageRequest
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

interface UserService {
    fun findAll(page: Int, pageSize: Int): List
    fun findById(id: Long): User?
    fun findByEmail(email: String): User?
    fun save(user: User)
    fun deleteById(id: Long)
}

@Service
@Transactional
class UserServiceImpl(val userRepository: UserRepository) : UserService{
    override fun findAll(page: Int, pageSize: Int): List {
        return userRepository.findAll(PageRequest.of(page,pageSize)).toList()
    }
    override fun findById(id: Long): User? {
        return userRepository.findById(id).orElse(null)
    }

    override fun findByEmail(email: String): User? {
        return userRepository.findByEmail(email).orElse(null)
    }

    override fun save(user: User) {
        userRepository.save(user)
    }

    override fun deleteById(id: Long) {
        userRepository.deleteById(id)
    }
}

                    
                
            

UserService.kt

                

import com.the.sample.app.model.User
import com.the.sample.app.repository.UserRepository
import org.springframework.data.domain.PageRequest
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

trait UserService {
  def findAll(page: Int, pageSize: Int): java.util.List[User]

  def findById(id: Long): Option[User]

  def findByEmail(email: String): Option[User]

  def save(user: User): Unit

  def deleteById(id: Long): Unit
}

@Service
@Transactional
class UserServiceImpl(userRepository: UserRepository) extends UserService {
  import scala.jdk.OptionConverters._
  override def findAll(page: Int, pageSize: Int): java.util.List[User] =
    userRepository.findAll(PageRequest.of(page, pageSize)).toList

  override def findById(id: Long): Option[User] = userRepository.findById(id).toScala

  override def findByEmail(email: String): Option[User] = userRepository.findByEmail(email).toScala

  override def save(user: User): Unit = userRepository.save(user)

  override def deleteById(id: Long): Unit = userRepository.deleteById(id)
}

                    
                
            

UserService.scala

7. Grpc Message / Service Definition

This is where we define the Messages and Services where our microservice can be called by gRPC using a binary protocol reducing latency. this definition is made on protobuf

                
syntax = "proto3";
import "google/protobuf/empty.proto";
option java_package = "com.the.sample.app.grpc";

message CreateUserRequest{
  string email = 1;
  string fullName = 2;
}

message UpdateUserRequest{
  uint64 id = 1;
  string email = 2;
  string fullName = 3;
}

message UserByIdRequest{
  uint64 id = 1;
}

message UserByEmailRequest{
  string email = 1;
}

message UserDto{
  uint64 id = 1;
  string email = 2;
  string fullName = 3;
}

message UserResponse{
  UserDto user = 1;
}

service UserServiceEndpoint{
    rpc findById(UserByIdRequest) returns (UserResponse);
    rpc findByEmail(UserByEmailRequest) returns (UserResponse);
    rpc save(CreateUserRequest) returns (google.protobuf.Empty);
    rpc update(UpdateUserRequest) returns (google.protobuf.Empty);
    rpc deleteById(UserByIdRequest) returns (google.protobuf.Empty);
}
                
            

this definition will be compile by the protobuf compiler and generate the supported classes (Stub and Messages classes) to implement the gRPC service, in order to facilitate this compilation they are many maven, gradle and sbt plugins that automatically makes this compilation.

                

import com.google.protobuf.Empty;
import com.the.sample.app.model.User;
import com.the.sample.app.service.UserService;
import io.grpc.stub.StreamObserver;
import lombok.AllArgsConstructor;
import org.lognet.springboot.grpc.GRpcService;

import static com.the.sample.app.grpc.UserServiceEndpointGrpc.UserServiceEndpointImplBase;
import static com.the.sample.app.grpc.UserServiceEndpointOuterClass.UserByIdRequest;
import static com.the.sample.app.grpc.UserServiceEndpointOuterClass.UserResponse;
import static com.the.sample.app.grpc.UserServiceEndpointOuterClass.CreateUserRequest;
import static com.the.sample.app.grpc.UserServiceEndpointOuterClass.UpdateUserRequest;
import static com.the.sample.app.grpc.UserServiceEndpointOuterClass.UserDto;
import static com.the.sample.app.grpc.UserServiceEndpointOuterClass.UserByEmailRequest;
import java.util.Optional;

@GRpcService
@AllArgsConstructor
public class UserServiceEndpointImpl extends UserServiceEndpointImplBase {
    private final UserService userService;

    @Override
    public void findById(UserByIdRequest request, StreamObserver responseObserver) {
        UserDto userDto = Optional.ofNullable(request).map(UserByIdRequest::getId).
                map(userService::findById).
                flatMap(res ->
                res.map(userData->UserDto.newBuilder().
                        setEmail(userData.getEmail()).
                        setFullName(userData.getFullName()).build())).orElse(null);
        responseObserver.onNext(UserResponse.newBuilder().setUser(userDto).build());
        responseObserver.onCompleted();
    }

    @Override
    public void findByEmail(UserByEmailRequest request, StreamObserver responseObserver) {
        UserDto userDto = Optional.ofNullable(request).map(UserByEmailRequest::getEmail).
                map(userService::findByEmail).
                flatMap(res ->
                        res.map(userData->UserDto.newBuilder().
                                setEmail(userData.getEmail()).
                                setFullName(userData.getFullName()).build())).orElse(null);
        responseObserver.onNext(UserResponse.newBuilder().setUser(userDto).build());
        responseObserver.onCompleted();
    }

    @Override
    public void save(CreateUserRequest request, StreamObserver responseObserver) {
        Optional.ofNullable(request).ifPresent(req ->
                userService.save(User.builder().
                    email(req.getEmail()).
                    fullName(req.getFullName()).build()));
        responseObserver.onNext(Empty.newBuilder().build());
        responseObserver.onCompleted();
    }

    @Override
    public void update(UpdateUserRequest request, StreamObserver responseObserver) {
        Optional.ofNullable(request).ifPresent(req ->
                userService.save(User.builder().
                        id(req.getId()).
                        email(req.getEmail()).
                        fullName(req.getFullName()).build()));
        responseObserver.onNext(Empty.newBuilder().build());
        responseObserver.onCompleted();
    }

    @Override
    public void deleteById(UserByIdRequest request, StreamObserver responseObserver) {
        Optional.ofNullable(request).
                map(UserByIdRequest::getId).
                ifPresent(id->userService.deleteById(id));
        responseObserver.onNext(Empty.newBuilder().build());
        responseObserver.onCompleted();
    }
}                               
                
            

UserServiceEndpointImpl.java

                

import com.google.protobuf.Empty
import com.the.sample.app.grpc.UserServiceEndpointGrpc.UserServiceEndpointImplBase
import com.the.sample.app.grpc.UserServiceEndpointOuterClass.*
import com.the.sample.app.model.User
import com.the.sample.app.service.UserService
import io.grpc.stub.StreamObserver
import org.lognet.springboot.grpc.GRpcService
import java.util.*

@GRpcService
class UserServiceEndpointImpl(val userService: UserService) : UserServiceEndpointImplBase() {
    override fun findById(request: UserByIdRequest, responseObserver: StreamObserver) {
        val userDto: UserDto? = Optional.ofNullable(request).
            map { request: UserByIdRequest -> request.id }.
            map { id: Long -> userService.findById(id) }.
            flatMap { res-> Optional.ofNullable(res).map { userData: User ->
            UserDto.newBuilder().setEmail(userData.email).setFullName(userData.fullName).build() } }.
            orElse(null)
        responseObserver.onNext(UserResponse.newBuilder().setUser(userDto).build())
        responseObserver.onCompleted()
    }

    override fun findByEmail(request: UserByEmailRequest, responseObserver: StreamObserver) {
        val userDto: UserDto? = Optional.ofNullable(request).
            map { request: UserByEmailRequest -> request.email }.
            map { email: String -> userService.findByEmail(email) }.
            flatMap { res-> Optional.ofNullable(res).map { userData: User ->
                UserDto.newBuilder().setEmail(userData.email).setFullName(userData.fullName).build() } }.
            orElse(null)
        responseObserver.onNext(UserResponse.newBuilder().setUser(userDto).build())
        responseObserver.onCompleted()
    }

    override fun save(request: CreateUserRequest, responseObserver: StreamObserver) {
        Optional.ofNullable(request).ifPresent { req: CreateUserRequest ->
            userService!!.save(
                User(email = req.email, fullName = req.fullName)
            )
        }
        responseObserver.onNext(Empty.newBuilder().build())
        responseObserver.onCompleted()
    }

    override fun update(request: UpdateUserRequest?, responseObserver: StreamObserver) {
        Optional.ofNullable(request).ifPresent { req: UpdateUserRequest ->
            userService!!.save(
                User(id = req.id,
                    email = req.email,
                    fullName = req.fullName)
            )
        }
        responseObserver.onNext(Empty.newBuilder().build())
        responseObserver.onCompleted()
    }

    override fun deleteById(request: UserByIdRequest, responseObserver: StreamObserver) {
        Optional.ofNullable(request).map { obj: UserByIdRequest -> obj.id }.ifPresent { id: Long? ->
            userService!!.deleteById(
                id!!
            )
        }
        responseObserver.onNext(Empty.newBuilder().build())
        responseObserver.onCompleted()
    }
}

                    
                
            

UserServiceEndpointImpl.kt

                

import com.the.sample.app.model.User
import com.google.protobuf.empty.Empty
import com.the.sample.app.grpc.UserServiceEndpoint.{CreateUserRequest, UpdateUserRequest, UserByEmailRequest, UserByIdRequest, UserDto, UserResponse, UserServiceEndpointGrpc}
import com.the.sample.app.service.UserService
import org.lognet.springboot.grpc.GRpcService

import scala.concurrent.Future
@GRpcService
class UserServiceEndpointImpl(val userService: UserService) extends UserServiceEndpointGrpc.UserServiceEndpoint{
  override def findById(request: UserByIdRequest): Future[UserResponse] =
    Future.successful(UserResponse(user = Option(request).map(_.id).
      flatMap(userService.findById(_)).map(user=>
      UserDto(id = user.id,
        email = user.email,
        fullName = user.fullName))))


  override def findByEmail(request: UserByEmailRequest): Future[UserResponse] =
    Future.successful(UserResponse(user = Option(request).map(_.email).
      flatMap(userService.findByEmail(_)).map(user=>
      UserDto(id = user.id,
        email = user.email,
        fullName = user.fullName))))

  override def save(request: CreateUserRequest): Future[Empty] = {
    Option(request).
      map(req=>new User(req.email,req.fullName)).
        foreach(userService.save(_))
    Future.successful(Empty.defaultInstance)
  }

  override def update(request: UpdateUserRequest): Future[Empty] = {
    Option(request).
      map(req=>{
        val user = new User(req.email,req.fullName)
        user.id = req.id
        user
      }).
      foreach(userService.save(_))
    Future.successful(Empty.defaultInstance)
  }

  override def deleteById(request: UserByIdRequest): Future[Empty] = {
    Option(request).
      map(_.id).
      foreach(userService.deleteById(_))
    Future.successful(Empty.defaultInstance)
  }
}

                    
                
            

UserServiceEndpointImpl.scala

9. Spring Boot Context Configuration

This is the main component that starts our Spring Context and our JPA session manager

                

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@SpringBootApplication
@EntityScan(basePackages = "com.the.sample.app")
@EnableJpaRepositories(basePackages = {"com.the.sample.app"})
@EnableTransactionManagement
public class SpringHibernateCrudApplication {
	public static void main(String[] args) {
		SpringApplication.run(SpringHibernateCrudApplication.class, args);
	}
}                                 
                
            

SpringHibernateCrudApplication.java

                

import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.autoconfigure.domain.EntityScan
import org.springframework.data.jpa.repository.config.EnableJpaRepositories
import org.springframework.transaction.annotation.EnableTransactionManagement

@SpringBootApplication
@EntityScan(basePackages = ["com.the.sample.app"])
@EnableJpaRepositories(basePackages = ["com.the.sample.app"])
@EnableTransactionManagement
class SpringHibernateCrudApplication

fun main(args: Array) {
    SpringApplication.run(SpringHibernateCrudApplication::class.java, *args)
}

                    
                
            

SpringHibernateCrudApplication.kt

                

import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.autoconfigure.domain.EntityScan
import org.springframework.data.jpa.repository.config.EnableJpaRepositories
import org.springframework.transaction.annotation.EnableTransactionManagement


@SpringBootApplication
@EntityScan(basePackages = Array("com.the.sample.app"))
@EnableJpaRepositories(basePackages = {
  Array("com.the.sample.app")
})
@EnableTransactionManagement
class SpringHibernateCrudApplication

object SpringHibernateCrudApplication extends App{
  SpringApplication.run(classOf[SpringHibernateCrudApplication])
}

                    
                
            

SpringHibernateCrudApplication.scala

Database connection parameters are defined in the src/main/resources/application.properties
  • spring.datasource.driver-class-name - Defines the JDBC driver class name to use (JPA support the most common RDMS databases such as MySql, Postgres, Oracle, DB2)
  • spring.datasource.url - defines the JDBC connection url
  • spring.jpa.properties.hibernate.dialect - defines the database dialect to use for Queries or DDL statements

10. Source Code

Checkout the whole source code for the following programming languages: