javaspring-bootgrpcgrpc-javaspring-boot-3

Using GRPC + Springboot, the GRPC plugin is not able to autogenerate the Streaming request object


I am using the new Springboot starter from org.springframework.boot (more details below) and I am able to use it to make a simple GRPC API to return Hello world. Now I was trying to upgrade the same program to support streaming HTTP 2 request but when I try to build my project which eventually autogenerates the models is not building the Request models.

Code exerpts (shortened code wherever I can, code is bootstrapped from Spring intializer.)

build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.4.4'
    id 'io.spring.dependency-management' version '1.1.7'
    id 'com.google.protobuf' version '0.9.4'
}

repositories {
    mavenCentral()
}

ext {
    set('springGrpcVersion', "0.4.0")
}

dependencies {
    implementation 'io.grpc:grpc-services'
    implementation 'org.springframework.grpc:spring-grpc-spring-boot-starter'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.grpc:spring-grpc-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    implementation 'io.grpc:grpc-netty-shaded'
    modules {
        module("io.grpc:grpc-netty") {
            replacedBy("io.grpc:grpc-netty-shaded", "Use Netty shaded instead of regular Netty")
        }
    }
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.grpc:spring-grpc-dependencies:${springGrpcVersion}"
    }
}

protobuf {
    protoc {
        artifact = 'com.google.protobuf:protoc'
    }
    plugins {
        grpc {
            artifact = 'io.grpc:protoc-gen-grpc-java'
        }
    }
    generateProtoTasks {
        all()*.plugins {
            grpc {
                option 'jakarta_omit'
                option '@generated=omit'
            }
        }
    }
}

// ...

Hello World.proto

syntax = "proto3";

option java_package = "com.asr.example.grpc.demo.model";
option java_multiple_files = true;

message HelloWorldRequest {
    optional string name = 1;
}

message HelloWorldResponse {
    string greeting = 1;
}

service HelloWorldService {
    rpc sayHello(stream HelloWorldRequest) returns (stream HelloWorldResponse);
}

HelloWorldController.java

import com.asr.example.grpc.demo.model.HelloWorldRequest;
import com.asr.example.grpc.demo.model.HelloWorldResponse;
import com.asr.example.grpc.demo.model.HelloWorldServiceGrpc;
import io.grpc.stub.StreamObserver;
import org.springframework.grpc.server.service.GrpcService;
import org.springframework.util.StringUtils;

import java.text.MessageFormat;

@GrpcService
public class HelloWorldController extends HelloWorldServiceGrpc.HelloWorldServiceImplBase {

    @Override
    public StreamObserver<HelloWorldRequest> sayHello(StreamObserver<HelloWorldResponse> responseObserver) {
        return super.sayHello(responseObserver);
    }
    
    /* Expected:
    public StreamObserver<HelloWorldResponse> sayHello(HelloWorldRequest request, StreamObserver<HelloWorldResponse> responseObserver) {
        // Further Implementation
    }
     */

    // Original without streaming
    // @Override
    public void sayHello(
            HelloWorldRequest request,
            StreamObserver<HelloWorldResponse> responseObserver) {
        responseObserver
                .onNext(HelloWorldResponse.newBuilder()
                        .setGreeting(MessageFormat.format(
                                "Hello {0}!!!",
                                StringUtils.hasText(request.getName()) ? request.getName() : "Arvind")
                        )
                        .build()
                );
        responseObserver
                .onCompleted();
    }
}

Code explanation:

  1. Bootstrapped code from Spring Initializr.
  2. Added Protobuf specs without stream keyword.
  3. Implemented the generated controller using the original method.
  4. Works fine.
  5. Added stream keyword as prefix in HelloWorld request in Protobuf spec.
  6. Expected generated code is a bit different from what I expected, and unable to find a way to fetch the request.

Questions:

  1. I believe feature is still experimental , is this the reason this is not working ?
  2. I have seen similar dependency from io.github.lognet and net.devh, are any of these 2 viable alternatives for LTS ?

Solution

  • Hey @Kannan J and @Vy Do

    Thanks for your answers, but for clarifications for newbies like me. Getting into more details.

    Yes, as @Kannan J mentioned, the created stub for HelloWorldService.sayHello is expected as it is mentioned in question. The parameter for HelloWorldResponse can be considered as a Consumer of the response (Drawing analogy from Java 8's Consumer methods) which needs to be consuming the response which needs to be sent back to the client. Whereas, Request is also a consumer (again, the same Java 8's Consumer method's analogy) here will be called once a HelloWorldRequest is received, so we need to define what needs to be done by implementing and returning that.

    Here's the sample implementation of the same:

        @Override
        public StreamObserver<HelloWorldRequest> sayHello(StreamObserver<HelloWorldResponse> responseObserver) {
    
            return new StreamObserver<>() {
                @Override
                public void onNext(HelloWorldRequest helloWorldRequest) {
                    String name = helloWorldRequest.getName();
                    String greeting = null;
                    if (StringUtils.hasText(name)) {
                        if (!name.startsWith("Hello"))
                            greeting = "Hello " + name;
                    } else {
                        greeting = "Hello World";
                    }
                    if (StringUtils.hasText(name)) {
                        HelloWorldResponse response = HelloWorldResponse.newBuilder()
                                .setGreeting(greeting)
                                .build();
                        responseObserver.onNext(response);
                    }
                }
    
                @Override
                public void onError(Throwable throwable) {
                    // Handle error
                    log.error("Error occurred: {}", throwable.getMessage(), throwable);
                    responseObserver.onError(throwable);
                }
    
                @Override
                public void onCompleted() {
                    // Complete the response
                    log.info("Request completed");
                    HelloWorldResponse response = HelloWorldResponse.newBuilder()
                            .setGreeting("Quitting chat , Thank you")
                            .build();
                    responseObserver.onNext(response);
                    responseObserver.onCompleted();
                }
            };
        }
    

    Here, I am creating a new StreamObserver for the request , which tells what to do with the incoming messages (since, it is a stream there could be more than one, like a P2P chat). onNext tells what to do when a message is received, which can be used using the parameter provided for the same. onError when something breaks, and finally onCompleted when a streaming connection is closed. Within these methods responseObserver for sending messages (or emitting messages, analogy from Reactor streams) back to the client.