Web Development & JavaScript

Spring for GraphQL

Schnellstart für moderne Web-APIs

Michael Schäfer

GraphQL wurde 2016 von Facebook veröffentlicht. Damit lassen sich Web-APIs implementieren, die exakt die vom Client angefragten und benötigten Daten liefern. Während sich GraphQL im JavaScript-Umfeld bereits großer Beliebtheit erfreut, ist es im Java-Umfeld noch recht unbekannt. Das könnte sich allerdings in Zukunft ändern. Mit Spring for GraphQL hat Spring ein neues Projekt veröffentlicht, das Java-Entwicklern bei der Implementierung von modernen GraphQL-Web-APIs unterstützt.

GraphQL wurde 2012 im Rahmen des Rebuild Native Mobile Apps Project [1] von Facebook entwickelt und 2016 offiziell vorgestellt. GraphQL ist neben SOAP, REST und gRPC eine weitere Möglichkeit, Web-APIs bereitzustellen. Während REST dem Client das Web-API seitens des Servers fest vorgibt, kann der Client bei GraphQL beliebige Daten auf Basis der Graph Query Language abfragen. GraphQL folgt damit dem Prinzip der Client-directed Queries (Kasten: „Client-directed Query“). Das ist gerade dann sehr vorteilhaft, wenn viele verschiedene Clients angeboten werden müssen, die auf einen Server zugreifen, aber unterschiedliche Daten benötigen.

Client-directed Query

Mittels einer Client-directed Query ist der Client in der Lage, den Navigationspfad und die Ergebnisgranularität seiner speziellen Datenabfragen vollständig zu steuern. Der Server konzentriert sich nur noch auf die verfügbaren Datenentitäten, die bestehenden Datenbeziehungen zum Navigieren und die Autorisierung für jede beteiligte Datenentität. Komplexe Schnittstellkontrakte entfallen dadurch.

Wie eingangs erwähnt, ist GraphQL im Java-Umfeld noch recht unbekannt. Das könnte daran liegen, dass es bisher keine bekannten GraphQL Java Frameworks gab, die Java-Entwickler bei der Implementierung von Web-APIs unterstützen. Diese Situation hat sich allerdings in den letzten Jahren geändert und es gibt eine Menge von interessanten GraphQL Frameworks im Java-Ökosystem.

GraphQL im Spring-Ökosystem

Im Spring-Ökosystem gibt es drei bekannte GraphQL Frameworks. Das älteste ist sicherlich das Kickstart-Projekt [2] von Michiel Oliemans aus dem Jahr 2016, das über GitHub unter der MIT-Lizenz bereitgestellt wird. Seit Dezember 2020 stellt Netflix zudem Domain Graph Service (DGS) [3] über GitHub unter der Apache-2.0-Lizenz zur Verfügung. Diese Veröffentlichung hatte bereits positive Auswirkungen auf die Bekanntheit von GraphQL im Java-Umfeld. Parallel dazu reagierte auch VMware und brachte 2020 in Zusammenarbeit mit dem Gründer von GraphQL Java, Andreas Marek, das Spring-for-GraphQL-Projekt [4] auf den Weg.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Das ist Spring for GraphQL

Spring for GraphQL ist ein recht neues Spring-Projekt. Das erste Release erschien im September 2021 unter der MIT-Lizenz auf GitHub. Es wurde im gleichen Jahr auf der SpringOne 2021 von Rossen Stoyanchev und Andreas Marek vorgestellt. VMware konnte Andreas Marek, den Gründer von GraphQL Java, für Spring for GraphQL gewinnen, um GraphQL Java optimal in Spring zu integrieren.

Spring for GraphQL hat drei Aufgaben. Die erste Aufgabe besteht aus der Integration von GraphQL Java in Spring for GraphQL. Die zweite Aufgabe ist, die Funktionen aus Spring für GraphQL nutzbar zu machen, die Java-Entwickler für die Implementierung von modernen Web-APIs benötigen, etwa für die Sicherung der Web-APIs über Spring Security oder das Testen der Web-APIs über Spring Test. Die dritte Aufgabe besteht aus der Bereitstellung von starter, auto-configuration und properties für Spring Boot, damit eine Spring-basierte Webanwendung mit Spring for GraphQL sehr einfach und schnell implementiert werden kann. Mit Spring Boot 2.7 wurde Spring for GraphQL erstmals ausgeliefert.

