Volanie Javy z Go prostredníctvom gRPC

Pre jedného z našich klientov sme nedávno vyvinuli softvérový systém, ktorého súčasťou je aj transformácia PDF súborov do ich textovej reprezentácie (PDF parser) a ich následné ďalšie spracovanie. Systém pozostáva z viacerých komponent, ale pre účel tohto postu nás budú zaujímať dva z nich - Web scraper a PDF parser. Web scraper je softvér napísaný v jazyku Go, ktorý získava informácie z webových stránok a pokiaľ na ne narazí, tak aj z PDF súborov. Pokiaľ ste sa niekedy pokúšali programaticky extrahovať informácie s PDF súborov, viete že to je vcelku netriviálna záležitosť a existuje len veľmi málo použiteľných knižníc. Žiaľ pre Go sme nenašli knižnicu, čo by dokázala uspokojivo extrahovať text z PDFiek. V Jave však existuje knižnica Apache PDFBox, ktorá produkuje vcelku uspokojivé výsledky. Takže nám už len zostávalo vyriešiť otázku ako budeme volať Javu z Go:

  1. Natívnym volaním cez JNI, bolo by ale treba použiť cgo. Túto možnosť sme teda zavrhli.
  2. Volaním exec.Command("java", "-jar", ...) a zachytením výstupu. Túto možnosť sme zavhli hlavne kvôli pomalému štartu JVM.
  3. Na strane PDF parsera vystavíme API, ktoré budeme volať z Web scrapera. Touto cestou sme sa rozhodli ísť.

RPC prostredníctvom gRPC

Takže úloha bola jasná, na strane PDF parsera vystavíme API, ktoré potom budeme volať z Web scrapera vždy keď bude potrebné získať nazad textovú reprezentáciu PDF súboru. Niečo ako REST API s JSON payload-mi sme zavrhli hneď. Keďže ide čisto o server side integráciu rozhodli sme sa použiť gRPC, čo je open-source RPC framework so širokou podporou rôznych programovacích jazykov. Na pozadí gRPC používa binárnu, na schéme založenú serializáciu prostredníctvom Protocol Buffers. RPC API kontrakt postavený na gRPC a Protocol Buffers je definovaný prostredníctvom schémy napísanej v jazyku proto3. Súčasťou distribúcie Protocol Buffers je tooling, ktorý prostredníctvom gRPC pluginov dokáže z tejto schémy vygenerovať stub RPC klienta a kostru servera v jazyku podľa vašej voľby.

PDF parser - gRPC Java Server

API rozhranie pre PDF parser je pomerne jednoduché - na vstupe je binárny obsah PDF súboru a výstupom je text reprezentujúci obsah PDFka. Zapísané prostredníctvom proto3 to vyzerá nasledovne:

syntax = "proto3";

option java_multiple_files = true;
option java_package = "eu.redbyte.pdfparser.grpc";
option java_outer_classname = "PDFParserApi";

package pdfparserapi;

message ParserRequest {
    bytes content = 1;
}

message ParserResponse {
    string text = 1;
}

service PDFParser {

    rpc Parse (ParserRequest) returns (ParserResponse) {
    }
}

Ako som už spomenul, Protocol Buffers obsahuje tooling na vygenerovanie kódu z proto3 schémy. V projekte používame Gradle, použili sme preto protobuf plugin, ktorý sa pri builde projektu postará o kompiláciu proto3 schémy do Java tried. Relevantné časti Gradle buildu vyzerajú nasledovne:

plugins {
    id "com.google.protobuf" version "0.8.6"
    // ...
}

// ...

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.6.1"
    }
    plugins {
        grpc {
            artifact = "io.grpc:protoc-gen-grpc-java:1.14.0"
        }
    }
    generateProtoTasks {
        all()*.plugins { grpc {} }
    }
}

Keď si odmyslíme implementačné detaily transformácie PDFiek do textu, celá služba PDF Parser je veľmi jednoduchá:

@GRpcService
public class PDFParserService extends PDFParserGrpc.PDFParserImplBase {

    private PDFExtractor pdfExtractor;

    @Autowired
    public PDFParserService(PDFExtractor pdfExtractor) {
        this.pdfExtractor = pdfExtractor;
    }

    @Override
    public void parse(ParserRequest request, StreamObserver<ParserResponse> responseObserver) {
        try {
            String text = pdfExtractor.extract(request.getContent().newInput());
            ParserResponse response = ParserResponse.newBuilder().setText(text).build();
            responseObserver.onNext(response);
            responseObserver.onCompleted();
        } catch (Exception e) {
            responseObserver.onError(e);
        }
    }
}

Pokiaľ použijete gRPC spring boot starter tak ako my, oanotujete triedu @GRpcService, tak po spustení aplikácie bude na porte 3000 počúvať vaša gRPC služba.

gRPC Go Klient

Služba extrakcie textu z PDF už je hore a počúva na localhost:3000, poďme teda rýchlo vygenerovať klienta pre Go. Prostredníctvom protoc kompilera a pluginu pre jazyk Go vygenerujeme stub klienta nasledovne (v skutočnosti sme takéto generovanie Go kódu realizovali ako jeden z krokov Makefile skriptu):

protoc -I grpc/pdfparserapi grpc/pdfparserapi/pdfparserapi.proto --go_out=plugins=grpc:grpc/pdfparserapi

Samotné použitie vygenerovaného Go klienta je potom už priamočiare:

// gRPC client initialization
conn, err := grpc.Dial("localhost:3000", grpc.WithInsecure())
if err != nil {
	log.Fatalln("unable to connect to localhost:3000")
}
defer conn.Close()
pdfParser := pdfparserapi.NewPDFParserClient(conn)

// ...

// gRPC client usage
response, err := pdfParser.Parse(context.Background(), &pdfparserapi.ParserRequest{Content: pdfContent})
if err != nil {
	return errors.WithStack(err)
}
// use the response
fmt.Println(response.Text)

Zhrnutie

Na pomerne jednoduchom príklade sme si ukázali použitie RPC frameworku gRPC ako (pragmatickejšej) alternatíve ku “klasickému” REST API prístupu. Tu ale možnosti gRPC len začínajú, nedotkli sme sa tém ako streaming, autentifikácia, zabezpečenie spojenia použitím TLS, či použitiu middleware (napr. pre rate limiting). O tom možno nabudúce :) …