Referenzen und Objekte

Objekt-orientierte Programmierung ist bekanntermaßen ein sehr verbreitetes Konzept zur Modellierung komplexer Anwendungen. Die Programmiersprache Java hat bereits seit ihrer Einführung 1995 dieses Konzept aufgenommen. Auch die Laufzeitumgebung, also die Java Virtual Machine (JVM) berücksichtigt dieses Konzept und implementiert deshalb ein Speichermodell, das auf Referenzen und Objekte ausgerichtet ist.

Stack und Heap

Der Speicher der Java Virtual Machine ist in einen Stack- und einen Heap-Bereich unterteilt.

Stack und Heap der JVM

Diese Unterteilung ist zwar etwas grob, genügt aber für die folgende Betrachtung völlig.

Jede Java-Methode bekommt ihren eigenen Stack-Bereich zugeordnet. Innerhalb des Stacks werden sämtliche lokalen Variablen abgelegt. Wie im Objekt-orientierten notwendig sind diese Variablen in Java stets Referenzen auf Objekte. Diese werden im Heap-Bereich abgelegt.

Dies sei am Beispiel der folgenden main-Methode aufgezeigt:

public static void main(String[] args){
String message = "Hello";
String[] names = {"John", "Fritz"};
}

Im Speicher der JVM ergibt sich das folgende Bild:

Die main-Methode mit den lokalen Variablen message und names

Methodenaufruf

Nun definieren wir eine zweite Methode, called, diesmal mit 2 Parametern

public static void called(String str, String[] list){

}
Die methoden called mit den beiden Parametern str und list

Diese Methode wird aus main heraus aufgerufen:

public static void main(String[] args){
String message = "Hello";
String[] names = {"John", "Fritz"};
called(message, names);
}

Wie erfolgt nun jedoch die Übergabe der lokalen Variablen message und names in die Parameter str und list?

Wie werden die Parameter belegt?

Dazu wird die JVM die Werte der Referenzen als Kopie übergeben, also ein “CallByValue”-Verhalten auf der Ebene der Referenzen:

Copy By Value mit Referenzen

Im Endeffekt zeigen die Referenzen auf die selben Objekte im Heap.

Zuweisung von Werten

Was passiert aber nun, wenn in der aufgerufenen Methode den Parametern neue Werte zugewiesen werden? Hier ergibt sich ein feiner aber unglaublich wichtiger Unterschied: Belegen wir den Parameter str einfach mit einem neuen Wert, z.B. mit

str = "Goodbye";

so wird die Variable im Stack einfach mit einer Referenz auf das neue Objekt im Heap überschrieben:

Zuweisung eines neuen Wertes an den Parameter str

Wichtig hier ist, dass in der aufrufenden main-Methode die Variable message davon völlig unbeeinflusst bleibt.

Was passiert aber, wenn wir die Referenz auf das Array benutzen, um beispielsweise den Wert des zweiten Elements mit Index 1 auf “Paul” zu setzen?

list[1] ="Paul";

Nun ergibt sich ein anderes Bild:

Zuweisung eines neuen Wertes an das zweite Element der Liste

Hier ist klar ersichtlich, dass diese Änderung des Array-Objekts innerhalb der Methode called auch direkt Auswirkungen auf die aufrufende main-Methode hat: Nachdem die Methode called terminiert ist referenziert auch die Variable message auf das Array, dessen zweites Element nun auf “Paul” zeigt.

Also innerhalb von main

public static void main(String[] args){
String message = "Hello";
String[] names = {"John", "Fritz"};
called(message, names);
System.out.println(names[1]); //-> Paul
}

Zusammenfassung und Ausblick

Durch die Betrachtung des Speichermodells der Java Virtual Machine wird die Arbeitsweise eines Objekt-orientierten Programms unter Verwendung von Referenzen klar und verständlich. Das komplette Beispiel liegt als fertiges Eclipse-Projekt vor und enthält zusätzlich Konsolen-Ausgaben, die den Ablauf des Programms demonstrieren. Selbstverständlich kann aber auch der Eclipse-Debugger benutzt werden, um die Ausführung nachzuvollziehen.

