"Loserere" Kopplung

Dynamische Schnittstellenkommunikation mit JSON und REST

Stefan Ullrich

© Shutterstock.com / Rosie Apples

Dieser Artikel zeigt, wie durch REST mit JSON praktisch ohne festen Kontrakt mit Services kommuniziert werden kann. Möglich wird dies durch generisches Mapping von benötigten und erhaltenen Feldern mittels rekursiver Tiefensuche im JSON-Objektgraphen. Dadurch wird der Unterschied in der Kopplung zwischen RPC-Verfahren wie SOAP mit Apriori-Schnittstellenkontrakt (wsdl, xsd), generierten Clientbibliotheken, Versionen etc. und dieser geradezu kontaktfreien dynamischen Kommunikation auf die Spitze getrieben.

Normalerweise denke ich mir eine Schnittstelle als eindeutig definierte Methode eines Objekts mit einer Anzahl von typisierten Transferobjekten für Parameter und Rückgabe. Mit anderen Worten: Wenn ich eine Schnittstelle nutzen will, muss ich das Objekt (bzw. den Aufruf) kennen und die benötigten Transferobjekte zur Verfügung haben. Das kennen wir von SOAP Web Services: Die WSDL beschreibt den nötigen Aufruf als Abstraktion eines Prozeduraufrufs (Remote Procedure Call, RPC) und das XSL Schema die benötigten Transferobjekte.

Damit habe ich mich von der Schnittstelle und ihren Versionen abhängig gemacht. Mehr noch: Ich habe mir ein fremdes (und vielleicht unpassendes) Objektmodell in den Code geholt – eine generierte SOAP-Clientbibliothek mit evtl. hässlichem Namen und fremder Komposition. Ich kann versuchen, die Verbreitung dieser SOAP-Clientbibliothek in meinem Code zu begrenzen, aber dazu muss ich die Transferobjekte in einer eigenen Komponente isolieren und deren Daten in eigene Objekte umfüllen. Das ist aufwändig und überflüssig.

Es geht auch anders

Bei Benutzung von REST bestehen die zu übertragenden Daten aus JSON. Es müsste doch möglich sein, sich dieses JSON von einer remote Schnittstelle zu holen, ohne einen solchen Aufwand betreiben und ein fremdes Objektmodell importieren zu müssen. Und das geht tatsächlich: Der Schlüssel liegt in der JSON-Deserialisierung, die aus JSON Objekte erzeugt und befüllt. Normalerweise müssen die übergebenen Objekte der JSON-Struktur entsprechen, um erfolgreich gefüllt zu werden, und zwar sowohl syntaktisch als auch strukturell. Hätte man aber einen flexibleren JSON Object Mapper zur Verfügung, könnte man ihm ganz andere Objekte übergeben, und er könnte in JSON nach den zu den Feldern passenden Daten suchen, egal wo. Diese Flexibilität ist natürlich nicht komplett generisch zu erreichen: Es muss Kriterien geben, nach denen der JSON Object Mapper entscheiden kann, ob ein gegebenes Datum das richtige für ein bestimmtes Feld eines Objekts ist. Die einfachsten Kriterien sind Name und Typ des Felds. Dies ist gleichzeitig die Grenze der Abhängigkeit von der remote Schnittstelle, denn nur die Feldnamen müssen bekannt und gleichlautend sein. Vom Umfang der Daten und ihrer Struktur ist man so aber völlig unabhängig.

Der GenericServiceClient

Diese Funktionalität wird im Haspa-Serviceframework (siehe: „Weckruf der Moderne“ in Java Magazin 5.14) von der Klasse GenericServiceClient bereitgestellt. Der GenericServiceClient übernimmt sowohl das REST-Kommunikationsprotokoll (Service-Look-up, HATEOAS, Serialisierung, Deserialisierung etc.) als auch die Befüllung der Zielobjekte. Letztlich reicht dem Aufrufer der folgende Zweizeiler:

Auftraggeber auftraggeber = new GenericServiceClient(MITARBEITER_SERVICE)
   .getTransformed(personalnummer, Auftraggeber.class, false);

Was hier geschieht, ist die Transformation von JSON für ein Objekt Mitarbeiter vom Mitarbeiterservice in ein Objekt Auftraggeber eines Druckauftrags im Druckservice. Es bedeutet, dass der GenericServiceClient sich vom Mitarbeiterservice JSON für einen „Mitarbeiter“ besorgt (für den der Druckservice gar keine Klasse hat), das empfangene JSON aber auf die übergebene Klasse Auftraggeber des Druckservice anwendet (die dem Mitarbeiterservice unbekannt ist). Abbildung 1 zeigt deutlich, dass die Klasse Auftraggeber aus Feldern der Klasse Mitarbeiter und den Unterklassen Betriebsstelle und Anmeldung besteht. Es ist also nicht wichtig, wo sich die Felder befinden. Wichtig ist, dass die Namen (und Typen) der Felder übereinstimmen.

