Java-Development ansprechender machen

REST API Vision mit Manifold

Scott McKinney

© Shutterstock / Nomad_Soul

Manifold ist eine einzigartige Open-Source-Technologie, die man in jedem Java-Projekt verwenden kann, um innovative Sprachfunktionen wie typsichere Metaprogrammierung, Erweiterungsmethoden, Templating und strukturelle Typisierung nutzen zu können. Im dritten Teil unserer Artikelserie zeigt Scott McKinney, wie man Manifold einsetzen kann, um JSON Schema als REST API Single Source of Truth (SSoT) festzulegen. Er geht dabei auch darauf ein, wie das Framework JSON-Schema- und YAML-Ressourcen auf direktem Wege mit Java verbindet, ohne dabei auf Code-Generatoren, kommentierte POJOs oder andere Zwischenlösungen angewiesen zu sein.

Das Problem mit dem REST-API-Design

Die meisten REST APIs werden lediglich im Sinne der Request-/Response-Formate beschrieben, die sie tragen und die in einer bestimmten Definitionssprache verfasst sind, etwa JSON Schema. Ein vollständiges REST-API-Schema stellt aber folgende Informationen bereit:

  • sprachneutrale Typdefinitionen
  • formale Typ- und Datenbeschränkungen
  • eine umfassende Dokumentation

JSON Schema ist allerdings ein sprachneutrales Format, auf das man nicht direkt von Java aus zugreifen kann. Die Typdefinitionen, die durch das Schema definiert sind, sind für das Typsystem von Java nicht sichtbar. Die bevorzugte Lösung für dieses Problem besteht darin, dass man einen Code-Generator in den Build steckt, mit dem die strukturierten Daten zu Java transformiert werden. Dieser Ansatz ist allerdings auch Jahrzehnte alt und birgt andere gefahren: Man macht hier nichts anderes, als ein Problem gegen ein anderes auszutauschen – der Zwischenschritt zur Codegenerierung behindert das, was ansonsten eine sehr geradlinige Entwicklererfahrung (Development Experience) hätte sein können. Weitere Probleme, die mit den Code-Generatoren zusammenhängen, sind unter anderem:

  • Kein Feedback: Die Veränderungen im JSON Schema sind erst dann für Java sichtbar, wenn man das API umbaut
  • Klassen, die mit veralteten Daten generiert werden (Stale generated Classes)
  • Erhöhte Build-Zeiten
  • Probleme mit der Skalierbarkeit: gegenseitige Abhängigkeiten zum Code-Generator, Caching, Integrationen usw.
  • Schwerwiegende Probleme mit benutzerspezfischen Class Loadern, Runtime Agents und Annotationsprozessoren
  • Unterbrechen von Gedankengängen
  • Schwache IDE-Integration:
    • Keine sofortige Rückmeldung bei Änderungen
    • Keine inkrementelle Kompilierung
    • Man kann nicht von der Code-Referenz zum entsprechenden JSON-Schema-Element navigieren
    • Es kann nicht ausfindig gemacht werden, wo JSON-Schema-Elemente Code nutzen
    • Ein Refactoring oder Umbenennen der JSON-Schema-Elemente ist nicht möglich
W-JAX 2019 Java-Dossier für Software-Architekten

Kostenlos: 30+ Seiten Java-Wissen von Experten

Sie finden Artikel zu EnterpriseTales, Microservices, Req4Arcs, Java Core und Angular-Abenteuer von Experten wie Uwe Friedrichsen (codecentric AG), Arne Limburg (Open Knowledge), Manfred Steyer (SOFTWAREarchitekt.at) und vielen weiteren.

API Summit 2019
Thilo Frotscher

API-Design – Tipps und Tricks aus der Praxis

mit Thilo Frotscher (Freiberufler)

Golo Roden

Skalierbare Web-APIs mit Node.js entwickeln

mit Golo Roden (the native web)

Die REST-API-Designlösung