Allerdings sind sicherlich auch noch einige Punkte nicht fundiert behandelt worden:

  • Das vorgestellte Speichermodell ist stark vereinfacht. Wie schaut es denn tatsächlich aus?
  • Was passiert eigentlich mit dem armen Objekt “Fritz”, das nun komplett losgelöst im Heap vorhanden ist?

Diese beiden Punkte werden in einem folgenden Artikel, der den Garbage Collector der JVM als Thema hat, behandelt.


Seminar zum Thema

Weiterlesen

Docker Compose

Übersicht: Was ist Docker Compose?

Die Definition eines Docker-Containers erfolgte bisher durch die Parametrisierung des docker create-Befehls. Dies wurde so auch in früheren Artikeln so beschrieben und ist auch ein durchaus korrekter Ansatz. Allerdings werden diese Befehle spätestens in dem Moment sehr unhandlich und unübersichtlich, wenn mehrere Container miteinander verlinkt werden und damit voneinander abhängig sind.

Mit Docker Compose wird die Definition verlinkter Container besser gelöst: Statt eines Aufrufs von docker createmit vielen Parametern werden diese in einer einfach strukturierten Textdatei abgelegt. Als Dateiformat hat sich die Docker-Community für YAML entschieden, eine sinnvolle und nachvollziehbare Entscheidung.

Damit wird werden Dependencies zwischen Docker-Containern deutlich vereinfacht. Dies kann auch sofort dem WordPress-Beispiel aus der Docker Compose-Dokumentation entnommen werden:

 

version: '3.3'

services:
   db:
     image: mysql:5.7
     volumes:
       - db_data:/var/lib/mysql
     restart: always
     environment:
       MYSQL_ROOT_PASSWORD: somewordpress
       MYSQL_DATABASE: wordpress
       MYSQL_USER: wordpress
       MYSQL_PASSWORD: wordpress

   wordpress:
     depends_on:
       - db
     image: wordpress:latest
     ports:
       - "8000:80"
     restart: always
     environment:
       WORDPRESS_DB_HOST: db:3306
       WORDPRESS_DB_USER: wordpress
       WORDPRESS_DB_PASSWORD: wordpress
       WORDPRESS_DB_NAME: wordpress
volumes:
    db_data: {}

Selbst ohne bisherige Kenntnisse in Docker Compose ist einem Leser mit Docker-Hintergrund sofort klar, was mit dieser Datei bezweckt werden soll:

  1. Es wird ein MySQL-Container aus dem mysql:5.7-Image erzeugt.
  2. Dieser benutzt ein Volume namens db_data
  3. und wird automatisch gestartet, sobald die Docker-Engine aktiv ist.
  4. Benutzername, Passwort und der Name der Datenbank werden über Environment-Parameter gesetzt.
  5. Ein weiterer Container beruht auf dem wordpress:latest-Image und stellt einen WordPress-Server bereit.
  6. Auch hier erfolgt die Konfiguration über Environment-Parameter, zusätzlich erfolgt ein Port-Mapping.
  7. Mit depends-on wird dieser Container aber nun mit dem MySQL-Container verlinkt. Damit steht für beide Container ein gemeinsames Netzwerk bereit, so dass der Web Server von WordPress direkt auf den MySQL-Server zugreifen kann.

Falls nun Docker Compose installiert ist wird diese Information ganz einfach in einer Datei namens docker-compose.yml abgelegt.

Und dann genügt tatsächlich der Befehl docker-compose up, um die gesamte Anwendung zu starten!

Einige Details

Installation

Die Installation ist detailliert hier beschrieben, wird aber hier exemplarisch für eine Linux-Distribution, z.B. SLES 12, durchgeführt:

Als erstes wird die für die Distribution relevante Compose-Version von GitHub geladen und ausführbar gemacht:

sudo curl -L https://github.com/docker/compose/releases/download/1.21.2/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose

chmod +x /usr/local/bin/docker-compose

Zusätzlich kann die Bash-Completion installiert werden:

curl -L https://raw.githubusercontent.com/docker/compose/1.21.2/contrib/completion/bash/docker-compose -o /etc/bash_completion.d/docker-compose