Abb. 1: Transformation einer Objektrepräsentation in eine andere

Abb. 1: Transformation einer Objektrepräsentation in eine andere

Was passiert hier genau?

Der Aufrufer (Service-Consumer) wird durch den GenericServiceClient weitgehend von der Kommunikation mit dem benötigten Service und dem Mapping der Daten entlastet. Er muss nur den Namen des Service und ein passendes Objekt für die Daten haben. Nach der Instanziierung des GenericServiceClient beginnt dieser das REST-Kommunikationsprotokoll mit dem Service-Look-up. Der Registryservice wird mit einem HTTP GET und dem Servicenamen im URI aufgerufen und liefert daraufhin den URL einer aktiven Instanz des Mitarbeiterservice. Diesen URL benutzt der GenericServiceClient, um die Einstiegslinks vom Mitarbeiterservice zu holen. Damit ist die Servicekommunikation vorbereitet (Listing 1).

Listing 1: REST- Kommunikationprotokoll
> GET /service/registry/de.haspa.gp.mitarbeiter HTTP/1.1
>
< HTTP/1.1 200 OK
http://server/service/mitarbeiter
> GET /service/mitarbeiter HTTP/1.1
>
< HTTP/1.1 200 OK
< Link: <http://server/service/mitarbeiter>; rel="info"; 
  type="text/html"; title="Einstiegslinks"; verb="GET,OPTIONS",
 <http://server/service/mitarbeiter/mitarbeiter>; rel="all"; 
  type="application/json"; title="Angemeldete Mitarbeiter"; verb="GET", 
 <http://server/service/mitarbeiter/mitarbeiter?name=>; rel="suche";
  type="application/json"; title="Mitarbeitersuche"; verb="GET", 
 <http://server/service/mitarbeiter/anmeldung>; rel="new";
  type="application/json"; title=" Mitarbeiterlogin"; verb="POST",
< location: http://server/service/mitarbeiter

Nun kann der Service-Consumer die Methode getTransformed mit den Argumenten Personalnummer und Zielobjekt aufrufen. Der GenericServiceClient ergänzt den all-Link mit der Personalnummer zu einem URL für den gewünschten Mitarbeiter und erhält dessen JSON als Antwort auf ein GET. Schließlich übergibt er das erhaltene JSON und das Auftraggeberobjekt dem JsonMapper, der sich um das Mapping kümmert (Abb. 2 und Listing 2).

Abb. 2: Sequenzaufruf „GenericServiceClient“

Abb. 2: Sequenzaufruf „GenericServiceClient“

weiter

Listing 2: GenericServiceClient holt JSON vom Mitarbeiterservice
public <E> E getTransformed(Object id, Class<E> clazz, boolean override, boolean lenient) {
  Link link = this.getLinks().get(LinkHeaderType.ALL);
  String url = this.concatenatePath(link.getHref(), id.toString());
  ClientResponse<String> response = new ClientRequest(url).get(String.class);
  this.setLinks(response.getLinkHeader().getLinksByRelationship());
  return JsonMapper.getAvailableFromJson(response.getEntity(), clazz.newInstance(), override, lenient);
}

 Die Suche nach passenden Feldern orientiert sich an Jackson und Java Reflection: Aus dem JSON wird eine com.fasterxml.jackson.databind.node.ObjectNode generiert und dann über die Felder des zu füllenden Objekts iteriert (Listing 3).

Listing 3: JSON-Deserialisierung und Mapping im JsonMapper
public static <E> E getAvailableFromJson(String json, E object, boolean override, boolean lenient) {
  ObjectNode objectNode = objectMapper.readValue(json, ObjectNode.class);
  for(Field field : object.getClass().getDeclaredFields()) {
    field.setAccessible(true);
    Types.getType(field.getType())
.setValue(object, field, objectNode, override, lenient);
}
}

Die bisher gesammelten Informationen werden einer Enumeration Types übergeben, die mögliche Feldtypen (String, Integer, Long, Double, Object, Collection etc.) repräsentiert. Zunächst wird in der übergebenen ObjectNode nach einem Objekt mit dem Feldnamen gesucht. Dieses Objekt ist eine JsonNode oder eine ihrer Subklassen. In setValue(…) setzt jeder Feldtyp den Wert des Felds auf seine spezifische Weise (das erspart if-else-Kaskaden im JsonMapper, Listing 4).