In einer Programmiersprachen-Fantasywelt würde der Java-Compiler das JSON Schema direkt verstehen und der Schritt der Code-Generierung wäre nicht nötig – ähnlich der Metaprogrammierung, die Code auf magische Art mit strukturierten Daten in dynamischen Sprachen verbindet. Manifold leistet genau das, ohne dabei die Typsicherheit zu gefährden, denn das universelle Framework erweitert Javas Typsystem auf typsichere Art und Weise. Aufbauend auf dem Framework bietet Manifold auch die Unterstützung für mehrere spezifische Datenformate wie JSON, JSON Schema, YAML und andere. Durch Manifold verfügt Java über die komplette Bandbreite von JSON Schema:

  • Der Java-Compiler versteht JSON-Schema-Typdefinitionen
  • JSON Schema ist die Single Source of Truth (SSoT) des APIs
  • Der Schritt zur Code-Generierung im Build wird eliminiert
  • Skalierbar: JSON-Dateien sind Quelldateien
  • Keine benutzerdefinierten Class Loader, keine Runtime Agents, keine Annotationsprozessoren
  • Erstklassige IDE-Integration:
    • JSON-Schema-Änderungen können vorgenommen und sofort im eigenem Code verwendet werden, ohne Kompilieren
    • Inkrementell, verarbeitet nur die vorgenommenen Änderungen des JSON-Schemas, schnellere Builds
    • Navigation von einer Code-Referenz zu einem JSON-Schema-Element
    • JSON-Schema-Elemente lassen sich je nach Nutzung durchsuchen, um Code-Referenzen zu finden
    • Refactoring und Umbenennung von Schema-Elementen
    • Hotswap-Debugging-Unterstützung

Manifold lässt sich ganz leicht im Projekt einsetzen. Dafür muss man lediglich das Manifold Jar dem Projekt hinzufügen, schon kann man die Vorteile nutzen.

Eine gute Implementierung in der IDE ist für die meisten Entwickler essentiell, da sie praktisch in der Entwicklungsumgebung leben und atmen. Manifold enttäuscht in dieser Hinsicht nicht und bietet ein komplettes, nahtloses Dev-Erlebnis mit dem Plug-in für die IntelliJ IDEA. Dies kann direkt aus der IDE heraus installiert werden, dafür muss man schlicht die Suche via Settings ➜ Plugins ➜ Browse repositories ➜ Search aufrufen und nach „Manifold“ suchen.

Sehen heißt glauben

Die JSON-Schema-Datei com/example/api/User.json muss nun in das Verzeichnis resources abgelegt werden:

{
  "$id": "https://example.com/restapi/User.json",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "User",
  "description": "A simple user type to uniquely identify a secure account",
  "type": "object",
  "definitions": {
    "Gender": {"enum": ["male", "female"]}
  },
  "properties": {
    "id": {
      "description": "Uniquely identifies a user",
      "type": "string",
      "format": "email",
      "readOnly": true
    },
    "password": {
      "description": "The user's password",
      "type": "string",
      "format": "password",
      "writeOnly": true
    },
    "name": {
      "type": "string",
      "description": "A public name for the user",
      "maxLength": 128
    },
    "dob": {
      "type": "string",
      "description": "The user's date of birth",
      "format": "date"
    },
    "gender": {
      "$ref": "#/definitions/Gender"
    }
  },
  "required": ["id", "password", "name"]
}

Nun kann man sofort loslegen und den Typ com.example.api.User direkt im eigenen Code verwenden – ohne Kompilierungsschritt:

User user = User.builder("scott", "mypassword", "Scott")
  .withGender(male)
  .build();

Dieser Code verwendet die builder()-Methode, die für alle JSON-Schema-Typen verfügbar ist. Die Parameter von builder() spiegeln die im Schema spezifizierten erforderlichen Typen wider. Die withGender()-Methode steht für die gender-Property, die nicht erforderlich ist.

REST-Aufrufe können direkt von User aus getätigt werden:

Requester<User> req = User.request("http://localhost:4567/users");
 