Ab jetzt sollte der Befehl docker-compose verfügbar sein, also beispielsweise

docker-compose -v

Arbeitsweise

Docker Compose ist keine Erweiterung der Docker-Engine sondern “nur” ein Generator für Docker Befehle auf Grundlage der yml-Datei. So erzeugt der up-Befehl von docker compose ganz normale Docker-Container, die beispielsweise über ein docker ps aufgelistet werden können. Entsprechend wird der down-Befehl diese Container wieder löschen.

Kommando-Referenz

Selbstverständlich müssen die über docker compose definierten Container nicht jedesmal erneut mit up erzeugt werden. Zum Starten und Stoppen dienen die Befehle start und stop, die die vorhandenen Container benutzen.

Die vollständige Kommando-Referenz ist Bestandteil der Docker-Compose Dokumentation, ebenso die Command Line-Befehle.

Compose und Build-Prozess

Für eine agile Entwicklung kann die docker-compose.yml auch ein Dockerfile angeben und vor der Erstellung der Container erst einmal das zugehörige Image neu bauen. Damit kann compose auch sinnvoll in den Buildprozess einer Docker-Anwendung integriert werden.

 

Weiterlesen

Blockchain

Eine Blockchain mit Java

Eine Blockchain mit Java

Im vorherigen Artikel wurde die prinzipielle Arbeitsweise einer Blockchain allgemein diskutiert. Eine Blockchain mit Java zu implementieren ist prinzipiell nicht sonderlich schwer und wird im Folgenden gezeigt.

Einige Hinweise sind vorab notwendig, um beim Leser keine übertriebenen Erwartungen zu wecken:

  • Diese Java-Klassen alleine stellen natürlich kein funktionierendes verteiltes Datenbanksystem dar. Dazu fehlt das Netz der Server-Knoten.
  • Eine konkrete Validierung der Records, die in einen Block eingestellt wird erfolgt nur rudimentär. Dies ist selbstverständlich eine Anforderung, die aus einer fachlichen Spezifikation entnommen werden muss. Eine Blockchain für Gesundheitsdaten hat komplett andere Regeln als eine für Finanz-Transaktionen.
  • Komplett weggelassen wurde das Thema “Consense Modelle”, also beispielsweise ein “Proove of Work” wie beim Bitcoin-Mining.

Das Beispielprojekt

Die im Folgenden gezeigten Beispiele sind im GitHub-Account des Autoren abgelegt. Das Projekt ist frei verfügbar und kann für eigene Demonstrationen und Ergänzungen gerne benutzt werden.

Hashing

Zwingend notwendig für die Arbeit einer Blockchain ist die Bestimmung der hochwertigen Hashes. Dieses Problem ist in der Java-Distribution schon lange gelöst, dafür gibt es die Klasse MessageDigest.  Nachdem diese jedoch etwas sperrig in der Benutzung ist, wird das DigestUtil aus dem Apache Commons Codec-Projekt benutzt:

Placeholder

Der Test beweist, dass die Anforderungen an die Hash-Erzeugung erfüllt sind:

  • Unabhängig von der Länge der Eingangsdaten sind alle Hash-Werte gleich lang, nämlich 32 Zeichen.
  • Die Hash-Werte zweier minimal unterschiedlicher Eingangsdaten weisen keinerlei Ähnlichkeiten auf.
  • Der Wertebereich des Hashes ist so groß, dass zufällige Identitäten ausgeschlossen werden können.

Der Block

Die Implementierung eines für die Chain geeigneten Blocks ist tatsächlich sehr einfach. Ein Block enthält

  • die Daten, die für ein einfaches Beispiel beliebig sein können
  • den Hashwert des Vorgängers
  • einen eigenen Hashwert
  • einen Zeitstempel

Eine direkte Umsetzung zeigt folgende Klasse:

Placeholder

Eine korrekte Arbeitsweise beweist dieser Unit-Test:

Placeholder

Die Blockchain

Die Blockchain-Implementierung macht folgendes:

    • Die Klasse hält eine Liste von Blöcken
    • Eine add-Methode nimmt hinzuzufügende Daten entgegen. Diese werden exemplarisch validiert und, nach Erfolg, in einen Block umgesetzt. Dieser wird anschließend in der Liste hinzugefügt.
    • Mit Angabe eines Indexes kann ein bestimmter Datensatz wiederum gelesen werden.
    • Ebenso kann eine Liste aller Daten erhalten werden.
Placeholder

Im Endeffekt bildet die verkettete Liste der Blöcke einen Hash-Baum oder Merkle-Tree.

Auch hier zeigt ein Test das korrekte Funktionieren der Implementierung:

Placeholder

Der Versuch eines Angriffs

Wie kann nun ein Angriff auf diese Blockchain erfolgen? Bevor der Versuch eines Angriffs näher erläutert wird ist es aber wichtig, sich bewusst zu machen, dass die Informationen, die in der Chain abgelegt sind, in der Praxis auf vielen Knoten eines verteilten Systems abgelegt sein werden. Das bedeutet damit, dass der Angreifer seine Attacke nicht nur gegen einen Rechner fahren muss, sondern im Endeffekt gegen eine Majorität der Rechner. Dies macht die Sache schon deutlich komplexer. Andererseits müssen die Knoten ihre Daten untereinander synchronisieren, was durch Abhören der Netzwerkkommunikation einen erstmals durchaus vielversprechenden Angriffsvektor definiert.

Hat der Angreifer Zugriff auf einen oder mehrere Blöcke, so können die darin enthaltenen Informationen ausgelesen werden. Um ein Bloßlegen sensibler Daten zu verhindern, müssen damit die im Block enthaltenen Daten zusätzlich verschlüsselt werden. Das ist in dieser einfachen Implementierung nicht gegeben, in der Praxis aber ein eher einfach zu lösendes Problem.

Erweitern wir nun den Zugriff des Angreifers: Dieser hätte einen Server-Knoten komplett unter seine Gewalt bekommen. Und wir gehen noch weiter: Die Informationen der Blöcke sind im Klartext bekannt. Diese Situation wäre in einer klassischen Architektur vergleichbar mit einem korrumpierten Datenbank-Administrator, der den Datenbank-Host kontrolliert. Dass damit bereits beträchtlicher Schaden angerichtet ist, ist vollkommen klar. Aber fast noch schlimmer wäre es nun, die Daten manipulieren zu können. So könnte beispielsweise versucht werden, eine wichtige Information nachträglich zu ändern. Was muss der Angreifer alles machen:

  1. Der Block muss die geänderte Information gesetzt bekommen.
  2. Er muss einen neuen Hash erzeugen und auf diese Art und Weise einen komplett neuen, konsistenten Block erzeugen. Dies ist bereits die erste Komplikation.
  3. Nun muss der manipulierte Block in die Blockchain integriert werden. Und nun wird es wirklich kompliziert: Denn nicht nur der eine manipulierte Block muss geändert werden, sondern auch alle (!) Nachfolger. Denn diese beziehen sich auf einen Parent-Block, der so ja gar nicht mehr in der Chain enthalten ist. Der Aufwand für den Angreifer und damit die Wahrscheinlichkeit, dass die Manipulation entdeckt wird steigt bereits hier drastisch.
  4. Und zum Schluss muss der Angreifer die anderen Knoten des verteilten Systems “überzeugen”, diese Änderungen komplett zu übernehmen.

Schon der dritte Punkt ist so aufwändig zu realisieren, dass die Wahrscheinlichkeit eines Erfolgs des Angriffs drastisch sinkt. Punkt 4 übersteigt endgültig die Fähigkeiten eines realistischen Angreifers.

Der folgende Test simuliert einen Angreifer, der die Java-Anwendung angreift. Der Versuch ist erfolgreich bis zu Punkt zwei, aber mehr auch nicht:

Placeholder

Ausblick

Um ein funktionierendes, auf Blockchain-Technologie basierendes System in Betrieb nehmen zu können fehlen noch

  • Das verteilte System mit den kommunizierenden Knoten
  • Authentifizierung, Autorisierung und Verschlüsselung
  • Eine Implementierung einer fachlich konsistenten Validierung der Block-Daten
  • Und schließlich noch eine Logik für fachliche Abfragen.

Bild von analogicus auf Pixabay

Weiterlesen

Begriffe der Aspektorientierten Programmierung

Die Begriffe der Aspektorientierte Programmierung

Das AOP-Konzept wurde in einem vorherigen Artikel vorgestellt. Darin wurden Aspekte an Hand von Querschnittsfunktionen eingeführt, die auf Schnittstellen-Ebene vorhandene Implementierungen erweitern. AOP verfeinert dieses Vorgehen jedoch noch deutlich. Die zum detaillierten Verständnis notwendigen Begriffe der Aspektorientierten Programmierung werden im Folgenden definiert.

Joinpoints

Ein Joinpoint ist eine identifizierbare Stelle innerhalb eines compilierten Programms. Prinzipiell ist damit jegliche native Bytecode-Anweisung ein Joinpoint. AOP schränkt diese Möglichkeiten jedoch drastisch ein und definiert als mögliche Joinpoints Funktionsaufrufe (call), Funktionsausführungen (execution), aber auch eine Zugriffe auf Attribute einer Datenstruktur (get, set), Exception Handler (handler) oder Konstruktoraufrufe. Schleifen oder Abfragen sind zwar auch “identifizierbare Stellen”, werden aber von AOP nicht unterstützt. Es fällt auch schwer, für Kontrollstrukturen allgemeine Aspekte zu finden.

Zwei Hinweise seien hier noch gegeben:

  • Joinpoints werden in der Literatur auch als “primitive Pointcuts” bezeichnet.
  • Ein “Jointpoint” wie der Münchner Monopteros ist natürlich etwas vollkommen anderes. Allerdings liefert eine Recherche nach den Begriffen “AOP + Jointpoint” erfreulich viele Treffer…

Pointcuts

Ein Pointcut ist eine logische Oder-Verknüpfung einzelner Joinpoints. Um die Formulierung von Pointcuts zu erleichtern verwendet AOP eine Selektionssprache mit Jokerzeichen. So könnte beispielsweise ein Pointcut definieren, dass alle Funktionen find* selektiert werden sollen.

Advices

Ein Advice ist die eigentliche Aspekt-Logik von Anweisungen, die an einem Pointcut ausgeführt werden soll. Advices können vor (before), nach (after) sowie den Pointcut klammernd (around) ausgeführt werden.

Aspect

Ein Aspect schließlich dient als Klammerung für einen Pointcut und einen Advice.

Arbeitsweise eines AOP-Frameworks

Mit diesen Begriffen kann die Arbeitsweise eines AOP-Frameworks folgendermaßen beschrieben werden:

  1.  Ausgangspunkt ist der Bytecode der Anwendung. Dieser enthält keine Aspekt-Logik.
  2. Der im Aspekt definierte Pointcut selektiert im Bytecode die Stellen der Anwendung, an denen das Framework die Aspekt-Logik hinzufügen soll.
  3. An Hand der gegebenen Aspekt-Logik wird Bytecode generiert und nach den Regeln des Advices an die selektierten Pointcuts “eingewebt”. Damit wird effektiv die Aspekt-Logik an verschiedene Stellen dupliziert eingetragen. Dies ist jedoch keine unzulässige Code-Replikation, der Quellcode der Anwendung bleibt “sauber”.

Dieser Ablauf muss selbstverständlich bei jeder Änderung und Compilierung der Sourcen wiederholt werden. Diese erreicht man durch eine Integration des Frameworks in eine Entwicklungumgebung oder einen automatisierten Buildprozess.

Realisierung von Aspekten

Mit den eben definierten Begriffen ist es möglich, Aspekte abstrakt zu definieren. Für die konkrete Umsetzung muss die verwendete Plattform Werkzeuge und Verfahren zur Verfügung stellen. Dies ist in dynamischen Skript-Sprachen wie JavaScript oder Ruby trivial. Komplizierter wird es in statisch typisierten Sprachen wie Java. Dies wird im dritten Teil dieser Serie behandelt.