Listing 4: Types-Enum mit spezifischen Methodenimplementierungen
private static enum Types {
  STRING {
    public void setValue(Object object, Field field, ObjectNode objectNode, boolean override, boolean lenient) {
        JsonNode value = objectNode.findValue(field.getName());
      if (lenient || (value != null && value.isTextual())) {
      if (override || field.get(object) == null) {
            field.set(object, value.asText());
             }
        }
     }
},
     ...
OBJECT {
     public void setValue(Object object, Field field, ObjectNode objectNode, boolean override, boolean lenient) {
    JsonNode value = objectNode.findValue(field.getName());
        if (lenient || (value != null && value.isObject())) {
      if (override || field.get(object) == null) {    
         Object objct = field.getType().newInstance();
         JsonMapper.getAvailableFromJson(objectNode.toString(), objct, override, lenient);
         field.set(object, objct);
}
       }
     }
}
    
  public static Types getType(Class<?> clazz) {
   String name = null;
   if (clazz.isPrimitive() || 
        clazz.getPackage().getName().startsWith("java.")) {
      name = clazz.getSimpleName().toUpperCase();
        if ("INT".equals(name)) {
      name = "INTEGER";
        }
     } else {
        name = "OBJECT";
     }
     return Types.valueOf(name);
}
    
public abstract void setValue(Object object, Field field, ObjectNode node, boolean override, boolean lenient);
}

Wie in Listing 4 zu sehen, wird die Befüllung der Felder durch zwei boolesche Flags konfiguriert: Das Flag override bestimmt, ob gefüllte Felder eines bestehenden Objekts mit den Werten aus JSON überschrieben werden sollen. Das Flag lenient modifiziert die erforderliche Typgenauigkeit: lenient = false bedeutet, dass der Datentyp des JSON-Werts genau dem des Felds entsprechen muss, lenient = true ermöglicht es dem Enum, nur nach den Feldnamen zu gehen und eine Datenkonversion (z. B. Long -> Integer, Boolean -> String oder Integer etc.) zu versuchen. Auf diese Weise ist man nicht so abhängig von den Datentypen der Quellklasse und bekommt die Werte trotzdem. Es ist also gar nicht nötig, in der Zielklasse exakt die gleichen Datentypen wie in der Quellklasse zu verwenden. Listing 5 und Abbildung 3 zeigen beispielhaft, dass die Werte der Klasse A über ihr JSON problemlos in die Klasse B transferiert werden können (und zurück), obwohl die Felder der beiden Klassen ganz unterschiedliche Typen haben (deshalb muss hier das Flag lenient = true sein).

Listing 5: Hin- und Rücktransformation zweier unterschiedlicher Klassen
A a1 = new A();
a1.a = false;
a1.b = Boolean.TRUE;
a1.c = 9824598;
a1.d = 984L;
a1.e = Arrays.asList(true, false, true);

String json = JsonMapper.getObjectMapper().writeValueAsString(a1);
B b1 = JsonMapper.getAvailableFromJson(json, B.class, false, true);

json = JsonMapper.getObjectMapper().writeValueAsString(b1);
A a2 = JsonMapper.getAvailableFromJson(json, A.class, false, true);
Abb. 3: Anpassung von Datentypen bei der Transformation

Abb. 3: Anpassung von Datentypen bei der Transformation

Fazit

Dieser Artikel hat gezeigt, wie man REST und JSON dazu nutzen kann, eine Kommunikation zwischen Komponenten mit minimaler Kopplung zu erreichen. Im Gegensatz zu schwergewichtigen Verfahren, die Abhängigkeiten durch Servicekontrakte und Clientbibliotheken erfordern, kommunizieren hier Services ohne Softwareabhängigkeiten nur auf der Basis von JSON-Strings. Auf diesem Weg macht man sich zwar unabhängig von zu importierenden Artefakten und ihren Versionen, es ist dann aber besonders wichtig, die empfangenden Services so zu programmieren, dass sie sich melden, wenn erwartete Daten plötzlich nicht mehr kommen (z. B. weil ein sendender Service sein JSON geändert hat). Im hier vorliegenden Fall handelt es sich allerdings ausschließlich um hausinterne Services, was dieses Risiko unwahrscheinlich macht.

Aufmacherbild: Two tin cans joined with a cord via Shutterstock.com / Urheberrecht: Rosie Apples

Geschrieben von
Stefan Ullrich
Stefan Ullrich
Stefan Ullrich hat Informatik studiert und ist Sun Certified Enterprise Architect mit 17 Jahren Java-Erfahrung. Er hat eine RESTful SOA („ROA“) implementiert (siehe „Weckruf der Moderne“ im Java Magazin 5.2014) und diverse weitere Artikel im Java Magazin veröffentlicht.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
4000
  Subscribe  
Benachrichtige mich zu: