🧑‍💻 Tutorial de gRPC con Spring Boot y Java

Ahora que ya sabemos que es gRPC, vamos a desarrollar nuestra aplicación de Spring Boot. Puedes acceder al código de este tutorial en mi repositorio de GitHub.

Si lo prefieres, puedes checar el tutorial en video en mi canal de YouTube en donde explico paso a paso como crear y probar el servicio gRPC utilizando Java, Spring Boot y Postman

Requisitos previos

Utilizaremos Java 8, puedes usar el editor de código que quieras y para probar utilizaremos Postman. Crearemos dos aplicaciones, la primera es la interfaz y la segunda es el servicio.

1. Desarrollo de la interfaz

La interfaz es un proyecto separado del servicio gRPC que vamos a desarrollar en Spring Boot y nos va a permitir lo siguiente:

  • Definir los servicios y métodos que se van a ejecutar en el servidor y en el cliente
  • Definir los datos que cada método va a recibir y responder en cada ejecución
  • Compilar los protobuf en interfaces de código Java en un paquete individual para que puedan ser utilizados por el cliente y servidor

Y este último punto es importante, porque si el cliente es Java también, entonces podemos simplemente importar este proyecto separado y listo, ya tenemos definido el cliente. En este proyecto podemos definir todos los servicios que queramos e implementarlo en tantos módulos queramos dividirlos. 

1.1 Crear el proyecto de interfaz en maven

En una carpeta vacía, vamos a abrir la terminal o línea de comandos y vamos a ejecutar lo siguiente:

$ mvn archetype:generate "-DgroupId=com.faxterol.grpcdemo" "-DartifactId=grpc-interfaces" "-DarchetypeArtifactId=maven-archetype-quickstart" "-DinteractiveMode=false"

Esto va a generar un proyecto maven desde cero y a crear el pom file y las carpetas necesarias para nuestro proyecto Java.

1.2 Actualizar el POM file del proyecto de la interfaz

Vamos a agregar al POM file las dependencias necesarias para compilar los archivos de protobuf, el compilador de protobuf se llama protoc.

<dependency>
     <groupId>io.grpc</groupId>
     <artifactId>grpc-stub</artifactId>
     <version>${grpc.version}</version>
</dependency>
<dependency>
     <groupId>io.grpc</groupId>
     <artifactId>grpc-protobuf</artifactId>
     <version>${grpc.version}</version>
</dependency>
<!-- Si usas Java 11, este paquete no viene de forma nativa, incluyelo en tu pom file -->
<dependency>
      <groupId>javax.annotation</groupId>
      <artifactId>javax.annotation-api</artifactId>
      <version>1.3.2</version>
</dependency>

Después vamos a poner las propiedades necesarias para decirle a maven que vamos a utilizar Java 8 y a definir las versiones del compilador de protobuf. 

<properties>
    <protobuf.version>3.19.2</protobuf.version>
    <protobuf-plugin.version>0.6.1</protobuf-plugin.version>
    <grpc.version>1.47.0</grpc.version>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
</properties>

Por último, vamos a agregar las propiedades del build. Esto básicamente es para indicarle a maven que, cada vez que nosotros hagamos un build del proyecto, se van a compilar los archivos protobuff y se van a crear las interfaces Java necesarias que van a definir los datos y servicios gRPC.

<build>
    <extensions>
        <extension>
            <groupId>kr.motd.maven</groupId>
            <artifactId>os-maven-plugin</artifactId>
            <version>1.7.0</version>
        </extension>
    </extensions>
 
    <plugins>
        <plugin>
            <groupId>org.xolstice.maven.plugins</groupId>
            <artifactId>protobuf-maven-plugin</artifactId>
            <version>${protobuf-plugin.version}</version>
            <configuration>
               <protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
                <pluginId>grpc-java</pluginId>
                <pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>compile</goal>
                        <goal>compile-custom</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

1.3 Definir un método gRPC con su petición y respuesta. 

A continuación vamos a definir un archivo protobuf, en el cual vamos a especificar el nombre del servicio, el o los métodos que va a tener y los datos que va a recibir y responder cada método. Primero vamos a entrar a src/main y crear una carpeta llamada proto. Dentro de src/main/proto, vamos a crear el archivo protobuf, puede ser el nombre que quieras pero debe tener extensión .proto. 

//Le indicamos que la sintaxis del protobuff es versión 3
syntax = "proto3";
 
//Definimos el paquete al que pertenece el protobuff, con esto podemos evitar conflictos si tenemos servicios y datos con el mismo nombre
package com.faxterol.grpcdemo.interfaces;
 
//Estas son como variables que le indicamos al compilador protoc, las opciones dependen de cada lenguaje.
option java_multiple_files = true; //Si es falso, se van a crear un solo archivo en el cual va a venir todo el código java generado
option java_package = "com.faxterol.grpcdemo.interfaces"; //El paquete java donde se van a crear las interfaces
option java_outer_classname = "ChatServiceProto"; //Es el nombre de la clase que se va a generar para este archivo proto y que necesita el compilador para generar las interfaces. Si no es definido, entonces se va a tomar el nombre del archivo en formato camel case.
 
//Definición de un servicio
service ChatService {
    //Definición de un método, comunicación unaria
    rpc enviarMensaje (EnviarMensaje) returns (RecibirMensaje) {}
    //Stream unidireccional cliente a servidor
    rpc enviarMultiplesMensajes (stream EnviarMensaje) returns (RecibirMensaje) {}
    //Stream unidireccional servidor a cliente
    rpc recibirMultiplesRespuestas (EnviarMensaje) returns (stream RecibirMensaje) {}
    //Stream bidireccional
    rpc enviarRecibirMultiplesMensajes (stream EnviarMensaje) returns (stream RecibirMensaje) {}
}
 
//Objeto de tipo mensaje, en este caso es para una petición que va a reibir un metodo
message EnviarMensaje {
    uint32 to = 1; //el tipo de dato del campo (uint32), el nombre del campo (to) y el número de orden irrepetible del campo. Los campos más utilizados son los primeros. Una vez especificado el mensaje y en produccion, el orden de los campos no puede cambiar. (o si, pero meterás en problemas a los stubs.)
    string message = 2;
}
 
//Objeto de tipo mensaje, es lo que va a responder un método.
message RecibirMensaje {
    uint32 from = 1;
    string message = 2;
}

1.4 Hacer un build

Vamos a regresar a nuestra línea de comandos y ejecutar

$ mvn clean install -DskipTests 

Con esto, se va a crear el jar file y maven lo va a poner en el repositorio local para utilizarlo en nuestro proyecto de Spring. Vamos a hacer este build cada vez que cambiemos algo en los archivos protobuf. 

Estas son las clases que protoc creó con base en el archivo proto. Tenemos las clases EnviarMensaje.java y RecibirMensaje.java que son para la petición y la respuesta del método que creamos. El archivo que tiene sufijo Grpc.java que contiene las clases y subclases que vamos a extender en nuestro servidor y cliente, el archivo con sufijo Proto.java que lo genera protoc para compilar el archivo y los archivos con sufijo OrBuilder.java que son interfaces que van a implementar sus propias clases (EnviarMensaje.java implementa la interfaz EnviarMensajeOrBuilder.java).

2. Desarrollar el servidor

Ahora que ya tenemos el proyecto de interfaz listo, vamos a crear el servidor gRPC. Para ello, vamos a empezar a crear un proyecto en Spring Boot. Puedes utilizar start.spring.io o tu herramienta preferida para hacerlo. En mi caso incluí las siguientes dependencias de Spring:

  • Spring Starter
  • Spring DevTools

2.1 Agregar dependencias de gRPC

Ahora vamos a agregar las dependencias necesarias para correr nuestro servicio gRPC, para ello, abrimos el POM file y agregamos lo siguiente:

<dependency>
    <groupId>io.github.lognet</groupId>
    <artifactId>grpc-spring-boot-starter</artifactId>
    <version>4.7.1</version>
</dependency>
<dependency>
    <groupId>com.faxterol.grpcdemo</groupId>
    <artifactId>grpc-interfaces</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

La primera dependencia es una librería open source que alguien implementó para facilitar la creación de servicios gRPC en Spring Boot. La segunda es nuestro proyecto de interfaz. Recuerda que antes de correr el servidor, debes compilar el proyecto para que el jar file sea puesto en el repositorio maven local. 

2.2 Implementar el servicio gRPC y sus métodos

Ahora toca el turno de implementar los métodos especificados en el archivo proto de la interfaz. El build de la interfaz creó la clase ChatServiceImplBase, hay que crear una clase en el servicio y extenderla. Asimismo, hay que agregar la anotación @GRpcService, esto creará el bean en Spring para que el servidor y los servicios gRPC existan. 

//Servicio gRPC
@GRpcService
public class ChatService extends ChatServiceImplBase{ //ChatServiceImplBase fue creado en el proyecto de la interfaz
 
    //Metodo unario
    @Override
    public void enviarMensaje(EnviarMensaje request, StreamObserver<RecibirMensaje> responseObserver) {
        //Crea la respuesta
        RecibirMensaje response = RecibirMensaje.newBuilder()
                                        .setFrom(1)
                                        .setMessage("Hello back!")
                                        .build();
 
        //Envia el mensaje
        responseObserver.onNext(response);
 
        //Cierra la conexión
        responseObserver.onCompleted();
    }
 
    //Stream unidireccional de cliente a servidor
    @Override
    public StreamObserver<EnviarMensaje> enviarMultiplesMensajes(StreamObserver<RecibirMensaje> responseObserver) {
        return new MultiplesMensajesStream(responseObserver);
        /*
         * Una solución alternativa es crear una clase anomina, esto es hacer el new StreamObserver e implementar los métodos necesarios.
         */
        //return new StreamObserver<EnviarMensaje>(){}
    }
 
    //Stream bidireccional
    @Override
    public StreamObserver<EnviarMensaje> enviarRecibirMultiplesMensajes(StreamObserver<RecibirMensaje> responseObserver) {
        return new MultiplesRespuestasStream(responseObserver);
    }
 
    //Stream unidireccional servidor-cliente
    @Override
    public void recibirMultiplesRespuestas(EnviarMensaje request, StreamObserver<RecibirMensaje> responseObserver) {
        Random random = new Random();
        RecibirMensaje mensajeRespuesta = null;
        for(int i=0;i<10;i++){
            mensajeRespuesta = RecibirMensaje.newBuilder()
                                .setFrom(random.nextInt())
                                .setMessage("Hola stream desde servidor "+random.nextInt())
                                .build();
            responseObserver.onNext(mensajeRespuesta);
        }
 
        responseObserver.onCompleted();
    }
   
}

En el stream bidireccional y unidireccional de cliente a servidor, ambos métodos regresan un objeto de tipo StreamObserver. Este objeto es parte de la librería de gRPC y podemos implementarlo como clase anónima o como una clase separada en otro archivo java. Dependiendo la complejidad se puede utilizar una u otra opción. En mi caso, decidí ilustrar el ejemplo creando clases separadas. 

public class MultiplesRespuestasStream implements StreamObserver<EnviarMensaje>{
    private StreamObserver<RecibirMensaje> responseObserver;
    private Random random = new Random();
 
    //Necesitamos inyectar el stream observer para poder responder al cliente el mensaje
    public MultiplesRespuestasStream(StreamObserver<RecibirMensaje> responseObserver){
        this.responseObserver = responseObserver;
    }
 
 
    @Override
    public void onNext(EnviarMensaje value) {
        System.out.println("MultiplesMensajesStream - Nuevo mensaje recibido: "+value);
 
        //En cuanto recibimos un mensaje, podemos enviar muchos mensajes de regreso al cliente
        RecibirMensaje mensajeRespuesta = null;
        for(int i=0;i<10;i++){
            mensajeRespuesta = RecibirMensaje.newBuilder()
                                .setFrom(random.nextInt())
                                .setMessage("Hola stream bidireccional desde servidor "+random.nextInt())
                                .build();
            this.responseObserver.onNext(mensajeRespuesta);
        }
        //Tambien puedes cerrar el stream ejecutando el onCompleted();
        //this.responseObserver.onCompleted();
       
    }
 
    @Override
    public void onError(Throwable t) {
        // TODO Auto-generated method stub
       
    }
 
    @Override
    public void onCompleted() {
        RecibirMensaje mensajeRespuesta = null;
        for(int i=0;i<10;i++){
            mensajeRespuesta = RecibirMensaje.newBuilder()
                                .setFrom(random.nextInt())
                                .setMessage("Hola stream bidireccional desde servidor al completar "+random.nextInt())
                                .build();
            this.responseObserver.onNext(mensajeRespuesta);
        }
        this.responseObserver.onCompleted();
    }
   
}
public class MultiplesMensajesStream implements StreamObserver<EnviarMensaje>{
    private StreamObserver<RecibirMensaje> responseObserver;
    private Random random = new Random();
 
    //Necesitamos inyectar el stream observer para poder responder al cliente el mensaje
    public MultiplesMensajesStream(StreamObserver<RecibirMensaje> responseObserver){
        this.responseObserver = responseObserver;
    }
 
    @Override
    public void onNext(EnviarMensaje value) {
        System.out.println("MultiplesMensajesStream - Nuevo mensaje recibido: "+value);
       
    }
 
    @Override
    public void onError(Throwable t) {
        System.out.println("MultiplesMensajesStream - Ocurrio un error: "+t.getMessage());
       
    }
 
    @Override
    public void onCompleted() {
        //Los streams unidireccionales de cliente a servidor, solo requieren una respuesta del servidor.
        RecibirMensaje respuesta = RecibirMensaje.newBuilder()
                                    .setFrom(random.nextInt())
                                    .setMessage("Hola mundo desde el servidor: "+random.nextInt())
                                    .build();
 
        this.responseObserver.onNext(respuesta);
        this.responseObserver.onCompleted();
    }
   
}

2.3 Ejecutar un método gRPC usando Postman

Corremos nuestro servicio gRPC, abrimos postman y creamos un nuevo request. Al momento de crearlo, nos pide indicar que tipo de request va a ser, seleccionamos gRPC Request.

Una vez seleccionado gRPC Request, vamos a especificar los datos del request. En el campo server URL, vamos a especificar el servidor y puerto donde están corriendo, en mi caso es localhost:9090. Ahora necesitamos cargar los servicios, métodos y datos que vamos a probar. Damos clic en “Choose a way to load services and methods” y seleccionamos “import protobuf definition from local file”. 

Recordemos que el protobuf está en nuestro proyecto de interfaz, por lo que vamos a seleccionar el archivo protobuf en el navegador de archivos y cargarlo en Postman.

Una vez cargado, vamos a guardar la definición del servicio gRPC, especificamos nombre y versión y guardamos como una “New API Definition”. Por último damos clic en “Import definition”.

Una vez cargada la definición, ahora necesitamos ejecutar alguno de los métodos que queremos probar. Si no tenemos un mensaje definido, podemos apoyarnos de postman y crear un mensaje de ejemplo, damos clic en “Generate example message”. Vas a ver que el mensaje se va a crear en JSON, sin embargo, internamente, todo se envía en binario. Postman hace la representación en JSON para que sea más fácil de leer para nosotros.

Damos clic en “Invoke” y esperamos nuestra respuesta. En las capturas de pantalla vemos cómo ejecutamos un método unario. En postman también podemos ejecutar métodos en streaming. Seleccionamos un método en streaming y damos clic en “Invoke”. Arriba de “Status code” de la respuesta, hay dos botones: “End streaming” y “Send”. Con el primero cerramos la conexión de streaming y con el segundo enviamos un nuevo mensaje al servidor.

Conclusión.

RPC es un framework definido en los 80s que permite ejecutar métodos alojados en otros servidores. Una de las implementaciones más conocidas es SOAP. Fue una de las primeras formas de los servicios web que hoy conocemos y que nos permiten tener aplicaciones distribuidas. gRPC viene a mejorar implementaciones utilizando protocolos modernos y comunicación segura y binaria. En este tutorial vimos cómo implementar gRPC en una aplicación de Spring Boot, como definir e implementar los métodos y los mensajes, así como crear comunicación unaria, unidireccional y bidireccional.

Rate this post

Deja un comentario