In Abbildung 1 sind die Zusammenhänge zwischen GraphQL Java, Spring for GraphQL und Spring Boot dargestellt. Im Folgenden werden die in Abbildung 1 gezeigten Features von Spring for GraphQL kurz vorgestellt, die für die Entwicklung von Web-APIs auf dem Server notwendig sind. Der Artikel liefert dazu ein Beispiel, das auf GitHub unter dem Namen „Spring for GraphQL Article“ [5] zu finden ist. Dort befinden sich alle Klassen und Konfigurationsdateien, auf die in diesem Artikel referenziert wird.

Abb. 1: Spring for GraphQL unterstützt die Entwickler mit zahlreichen notwendigen Funktionen

Webanwendung mit GraphQL – los geht’s

Wie man es von Spring Boot gewohnt ist, bindet man einen starter für ein bestimmtes Thema, etwa Security, ein, die mitgelieferte auto-configuration konfiguriert die Spring-Boot-Anwendung für das Thema und über properties lassen sich Anpassungen vornehmen. Das ist auch im Fall von Spring for GraphQL so. Es ist lediglich der Spring Boot GraphQL Starter wie Maven pom.xml in das Dependency-Management einzubinden. Über die properties, z. B. über die application.yaml, lässt sich der Starter konfigurieren und schon kann es losgehen. Die GraphQL-Schemadatei, beispielhaft die airline.graphql aus dem Codebeispiel, die das Web-API in GraphQL beschreibt, wird im resource-Verzeichnis abgelegt und dann automatisch gefunden und eingebunden.

Die Webintegration – ab ins Web

Für den Zugriff auf das GraphQL API über das Web stellt Spring for GraphQL drei Möglichkeiten bereit. Die erste Möglichkeit ist ein synchroner Zugriff über HTTP durch die Integration von Spring MVC. Dazu ist ein weiterer Starter, der Spring Boot Web Starter, im Dependency-Management pom.xml zu hinterlegen. Die zweite Möglichkeit ist ein asynchroner, auf Reactive Streams basierender Zugriff durch die Integration von Spring WebFlux. Dafür ist allerdings der Spring Boot WebFlux Starter notwendig. Eine dritte Möglichkeit ist ein ebenfalls asynchroner Zugriff über WebSockets durch die Integration von Spring WebSockets. Dazu ist der Spring Boot WebSocket Starter im Dependency-Management zu hinterlegen. Neben der Kommunikation über das Web wird noch eine asynchrone, binäre und Reactive Streams unterstützende Kommunikation über RSocket angeboten, die überwiegend bei der Kommunikation zwischen zwei Anwendungen auf dem Server zum Einsatz kommt. Dazu ist der Spring Boot RSocket Starter dem Dependency-Management hinzuzufügen.

Mit diesem breiten Angebot ist Spring for GraphQL eine sehr flexible Integration von GraphQL in Spring gelungen, denn der Java-Entwickler kann entscheiden, welche Möglichkeit er für sein Projekt bevorzugt.

Zugriff auf das API – GraphQL Controller

In GraphQL sendet der Client über HTTP, WebSockets oder RSocket eine GraphQL-Operation vom Typ Query, Mutation oder Subscription an den Server, die dann dort ausgeführt wird. Vergleichbar sind diese mit SQL-Operationen vom Typ Select, Update oder Delete, die ein Client an den Datenbankserver sendet, wobei es zu Subscriptions in SQL keine direkte Analogie gibt. Eine GraphQL-Operation bezieht sich immer auf eine Menge von GraphQL-Feldern, die das GraphQL-Schema definiert. Hinter jedem Feld steckt eine Funktion, die für das jeweilige Feld die Daten liefert. Dazu ruft die GraphQL Engine für jedes Feld einen Data Fetcher auf, der für den Aufruf der Funktion und die Bereitstellung der Daten verantwortlich ist.

Die Funktion kann in Spring for GraphQL über die in Spring bekannten Controller bereitgestellt werden. Dazu ist lediglich eine Spring Bean mit der @Controller-Annotation zu versehen. Das Mapping der Felder auf die Methoden des Controllers erfolgt über die Annotation @SchemaMapping. Die Spring Boot auto-configuration findet alle Controller mit einer @SchemaMapping-Annotation und erstellt dafür automatisch einen speziellen Schema Data Fetcher, der die Methoden auf dem Controller aufruft (Abb. 2). Das ist also ähnlich zu REST, denn dort werden die @RestController-Methoden über die Annotation @RequestMapping mit dem Web-API verbunden.

Abb. 2: Zugriff auf ein Datenfeld über GraphQL Engine, Schema Data Fetcher und Controller

Insgesamt fühlt sich damit die Entwicklung von GraphQL-basierten Web-APIs in Spring sehr ähnlich an wie bei anderen Web-APIs in Spring. Das erhöht die Akzeptanz von GraphQL und macht die Lernkurve flacher. Vielleicht hätte man für den Graph Controller anstatt der @Controller-Annotation noch eine spezifischere Annotation wie @GraphQLController spendieren können, um die Lesbarkeit zu erhöhen.

NEUES AUS DER JAVA-ENTERPRISE-WELT

Serverside Java-Track entdecken

 

Data Integration – auf Daten zugreifen

Der Zugriff auf die Daten aus den GraphQL Controllern heraus ist direkt über die bekannten Spring-Data-Repositories möglich. Allerdings bietet Spring for GraphQL noch einen einfacheren Zugriff auf Daten, der auf der Querydsl und Query by Example basiert. Dabei müssen lediglich die Spring-Data-Repositories um die Interfaces QuerydslPredicateExecutor oder QueryByExamplePredicateExecutor erweitert werden. Die Spring for GraphQL auto-configuration findet diese Repositories und erstellt automatisch spezielle QuerydslDataFetcher und QueryByExampleDataFetcher, die den Zugriff auf die Daten ermöglichen (Abb. 3).

Abb. 3: Die GraphQL Engine reicht das Feld an den Querydsl Data Fetcher weiter, der es an das Querydsl-Repository übergibt

Die Implementierung von GraphQL Controllern ist damit nicht mehr notwendig und es entfällt einiges an Boilerplate-Code. Lediglich im GraphQL-Schema müssen die Felder für den Zugriff auf die Daten nach vorgegebenen Konventionen definiert werden. Beispiele sind in der Schemadatei airline.graphql und dem TrackingRepository zu finden, die ausschnittsweise in Listing 1 und Listing 2 abgebildet sind.

type Query { 
  trackings : [Tracking]
  trackingById(id: ID!) : Tracking
  trackingByTracking(tracking: String!): Tracking
}
@GraphQlRepository
public interface TrackingRepository extends Repository<Tracking, Long>, QuerydslPredicateExecutor<Tracking> {    }

Der Ansatz ist sinnvoll, wenn keine Logik zwischen GraphQL Controller und Repositories implementiert werden muss – also eher bei einer reinen Datenpumpe. Ein Nachteil ist, dass dieser Ansatz nur für lesende Zugriffe funktioniert. Für schreibende Zugriffe müssten dann wieder die Spring-Data-Repositories parallel dazu implementiert werden.

Transaktionen werden von Spring for GraphQL nicht direkt unterstützt. So entfällt die Option, mehrere GraphQL-Operationen, etwa Mutationen, in eine Transaktion zu packen. Das ist auch ausdrücklich vom Spring-for-GraphQL-Team gewollt, da Mutationen laut GraphQL-Spezifikation unabhängig voneinander ausgeführt werden sollen [6]. Die Definition von Transaktionen liegt damit in der Verantwortung des Java-Entwicklers und kann auf der Ebene der Services und Repositories explizit über die Annotation @Transaction definiert werden.

Error Handling – aussagekräftige Fehlermeldungen

Die Fehlerbehandlung ist bei der Implementierung von Web-APIs auf dem Server besonders wichtig, damit der Client im Fehlerfall aussagekräftige Fehlermeldungen erhält. In GraphQL werden Errors über eine JSON-Struktur in der GraphQL Response zurückgegeben. Dabei spielt es keine Rolle, ob es sich um technische oder fachliche Fehler handelt. GraphQL Java stellt die Klasse GraphQLError zur Verfügung, die die JSON-Struktur in Java repräsentiert. Die GraphQL Engine sammelt alle GraphQLError ein, die während eines GraphQL Request auftreten, und konvertiert sie in eine JSON-Struktur. Diese wird dann über die GraphQL Response an den Client zurückgegeben.

Allerdings müssen zuvor die Exceptions, die in der Web-API-Anwendung auftreten können, in GraphQLError Objekte konvertiert und der GraphQL Engine zugeführt werden. Spring for GraphQL bietet dafür das Interface DataFetcherExceptionResolver an, das vom Java-Entwickler zu implementieren und über die Annotation @Component als Spring Bean zu definieren ist. Der Java-Entwickler hat somit die Aufgabe, für alle Exceptions, die in seiner Web-API-Anwendung auftreten können, einen DataFetcherExceptionResolver zu implementieren, der diese in einen GraphQLError konvertiert. Das Beispiel RouteExceptionResolver in Listing 3 zeigt eine solche Implementierung.

@Component
public class RouteExceptionResolver implements DataFetcherExceptionResolver {
  @Override
  public Mono<List<GraphQLError>> resolveException(Throwable ex, DataFetchingEnvironment env) {
    if (ex instanceof RouteException) {
      return Mono.fromCallable(() -> Arrays.asList(
        GraphqlErrorBuilder.newError(env)
                           .errorType(ErrorType.NOT_FOUND)
                           .message(ex.getMessage())
                           .build()));
    }
    return Mono.empty();
  }
}

Die Spring for GraphQL auto-configuration findet automatisch alle Spring Beans, die das Interface DataFetcherExceptionResolver implementieren, und übergibt sie in geeigneter Form an die GraphQL Engine. Leider unterstützt Spring for GraphQL nicht das bereits aus @RestController bekannte Konzept der @ExceptionHandler. Dieser Ansatz wäre allerdings der bessere gewesen, da er in Spring bereits sehr bekannt ist und die Akzeptanz von Spring for GraphQL erhöht hätte.

 

Validation – Argumente prüfen

Um die vom Client an den Server übergebenen GraphQL-Argumente etwa hinsichtlich Länge oder Format prüfen zu können, integriert Spring for GraphQL den Jakarta-Bean-Validation-Standard [7]. Mit dem Standard stehen Annotationen wie @Min, @Max oder @Size zur Verfügung, über die sich Regeln zur Validierung der Argumente definieren lassen. Dabei werden die Annotationen vor die Parameter der GraphQL-Controller-Methoden gesetzt, wie im Beispiel des RouteController zu sehen ist. Listing 4 zeigt einen Ausschnitt aus dem Controller.

@Controller
public class RouteController {
  @QueryMapping
  public Route route(@Argument @Size(min = 6) String flightNumber) {
  }
}

Eine weitere Möglichkeit ist die Validierung über Directives im GraphQL-Schema. Die Open-Source-Bibliothek GraphQL Java Extended Validation [8] stellt die Definition und die Implementierung der Directives für die Validierung zur Verfügung. Dabei orientieren sich die Namen und Funktionen der Directives am Jakarta-Bean-Validation-Standard. Ein Beispiel ist in airline.graphql zu finden. Im Folgenden ist ein Ausschnitt aus der Schemadatei zusehen:

type Query {
  route(flightNumber: String! @Size( min : 6, max : 6 )): Route
}

Beide Ansätze haben ihre Vor- und Nachteile. Der Java-Entwickler muss entscheiden, welcher Ansatz für sein Projekt besser geeignet ist. Zu beklagen ist, dass die GraphQL-Java-Extended-Validation-Bibliothek nicht direkt von Spring for GraphQL und Spring Boot unterstützt wird und der Entwickler sich daher selbst um das Versionsmanagement kümmern muss.

Web Security – den Zugriff auf das API regeln

Beim Thema Security geht es um die Authentifizierung und Autorisierung des Clients, der auf das GraphQL-Web-API zugreift. Die dafür benötigten Securitymechanismen wie Basic Auth oder OAuth2/JWT wurden durch Spring for GraphQL so integriert, dass sie genauso verwendet werden können wie bei anderen Controllern auch, etwa dem @RestController. Es muss dem Dependency-Management lediglich der richtige Spring Boot Security Starter hinzugefügt und die GraphQL-Controller-Methode über die @PreAuthorize-Annotation autorisiert werden, wie im Beispiel RouteController gezeigt wird. Listing 5 zeigt einen Ausschnitt aus dem Controller.

@Controller
public class RouteController {
  @QueryMapping
  @PreAuthorize("hasAuthority('SCOPE_read')")
  public List<Route> routes(GraphQLContext context) {
  }
}

Das ist ein großer Vorteil, denn damit muss die Security nicht neu oder anders definiert und implementiert werden. Leider bietet Spring for GraphQL keinen Schutz vor DDoS-Angriffen, die gerade bei rekursiven Abfragen auf Graphen eine Gefahr darstellen. Allerdings werden Themen wie DDoS oft in Infrastrukturkomponenten wie API-Gateways ausgelagert, sodass der Nachteil nicht so sehr ins Gewicht fällt.

Testing – APIs, die funktionieren

Für das Testen der GraphQL-Web-APIs mit JUnit stellt Spring for GraphQL das Interface GraphQLTester bereit. Damit lassen sich die GraphQL-Operationen Query, Mutation und sogar Subscription testen. Im Fluent-Pattern-Style können über den GraphQLTester die Methoden für das Laden, das Ausführen und das Prüfen der GraphQL-Queries hintereinander ausgeführt werden. Dabei können die zu testenden GraphQL-Queries in Dateien abgelegt und an den GraphQLTester übergeben werden. Das Ergebnis einer GraphQL-Query lässt sich sehr einfach durch einen an den GraphQLTester übergebenen JSON-Path-Ausdruck prüfen.

Der GraphQLTester unterstützt Tests über HTTP, WebSockets und RSocket. Das Testen von Subscriptions ist allerdings nur über WebSockets oder RSocket möglich. Eine konkrete Spring Bean des GraphQLTester wird von der auto-configuration des Spring Boot Test Starter (abhängig vom gewählten Protokoll) automatisch erstellt und kann in JUnit über die @Autowired-Annotation verwendet werden. Ein Beispiel ist in der Klasse TestWebRouteQueries zu finden. Listing 6 zeigt einen Ausschnitt aus der Klasse.

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class TestWebRouteQueries {
  @Autowired
  private GraphQlTester webGraphQlTester;
  @Test
  public void routes() {
    webGraphQlTester.documentName("routes")
                    .execute()
                    .path("routes[*]")
                    .entityList(Route.class)
                    .hasSize(3);
  }
}

Es ist allerdings auch möglich, den GraphQLTester in einer Mock-Umgebung ohne die Verwendung eines konkreten Protokolls zu verwenden. In diesem Fall ist der JUnit-Test mit der @GraphTest-Annotation zu versehen. Ein Beispiel ist in der Klasse TestRouteQueries zu finden. Rundum eine gelungene Lösung für das Testen von GraphQL-Web-APIs, die keine Wünsche mehr offenlässt.

Metriken – wissen, was los ist

Die Bereitstellung von Metriken wird zunehmend wichtiger in modernen Web-APIs. Spring Boot ermöglicht über den Actuator die Aufzeichnung von Metriken, die über Micrometer im Speicher abgelegt und an Metrik-Server wie Prometheus weitergeben werden können. Die im Speicher aufgezeichneten Metriken lassen sich über den Metrik-Endpoint /metric abfragen.

Neben den bereits existierenden Metriken können auch neue Metriken erstellt werden. Spring for GraphQL nutzt diese Möglichkeit und stellt über eine Instrumentation [9] eine Reihe von Metriken zur Verfügung, beispielsweise die Zeit, die für den letzten GraphQL Request benötigt wurde. Oder die Zeit, die für die Durchführung einer GraphQL-Controller-Methode benötigt wurde. Sogar die Anzahl der GraphQL Errors wird als Metrik bereitgestellt. Weitere mögliche Metriken zur Nutzung des API sind vorstellbar [10] und lassen sich individuell von einer eigenen Instrumentation hinterlegen.

 

Performance – das Zeitverhalten optimieren

Das Zeitverhalten eines GraphQL Request kann auf drei Ebenen optimiert werden. Auf der Datenebene integriert Spring for GraphQL das Java DataLoader Framework [11]. Dieses vermeidet, dass dieselben Daten für die GraphQL-Felder mehrfach aus einer Datenquelle gelesen und bereits gelesene Daten über einen Cache bereitgestellt werden. Auf der Threadebene arbeitet GraphQL Java bereits asynchron. Somit können über die GraphQL-Controller-Methoden CompletableFutures zurückgegeben werden, um die Ausführungszeit langlaufender Methoden zu optimieren. Für die Optimierung des Zeitverhaltens auf Netzwerkebene steht der Ansatz über WebFlux oder WebSockets zur Verfügung. Damit schöpft Spring for GraphQL alle Möglichkeiten aus, um das Zeitverhalten aus technischer Sicht zu verbessern.

Client – der GraphQL-Client

Neben der Implementierung von Web-API-Anwendungen auf dem Server ist auch die Implementierung eines GraphQL-Java-Clients mit Spring for GraphQL möglich. Damit können zwei Spring-for-GraphQL-Anwendungen untereinander über HTTP, WebSocket oder RSocket kommunizieren. Der Client wird über das Interface GraphQLClient bereitgestellt, das dem Fluent-Pattern folgt und damit recht einfach zu verwenden ist. Eine Instanz für die Verwendung des GraphQL-Clients wird, abhängig vom gewählten Protokoll, über die Factory-Methoden der Klassen HTTPGraphQlClient, WebSocketGraphQlClient oder RSocketGraphQlClient erstellt, wie das Beispiel in der Klasse GraphQlClientApplication zeigt. Ein Ausschnitt aus der Klasse ist in Listing 7 zu finden.

WebClient client = WebClient.create("http://localhost:8080/airline");
GraphQlClient graphQlClient = HttpGraphQlClient.create(client);
Mono<Route> route = graphQlClient.documentName("route") 
                                 .variable("flightNumber", "LH7902")
                                 .retrieve("route")
                                 .toEntity(Route.class);
route.subscribe(r -> System.out.println(r));

Fazit

Spring for GraphQL liefert alle Features, die für die Entwicklung von professionellen GraphQL-Web-APIs benötigt werden. Die Bereitstellung der Features für die Validierung und Fehlerbehandlung birgt Optimierungspotenzial. Allerdings ist Spring for GraphQL ein sehr junges Projekt im Spring-Ökosystem und wird sich in den nächsten Jahren entsprechend weiterentwickeln. Die Zeit wird zeigen, wie die Spring-for-GraphQL-Features die Entwickler bei der Implementierung des GraphQL-Web-API am besten unterstützen können. Ausreden, um auf REST-APIs zurückzufallen, sind damit obsolet.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

 

Links & Literatur

[1] Eve, Porcello; Banks, Alex: „Learning GraphQL“; OReilly, 2018

[2] https://github.com/graphql-java-kickstart

[3] https://github.com/Netflix/dgs-framework

[4] https://github.com/spring-projects/spring-graphql

[5] https://github.com/Thinkenterprise/spring-for-graphql-article

[6] https://github.com/spring-projects/spring-graphql/issues/274

[7] https://github.com/eclipse-ee4j/beanvalidation-api

[8] https://github.com/graphql-java/graphql-java-extended-validation

[9] https://www.graphql-java.com/documentation/instrumentation

[10] https://github.com/spring-projects/spring-graphql/issues/19

[11] https://github.com/graphql-java/java-dataloader

Top Articles About Web Development & JavaScript

Alle News der Java-Welt:

Behind the Tracks

Agile, People & Culture
Teamwork & Methoden

Clouds & Kubernetes
Alles rund um Cloud

Core Java & Languages
Ausblicke & Best Practices

Data & Machine Learning
Speicherung, Processing & mehr

DevOps & CI/CD
Deployment, Docker & mehr

Microservices
Strukturen & Frameworks

Performance & Security
Sichere Webanwendungen

Serverside Java
Spring, JDK & mehr

Software-Architektur
Best Practices

Web & JavaScript
JS & Webtechnologien

Digital Transformation & Innovation
Technologien & Vorgehensweisen

Domain-driven Design
Grundlagen und Ausblick

Spring Ecosystem
Wissen in Spring-Technologien

Web-APIs
API-Technologie, Design und Management

ALLE NEWS ZUR JAX!