Seminare zum Thema

Weiterlesen

Aspektorientierte Programmierung

Grundlagen der Aspektorientierten Programmierung

Aspektorientierte Programmierung (AOP) wird in praktisch allen modernen Software-Projekten eingesetzt. Grundelement dieses Ansatzes ist der Aspekt, welcher eine Querschnittsfunktion, englisch: ein Cross-cutting concern, realisiert. Querschnittsfunktionen werden im Modell der Anwendung dadurch erkannt, dass Schnittstellen-übergreifende gemeinsame Abläufe identifiziert werden. Diese gemeinsamen Sequenzen sollen nun aber natürlich nicht durch Code-Replikation umgesetzt werden; dies würde ja zu schlecht wartbaren Anwendungen führen. AOP vermeidet diese Duplikation und steigert damit die Qualität des Codes.

Beispiele für Aspekte sind schnell gefunden:

  • Authentifizierung und Autorisierung von Geschäftsprozessen
  • Deklarative Transaktionssteuerung beispielsweise in einem Applikationsserver
  • Tracing und Profiling
  • Kapselung des Netzwerk-Protokolls bei Remote-Zugriffen

Im Folgenden wird an einem einfachen Beispiel erläutert, wie Aspekte in eigenen Anwendungen identifiziert werden können.

Identifikation von Aspekten

Ausgangspunkt ist eine sehr einfache Anwendung, bei der einem Actor eine Service-Implementierung zur Verfügung gestellt wird. Hier benutzt das Rechnungswesen (Accounting) einen InvoiceService:

Der Invoice-Service

Nun wird die fachliche Anforderung erweitert: Sämtliche Aufrufe des InvoiceService sollen in einer Audit-Datei gespeichert werden. Darin enthalten sind beispielsweise Informationen über den Zeitpunkt des Aufrufs sowie die aufgerufene Funktion. Nachdem es sicherlich nicht sinnvoll ist, diese Zusatz-Funktion innerhalb der Implementierung zu realisieren (das Programm wird sonst sehr schnell unangenehm groß) ist eine naheliegende Lösung eine Vererbung:

Auditing mit Vererbung

Diese Lösung wird funktionieren, ist aber auf Dauer nicht wirklich stabil, da eine Vererbungshierarchie nur statisch ist. Besser ist ein Baukastensystem durch eine Dekoration:

Auditing mit Decorator

Das obige Klassendiagramm ist zwar formal korrekt, jedoch wenig einprägsam: Eigentlich erfolgt die Dekoration der Implementierung für den Actor, deshalb wird im nächsten Diagramm der AuditingInvoiceDecorator an die Assoziation zwischen Actor und Implementierung gehängt:

Der Decorator als Assoziationsklasse

Selbstverständlich können nun auch noch weitere Funktionalität dazu dekoriert werden:

Weitere Decorators

Und genauso selbstverständlich kann ein anderer Actor (Mahnungswesen, Dunning) den InvoiceService mit einem anderen Satz von Decorators benutzen:

Ein zweiter Actor für den InvoiceService

Dieses Modell der Anwendung ist übersichtlich und aussagekräftig. Allerdings ist noch kein Aspekt erkennbar. Dazu führen wir noch eine zweite Anwendung hinzu, Ordering:

Ordering: Ein zweiter Service

Dieser kann selbstverständlich auch wieder dekoriert werden:

Der zweite Service wird ebenfalls dekoriert

Nun kommt der entscheidende Schritt: Die Audit-Funktionalität soll auch dem OrderService hinzugefügt werden. Damit haben wir nun ein Modell, in dem ein Decorator an zwei Assoziationen zu unterschiedlichen Interfaces hängt. Und damit wird der Decorator zum Aspekt!

Der Auditing Aspekt

Formulierung von Aspekten

Die Identifikation von Aspekten in einem Klassendiagramm ist einfach. Allerdings müssen diese natürlich auch noch konkret formuliert werden. Dazu müssen einige neue Begriffe definiert werden. Dies wird im zweiten Teil dieser Serie behandelt.

Seminare zum Thema

Weiterlesen