In der vorletzten Ausgabe des JavaSPEKTRUMs ging es in der Kolumne von Sven Ruppert um Stellvertreterregelungen [1]. Der Text erinnerte mich daran, dass es im JDK ein API gibt, bei dem es sich auch um die Implementierung von Stellvertretern, um sogenannte Proxys, dreht.
Da sich mit Proxys beispielsweise der Support von Transaktionen implementieren lässt, behandle ich dieses Thema in vielen meiner Trainings zu Spring. Dabei stelle ich immer wieder fest, dass auch bei Teilnehmern mit vielen Jahren Erfahrung in der Java-Entwicklung dieses API unbekannt ist. Zudem merke ich in letzter Zeit immer wieder, dass es von Vorteil ist, die Grundlagen meiner eingesetzten Tools, Bibliotheken und Frameworks zu verstehen. Aus diesem Grund dreht sich dieser Artikel um das Dynamic Proxy Class API.
Proxy
Der Begriff Proxy wird in der IT in vielen verschiedenen Kontexten genutzt. Es gibt Web-Proxys, Proxy-Server, Reverse-Proxys und noch viele mehr. In unserem Kontext sprechen wir über das Entwurfsmuster Proxy. Dieses wurde bereits im berühmten „Gang of Four“-Buch „Design Patterns. Elements of Reusable Object-Oriented Software“ [2] beschrieben und gehört dort zu den strukturellen Mustern.
Wichtig bei einem Muster sind immer die Anwendungsfälle, für die es gedacht ist. Für das Proxy-Muster werden dabei die folgenden vier genannt:
- Ein Remote Proxy stellt eine lokale Repräsentation für ein Objekt in einem anderen Adressbereich dar. Ein Beispiel hierfür wären Remote EJBs. Für den Aufrufenden sieht der Methodenaufruf wie ein lokaler aus, in Wahrheit wird jedoch über das Netzwerk mit einem Server kommuniziert.
- Der Virtual Proxy wird dazu verwendet, Objekte erst dann zu laden, wenn diese wirklich verwendet werden. Dies bietet sich vor allem dann an, wenn die Objekte teuer oder groß sind und nur selten wirklich gebraucht werden. Der Lazy-Loading-Mechanismus von JPA verwendet zum Beispiel dieses Muster.
- Mit einem Protection Proxy lassen sich Zugriffsrechte auf ein Objekt vor dem wirklichen Aufruf prüfen und gegebenenfalls unterbinden.
- Zu guter Letzt gibt es noch den Smart Reference Proxy. Dieser ermöglicht es, beim Zugriff auf ein Objekt, in der Regel durch einen Methodenaufruf, zusätzliche Aktionen durchzuführen. Beispiele hierfür sind das Management von Transaktionen, Logging oder auch das Messen der Ausführungszeit.
Ein erster Proxy
In unserer Anwendung gibt es das Interface Worker
(s. Listing 1) und dazu eine
Implementierung SlowWorker
(s. Listing 2).
Nachdem es vermehrt Beschwerden darüber gab, dass unsere Anwendung
(s. Listing 3) zu langsam sei, wollen wir die Ausführungszeit der
doWork
-Methode messen und auf System.err
ausgeben. Dabei dürfen wir die
Implementierung von SlowWorker
nicht ändern, denn diese gehört einem anderen
Team. Deshalb entscheiden wir uns für das Proxy-Muster.
Um dieses umzusetzen, schreiben wir eine neue Klasse ProxyWorker
(s. Listing 4). Diese enthält eine Instanz, die Worker
implementiert, per
Konstruktor übergeben und merkt sich diese in einem Feld. Beim Aufruf der
Methode doWork
merken wir uns im ersten Schritt den aktuellen Zeitpunkt. Dann
wird die wirkliche Methode auf dem gemerkten Subjekt aufgerufen. Anschließend
holen wir uns erneut den aktuellen Zeitpunkt und berechnen durch Subtraktion die
vergangenen Nanosekunden. Diese geben wir nun über System.err
aus.
Nun sorgen wir noch dafür, dass in unserer Anwendung alle Stellen, die vorher
eine Instanz von SlowWorker
erstellen, eine Instanz von ProxyWorker
erzeugen
und den SlowWorker
als Konstruktorargument übergeben (s. Listing 5). Nach
dieser Änderung erhalten wir nach jedem Aufruf der doWork
-Methode die Ausgabe
der vergangenen Nanosekunden auf der Konsole.
Dynamischer Proxy
Eine „richtige“ Anwendung besteht normalerweise aus weiteren Interfaces und Klassen. Wollen wir auch bei diesen die Ausführungszeit der Methoden messen, müssten wir für jedes Interface eine eigene Proxy-Implementierung schreiben und dort jedes Mal dieselbe Logik verwenden. Dies führt bei größeren Anwendungen zu einer Explosion der verfügbaren Klassen, um im Grunde dieselbe generische Logik auf verschiedene Interfaces anzuwenden.
Um dieses Problem zu verringern, liefert das JDK bereits seit Version 1.3 das
Dynamic Proxy Class API mit. Dieses besteht vor allem aus der Klasse
java.lang.reflect.Proxy
und deren statischen Methoden, um einen Proxy zu
erzeugen, und dem Interface java.lang.reflect.InvocationHandler
, um das
Verhalten des Proxys zu implementieren.
Um unseren Anwendungsfall nun mit diesem API umzusetzen, implementieren wir
zuerst das InvocationHandler
-Interface innerhalb einer neuen Klasse
TimingProxy
(s. Listing 6). InvocationHandler
definiert die zu
implementierende Methode invoke
. Diese erhält als Argumente die Instanz des
Proxys, auf dem die Methode aufgerufen wurde, die Methode, die aufgerufen wurde,
und die Argumente, die beim Methodenaufruf übergeben wurden. Durch den
Rückgabetypen Object
und die Möglichkeit, Throwable
als Exception zu nutzen,
hat der Proxy hier alle Möglichkeiten, seine Logik zu implementieren.
Für unsere konkrete Implementierung merken wir uns erneut das Subjekt, dieses
Mal jedoch als Object
, da wir die Interfaces, für die der Proxy erzeugt werden
soll, nicht kennen. Beim Aufruf von invoke
nutzen wir die identische Logik wie
bisher. Wir merken uns den Zeitpunkt, rufen die Methode auf dem Subjekt auf und
geben anschließend die berechnete Dauer aus. Für den eigentlichen Aufruf auf dem
Subjekt nutzen wir die übergebene Methode, der wir auch die uns übergebenen
Argumente übergeben.
Um nun eine Instanz dieses Proxys zu erzeugen, nutzen wir die statische Methode
newProxyInstance
der Klasse Proxy
. Um die Benutzung für den Aufrufenden zu
vereinfachen, kapseln wir dies direkt in einer statischen Methode innerhalb
unserer TimingProxy
-Klasse (s. Listing 7).
Zur Erzeugung müssen wir dabei insgesamt drei Argumente übergeben. Der als
erstes Argument übergebene ClassLoader
muss sämtliche Interfaces und deren
Methoden, für die wir den Proxy erzeugen wollen, sehen können.
Als zweites Argument wird ein Array von Class
-Objekten übergeben. Diese geben
an, welche Interfaces der Proxy implementiert. All diese Objekte müssen sich
tatsächlich auf Interfaces beziehen. Die Erstellung eines Proxys für Klassen
wird nicht unterstützt. Soll der Proxy mehrere Interfaces implementieren, müssen
wir zudem darauf achten, dass bei Methoden mit der gleichen Signatur auch deren
Rückgabetypen kompatibel sind. Zudem kann die verwendete JVM die Anzahl der
Interfaces beschränken, die wir implementieren können.
Das letzte Argument ist schließlich eine Instanz des implementierten
InvocationHandler
-Interfaces, welches die eigentliche Logik des Proxys
enthält.
Damit nun diese Proxy-Implementierung innerhalb unserer Anwendung genutzt wird,
müssen erneut alle Stellen, an denen wir Worker
-Instanzen erzeugen, geändert
werden (s. Listing 8). Die Anwendung verhält sich identisch zur vorherigen
Variante. Allerdings haben wir den Vorteil, dass sich unsere neue
Proxy-Implementierung nun für jedes beliebige Interface verwenden lässt.
Fallstricke
Die Nutzung des dynamischen Proxys enthält einen kleinen Fallstrick, den es zu
kennen gilt. In unserer Implementierung werden auch die Methodenaufrufe für die
von java.lang.Object
zur Verfügung gestellten Methoden equals
, hashCode
und toString
über unseren InvocationHandler
geleitet.
Bei uns führt dies vor allem dazu, dass der Aufruf worker.equals(worker)
in
unserer Anwendung zum Ergebnis false
führen würde, obwohl es sich um den
Vergleich identischer Objekte handelt. Auch bei anderen Umsetzungen, zum
Beispiel bei der Verwendung als Remote Proxy, kann dieses Verhalten ungewollt
sein.
Um diesen Fallstrick bei Bedarf zu lösen, müssen wir also dafür sorgen, dass
Aufrufe auf diesen drei Methoden nicht an unser Subjekt weitergeleitet werden,
sondern diese direkt von unserem InvocationHandler
, beispielsweise mit Nutzung
der Hilfsmethoden von java.lang.Object
, abgehandelt werden. Möchten wir dies
nicht selbst lösen, können wir alternativ auf die Klasse
AbstractInvocationHandler
von Guava
zurückgreifen, die bereits diese Logik enthält.
Weitere Anwendungsmöglichkeiten
Neben der Umsetzung des Proxy-Musters gibt es noch eine weitere Möglichkeit, das Dynamic Proxy Class API einzusetzen. Diese Art der Verwendung wurde vor allem durch Bibliotheken wie Spring Data oder Feign bekannt gemacht.
Die Grundidee bei beiden ist es, den Nutzenden ein Interface definieren zu lassen, um auf deklarative Art zu beschreiben, was zu tun ist. Aus dieser Beschreibung leitet Spring Data Datenbankabfragen und Feign HTTP-Aufrufe ab. Die eigentliche Implementierung der Funktionalität könnte dann mittels eines dynamischen Proxys umgesetzt werden.
Eine sehr simple, an Feign erinnernde Umsetzung, um zu zeigen, wie sich eine solche Bibliothek mittels Dynamic Proxy Class API umsetzen lässt, ist in Listing 9 und Listing 10 zu sehen.
Alle Code-Listings dieses Artikels sind auch auf GitHub verfügbar.