// Get all Users via HTTP GET
IJsonList<User> users = req.getMany();
 
// Add a User with HTTP POST
User user = User.builder("scott", "mypassword", "Scott")
  .withGender(male)
  .build();
req.postOne(user);
 
// Get a User with HTTP GET
String id = user.getId();
user = req.getOne("/$id"); // string interpolation too 🙂
 
// Update a User with HTTP PUT
user.setDob(LocalDate.of(1980, 7, 7));
req.putOne("/$id", user);
 
// Delete a User with HTTP DELETE
req.delete("/$id");

Alle JSON-Schema-Typen verfügen über eine request()-Methode, die eine URL entgegennimmt, welche einen Basisstandort darstellt, von dem aus REST-Aufrufe gemacht werden können. Wie gezeigt, bietet request() Methoden an, um HTTP GET, POST, PUT, PATCH und DELETE bequem aufrufen zu können. Via request() können auch Authentifizierung, Header-Werte usw. bestimmt werden.

Man kann Änderungen an User.json vornehmen und diese sofort in seinen Code übernehmen. Refactoring und Umbennenungen sind ebenso möglich, wie das Ändern von Typen usw. – diese Änderungen kann man unmittelbar im Code nachverfolgen. Auch die Navigation von Code-Nutzungen zu den entsprechenden Deklarationen in inUser.json funtkioniert kinderleicht. Die eben erwähnten Code-Nutzungen lassen sich von jedem deklarierten Element in der Datei User.json ausfindig machen.

Fluent API

JSON-Typen sind als eine Reihe von „flüssigen“ Interface-APIs definiert. Der JSON-Typ User ist beispielsweise eine Java-Schnittstelle und bietet typsichere Methoden, um

  • einen User anzulegen,
  • einen User zu erstellen,
  • die Eigenschaften von einem User zu modifizieren,
  • einen User aus einem String, einer Datei oder einer URL über HTTP GET zu laden,
  • Web-Service-Prozesse über HTTP GET, POST, PUT, PATCH und DELETE anzufordern,
  • einen User im Format JSON, YAML oder XML zu (be-)schreiben,
  • einen User< zu kopieren,/li>
  • etwas an den User zu senden. Dies geht von jedem strukturell kompatiblen Typ aus, sogar von Map und ohne Proxys.

Zusätzlich wird das JSON-Schema-Format vollständig von dem API unterstützt, einschließlich:

  • Propertys, die mit readOnly oder writeOnly markiert sind
  • Nullable Propertys
  • additionalProperties und patternProperties
  • Eingebettete Typen
  • Rekursive Typen
  • Externe Typen
  • format-Typen
  • Kompositionstypen: allOf
  • Union-Typen: oneOf/anyOf

Jedoch werden die Beschränkungen des JSON-Schemas nicht im API durchgesetzt, denn:

  • Dies ist der Zweck eines JSON-Schema-Validators
  • Die Validierungen von Einschränkungen aufwendig sein kann – es sollte optional sein

Weitere Informationen über die Manifold-Unterstützung für JSON und JSON Schema gibt es in der Dokumentation.

Templates

Mit Manifold ist es möglich, Templates mit der vollen Ausdruckskraft von Java zu erstellen. Die fortschrittliche und leichtgewichtige Template Engine von Manifold, ManTL, unterstützt Java vollständig. Zu den weiteren Features gehört der Support von typsicheren Parametern für Templates, die typsichere Einbindung anderer Templates, gemeinsame Layouts und benutzerdefinierte Basisklassen für anwendungsspezifische Logik.

<%@ import java.util.List %>
<%@ import com.example.User %>
<%@ params(List<User> users) %>
<html lang="en">
<body>
<% users.stream()
   .filter(user -> user.getDob() != null)
   .forEach(user -> { %>
    User: ${user.getName()} <br>
    DOB: ${user.getDob()} <br>
<% }); %>
</body>
</html>

Danach kann das Template typsicher im eigenen Code genutzt werden:

UsersDob.render(users)

Mit dem Manifold-Plug-in für IntelliJ IDEA geht die Template-Authentifizierung noch leichter von der Hand. Dabei stehen Nutzern Features wie deterministische Code-Vervollständigung, In-Line-Hilfe, Nutzungssuche, Highlighting usw. zur Verfügung.

API Server

Man kann Manifold auch in Verbindung mit SparkJava nutzen, um REST-Services zu erstellen. SparkJava ist ein Micro Framework für das Erstellen von Webanwendungen in Java mit minimalem Aufwand. Hier ist eine HTTP PUT Request Route, um einen User zu aktualisieren:

// Update existing User
put("/users/:id", (req, res) ->
  UserDao.updateUser(req.params(":id"),
    User.load().fromJson(req.body())).write().toJson());

Zusätzliche Features

Es gibt viele weitere Funktionen, mit denen Nutzer ihren REST-Service entwickeln können. Leser werden vielleicht bemerkt haben, dass einige der obigen Code-Beispiele das String Templates Feature von Manifold nutzen, das auch als String Interpolation bekannt ist. Das $-Zeichen kann verwendet werden, um Variablen und Java-Ausdrücke direkt in ein beliebiges  String-Literal einzubetten:

int hour = 8;
String time = "It is $hour o'clock";  // prints "It is 8 o'clock"

Man kann auch seine eigenen Erweiterungsmethoden zu jedem Java-Typ hinzufügen, einschließlich von JRE-Klassen, wie String und List und sogar REST API Interfaces:

@Extension
public class MyUserMethods {
  public static Integer getAge(@This User thiz) {
    LocalDate dob = user.getDob();
    return dob == null ? null : 
      Period.between(user.getDob(), LocalDate.now()).getYears();
  }
   
  // more extension methods
  ...
}

Dann kann die Methode auch direkt verwendet werden:

User user = ...;
int age = user.getAge();

Dies sind nur einige Beispiele der Features des Manifold Frameworks, mit denen Sie Ihr Projekt verfeinern und verbessern können.

Zusammenfassung

In diesem Beitrag habe ich Manifold als ein bahnbrechendes Java-Framework vorgestellt, mit dem der Entwicklungsprozess für REST-APIs optimieren werden kann. Mit Manifold ist es möglich, Code-Generatoren aus dem Build-Skript, einschließlich der Vielzahl der damit verbundenen Probleme, zu eliminieren und mit dem Manifold IntelliJ-IDEA-Plug-in kann die User Experience weiter verbessert werden. Die direkte Navigation zu JSON-Schema-Elementen und die Suche nach deren Nutzung werden damit zum Kinderspiel. Nützlich sind auch das deterministische Refactoring- und Umbenennungs-Tooling und die Möglichkeit, Änderungen schrittweise zu kompilieren. Ich habe auch einige der JSON API Features behandelt, etwa die request()-Methode für direkte HTTP-Funktionalität. Des Weiteren, habe ich kurz gezeigt, wie Templates, String-Erweiterungen (Interpolation) und Erweiterungsmethoden in ein Projekt integriert werden können. Schließlich habe ich demonstriert, wie man zusammen mit Manifold und SparkJava – einer leichtgewichtigen und leistungsstarken Kombination – einen soliden REST-Service erstellen kann.

Manifold ist eine bahnbrechende Technologie für das Java-Ökosystem. Es ist zudem ein kostenloses Open-Source-Projekt auf GitHub. Alle Informationen zu Manifold gibt es im Repository.

Dieser Artikel wurde ursprünglich auf der Seite Medium veröffentlicht.

Verwandte Themen:

Geschrieben von
Scott McKinney
Scott McKinney
Scott McKinney is the founder and principle engineer at Manifold Systems. Previously, he was a staff engineer at Guidewire where he designed and created Gosu. He currently pounds code by the truckload while listening to way too much retro synthwave.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
4000
  Subscribe  
Benachrichtige mich zu: