Erhältlich ab: Mai 2015
Ursprünglich für den (Aus-)Bildungsbereich entwickelt, erzeugen populäre Mikrocontroller wie Arduino, Raspberry Pi und Co. nicht nur bei Geeks geradezu kindliche Freude. Nein, sie beflügeln geradezu auch die Fantasien der Industrie: An vielen Orten wird an neuen Geschäftsmodellen getüftelt, oder es werden existierende Geschäfte dank Internet-of-Things-(IoT-)Technologien um nützliche Aspekte erweitert.
Wie so oft, wenn sich neue Dinge wie Lauffeuer im Markt verbreiten, besteht der Kern der Innovation nicht in etwas gänzlich anderem, sondern vielmehr darin, dass Technologien quasi über Nacht für eine große Schar an Akteuren (Tüftler, Erfinder, Entwickler, Unternehmensgründer) zugänglich werden. Im Falle IoT ergibt sich der einfache Zugang erstens aus den günstigen Preisen für die Boards, zweitens aus der Tatsache, dass teilweise auch die Hardware Open-Source-Lizenzen unterliegt.
Gewiss kommen noch weitere Faktoren hinzu, wie etwa die verfügbare Bandbreite (auch im Mobilfunk), die den Trend zum IoT weiterhin begünstigen. Zudem sind die Programmiermodelle des Internet of Things für Entwickler, die seit Jahren Desktops oder Server programmieren, mit Leichtigkeit zu verstehen. Und so entwickelt sich vor unseren Augen wieder einmal der berühmte „Perfect Storm“, der uns in eine neue technologische Ära überführt.
In unserem aktuellen Titelthema geht es indes weniger um die Programmierung von Minicomputern, als vielmehr um wichtige Aspekte, die z. B. aus voneinander isolierten Geräten ein wirkliches Netzwerk – ein Netz der Dinge – machen: MQTT, das Quasistandardprotokoll für das Internet der Dinge, wird von Dominik Obermaier ab Seite 36 vorgestellt. Einen Vorschlag zur Verwendung von Binärformaten, die im Internet der Dinge den klassischen Textformaten als Relikte der XML-Ära deutlich überlegen sind, unterbreitet Maik Wojcieszak ab Seite 41. Last but not least beschäftigen wir uns mit Eclipse SmartHome, dem IoT-Framework für die Vernetzung von Haus und Hof, und zeigen auf, wie sich Bindings mit erstaunlich einfachen Mitteln realisieren lassen (Seite 48).
Sie sehen, das Feld möglicher Betätigungen für Sie erweitert sich ständig – Sie müssen nur am Ball bleiben und die technologischen Entwicklungen kontinuierlich mitverfolgen. Aber das tun Sie ja, denn Sie lesen ja das Java Magazin.
Ich wünsche Ihnen viel Spaß und Inspiration bei der Lektüre des aktuellen Java Magazins!
Java wird klassischerweise mit der objektorientierten Programmierung in Verbindung gebracht. Auch aspektorientierte und seit Java 8 funktionale Programmierung gehören zum Java-Universum. Obwohl sie bei Enterprise-Anwendungen durchaus vermehrt anzutreffen ist, wird die prozedurale Programmierung aber meist unterschlagen.
Als ich 1999 in der Ausbildung den ersten Java-Kurs machte, war das ein Kurs in objektorientierter Programmierung. Genau wie später an der Uni war Objektorientierung zum einen ganz klar ein Update zur bis dahin gelernten prozeduralen Programmierung und zum anderen fest mit Java verbunden. Java ist objektorientiert – Punkt. So war es auch lange normal, dass, wer Java programmiert, objektorientierte Software schreibt und sich spätestens als Seniorentwickler OOA/OOD (objektorientierte Architektur bzw. Design) in die Skill-Liste schreibt.
Auch wenn es sich nie „richtig“ anfühlte, zu jedem privaten Feld auch immer Getter und Setter generieren zu lassen, blieb es erst einmal dabei: Java ist objektorientiert. In den letzten Jahren habe ich mich vermehrt mit agiler Softwareentwicklung, Extreme Programming und Software Craftmanship beschäftigt. Je mehr ich dabei über Clean Code, Test-driven Development (TDD), Code Smells und objektorientierte Prinzipien gelernt habe, desto mehr hatte ich Probleme damit, diese im beruflichen Java-Enterprise-Umfeld anzuwenden. Zu groß wurde der Designbruch zum bestehenden Legacy-Code. Schließlich war es ein Ruby-Buch [1], welches mich darauf brachte, warum dies so ist: Das Design der Software ist eher prozedural als objektorientiert. Und das trifft bisher auf jede Enterprise-(Spring-)Codebasis zu, die ich in den letzten zehn Jahren gesehen habe.
Bevor wir tiefer in die Thematik einsteigen, wollen wir die zwei betrachteten Programmierparadigmen „prozedural“ und „objektorientiert“ genauer voneinander abgrenzen. Dabei ist es gar nicht so einfach, eine passende Definition zu finden. Die vorhandenen reichen von rein technischen bis hin zu recht theoretischen Definitionen. Meiner Ansicht nach trifft es die folgende am besten: „The focus of procedural programming is to break down a programming task into a collection of variables, data structures, and subroutines, whereas in object-oriented programming it is to break down a programming task into objects that expose behavior (methods) and data (members or attributes) using interfaces. The most important distinction is that while procedural programming uses procedures to operate on data structures, object-oriented programming bundles the two together, so an ‘object’, which is an instance of a class, operates on its ‘own’ data structure.“ [2]
Bei der prozeduralen Programmierung gibt es eine Trennung zwischen den Daten und Methoden, welche auf diesen angewendet werden, während bei der Objektorientierung die Daten und darauf angewendeten Methoden zu Objekten zusammengefasst werden. Es geht also weniger um die technischen Möglichkeiten als um das zugrunde liegende Designprinzip.
Ein gutes Beispiel zur tiefen Verankerung der Trennung von Daten und darauf angewendeten Routinen im Java-Universum bietet uns die Klasse java.lang.String und die Hilfsmethoden, welche in so ziemlich jeder Bibliothek, sei es von Apache, Google oder Spring, dafür zur Verfügung gestellt werden. Hier gibt es statische (objektexterne) Methoden wie z. B. isEmpty, isBlank usw., welche auf die Daten, also den String, angewendet werden. Eine objektorientierte Lösung könnte eine bestimmte Wrapper-Klasse sein (Listing 1).
Listing 1: Text.java
public class Text {
private final String text;
public Text (final String text) {
this.text = text == null ? "" : text;
}
public Text concat(final String append) {
return concat(new Text(append));
}
public Text concat(final Text append) {return new Text(text.concat(append.text));
}
// ... more needed methods from String
public boolean isEmpty() {
return "".equals(text);
}
public boolean isBlank() {
return "".equals(text.trim());
}
// ... equals, hashCode, toString ...
}
Die Instanzen dieser Klasse können selbst die üblichen Fragen wie isEmpty oder isBlank zu einem Text beantworten. Der Konstruktor sorgt dafür, dass der interne Wert nicht null sein kann und verhindert so die unbeliebten NullPointerExceptions. Erweiterungen dieser Klasse können so auch direkt eine Validierung durchführen.
Dadurch, dass Java neben reinen Objekten auch primitive Datentypen, also die Zuweisung von reinen Werten, unterstützt, verschwimmen die Grenzen hier noch leichter. Ein häufiges Problem sind dabei die leidigen NullpointerExceptions. Es ist uns ja erlaubt, dort, wo ein Objekt erwartet wird, nur einen Wert, nämlich null, zuzuordnen. Wenn ich aber den Wert null zuweise, verschiebe ich lediglich eine Entscheidung – die Entscheidung nämlich, wie sich das System verhalten soll, wenn ein Objekt an dieser Stelle nicht vorhanden oder nicht definiert ist. Diese Entscheidung wird auf die Stellen im Code verschoben, an denen das Objekt verwendet werden soll. Wenn an dieser Stelle bekannt ist, dass diese Entscheidung zu treffen ist, dann führt das zu überall im Code verstreuten Nulltests. Wenn nicht, kommt es zur NullPointerException.
Eine Lösung für dieses Dilemma können „Neutrale Objekte“ oder auch „NullObjekte“ sein. Diese Objekte sind Instanzen der entsprechenden Klasse mit Werten, welche sich im System möglichst neutral bzw. definiert verhalten. Am Beispiel der Klasse Text wäre es z. B. eine Instanz mit leerem Text. In den meisten Fällen wird sich diese Instanz im System neutral verhalten, wenn deren Methoden aufgerufen werden. Es gibt erfahrungsgemäß sehr wenige Stellen, an denen die Entscheidung, ob der Wert vorhanden ist, wichtig ist. Um eine Abfragemöglichkeit zu schaffen, hinterlegt man diese Instanz einfach statisch in der definierenden Klasse. Anschließend kann man über equals abfragen, ob es sich um das neutrale Objekt handelt, oder man spendiert ihr eine Abfragemethode (Listing 2).
Listing 2: TextMitNeutralObj.java
public class TextMitNeutralObj {
private final String text;
public static final TextMitNeutralObj NEUTRAL = new TextMitNeutralObj("");
public TextMitNeutralObj(final String text) {
this.text = text == null ? "" : text;
}
// ... all other methods ...
public boolean isNeutral() {
return this.equals(NEUTRAL);
}
}
Bei Collections machen wir das übrigens meist schon instinktiv, indem wir anstelle von null eine leere Collection zurückgeben, welche sich dann z. B. in einem foreach neutral verhält.
Aber auch unsere eigene Codebasis ist meistens eher prozedural als objektorientiert organisiert. Da werden unter dem Schlagwort „Service“ Methoden, welche auf den Daten arbeiten, in Klassen oder – prozedural gesprochen – Modulen zusammengefasst. Die gekapselten Daten sind hier entweder Konfigurationen oder weitere Services. Weil statische Variablen und Methoden aber eher als verrufen gelten, werden Singletons verwendet. Die Daten werden in eigenen Klassen zusammengefasst. Diese enthalten dann nur Daten. Weil wir aber objektorientiert arbeiten, werden diese brav gekapselt, und zwar indem sie privat definiert und mit (public) Gettern und Settern versehen werden. Das nennen wir dann gerne POJOs (Plain Old Java Objects).
Ursprünglich wurde der Begriff „POJO“ von Martin Fowler, Rebecca Parsons und Josh MacKenzie auf einer Konferenz 2000 verwendet: „The term was coined while Rebecca Parsons, Josh MacKenzie and I were preparing for a talk at a conference in September 2000. In the talk we were pointing out the many benefits of encoding business logic into regular java objects rather than using Entity Beans. We wondered why people were so against using regular objects in their systems and concluded that it was because simple objects lacked a fancy name. So we gave them one, and it’s caught on very nicely.“ [3].
Kein Wort von „Getter“ oder „Setter“, sondern Businesslogik sollten diese Objekte enthalten. Auch Wikipedia verweist auf das Mehr als Getter und Setter [4]: „Befreit von Konventionen wird ein POJO als ein Objekt im eigentlichen Sinne der Objektorientierung verstanden, d. h. eine Einheit bestehend aus Daten und Verhalten, auf die die bekannten Grundsätze niedrige Kopplung und starke Kapselung angewendet werden. Ein POJO ist somit im Regelfall mehr als nur eine Ansammlung von Gettern und Settern.“
Dabei waren mit POJOs ursprünglich explizit Objekte mit Businesslogik gemeint (Kasten: „Die ursprüngliche Bedeutung von POJO“). Wenn unsere POJOs nun eigentlich „nur“ Datenstrukturen sind und dafür gedacht, dass andere die Werte bearbeiten, warum „kapseln“ wir sie dann und gönnen uns die Getter und Setter? Es wäre viel einfacher, die Felder einfach public zu definieren – was verlieren wir da in der Praxis? Womit wir wieder beim anfangs erwähnten Unbehagen in Bezug auf Getter und Setter wären. Sie sind eben eher Makulatur als echte und sinnvolle Kapselung.
Wer sich nun nicht vorstellen kann, wie eine objektorientierte Alternative zu den POJOs und Services aussehen kann, sollte versuchen, ein Design mittels Class Responsibility Collaboration Cards (Kasten: „CRC-Karten“) oder Sequenzdiagrammen anstelle von Klassendiagrammen zu entwerfen. Auch das Buch „Growing Object-Oriented Software, Guided by Tests“ [5] ist hier zu empfehlen.
CRC (Class Responsibility Collaboration) Cards [6] helfen beim Brainstorming zu objektorientiertem Design. Dabei werden Karteikarten in eine Titelzeile und darunter in zwei Hälften aufgeteilt. Die Titelzeile erhält den Namen der Klasse. Auf der linken Hälfte trägt man die Verantwortlichkeiten der Klasse ein (Single-Responsibility-Prinzip beachten!). Auf der anderen Seite werden die Namen der Klassen eingetragen, mit denen diese Klasse zusammenarbeiten muss, um ihre Verantwortlichkeiten zu erfüllen.
Natürlich kann man es als Verantwortlichkeit ansehen, gewisse Daten zusammenzuhalten, aber in der Regel entsteht dabei kein Design mit reinen Datenobjekten und reinen Routinesammlungen.
Die Grabenkämpfe um das bessere Programmierparadigma überlasse ich gerne Leuten mit zu viel Zeit. Für mich entscheidend ist nur die Erkenntnis, dass ein vorliegender Code nicht zwingend objektorientiert sein muss, weil er in Java erstellt wurde und auch syntaktisch die entsprechenden Sprachelemente und Mechaniken wie Klassen und Vererbung benutzt.
Diese Erkenntnis ist schon bei der Umsetzung so einfacher und allgemein bekannter Prinzipien wie DRY (Don’t Repeat Yourself) hilfreich. In meinen Services komme ich immer wieder an den Punkt, an dem ich eine Routine auf Datenobjekte an mehreren Stellen, ggf. sogar in mehreren Services benötige. Im Sinne des DRY-Prinzips versuche ich diese dann zu extrahieren. In den meisten Umgebungen wird bei kleineren und isolierten Routinen kein neuer Service entstehen, sondern eine statische Methode. Und wo packe ich diese hin, damit sie an allen benötigten Stellen auch verfügbar ist? In eine Utility-Klasse, im Package commons. Objektorientiert gesehen fühlt sich das zumindest verkehrt an, denn diese Logik gehört wohl eher zu den Daten und somit in das Objekt. Und commons.utility.irgendwas haben ohnehin einen schlechten Ruf. Bin ich mir aber bewusst, dass ich mich in einem prozeduralen Umfeld bewege, kann ich damit leben.
Wirklich blockiert hat mich das Fehlen dieser Erkenntnis beim Übergang von Test-First Development (TFD) zu TDD. Während Ersteres lediglich das Schreiben des Testcodes vor der Implementierung der Logik vorstellt, treibt TDD auch das Design. In Dojos oder bei Code-Retreats klappte es zwar recht schnell, dass sich aus den Tests mit dem erwarteten Ergebnis das Design ergab. Ich hatte mir bei der Umsetzung nur zu überlegen, welche Message (Methodenaufruf) an wen (Objekt) zu senden war, um das gewünschte Ergebnis zu erhalten. Im Job aber wollte das nicht so richtig klappen. Es ergab sich da kein Service, der ein Interface hatte und meine POJOs transformierte. Also lief es hier immer auf einen Test-First-Ansatz heraus, bei dem zuerst zu überlegen war, welche Interfaces und Klassen benötigt wurden, um dann zumindest noch auf Methodenebene den Test zu haben.
Es ergibt sicherlich keinen Sinn, alle Anwendungen auf objektorientiert umzustellen oder gar völlig in Frage zu stellen. Sie funktionieren seit Jahren. Aber einzelne Punkte kann man nun durchaus anders betrachten: Brauche ich wirklich Getter und Setter? Ist eine Utility-Klasse schlecht? Sollte ich mein neues Wissen zu objektorientiertem Design hier anwenden oder zerbricht es das gesamte Design im Sinne der Wartbarkeit?
Und schließlich: „Mache ich das nun so, weil es Sinn ergibt, oder weil man das halt so tut“?
Christoph Pater entwickelt seit über fünfzehn Jahren Software. Als Initiator der lokalen Software-Craftsmanship-Community setzt er sich aktiv für die Professionalisierung der Softwareentwicklung ein.
[1] Metz, Sandi: „Practical Object-Oriented Design in Ruby: An Agile Primer“, Addison-Wesley 2012
[2] http://en.wikipedia.org/wiki/Procedural_programming#Comparison_with_object-oriented_programming
[3] http://www.martinfowler.com/bliki/POJO.html
[4] http://de.wikipedia.org/wiki/Plain_Old_Java_Object#Verwendung
[5] Freemann, Steve; Pryce, Nat: „Growing-Object-Oriented Software, Guided by Tests“, Addison-Wesley 2009
[6] http://en.wikipedia.org/wiki/Class-responsibility-collaboration_card
Unit Testing ist wichtig für die Codequalität und das Gelingen von Softwareentwicklungsprojekten. Wie aber kann ich mit JUnit das Beste dafür herausholen?
In seinem Buch stellt Michael Tamm die Frage nach Sinn und Unsinn von Unit Tests nicht. Vielmehr sieht er diese Frage im Sinne des vorhergehenden Statements beantwortet und liefert in seinem Buch eine detaillierte Anleitung, mehr herauszuholen als nur ein paar einfache Tests. Dabei beschränkt er sich nicht nur auf JUnit, sondern beschreibt auch einige nützliche Helfer, um Testobjekte bereitzustellen. Denn was ist ein isolierter Test, wenn nicht erforderliche Objekte durch Dummy-, Pseudo-, Stub- oder Mock-Objekte ersetzt werden können? Schließlich soll häufig nur eine (kleine) Codeeinheit und nicht ein komplett aufgespannter Objektgraph getestet werden. Also nimmt der Autor auch einige bekannte Mock-Frameworks in das Buch auf und zeigt, wie diese für effektive Tests mit JUnit genutzt werden können.
Der Autor startet kleinschrittig: Was bedeutet der sprichwörtliche grüne Balken, was sind funktionale und nicht funktionale Tests? Was sind Testklassen, Testmethoden, Assertions, Testfixtures und Testsuites? Auch wenn es sich dabei um Grundbegriffe jenseits eines konkreten Testframeworks handelt, beschreibt der Autor sie immer vor dem Hintergrund des titelgebenden Systems. Dabei startet er mit JUnit 3, das nach wie vor noch an vielen Stellen eingesetzt wird, um dann auf Version 4 zu schwenken.
Viele Entwickler nutzen JUnit in ihren Projekten, schreiben Tests und nutzen Methoden- oder klassenweite Before- und After-Methoden, um die Umgebung von Tests auf- und wieder abzubauen. Schließlich ist dies der Bereich, für den manch eine Entwicklungsumgebung bereits ein Testskelett generieren kann. Wann aber ist deren Einsatz sinnvoll? Und wieso werden diese oft falsch eingesetzt? Die Nutzung von @Rule oder @RunWith ist noch weniger bekannt. Aber hier bietet das Buch Hilfestellung, JUnit bis ins letzte Detail voll auszureizen, wobei der Autor häufig angibt, ab welcher Version welches Feature, z. B. Hamcrest, zur Verfügung steht oder wie erweiterte Features einzubinden sind.
Testgetriebene Entwicklung thematisiert er auch kurz; dies ist in vorliegendem Buch aber eher eine Randerscheinung, geht es doch hauptsächlich um die technische Nutzung von JUnit und weniger um Vorgehensweisen. Dafür werden aber wie erwähnt Mock-Frameworks und deren Verwendung vorgestellt, namentlich jMock, EasyMock und Mockito sowie on Top PowerMock. Danach widmet er sich dann aber doch noch ein wenig Themen abseits der reinen Technik: Wie werden Tests verständlich geschrieben? Wie können sie beschleunigt werden, falls der zeitliche Aufwand für deren Ausführung zu groß wird? Wie wird abseits der Happy Paths getestet? Und wie verhält es sich mit nicht funktionalen Tests? Damit klingt auch schon ein wenig von der Kunst an, gute Tests zu schreiben.
Zum Abschluss geht der Autor auf die Einbindung des Testframeworks in einigen bekannten Entwicklungsumgebungen ein, sowie auf die Nutzung mit den Build-Werkzeugen Ant und Maven. So ergibt sich insgesamt ein rundes Bild der vielfältigen Nutzungsmöglichkeiten von JUnit. Mithilfe zahlreicher Codebeispiele, Grafiken und Einschübe schreibt der Autor verständlich, sodass es nicht erforderlich wird, das Buch vor dem PC zu lesen. Dies kann ganz entspannt auf der Couch erfolgen. Dennoch: Es schadet sicher nicht, einige Beispiele zeitnah auf das eigene Projekt zu übertragen.
Michael Tamm
JUnit-Profiwissen
Effizientes Arbeiten mit der Standardbibliothek für automatisierte Tests in Java
370 Seiten, 34,90 Eurodpunkt.verlag 2013ISBN 978-3-86490-020-4
„Pacta sunt servanda“, oder zu deutsch „Verträge sind einzuhalten“: Was schon im Mittelalter galt, soll in der modernen Welt der Software auch verbindlich sein. Mithilfe von (API-)Verträgen, die nicht nur durch einen, sondern mehrere Vertragspartner gestaltet werden, lassen sich Microservices-Architekturen einfach und effizient testen und weiterentwickeln.
„Microservices“ [1] ist eines der Schlagwörter schlechthin, wenn im Moment über moderne Softwarearchitektur gesprochen und geschrieben wird. Dabei handelt es sich um einen Architekturstil, bei dem die Geschäftsfunktionen einer Anwendung auf viele kleine Services verteilt werden. Jeder Service läuft in seinem eigenen Prozess und ist typischerweise für genau eine klar definierte Geschäftsfunktion zuständig. Die Services kommunizieren untereinander meist über Webtechnologien wie z. B. HTTP/REST oder aber auch über einen Message Bus. Eine Microservices-Applikation erfüllt die an sie gestellten Anforderungen durch das Zusammenspiel der einzelnen Microservices. So kann eine Applikation zur Darstellung von Produktinformationen z. B. aus folgenden Microservices bestehen: Beschreibung, Preis, Kundenbewertungen und Lagerbestand.
Vorteile einer Microservices-Architektur sind unter anderem Skalierbarkeit (sowohl der Software selbst als auch des Entwicklungsteams), Ausfallsicherheit, Sprach- und Technologieunabhängigkeit sowie erhöhte Flexibilität. Allerdings bringt ein solches verteiltes System auch Nachteile mit sich, z. B. den komplizierteren Betrieb, Codeduplikation, Netzwerklatenz, verteilte Transaktionen, zusätzliche Schnittstellen und schwierigere Testbarkeit [2]. Für all diese Probleme existieren aber Lösungsansätze. In diesem Artikel sollen Consumer-driven Contracts als Lösungsmöglichkeit für die beiden letztgenannten Nachteile der zusätzlichen Schnittstellen und Testbarkeit vorgestellt werden.
Beim Testen einer Anwendung, die aus Microservices besteht, ergeben sich gegenüber einer monolithischen Architektur eine Reihe spezieller Herausforderungen [3]. Die auf den unteren Ebenen angesiedelten Tests (beispielsweise Unit Tests) können weiter wie gewohnt entwickelt werden. Damit aber eine Applikation ihre Aufgabe erfüllen kann, ist in einer Microservices-Architektur stets ein Zusammenwirken einzelner Microservices notwendig. Sobald also Schnittstellen zu anderen Microservices im Spiel sind oder die Antworten eines anderen Microservice benötigt werden, um die eigene Funktionalität zu testen, stellt sich die Frage, wie diese Abhängigkeiten zur Verfügung gestellt werden können.
Üblicherweise will man zum Testen eines Microservice nicht die komplette Landschaft der Services hochfahren. Dadurch würden die großen Vorteile einer Microservices-Architektur verspielt werden, nach denen einzelne Microservices unabhängig entwickelt, gewartet und in Betrieb genommen werden können. Der einzelne Service soll also möglichst isoliert getestet werden. Andererseits soll aber sichergestellt werden, dass die Services im Zusammenspiel wie erwartet funktionieren. Diese Herausforderung lässt sich mithilfe des Consumer-driven Contract Patterns [4] lösen.
Das Konzept der Consumer Contracts ist zwar nicht neu, gewinnt jedoch im Zuge des Microservices-Architekturstils an Aktualität. Ein Consumer Contract beschreibt aus der Sicht eines Aufrufers, wie die Antwort strukturiert sein soll, die er beim Aufruf eines Service erwartet.
Im Vergleich dazu sind Provider Contracts besser bekannt. Hier beschreibt ein Service, welche Antworten er auf welche Anfragen liefern wird. Typische Vertreter sind z. B. die WSDL eines Web Service oder die Swagger-Dokumentation [5] eines REST-Service. Provider Contracts stellen meist eine umfassende Dokumentation aller Fähigkeiten eines Service dar. Der anbietende Service stellt diesen Schnittstellenvertrag selbst zur Verfügung.
Ein Consumer Contract ist dagegen die Spezifikation eines Ausschnitts aus der Gesamtfunktionalität eines aufgerufenen Service, den ein spezieller Aufrufer benötigt, um wiederum die an ihn gestellten Anforderungen erfüllen zu können.
Der Consumer-driven Contract eines Service setzt sich aus allen Consumer Contracts seiner Aufrufer zusammen (Abb. 1). Es ist deshalb möglich, dass ein Service gar nicht seinen vollständigen Provider Contract implementieren muss, weil es zu einem bestimmten Zeitpunkt im Consumer-driven Contract keinen Aufrufer einer bestimmten Funktionalität gibt. Eine so aufgebaute Microservices-Applikation unterstützt also auch das YAGNI-Prinzip [6], indem eine in einem Service bereits angedachte oder spezifizierte Funktionalität erst implementiert werden muss, sobald im Consumer-driven Contract ein Aufrufer dieser Funktionalität auftaucht.
Ein solches Vorgehen ist vor allem für eine Umgebung sinnvoll und geeignet, in der Kontrolle sowohl über Consumer als auch über Provider besteht, z. B. weil die Services von Teams der gleichen Organisation entwickelt werden.
Consumer Contracts können im Test auf zwei Arten verwendet werden. Beim Testen des Consumers kann aus ihnen ein Provider Stub erzeugt werden, der dem Consumer im Test genau die Antworten liefert, die er vom Provider erwartet. Zum Testen des Providers werden Requests aus dem Contract gegen die Schnittstelle des Providers erzeugt und die gelieferten Antworten geprüft (Abb. 2). Sind beide Tests erfolgreich, können sowohl Consumer als auch Provider bedenkenlos in Betrieb genommen werden. Bricht der Test des Providers ab, darf dieser nicht deployt werden, weil er die an ihn gestellten Anforderungen nicht (mehr) erfüllt. Diese Situation tritt z. B. ein, wenn ein Provider ein Feld aus seiner Antwortdatenstrukur entfernen will, aber mindestens ein Consumer dieses Feld noch erwartet.
Schlagen Tests des Consumers fehl, kann er nicht (mehr) mit den Antworten umgehen, die der Provider weiterhin liefern wird (solange dessen Tests gegen den Consumer Contract erfolgreich sind). Er darf deshalb ebenfalls nicht deployt werden.
Mit dieser Strategie ist es nun möglich, Services relativ unabhängig voneinander auf ihr Zusammenspiel hin zu testen, ohne die jeweiligen Partnerservices hochfahren zu müssen. Diese Tests liefern im Regelfall sehr schnelles Feedback bei der Änderung einzelner Services bei hoher Konfidenz über die Integrität der Gesamtapplikation.
Damit diese Sicherheit besteht, müssen alle Consumer ihre Contracts aktuell halten und den Providern für ihre Tests ohne Verzögerung zur Verfügung stellen. Dies kann beispielsweise über ein zentrales Repository geschehen, in dem die Consumer ihre Contracts veröffentlichen und das von den Providern beim Testen auf Neuerungen abgefragt wird.
Wie geschieht die praktische Umsetzung von Consumer-Contract-Tests? In einem Monolithen ließe sich das ganze Prinzip durch einfache JUnit-Tests implementieren. Allerdings gestaltet sich dies im Fall von Microservices-Anwendungen wesentlich aufwändiger. Denn hierbei müssen nicht nur einzelne Objekte und Methodenaufrufe gemockt werden, sondern ganze REST-Services, die über HTTP aufgerufen werden sollten. Zur Lösung dieser Aufgabe gibt es bereits Open-Source-Tools. Einer der bekanntesten Vertreter ist ein noch recht junges Tool mit dem passenden Namen Pact [7]. Es wurde ursprünglich in Ruby entwickelt, ist aber bereits für die unterschiedlichsten Plattformen wie JavaScript, .NET und auch für die JVM portiert worden.
Im Folgenden wollen wir uns anhand einer einfachen Anwendung anschauen, wie man mit Pact-JVM [8] die Schnittstelle zwischen Microservices durch Consumer-driven Contracts testen kann.
Getestet wird die Schnittstelle zwischen dem ProductDetailsService, der detailliertere Informationen, wie z. B. die Beschreibung zu einem Produkt, bereitstellt und dem ProductService, der diese Informationen benötigt. Somit ist der ProductService der Consumer, und der ProductDetailsService nimmt in diesem Fall die Rolle des Providers ein. Die Kommunikation findet über eine REST-Schnittstelle statt, die der ProductDetailsService bereitstellt.
Um das Erzeugen und Konsumieren des Service zu vereinfachen, verwenden wir in unserem Beispiel Spring Boot und legen für jeden Service ein eigenes Gradle-Projekt an. Zum detaillierten Nachvollziehen des Codes stehen beide Projekte auf GitHub zur Verfügung [9].
Nach dem Consumer-driven-Ansatz wird zuerst der Consumer, also unser ProductService, entwickelt. Da die Kommunikation zwischen ihm und dem Provider getestet werden soll, ist der Aufruf der REST-Schnittstelle der interessanteste Teil der Implementierung. Wie Listing 1 zeigt, wurde diese Schnittstelle in die Klasse ProductDetailsFetcher gekapselt und enthält nur die Methode fetchDetails. Sie verwendet ein RestTemplate, um die Ressource von dem gegebenen URI zu laden und aus den Antwortdaten eine Instanz der Klasse ProductDetails zu erstellen. Um den Quellcode kompakt zu halten, wurde in Listing 1 auf die Fehlerbehandlung verzichtet.
Listing 1
public class ProductDetailsFetcher {
public ProductDetails fetchDetails(URI productDetailsUri) {
return new RestTemplate().getForObject(productDetailsUri,
ProductDetails.class);
}
}
Aber wie testet man diesen Teil der Anwendung? Da noch kein Provider existiert, muss die erwartete Schnittstelle aus Sicht des Consumers definiert werden. Hier kommt Pact ins Spiel. Dazu wird die Klasse ProductDetailsServiceConsumerTest (Listing 2) erstellt. Sie erbt von der abstrakten Klasse ConsumerPactTest, die von Pact-JVM bereitgestellt wird. Der Test fordert die Implementierung der Methode createFragment. Zurückgegeben wird ein PactFragment, das alle Interaktionen enthält, die während des Tests anfallen, also jeden Request des Consumers und die erwarteten Antworten des Providers dazu. Die Interaktionen können recht angenehm über die ConsumerPactBuilder-DSL definiert werden. Der als Parameter übergebene builder erwartet immer einen Methodenaufruf uponReceiving, gefolgt von einem willRespondWith. In unserem Beispiel erwarten wir vom Provider, dass er auf die GET-Anfrage auf den Pfad /productdetails/1 mit dem Statuscode 200 und dem erwarteten JSON als Body antwortet.
Mit dem PactFragment haben wir die Erwartung des Consumers an den Provider definiert. Nun können wir in der Methode runTest testen, ob unsere Implementierung des ProductDetailsFetcher den definierten Contract einhält. Diese Methode wird ausgeführt, nachdem Pact den Provider Stub auf Basis des PactFragments gestartet hat und durch den Parameter url die Adresse des Stubs mitgeteilt bekommt.
Das Testen geschieht, wie bei Unit Tests üblich, durch das Ausführen der Komponenten und Überprüfen der Resultate durch asserts. Wenn nun der Consumer eine nicht erwartete Anfrage stellt, wird der Test fehlschlagen. Ebenso wird getestet, ob der Consumer mit der Antwort des Stubs zurechtkommt.
Damit eine Zuordnung des Contracts zu einem Consumer und einem Provider möglich ist, müssen noch deren Namen über die Methoden providerName und consumerName festgelegt werden.
Um nun den Test durch Pact JVM auszuführen, müssen wir nur noch die Dependency testCompile 'au.com.dius:pact-jvm-consumer-junit_2.11:2.1.7' zum Gradle-Projekt des ProductDetailsService hinzufügen.
Listing 2
public class ProductDetailsServiceConsumerTest extends ConsumerPactTest {
@Override
protected PactFragment createFragment(ConsumerPactBuilder.PactDslWithProvider builder) {
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json;charset=UTF-8");
return builder.uponReceiving("a request for product details")
.path("/productdetails/1")
.method("GET")
.willRespondWith()
.headers(headers)
.status(200)
.body("{\"id\":1,\"description\":\"This is the description for product 1\"}")
.toFragment();
}
@Override
protected String providerName() {
return "Product_Details_Service";
}
@Override
protected String consumerName() {
return "Product_Service";
}
@Override
protected void runTest(String url) {
URI productDetailsUri = URI.create(String.format("%s/%s/%s", url, "productdetails", 1));
ProductDetailsFetcher productDetailsFetcher = new ProductDetailsFetcher();
ProductDetails productDetails = productDetailsFetcher.fetchDetails(productDetailsUri);
assertEquals(productDetails.getId(), 1);
}
}
Der ProductDetailsServiceConsumerTest bietet alle Informationen, die nötig sind, um den Contract für die Schnittstelle zu erstellen und die eigenen Komponenten zu testen, die auf diese Schnittstelle zugreifen. Der Test kann nun mit gradle test im Verzeichnis des ProductService ausgeführt werden. Dabei wird der Provider Stub gestartet und die eigenen Komponenten gegen diesen getestet. Zusätzlich erzeugt Pact während des Tests noch eine JSON-Datei – das so genannte Pact File. In unserem Beispiel ist es nach dem Test unter ./target/pacts/Product_Service-Product_Details_Service.json zu finden. Dieses File stellt den eigentlichen Contract dar und kann verwendet werden, um zu prüfen, ob er vom Provider eingehalten wird. Es enthält alle Requests und Antworten, die im Consumer-Test definiert wurden. Listing 3 zeigt das Pact File für den Contract zwischen ProductDetailsService und ProductService. Durch die Plattformunabhängigkeit des Pact Files und die zahlreichen Portierungen von Pact ist es ebenfalls möglich, dieses auch mit Microservices zu verwenden, die in anderen Programmiersprachen entwickelt wurden. Somit unterstützt Pact die Entwicklung polyglotter Anwendungen, die ein oft genannter Vorteil des Microservices-Architekturstils ist.
Listing 3
{
"provider" : {
"name" : "Product_Details_Service"
},
"consumer" : {
"name" : "Product_Service"
},
"interactions" : [ {
"description" : "a request for product details",
"request" : {
"method" : "GET",
"path" : "/productdetails/1"
},
"response" : {
"status" : 200,
"headers" : {
"Content-Type" : "application/json;charset=UTF-8"
},
"body" : {
"id" : 1,
"description" : "This is the description for product 1"
}
}
} ],
"metadata" : {
...
}
}
In Form des Pact Files haben wir den Consumer Contract und können mit diesem nun den passenden Provider entwickeln. Die Klasse Application im Projekt des ProductDetailsService erstellt hierfür die entsprechende REST-Schnittstelle. Dies gelingt dank Spring recht einfach (Listing 4). Die Methode fetchProductDetails wird bei jedem Request ein serialisiertes ProductDetails-Objekt zurückgeben. Damit unsere Beispielanwendung übersichtlich bleibt, erhalten alle Produkte einen statischen Text als Beschreibung.
Listing 4
@RestController
public class Application {
@RequestMapping(value = "/productdetails/{id}", method = RequestMethod.GET)
public ProductDetails fetchProductDetails(@PathVariable final long id) {
return new ProductDetails(id, "This is the description for product " + id);
}
public static void main(final String[] args) {
SpringApplication.run(Application.class, args);
}
}
Um zu verifizieren, ob unser Provider den gegebenen Contract einhält, müssen die Anfragen des Pact Files an die REST-Schnittstelle gestellt werden. Antwortet der Service so wie im Pact definiert, gilt der Contract als erfüllt. Es ist sinnvoll, die Verifikation in den Build-Prozess des Service zu integrieren. Dafür bietet Pact-JVM ein Plug-in [10] für Gradle an. Es wird mit apply plugin: 'au.com.dius.pact' zum Gradle-Projekt des ProductDetailsService hinzugefügt. Listing 5 zeigt, wie mit dem Plug-in ein Pact in Gradle definiert wird. So muss die Lokation des Providerservice und der Pfad zum Pact File angegeben werden. Neben dem Verwenden von Dateien ist es auch möglich, auf entfernte Pact Files mit url() zuzugreifen. Dadurch können beispielsweise die Pact Files auf einem zentralen Webserver bereitgestellt werden.
Damit Pact unseren Service testen kann, muss er erst gestartet werden. Da unser Projekt Spring Boot verwendet, können wir unseren Provider einfach mit dem Befehl gradle bootRun starten. Sobald der Service erreichbar ist, lassen wir mit gradle pactVerify die Verifikation durchführen. So ist es möglich, die Schnittstelle des ProductDetailsService entkoppelt vom ProductService zu testen.
Listing 5
pact {
serviceProviders {
productDetailsServiceProvider {
protocol = 'http'
host = 'localhost'
port = 10100
path = '/'
hasPactWith('productServiceConsumer') {
pactFile = file("../product-service/target/pacts/Product_Service-Product_Details_Service.json")
}
}
}
}
Consumer-driven Contracts entschärfen eine der größten Herausforderungen in Microservices-Architekturen. Die Gefahr, die Vorteile der getrennten Deploybarkeit durch eng gekoppelte Tests wieder zu verlieren, wird durch diese Art der Schnittstellenspezifikation und des Testaufbaus stark vermindert. Trotzdem bleibt eine hohe Sicherheit über das korrekte Zusammenspiel der Microservices erhalten. Unser Beispiel hat gezeigt, dass mit Pact ein vergleichsweise ausgereiftes Tool zur Unterstützung von Consumer-driven Contract Testing in (polyglotten) Microservices-Umgebungen existiert.
Tobias Bayer ist Softwarearchitekt und Senior Developer bei der inovex GmbH. Seine Schwerpunkte liegen auf der Entwicklung von skalierbaren Webapplikationen mit Java und mobilen Apps für iOS. Außerdem beschäftigt er sich mit funktionaler Programmierung in Clojure.
Hendrik Still ist Werkstudent im Bereich Application Development bei der inovex GmbH. Zurzeit beschäftigt er sich mit den Problemstellungen von Microservices-Architekturen und deren Umsetzung mit Docker.
[1] http://martinfowler.com/articles/microservices.html
[2] http://highscalability.com/blog/2014/4/8/microservices-not-a-free-lunch.html
[3] http://martinfowler.com/articles/microservice-testing
[4] http://martinfowler.com/articles/consumerDrivenContracts.html
[6] http://de.wikipedia.org/wiki/YAGNI
[7] https://github.com/realestate-com-au/pact
[8] https://github.com/DiUS/pact-jvm
[9] https://github.com/inovex/pact-example
[10] https://github.com/DiUS/pact-jvm/tree/master/pact-jvm-provider-gradle
Das schlanke und leichtgewichtige Kommunikationsprotokoll MQTT hat sich als wichtigstes Internet-of-Things-(IoT-)Standardprotokoll durchgesetzt. Die Anwendungsfälle für MQTT reichen von industriellen Anwendungen über Connected Cars bis zu Home Automation und Do-it-yourself-(DIY-)Projekten. Grund genug, das IoT-Allrounder-Protokoll aus der Java-Perspektive zu betrachten und zu erforschen, welche Möglichkeiten sich mit dem Einsatz von MQTT in Java-Applikationen ergeben.
Das Protokoll MQTT wurde 1999 als ein M2M-Kommunikationsprotokoll für SCADA-Systeme mit minimalem Protokolloverhead entwickelt. Es bietet vernetzten Geräten eine Möglichkeit, bandbreiten- und batterieschonend miteinander zu kommunizieren. Das Protokoll MQTT zeichnet sich außerdem durch seine außerordentliche Leichtgewichtigkeit aus und ist clientseitig sehr einfach zu implementieren.
Seit 2010 ist die Protokollspezifikation in Version 3.1 lizenzfrei verfügbar und seit 2014 auch die aktuellste Version 3.1.1, die erstmals bei dem Standardisierungsgremium OASIS spezifiziert wurde [1]. Das Protokoll schlägt mit einem ereignisgesteuerten Push-Ansatz einen anderen Weg ein als beispielsweise HTTP, das auf Request/Response basiert.
MQTT ist besonders geeignet für eine zuverlässige Nachrichtenübertragung in unzuverlässigen und instabilen Netzwerken, wie etwa bei Mobilfunknetzwerken. Folgende Aspekte machen MQTT zu einem optimalen Protokoll für das Internet der Dinge und die mobile Kommunikation:
MQTT ist komplett datenagnostisch. Es ist daher geeignet, Daten jeder Art zu übertragen, egal ob es sich um Text oder binärkodierte Inhalte handelt.
MQTT ist einfach. Die Konzepte sind leicht zu erlernen und eigene Clientimplementierungen problemlos zu realisieren.
MQTT erfindet das Rad nicht neu. Es baut auf TCP auf, und die Übertragung kann jederzeit mittels SSL/TLS verschlüsselt werden.
MQTT ermöglicht echte Push-Kommunikation: Anders als bei anderen Protokollen gibt es bei MQTT kein Polling. Nachrichten werden sofort verteilt, wenn ein Ereignis auftritt. Das schont Bandbreite und CPU, da MQTT-Clientanwendungen reagieren können, sobald eine Nachricht ankommt, anstatt beim Server nach neuen Nachrichten zu fragen.
MQTT implementiert das Publish/Subscribe-Pattern (Abb. 1). Das bedeutet, dass jede Kommunikation über einen zentralen Verteiler, den so genannten MQTT Message Broker, stattfindet. Jede Nachricht, die ein Client sendet, enthält ein so genanntes Topic und die tatsächlichen Nutzdaten.
Ein Topic ist ein Text, der Trennzeichen enthalten kann, und er kann, ähnlich dem POSIX-Dateisystempfad, eine Hierarchie abbilden. Die Struktur heimautomatisierung/peters_haus/wohnzimmer/gluehbirne/1 wäre beispielsweise ein gutes Topic für Nachrichten von Glühbirne 1 im Wohnzimmer von Peters Haus. Jeder MQTT-Client, der Nachrichten für dieses Topic empfangen möchte, abonniert es beim Message Broker. Da die interessierten Clients beim Eintreffen neuer Nachrichten durch den Broker benachrichtigt werden, anstatt selbst beim Server nach Änderungen zu fragen, wird eine hocheffiziente Kommunikation zwischen den Teilnehmern gewährleistet. Somit ist eine echte Push-Kommunikation möglich. Entscheidend ist, dass die Teilnehmer der Kommunikation nichts voneinander wissen, da jeder Client nur den Message Broker kennt, nicht jedoch die anderen Teilnehmer.
MQTT besitzt neben dem reinen Publish/Subscribe eine Reihe von bemerkenswerten Protokollfeatures, die im Kontext des IoT und von Mobile optimal sind.
Quality of Service: Um sicherzustellen, dass eine gesendete Nachricht beim Empfänger ankommt, definiert MQTT drei verschiedene Quality-of-Service-Levels (QoS), mit denen eine Nachricht gesendet werden kann: at most once (0), at least once (1) und exactly once (2).
Retained Messages: Die letzte gesendete Nachricht eines Topics kann am Broker hinterlegt werden. Jeder neue Subscriber auf diesem Topic erhält automatisch die zuletzt gesendete Nachricht.
Last Will and Testament (LWT): Ein Client kann eine MQTT-Nachricht als „letzten Willen“ beim Verbindungsaufbau am Broker hinterlegen. Wenn der Broker feststellt, dass dieser Client nicht mehr verbunden ist und der Client sich vorher nicht abgemeldet hat, wird dieser letzte Wille an alle Subscriber versendet. Der Broker ist also verantwortlich dafür, den letzten Willen des Clients auszuführen.
Persistent Sessions: In Anwendungsfällen, bei denen mit häufigen Verbindungsabbrüchen von Clients zu rechnen ist, kann der Broker eine persistente Session vorhalten. Wenn ein Client sich erneut verbindet, schickt der Broker alle verpassten Nachrichten für seine Subscriptions an den Client. Natürlich muss der Client sich nicht neu auf vorher abonnierte Topics subscriben, da der Broker die vorherige Session einfach fortführt.
Eclipse Paho [2] ist ein Projekt unter dem Schirm der Eclipse Foundation, das MQTT-Clientimplementierungen in verschiedenen Programmiersprachen zur Verfügung stellt. Das sind neben C die folgenden: C++, C#, Python, JavaScript, Go und natürlich auch Java. Das Paho-Projekt existiert seit 2012, die Java- und C-Implementierungen werden jedoch seit vielen Jahren bei IBM, die auch die initiale Implementierungen bereitgestellt haben, eingesetzt. Sie sind für den produktiven Einsatz freigegeben. Neben einem synchronen API bietet Paho auch ein asynchrones API, das mehr Performance verspricht, dafür etwas komplizierter in der Benutzung ist. In den folgenden Beispielen wird das synchrone Paho API verwendet.
Eclipse Paho ist als .jar-Datei zum Download verfügbar [3] und kann in ein Java-Projekt eingebunden werden. Wer Apache Maven für sein Dependency Management bevorzugt, kann den Eintrag aus Listing 1 in der pom.xml zu seinem Projekt hinzufügen.
Paho ist aktuell nicht im Maven Central Repository verfügbar. Es existiert jedoch ein eigenes Repository, das man, wie in Listing 1 gezeigt, in seiner pom.xml hinzufügen kann.
Listing 1
...
<dependencies>
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.0.2</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>Eclipse Paho Repo</id>
<url>https://repo.eclipse.org/content/repositories/paho-releases/</url>
</repository>
</repositories>
...
Der nachfolgende Code baut eine Verbindung zu einem MQTT-Broker auf, schickt eine Nachricht an das Topic javamagazin/mqttarticle mit dem Text „Hallo Welt“ und trennt die Verbindung zum MQTT wieder.
MqttClient client = new MqttClient(
"tcp://broker.mqttdashboard.com", generateClientId());
client.connect();
MqttMessage message = new MqttMessage("Hallo Welt".getBytes());
client.publish("javamagazin/mqttarticle", message);
client.disconnect();
Wie man sieht, sind für das Senden von Nachrichten nur wenige Zeilen Code notwendig. Auffallend in dem Codebeispiel ist, dass der String Hallo Welt als Bytearray übergeben wird. Das hat damit zu tun, dass die Daten bei MQTT immer binär sind und es dem Protokoll egal ist, ob ein Text, ein Bild oder ein sonstiges Bytearray übertragen wird.
Folgender Code schickt dieselbe MQTT-Nachricht wie das vorherige Codebeispiel, diesmal jedoch als Retained Message und mit Quality of Service 2. Damit stellt das Protokoll sicher, dass die Nachricht exakt ein Mal beim Empfänger ankommt.
client.publish("javamagazin/mqttarticle", "Hallo Welt".getBytes(), 2, true);
Natürlich wäre es witzlos, wenn zwar Nachrichten gesendet werden, aber niemand diese Nachrichten empfangen würde. Einen Subscriber-Client mit Paho zu implementieren, ist dabei eine einfache Sache. Der in Listing 2 dargestellte Beispielcode zeigt die Implementierung eines Clients, der sämtliche Nachrichten für das Topic javamagazin/mqttarticle empfängt und die Nachrichten auf der Konsole ausgibt.
Listing 2
...
MqttClient client = new MqttClient(
"tcp://broker.mqttdashboard.com", generateClientId());
client.setCallback(new MqttCallback() {
@Override
public void connectionLost(Throwable throwable) { }
@Override
public void messageArrived(String t, MqttMessage m) throws Exception {
System.out.println(new String(mqttMessage.getPayload()));
}
@Override
public void deliveryComplete(IMqttDeliveryToken t) { }
});
client.connect();
client.subscribe("javamagazin/mqttarticle");
...
Um auf ankommende Nachrichten reagieren zu können, kann man dem MqttClient mittels der setCallback()-Methode einen MqttCallback anhängen. Dieser bietet mittels der messageArrived()-Methode die Möglichkeit, auf eingehende Nachrichten zu reagieren. Der Callback wird in einem Thread im Hintergrund aufgerufen, daher wird der Hauptthread der Applikation nicht geblockt, wenn eine neue Nachricht eingeht.Der Code ist reaktiv.
Im MqttCallback können noch weitere Methoden implementiert werden: connectionLost() wird aufgerufen, sobald die TCP-Verbindung getrennt wurde. Diese Methode kann benutzt werden, um beispielsweise einen Reconnect zu implementieren. Die Methode deliveryComplete() ruft Paho auf, sobald eine Nachricht erfolgreich abgeschickt wurde.
Paho bietet eine volle Implementierung aller MQTT-Features, u. a. Last-Will-and-Testament-(LWT-) und Persistent Sessions. Listing 3 zeigt, wie eine LWT-Nachricht beim Verbindungsaufbau mitgegeben werden kann und wie man eine persistente Session für einen Client aktiviert. Zusätzlich werden ein Benutzername und ein Passwort für die Authentifizierung am Broker übergeben.
Listing 3
...
MqttClient client = new MqttClient(
"tcp://broker.mqttdashboard.com", generateClientId());
MqttConnectOptions options = new MqttConnectOptions();
// Setzen einer Persistent Session
options.setCleanSession(false);
options.setUserName("username");
options.setPassword("password".toCharArray());
options.setWill(
"will/topic", // Topic
"Disconnected!".getBytes(), // Nachrichteninhalt
2, // QoS
false); // Retained message?
client.connect(options);
...
Das MqttConnectOptions-Objekt bietet noch mehr Eingriffsmöglichkeiten in die Standardkonfiguration von Paho. SSL kann beispielsweise aktiviert und es können Timeouts gesetzt werden.
Wie gezeigt, ist es sehr leicht, einen eigenen MQTT-Client mit Eclipse Paho zu entwickeln. Dabei eignet sich Paho genauso zur Einbindung in Java-Enterprise-Anwendungen, wie auch in Android-Applikationen oder für Embedded Devices. Neben dem synchronen API gibt es noch ein asynchrones API, das optimal geeignet ist, wenn sehr viele Nachrichten gleichzeitig geschickt werden sollen oder falls es nötig ist, Paho tiefer in die eigene Applikation zu integrieren. Paho hat eine aktive Community und wird aktiv weiterentwickelt.
Der MQTT-Broker ist das Herzstück der Publish/Subscribe-Kommunikation, da alle MQTT-Clients über den Broker via Topics kommunizieren. Unter den verschiedenen verfügbaren MQTT-Brokern haben wir uns in unserem Beispiel für den Java-basierten Broker HiveMQ [5] entschieden. HiveMQ ist ein MQTT-Broker für den professionellen Einsatz und bietet neben seiner hohen Performance und der Cluster-Funktionalität ein Java-basiertes Open-Source-Plug-in-System.
In den nachfolgenden Beispielen soll das Java-basierte Plug-in-System genutzt werden, um den MQTT-Broker um folgende Funktionalitäten zu erweitern:
Authentifizierung: Ein MQTT-Client muss eine valide Username/Passwort-Kombination beim Verbindungsaufbau benutzen; andernfalls wird der Verbindungsaufbau vom MQTT-Broker abgelehnt.
Jede MQTT-Nachricht wird vom MQTT-Broker persistiert, damit andere Applikationen die Daten später auswerten können.
Mit dem HiveMQ-Plug-in-System ist es möglich, die oben genannten Anforderungen, wie das Abspeichern von MQTT-Messages und die Authentifizierung von MQTT-Clients, mit wenigen Zeilen Code umzusetzen. Dazu werden von HiveMQ verschiedene Callbacks angeboten, die in einem Plug-in implementiert werden können. Neben Callbacks gibt es auch noch so genannte Services, die benutzt werden können, um aus dem Plug-in-System heraus mit dem Broker zu interagieren. Es ist beispielsweise möglich, manuell neue Subscriptions für bestimmte Clients anzulegen oder eine Nachricht direkt aus dem Plug-in-System heraus an bestimmte Topics zu schicken. Das Plug-in-System ist unter der Apache-Lizenz frei verfügbar, und der Quellcode kann auf GitHub [6] eingesehen werden. Um mit der Plug-in-Entwicklung zu starten, gibt es einen Maven Archetype, eine ausführliche Dokumentation [7] und viele Beispiel-Plug-ins auf GitHub [8].
In Produktionsumgebungen ist es üblich, dass ein MQTT-Client sich authentifizieren muss, bevor er Nachrichten senden oder empfangen kann. In kleineren Umgebungen werden dazu oft Access Control Lists (ACLs ) benutzt, also eine Datei mit erlaubten Benutzernamen und dem entsprechendem Passwort. Für HiveMQ gibt es dazu bereits ein fertiges Plug-in [9], das ohne programmatischen Aufwand benutzt werden kann. Natürlich reichen ACLs oft nicht aus, denn die Accounts müssen oft aus dem firmeneigenen LDAP in Datenbanken oder über (Micro-)Services ausgelesen werden. Da jeder Anwendungsfall hier unterschiedlich ist, bietet es sich an, ein eigenes Plug-in für das genaue Einsatzszenario zu entwickeln. Das HiveMQ-Plug-in-System bietet dazu den OnAuthenticationCallback an. Eine beispielhafte Implementierung ist in Listing 4 zu sehen.
Listing 4
public class AuthenticationExample implements OnAuthenticationCallback {
@Override
public Boolean checkCredentials(final ClientCredentialsData clientData)
throws AuthenticationException {
if (clientData.getClientId().startsWith("mqtt-client")) {
return true;
}
return false;
}
@Override
public int priority() {
return CallbackPriority.HIGH;
}
}
Die Methode checkCredentials wird von HiveMQ aufgerufen, sobald ein neuer Client sich verbinden möchte. In Listing 4 wird überprüft, ob der Client einen Client Identifier besitzt, der mit dem String mqtt-client beginnt. Natürlich würden in einem realen Anwendungsfall eher der Benutzername und das Passwort überprüft und ein Lookup in einer Datenbank, einem LDAP oder einem firmeninternen Service durchgeführt werden. Das Beispiel veranschaulicht jedoch, wie einfach eine solche Integration zu realisieren ist: Es kann jede beliebige Java-Bibliothek eingebunden werden, weshalb es also ohne Weiteres möglich ist, z. B. ein ORM-Framework wie Hibernate im Plug-in-System zu benutzen.
Ein weiterer typischer Anwendungsfall für ein MQTT-Broker-Plug-in ist es, Nachrichten direkt vom Broker in eine Datenbank zu speichern oder an ein nachgelagertes System zu übergeben. HiveMQ bietet dazu den Callback OnPublishReceived an, der implementiert werden kann, um die MQTT-Publish-Message zu verarbeiten. Das Codebeispiel in Listing 5 zeigt, wie eine solche Nachricht in einer Datenbank gespeichert werden kann, falls der Absender der Nachricht einen Client Identifier besitzt, der mit important-client beginnt.
Listing 5
public class PublishExample implements OnPublishReceivedCallback {
@Override
public void onPublishReceived(PUBLISH publish, ClientData clientData) throwsOnPublishReceivedException {
if (clientData.getClientId().startsWith("important-client")) {
saveToDB(publish.getTopic(), publish.getPayload());
}
}
@Override
public int priority() {
return CallbackPriority.HIGH;
}
public void saveToDB(String topic, byte[] content) {
// Persist to database!
}
}
Es ist also ziemlich einfach, mit diesem Callback Nachrichten vorzufiltern, bevor sie in der Datenbank gespeichert werden. Dazu können wahlweise Sessiondaten des sendenden Clients benutzt werden oder aber auch eine Eigenschaft der gesendeten Nachricht. Dieser Callback kann natürlich auch mehrmals und jeweils mit einer anderen Businesslogik implementiert werden. So wäre es möglich, später ein Plug-in zu entwickeln, das eine SMS bei jeder Nachricht mit dem Topic alert an das Operations-Team schickt. Die Callbacks werden dann nach Priorität ausgeführt, wie dies in der priority()-Methode geschieht. Ein gängiger Anwendungsfall ist auch, im OnPublishReceivedCallback die Daten der Nachricht zu überprüfen, bevor sie weiterverarbeitet werden. Im Fehlerfall kann dann eine OnPublishReceivedException geworfen werden, die eine Weiterverarbeitung und ein Weitersenden der Nachricht verhindert.
Der Einstieg in MQTT ist für Java-Entwickler dank MQTT-Clients wie Eclipse Paho und Java-basierten Brokern wie HiveMQ so einfach wie nie zuvor. Es ist mit wenigen Zeilen Code möglich, aus bestehenden Java-Applikationen vollwertige MQTT-Clients zu machen, die sowohl Nachrichten senden als auch empfangen können. Zusätzliche Geschäftslogik, die über das Verteilen von MQTT-Nachrichten hinausgeht, lässt sich sehr leicht mit erweiterbaren MQTT-Brokern wie HiveMQ umsetzen. Dabei kann jede beliebige Java-Bibliothek in Plug-ins verwendet werden, was die Produktivität bei der Entwicklung stark erhöht.
Dominik Obermaier ist Geschäftsführer bei der dc-square GmbH, die hochskalierbare M2M- und IoT-Lösungen, wie den MQTT-Broker HiveMQ, entwickelt. Dominik berät Firmen im Bereich MQTT, Messaging und IoT. Er entwickelte MQTT 3.1.1 bei OASIS mit und ist regelmäßiger Autor und Speaker auf Konferenzen.
Das Internet der Dinge bringt eine Entwicklung auf die Tagesordnung, die dem Trend immer schnellerer Hardware und immer größerem Arbeitsspeicher diametral entgegensteht: Sensoren mit geringen Ressourcen, die stromsparend arbeiten müssen, werden im Netz in großer Anzahl miteinander verbunden und müssen ihre Daten effizient liefern.
Textbasierte Datenaustauschformate wie JSON oder XML benötigen Parser, die auf den jeweiligen Geräten ausgeführt werden. Binäre Daten, wie Bilder oder Audiodateien, werden für die Übertragung als Text aufwändig kodiert. Nicht nur der eigentliche Vorgang der Datenumwandlung benötigt dabei Rechenzeit und Speicher, sondern auch die Menge der zu übertragenen Daten steigt merklich an. Binäre Austauschformate wie Binary JSON [1] (BSON) oder binäre, an XML orientierte Formate wie WBXML [2] wurden für diesen Zweck entwickelt.
Wo sind die Grenzen für die Anwendung binärer Austauschformate? Warum sind binäre Formate nicht nur für das Internet der Dinge interessant?
Lange Zeit war XML [3] das bevorzugte Austauschformat für Daten im Netz. Syntaktisch angelehnt an HTML, einfach strukturiert und mit der Möglichkeit versehen, beliebig neue Datenstrukturen und Tags zu kreieren, bot XML eine Flexibilität, die bisher in keinem anderen Datenformat zu finden war. Es ist möglich, tief verschachtelte Datenmodelle zu entwickeln, und die Darstellung der Daten ist frei wählbar. Auf SOAP [4] basierende Netzdienste verwenden XML für den Austausch von Nachrichten. Viele Schnittstellen wurden im Laufe der Jahre, basierend auf den Möglichkeiten von XML, geschaffen und in unterschiedlichsten Systemen erfolgreich eingesetzt.
REST-basierte Netzdienste [5] verwenden bevorzugt JSON als Datenformat. Es hat sich die Meinung durchgesetzt, JSON sei die bessere Wahl, verglichen mit XML. Ein Pro-Argument ist das geringere Datenvolumen von JSON gegenüber XML [6]. Ob diese zusätzlichen Bytes jedoch eine Rolle spielen, hängt vom jeweiligen Anwendungsfall ab. Für die Verbreitung von JSON spielt sicher ebenfalls eine Rolle, dass JavaScript in Verbindung mit einem Browser JSON problemlos erzeugen und auswerten kann.
XML unterscheidet keine Datentypen, weshalb es der Anwendung überlassen bleibt, die enthaltenen Zeichenketten zu interpretieren. Das macht XML zwar flexibel, jedoch für klassische Daten auch aufwändiger in der Anwendung. Im Gegensatz dazu sind für JSON die möglichen Datentypen und ihre Formatierung festgelegt. Bei der Entwicklung von Schnittstellen erweist sich diese Festlegung als Vorteil, da eine Spezifikation nur die Struktur der Daten festlegt, nicht aber deren Darstellung. Auch die Implementierung wird vereinfacht: Die String-Konvertierung einer Gleitkommazahl ist beispielsweise in vielen Fällen sprachabhängig, was beim Austausch der Daten zwischen Systemen mit unterschiedlichen Sprachen zu Problemen führen kann. JSON erlaubt für Gleitkommazahlen keine sprachabhängige Darstellung und ist somit nicht anfällig für dieses Problem.
Auch wenn die Festlegung auf bestimmte Datentypen Vorteile mit sich bringt und verteilte Systeme stabiler macht, birgt dieses Vorgehen auch den Nachteil, sich auf diese Datentypen beschränken zu müssen.
Da JSON nicht erweiterbar ist, werden in der Praxis Daten, zu denen kein existierender Datentyp passt, zum Beispiel als String angegeben. Es ist dann die Aufgabe der Anwendung, diese Daten korrekt zu interpretieren.
Datum und Zeitangaben sind ein Beispiel für dieses Vorgehen. In der Praxis werden sie häufig als String dargestellt. Alternativ findet auch die numerische UNIX-Zeit oder ein Array mit Einträgen für Jahr, Monat, Tag usw. Verwendung. Wie diese speziellen Daten von einer Anwendung auszuwerten sind, legt eine Schnittstellenbeschreibung fest. Wie bei XML wird die Interpretation der Datentypen dabei in die Ebene der Anwendung verschoben, was Mehraufwand bei der Implementierung und möglicherweise geringere Flexibilität bei zukünftigen Änderungen bedeutet.
Binäre Daten wie Bilder oder Multimediadateien nehmen in textbasierten Austauschformaten eine Sonderstellung ein. Für ihren Transport werden zwei unterschiedliche Vorgehensweisen verwendet [7]:
Binäre Daten im Austauschformat einbetten: Binäre Daten werden beispielsweise Base64-kodiert [8] und zusammen mit der Nachricht übertragen. Eine Kodierung ist für den Transport in allen textbasierten Formaten erforderlich. Die Datenmenge wird dabei um den Faktor 1.33 vergrößert, und es entsteht zusätzlicher Aufwand bei der Konvertierung. Dieses Vorgehen wirkt sich nachteilig auf die Performance der Anwendung aus.
Verweise auf Dateien: Eingebettete URIs [9] verweisen auf binäre Daten auf einem FTP- oder HTTP-Server. Eine Kodierung der Dateien ist nicht erforderlich und die Nachricht bleibt klein, da sie nur den Verweis enthält. Das Ziel des Verweises muss allerdings für den Empfänger erreichbar sein. Ad hoc erstellte Binärdaten, wie zum Beispiel dynamisch erzeugte Diagramme, müssen vor dem Senden der Nachricht auf den Fileserver transportiert werden. Der Server ist dafür verantwortlich, veraltete Daten automatisch zu entfernen.
Für binäre Daten, die nicht als Datei auf einem Server liegen, sondern für eine Anfrage explizit erstellt und anschließend nicht mehr gebraucht werden, bietet sich das Einbetten in eine Nachricht an. Nachrichten und Dateien auf unterschiedlichen Wegen zu übertragen, birgt darüber hinaus Sicherheitsrisiken, da unterschiedliche Protokolle und Verbindungen gesichert werden müssen, anstatt eine bereits gesicherte Verbindung für die gesamte Kommunikation zu nutzen.
Für Geräte im Netz, denen nur eingeschränkte Rechenleistung zur Verfügung steht, ist jede zusätzliche Verbindung, jede temporär gespeicherte Datei und zusätzlicher Aufwand für Aufräumarbeiten und Verwaltung zu vermeiden.
Binäre Formate erlauben einen effizienten Austausch eingebetteter Daten. Das macht sie nicht nur für das Internet der Dinge interessant, sondern bietet auch Möglichkeiten bei der Anwendung in Web-APIs und Systemen, die über andere Protokolle als HTTP kommunizieren.
Netzwerkprotokolle für das Internet der Dinge wie CoAP [10] und MQTT [11] verwenden binäre Frames. Auch der kürzlich von der Internet Engineering Steering Group (IESG) [12] zur Standardisierung freigegebene Entwurf des HTTP/2-Protokolls setzt auf eine binäre Formatierung für mehr Effizienz bei der Übertragung. Der logische Schritt ist also, die von diesen Protokollen transportierten Daten ebenfalls binär darzustellen.
Es ist kein Wunder, dass immer neue binäre Datenformate auftauchen. Sie orientieren sich wahlweise an JSON oder XML. Daten hierarchisch verschachtelt zu strukturieren, hat sich in der Praxis bewährt. Neben der Möglichkeit, binäre Daten ohne Kodierung einzubetten, wird auch für klassische Datentypen die Menge der notwendigen Bytes für deren Darstellung reduziert. So kann der Wert 18446744073709551615 binär mit 8 Bytes dargestellt werden. JSON und XML sind Textdateien, die Unicode verwenden. Je nach Kodierung werden also für die Darstellung statt 8 mindestens 20 Bytes benötigt. Ist die Kodierung der Textdatei UTF32, werden daraus 80 Bytes, da jedes Zeichen 4 Bytes benötigt.
Daten abhängig von ihrem Datentyp binär darzustellen, spart Platz und Rechenzeit; Parsen und Umwandeln von Strings ist nicht erforderlich. Fehler werden vermieden, weil sichergestellt ist, dass zum Beispiel ein Zahlenwert vom Empfänger korrekt gelesen wird.
Klingt soweit gut, aber welche Datentypen werden benötigt? Natürlich ist davon auszugehen, dass klassische Datentypen auf jeden Fall dabei sind. Abstufungen wie 16, 32 und 64 Bit erlauben der Anwendung, nur so viel Speicher zu nutzen, wie unbedingt nötig.
Bei Strings zeigt sich ein weiterer Vorteil des binären Datenformats. Für reine Feldbezeichnungen wie Spaltennamen aus Datenbanktabellen werden ASCII-Strings mit nur einem Byte pro Zeichen verwendet. Unicode-Strings werden wie gewünscht kodiert, wobei in einem einzigen Dokument verschiedene Kodierungen möglich sind. Im Gegensatz dazu bezieht sich die Kodierung eines textbasierten Datenformats auf das gesamte Dokument. Umwandlungen erfordern auch hier wieder Rechenzeit und müssen sehr sorgfältig durchgeführt werden.
Datenbanken haben eine lange Geschichte, und die verwendeten Datentypen sind eine solide Grundlage für die Modellierung realer Zusammenhänge. Der Wert NULL spielt in diesem Zusammenhang eine wichtige Rolle und kennzeichnet ein leeres Datenfeld in einer Tabelle. Ein Vergleich mit dem Wert null im JSON-Format zeigt jedoch einen wesentlichen Unterschied. JSON null hat einen eigenen Datentyp, während in einer Datenbank NULL den Typ der jeweiligen Tabellenspalte trägt. In der Praxis wird eine Null aus einer Datenbank auch mit speziellen Werten wie 0 oder "" (leerer String) dargestellt, um den Typ des Felds zu transportieren. Eine Unterscheidung von NULL mit Datentyp für leere Werte und VOID für Felder, deren Typ unbekannt ist und die keine Daten enthalten, hilft hier genauer zu unterscheiden.
Bei der Entwicklung des UNIX-Betriebssystems wurde bekanntlich entschieden, Zeit als 32-Bit-Integer mit Vorzeichen festzulegen. Der Wert gibt die Anzahl der Sekunden seit dem 1. Jan. 1970 00:00 Uhr UTC (The Epoch) an. Im Jahre 2038 wird der Zahlenraum in dieser Definition erschöpft sein, daher verwenden moderne Systeme einen 64-Bit-Integer. Negative Zahlen geben Zeiten vor der Epoche an. Diese Darstellung eignet sich gut für binäre Datenformate und lässt sich leicht in ein Datum und eine Uhrzeit umrechnen.
Eine Zeit kann auch als String ausgedrückt werden („1971-08-23 15:32:12:000“), was für binäre Austauschformate zwar möglich ist, jedoch wie bei textbasierten Formaten aufwändige Konvertierungen erfordert.
Eine Darstellung der Zeit als Folge von ganzen Zahlen, die als Jahr, Monat, Tag, Stunde, Minute, Sekunde und Millisekunde interpretiert werden, bietet die Möglichkeit, eigene Typen für ein Datum oder eine Zeit zu definieren. Auch ein Zeitstempel mit einer Auflösung von einer Millisekunde ist so möglich.
Die Stärke von Austauschformaten wie XML und JSON ist die Möglichkeit, Datenfelder in Containern zusammenzufassen. Objekte, wie sie in JSON genannt werden, sind eine Sammlung von benannten Werten. Sie eignen sich gut für die Darstellung einzelner Tabellenzeilen. Ein Array ist eine Reihe von Werten in einer bestimmten Reihenfolge. Jeder Wert in JSON kann ein Array oder ein Objekt enthalten. Für die Darstellung von Tabellen eignet sich deshalb ein Array mit Objekten. Nachteil dieser Art der Darstellung ist die Wiederholung von Spaltenbezeichnungen vor jedem Wert, was unnötigen Platz verschwendet.
Für binäre Datenformate ist ein eigener Typ für Tabellen daher eine denkbare Erweiterung. Neben einer besseren Nutzung des Speicherplatzes bietet dieser Weg auch die Sicherheit, dass Spalten und Zeilen konsistent sind und keine Werte fehlen.
Sowohl XML als auch JSON sehen für ein Objekt nur Namen vor. Abhängig von Kodierung und XML-Spezifikation sind jedoch Rahmenbedingungen, wie erlaubte Zeichen, vorgegeben. Auch in einem binären Datenformat würde ein Objekt key/value-Paare enthalten. Der Schlüssel (key) sollte jedoch nicht auf Strings beschränkt sein. Jeder Wert ist als Schlüssel geeignet. Natürlich muss dabei der Datentyp berücksichtigt werden. Der Wert 0 ist nicht identisch mit 0.0 oder "0". Bei Namen sind Unicode-Bezeichnungen in diesem Fall kein Problem mehr. Wegen ihrer Komplexität eignen sich Objekte und Arrays nicht als Schlüssel, da sie miteinander verglichen werden müssen.
Je spezieller ein Datenformat ist, desto besser kann es optimiert werden. Binäre Formate legen Datentypen fest. Einige bieten die Möglichkeit, eigene Datentypen zu definieren und das Format damit zu erweitern. Anwendungen, die diese Erweiterungen nicht kennen, würden sie nicht verstehen. Das gilt selbstverständlich auch für textbasierte Formate. Natürlich kann in der Praxis ein vorhandener Datentyp von einer Anwendung interpretiert werden. Wie im Beispiel des Datums in JSON werden dann die Daten entsprechend ausgewertet. Eine Anwendung, die ein solches Feld auswertet und dessen Bedeutung nicht kennt, kann zumindest den Datentyp verstehen, auch wenn der Inhalt möglicherweise keinen Sinn ergibt.
Ein Datenformat, das allgemeingültig ist und über einen langen Zeitraum in unterschiedlichen Anwendungen verwendet wird, benötigt sicher die eine oder andere Erweiterung. Auch wenn es etwas Speicherplatz kostet, sollte ein Binärformat deshalb eine Version enthalten. Anwendungen können so prüfen, wie eine Nachricht auszuwerten ist oder ob sie ausgewertet werden kann, ohne vorher eine Version mit dem Sender auszuhandeln. Eine Version am Anfang des Dokuments erlaubt, falls nötig, umfangreiche Änderungen am Datenformat.
Eines der Argumente gegen die Verwendung binärer Datenformate ist die Lesbarkeit. Wer bereits versucht hat, ein textbasiertes Format wie JSON oder XML zu lesen oder zu editieren, hat möglicherweise drei Dinge festgestellt:
Bevor die Datei lesbar ist, muss sie in einem Texteditor geöffnet werden. Es wird also ein Werkzeug benötigt.
Das Lesen und Verstehen haben nur wenig miteinander zu tun.
Nachdem die Datei editiert und gespeichert wurde, stellt sich die Frage, ob es sich noch um ein gültiges Format handelt.
Ein Editor für ein standardisiertes Binärformat macht die Daten ebenfalls lesbar und verhindert darüber hinaus Fehler bei der Bearbeitung. Ob ein Austauschformat in jedem beliebigen Texteditor zu öffnen ist, sollte nicht die Grundlage für eine Entscheidung gegen binäre Formate sein. Vielmehr sollten geeignete Werkzeuge die Verwendung von Binärformaten, beispielsweise als Konfigurationsdateien, ermöglichen.
Binäre Formate unterscheiden sich stark voneinander und sind nur schwer vergleichbar. Sich für ein bestimmtes Format zu entscheiden, fällt daher nicht leicht. Die im Kasten „Binäre Austauschformate“ gezeigte Aufstellung und die in Tabelle 1 dargestellten Eigenschaften mögen als grobe Übersicht dienen. Die Liste der Formate ist auf typisierte Formate, die sich strukturell an JSON anlehnen, beschränkt. Ob ein Format für eine bestimmte Anwendung geeignet ist, hängt natürlich vom jeweiligen Einzelfall ab.
Vollständig ist die Liste sicher nicht. Eine eigene Recherche lohnt daher in jedem Fall. Sicher sind aber nützliche Anhaltspunkte dabei, wo mit der Suche zu beginnen ist und welche Eigenschaften eines Binärformats für die eigene Anwendung nötig sind.
Binary JSON (BSON) [13] wurde für die Speicherung JSON-ähnlicher Objekte (Maps) entwickelt. Inhalte einer Datei können nachträglich geändert werden, ohne sie komplett neu schreiben zu müssen. Diese Funktion unterscheidet BSON von anderen Formaten. BSON wird in der Datenbank MongoDB [14] zur Datenspeicherung eingesetzt.
Concise Binary Object Representation (CBOR) [15] ist definiert im 2013 veröffentlichten RFC 7049 [16]. Durch die extreme Kompaktheit der Daten eignet es sich gut für Anwendungen im Internet der Dinge. CBOR erlaubt Erweiterungen ohne eine Versionierung. Ziel dabei ist, in langlebigen Anwendungen auf veränderte Anforderungen reagieren zu können. Als Startpunkt diente das JSON-Datenmodell, das die Migration bestehender Systeme erleichtert. Darüber hinaus werden jedoch weitere Datentypen unterstützt.
MessagePack [17] wurde 2011 das erste Mal veröffentlicht und ähnelt CBOR in vielen Eigenschaften. Seitdem wurde das Format kaum verändert. Lediglich durch den Wunsch der Benutzer getrieben, wurde kürzlich ein binärer Datentyp eingeführt. Diese Stabilität ist vermutlich der Grund für den Einsatz von MessagePack zur dauerhaften Speicherung von Daten. Implementierungen für eine Vielzahl von Programmiersprachen, die über die Jahre entstanden sind und von einer großen Community gepflegt werden, sind vorhanden. Viele RPC-Anwendungen setzen MessagePack für den Nachrichtentransport ein.
Universal Binary JSON (UBJSON) [18] orientiert sich – mit dem Ziel, eine breite Akzeptanz zu erreichen – bewusst eng an JSON. Datentypen, die JSON nicht unterstützt, sind deshalb auch nicht enthalten. UBJSON ist optimiert für leichte Lesbarkeit und nicht für die kompakte Darstellung nativer Typen, wie zum Beispiel von half precision-Kommawerten.
UJO Binary Data Object Notation [19] ist das jüngste der hier aufgeführten Austauschformate. Die Spezifikation der ersten Version ist noch in Arbeit und kann im Internet kommentiert werden. Der Name UJO ist Esperanto und bedeutet „Container“. Ziel ist die Verwendung von UJO als Alternative zu XML im TML/SIDEX SDK [20]. Der Austausch von Daten zwischen existierenden Datenbanken und GUI-Anwendungen prägt die vorhandenen Datentypen. Es werden beispielsweise Datum und Zeit sowie typisierte NULL-Werte und Tabellen unterstützt.
BSON |
CBOR |
MessagePack |
UBJSON |
UJO | |
---|---|---|---|---|---|
Integer |
32, 64 Bit |
Eigener Typ für negative Integer |
8, 16, 32, 64 Bit |
8, 16, 32, 64 Bit |
8, 16, 32, 64 Bit |
Unsigned Integer |
8 Bit |
8, 16, 32, 64 Bit |
8, 16, 32, 64 Bit |
8 Bit |
8, 16, 32, 64 Bit |
Float |
double precision |
half, single, double precision |
single, double precision |
single, double precision |
half, single, double precision |
Boolean |
ja |
ja |
ja |
ja |
ja |
String |
UTF-8 |
UTF-8 |
UTF-8 |
UTF-8 |
UTF-8, UTF-16, UTF-32, cstring |
Binary |
ja |
ja |
ja |
nein |
ja |
Array |
Ja als Map mit "0","1", …"n" als Key |
ja |
ja |
ja |
ja |
Map |
ja |
ja |
ja |
ja |
ja |
Map Key Type |
cstring |
variant |
variant |
UTF-8 |
variant |
Table |
nein |
nein |
nein |
nein |
ja |
None (void) |
ja |
ja |
ja |
ja |
ja |
Null (typed) |
nein |
nein |
nein |
nein |
ja |
Date/Time |
UNIX datetime |
string, UNIX datetime |
nein |
nein |
date, time, datetime, timestamp, UNIX datetime |
Versioniert |
nein |
nein |
nein |
nein |
ja |
Erweiterbar |
unklar |
ja |
Anwendungs-spezifische Typen |
nein |
unklar |
Implementiert |
Java, JavaScript, Python, Perl PHP, Go, Ruby, C, C++, C#, Haskell, Erlang, Smalltalk, Elixir, Factor, Fantom |
Java, JavaScript, Python, Perl, PHP, Go, Ruby, C#, Erlang, Elixir |
Java, Python, PHP, Go, Ruby, C/C++, C#, Haskell, Erlang, Pascal, Delphi, Smalltalk, Elixir, u. v. a. m. |
Java, JavaScript, Python, PHP, C, C++, D |
C (in Arbeit) |
Tabelle 1: Vergleich binärer Austauschformate
Im Internet der Dinge stoßen textbasierte Austauschformate an ihre Grenzen. Speicher und Rechenleistung stehen nicht für Parser und Datenkonvertierungen oder aufwändige String-Operationen zur Verfügung. Die Vorteile binärer Formate liegen in ihrer Effizienz, weshalb sie sich für Anwendungen im Internet der Dinge besonders eignen.
Auch herkömmliche verteilte Anwendungen können von einer binären Darstellung der ausgetauschten Daten profitieren: Multimediadateien, Grafiken und Unicode-Strings können ohne Sonderbehandlung wie einfache Datenfelder in die Nachricht eingebettet werden. Freie Ressourcen stehen für zusätzliche Sicherheit zur Verfügung, und eine Infrastruktur für referenzierte Dateien ist überflüssig.
Bisher ist das Angebot an binären Formaten unübersichtlich und die Verfügbarkeit von Werkzeugen für deren Bearbeitung dürftig bis nicht vorhanden. Implementierungen und Bibliotheken existieren jedoch meist für viele Plattformen und Programmiersprachen. Dennoch ist die Unterstützung bei Weitem nicht mit der von JSON vergleichbar.
Neue Netzwerkprotokolle wie CoAP und HTTP/2 zeigen den Beginn einer Entwicklung weg von textbasierten Formaten, die für Maschinen nur mit hohem Aufwand lesbar sind, hin zu maschinenfreundlichen, binären Formaten. Nötig wird diese Entwicklung sowohl durch Anwendungen im Internet der Dinge als auch die Nutzung von Web-APIs für den Zugriff auf Daten und Datenbanken.
Maik Wojcieszak ist Gründer, Geschäftsführer und Entwicklungsleiter der Kieler Firma wobe-systems GmbH. Er entwickelt seit mehr als fünfzehn Jahren zusammen mit seinem Team verteilte Systeme für höchste Anforderungen bezüglich Geschwindigkeit und Ausfallsicherheit in der grafischen Industrie. Neben effizienten Nachrichtenformaten beschäftigen ihn dabei Netzwerkprotokolle der nächsten Generation, Parallel Computing und verteilte Datenbanken.
[1] Binary JSON (BSON): http://bsonspec.org
[2] WAP Binary XML Content Format: http://www.w3.org/TR/wbxml
[3] Extensible Markup Language (XML): http://www.w3.org/TR/2006/REC-xml-20060816
[4] Simple Object Access Protocol (SOAP): http://www.w3.org/TR/soap
[5] Representational State Transfer (REST): http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm
[6] JSON vs. XML: Some hard numbers about verbosity: http://www.codeproject.com/Articles/604720/JSON-vs-XML-Some-hard-numbers-about-verbosity
[7] Transferring large binary data with web services: https://weblogs.java.net/blog/adhirmehta/archive/2010/06/11/transferring-large-binary-data-web-services
[8] Base 64 Encoding: https://tools.ietf.org/html/rfc4648#section-4
[9] Uniform Resource Identifier (URI): https://tools.ietf.org/html/rfc3986
[10] The Constrained Application Protocol (CoAP): https://tools.ietf.org/html/rfc7252
[11] MQTT: http://mqtt.org sowie auch der Beitrag „IoT-Allrounder“ auf Seite 36 in diesem Java Magazin
[12] The Internet Engineering Steering Group (IESG): https://www.ietf.org/iesg
[13] Binary JSON: http://bsonspec.org
[14] MongoDB-Homepage: http://www.mongodb.org
[15] CBOR-Homepage: http://cbor.io
[16] RFC 7049, Concise Binary Object Representation (CBOR): http://tools.ietf.org/html/rfc7049
[17] MessagePack Homepage: http://msgpack.org
[18] Universal Binary JSON, Homepage: http://ubjson.org
[19] UJO Binary Data Object Notation: http://www.nxweblink.net
[20] TML/SIDEX SDK: https://www.tml-software.com