Neuigkeiten von trion.
Immer gut informiert.

gRPC Java Service mit Spring Boot

In verteilten Systemen, zu denen etwa solche mit Microservice- und Cloud-Architekturen zählen, müssen in der Regel über Anwendungsgrenzen hinweg Funktionsaufrufe erfolgen. Typischerweise kommt dabei für synchrone Aufrufe HTTP als Transportschicht zum Einsatz, das Format der ausgetauschten Daten kann XML, JSON oder eine andere spezifische Datenstruktur sein. Welche HTTP-Verben und welche Adressen (URLs) wie zu verwenden sind, ist dabei eine fast schon in philosophische Bereiche ausufernde Debatte zwischen den verschiedenen Ansätzen. Am häufigsten finden sich REST und Abwandlungen davon als Schnittstellenkonzept.

Im Gegensatz zu der reichhaltigen API-Oberfläche von REST findet sich SOAP als XML-basiertes RPC- (Remote-Procedure-Call-) Verfahren, bei dem lediglich HTTP POST-Requests zum Einsatz kommen. Neben dem oft als schwerfällig bezeichneten Vorgehen von Design und Implementierung von SOAP-Schnittstellen hat XML als Transportformat auch deutliche Auswirkungen auf möglichen Durchsatz und erforderliche CPU- und Speicherresourcen bei der Verarbeitung.

Als Alternative zu diesen Ansätzen hat Google gRPC entwickelt: gRPC setzt ebenfalls auf HTTP auf, verwendet jedoch Google Protocol Buffers als optimiertes Datenformat. Wie der Einsatz im Kontext einer Java-Spring-Boot-Anwendung aussehen kann, wird im Folgenden dargestellt.

Die folgenden Schritte werden in diesem Artikel beschrieben:

  1. Setup der Spring-Boot-Anwendung

  2. Spring-Boot gRPC-Abhängigkeiten ergänzen

  3. Protocol Buffers Schnittstellen-Beschreibung

  4. Service-Implementierung

Für die Integration von gRPC in Spring Boot wird der gRPC-Spring-Boot-Starter von https://github.com/LogNet/grpc-spring-boot-starter verwendet. Die Übersetzung der Protocol Buffers IDL zu Java-Code im Rahmen des Maven Builds nutzt das von https://github.com/xolstice/protobuf-maven-plugin bereitgestellte Maven Plugin.

Spring Boot gRPC Anwendung

Zunächst wird eine Spring-Boot-Anwendung erzeugt. Dies kann per Browser über https://start.spring.io/ erfolgen, oder auch direkt via cURL-Aufruf mit anschließender Extraktion des Projektarchives.

Erzeugung der Spring Boot gRPC Anwendung
$ curl https://start.spring.io/starter.tgz \
-d dependencies=web,actuator,devtools \
-d language=java -d type=maven-project \
-d groupId=de.trion -d name=grpc-demo \
-d packageName=de.trion.grpcdemo \
-d baseDir=spring-grpc \
-d packaging=jar -d javaVersion=1.8 \
-d bootVersion=2.1.0.RELEASE | tar -xzf -

Da ein Maven-Projekt verwendet wird, muss nun in der pom.xml die Abhängigkeit auf den Spring Boot-Starter für gRPC ergänzt werden. Bei der Gelegenheit kann auch das protobuf-maven-plugin ergänzt werden. Da der Protobuf-Compiler plattformspezifisch ist, muss für das jeweils verwendete Betriebssystem die richtige Abhängigkeit eingetragen werden. Um die Konfiguration variabel zu gestalten, kommt das os-maven-plugin zum Einsatz, das entsprechende Variablen befüllt, die in der Abhängigkeitsdefinition referenziert werden können.

gRPC Spring Boot-Starter und Maven-Protobuf-Plugin in Maven pom.xml
<project>
  ...
  <dependencies>
    ...
    <dependency>
      <groupId>io.github.lognet</groupId>
      <artifactId>grpc-spring-boot-starter</artifactId>
      <version>3.0.0</version>
    </dependency>
  </dependencies>
  ...
  <build>
    <extensions>
      <!-- determine platform -->
      <extension>
        <groupId>kr.motd.maven</groupId>
        <artifactId>os-maven-plugin</artifactId>
        <version>1.6.1</version>
      </extension>
    </extensions>
    <plugins>
      ...
      <!-- protobuf-maven-plugin -->
      <plugin>
        <groupId>org.xolstice.maven.plugins</groupId>
        <artifactId>protobuf-maven-plugin</artifactId>
        <version>0.6.1</version>
        <configuration>
          <protocArtifact>com.google.protobuf:protoc:3.6.1:exe:${os.detected.classifier}</protocArtifact>
          <pluginId>grpc-java</pluginId>
          <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.16.1:exe:${os.detected.classifier}</pluginArtifact>
        </configuration>
        <executions>
          <execution>
            <goals>
              <goal>compile</goal>
              <goal>compile-custom</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

Das Projekt ist nun soweit vorbereitet. Um sicher zu gehen, dass alles korrekt konfiguriert ist, kann ein Maven Build durchgeführt werden. In der Ausgabe ist dann direkt zu erkennen, welche Plattform durch das os-maven-plugin erkannt wurde.

Build zur Prüfung des Projekts
$ ./mvnw clean compile
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Detecting the operating system and CPU architecture
[INFO] ------------------------------------------------------------------------
[INFO] os.detected.name: linux
[INFO] os.detected.arch: x86_64
[INFO] os.detected.version: 4.18
[INFO] os.detected.version.major: 4
[INFO] os.detected.version.minor: 18
[INFO] os.detected.release: ubuntu
[INFO] os.detected.release.version: 18.10
[INFO] os.detected.release.like.ubuntu: true
[INFO] os.detected.release.like.debian: true
[INFO] os.detected.classifier: linux-x86_64
[INFO]
...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.489 s

Nachdem das Projekt grundsätzlich konfiguriert ist, geht es nun an die Definition der Services in Protocol Buffers.

Protocol Buffer Definition

Google Protocol Buffers verwendet eine programmiersprachenunabhängige Definition der Schnittstellen und Datenstrukturen. Grundsätzlich ist gRPC Interoperabel, sofern es eine Implementierung für die jeweils gewünschte Programmiersprache gibt.

Definition der Protocol Buffers Strukturen
syntax = "proto3";

option java_multiple_files = true;
package de.trion.grpcdemo.greeter;

message Person {
  string first_name = 1;
  string last_name = 2;
}

message Greeting {
  string message = 1;
}

service GreeterService {
  rpc greet (Person) returns (Greeting);
}

Nachdem die Schnittstelle spezifiziert ist, werden sprachspezifische Implementierungen generiert, die dann im eigenen Programmcode integriert werden. Das protobuf-maven-plugin übersetzt automatisch die unter src/main/proto gefundenen Definitionen, wie im Build zu sehen.

Generierung der Protobuf Java-Klassen
$ ./mvnw clean compile
...
[INFO] --- protobuf-maven-plugin:0.6.1:compile (default) @ grpcdemo ---
[INFO] Compiling 1 proto file(s) to spring-grpc/target/generated-sources/protobuf/java
[INFO]
[INFO] --- protobuf-maven-plugin:0.6.1:compile-custom (default) @ grpcdemo ---
[INFO] Compiling 1 proto file(s) to spring-grpc/target/generated-sources/protobuf/grpc-java
[INFO]
...

gRPC Service-Implementierung

Für die Implementierung wurde durch den Protobuf-Compiler eine Java-Basisklasse generiert: GreeterServiceGrpc.GreeterServiceImplBase. Diese wird nun erweitert, um die eigentliche Funktionalität zur Verfügung zu stellen.

Zwei Dinge gibt es dabei zu beachten: Zum Einen werden für die verwendeten Datentypen durch Protobuf entsprechende Builder zur Verfügung gestellt, eine direkte Instanziierung ist nicht möglich. Zum Anderen werden gRPC Aufrufe mit einer reaktiven API durchgeführt. Das Ergebnis des Serviceaufrufs wird also nicht durch die Methode zurückgeliefert, sondern auf einem StreamObserver, der als Methodenparameter übergeben wird, bereitgestellt.

Beispiel einer Java-Implementierung des gRPC-Service in Spring Boot
package de.trion.grpcdemo.server;

import de.trion.grpcdemo.greeter.GreeterServiceGrpc;
import de.trion.grpcdemo.greeter.Greeting;
import de.trion.grpcdemo.greeter.Person;
import io.grpc.stub.StreamObserver;
import org.lognet.springboot.grpc.GRpcService;

@GRpcService
public class GreeterServiceImpl extends GreeterServiceGrpc.GreeterServiceImplBase
{
    @Override
    public void greet(Person request, StreamObserver<Greeting> responseObserver)
    {
        final Greeting.Builder builder = Greeting.newBuilder();
        builder.setMessage("Hello: " + request.getFirstName() + " "+ request.getLastName());

        final Greeting greeting = builder.build();

        responseObserver.onNext(greeting);
        responseObserver.onCompleted();
    }
}

Wenn die Anwendung gestartet wird, steht der gRPC Dienst unter einem gesonderten Port zur Verfügung: Standardmässig wird der Port 6565 verwendet. Eine Mischung von regulären HTTP- und gRPC-Services auf demselben Port ist derzeit nicht möglich, lässt sich jedoch mit anderen Mitteln, beispielsweise einem Reverse-Proxy, realisieren - falls die Anforderung besteht. Jetzt kann der Service getestet werden.

Reflection API

Durch die Reflection API ist es einem Client möglich, sowohl die zur Verfügung stehenden Methoden, als auch die eingesetzten Datenstrukturen zu entdecken. Damit ist prinzipiell ein Client auch dann in der Lage, eine Schnittstelle zu verwenden, wenn die Protobuf-Definitionen nicht vorliegen. Davon kann man Gebrauch machen, um mittels eines Kommandozeilenwerkzeugs die Schnittstelle zu testen. Um Reflection zu aktivieren, muss das Property grpc.enableReflection auf true gesetzt werden, z.B. in der application.properties Datei von Spring Boot.

Reflection API für gRPC in application.properties
grpc.enableReflection=true

Als Kommandozeilenwerkzeug bietet sich grpcurl an, das auch als Docker-Image zur Verfügung steht. Damit werden Experimente besonders einfach, da keine zusätzliche Installation erfolgen muss.

Aufruf des gRPC Servers mit grpcurl aus einem Docker-Container heraus
$ docker run --rm --net=host networld/grpcurl ./grpcurl \
  -plaintext localhost:6565 \
  describe de.trion.grpcdemo.greeter.GreeterService
de.trion.grpcdemo.greeter.GreeterService is a service:
{
  "name": "GreeterService",
  "method": [
    {
      "name": "greet",
      "inputType": ".de.trion.grpcdemo.greeter.Person",
      "outputType": ".de.trion.grpcdemo.greeter.Greeting"
    }
  ]
}
$ docker run --rm --net=host networld/grpcurl ./grpcurl \
  -plaintext \
  -d '{"first_name": "Thomas", "last_name": "Tester"}' \
  localhost:6565 \
  de.trion.grpcdemo.greeter.GreeterService/greet
{
  "message": "Hello: Thomas Tester"
}

Fazit

Mit gRPC lassen sich sehr leicht remote-Schnittstellen definieren und implementieren. Neben dem effizienten Transportformat bietet gRPC den Vorteil, dass sich über die neutrale Schnittstellenspezifikation auf einfache Weise interoperable und typsichere Dienste bereitstellen lassen. Zusammen mit Spring Boot entsteht so eine produktive Option für einen Microservice-Stack.




Zu den Themen Spring Boot und Cloud Architektur bieten wir sowohl Beratung, Entwicklungsunterstützung als auch passende Schulungen an:

Auch für Ihren individuellen Bedarf können wir Workshops und Schulungen anbieten. Sprechen Sie uns gerne an.

Feedback oder Fragen zu einem Artikel - per Twitter @triondevelop oder E-Mail freuen wir uns auf eine Kontaktaufnahme!

Zur Desktop Version des Artikels