Dynamische Proxys mit dem JDK umsetzen

Der dynamische Stellvertreter

Bei der Entwicklung mit Java verwenden wir zwangsweise einige der im JDK enthaltenen APIs. Das Collections-API mit seinen Listen, Maps und Sets wird beispielsweise in so gut wie jedem Projekt genutzt. Neben solchen bekannten gibt es jedoch auch einige APIs, die vielen, auch nach mehreren Jahren Java-Entwicklung, noch nicht begegnet sind. In diesem Artikel wollen wir uns eine solche unbekanntere Programmierschnittstelle anschauen, nämlich das Dynamic Proxy Class API.

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).

public interface Worker {

    int doWork(String input);
}
Listing 1: Worker
public class SlowWorker implements Worker {

    private final Random random = new Random();

    @Override
    public int doWork(String input) {
        try {
            TimeUnit.SECONDS.sleep(random.nextInt(5));
        } catch (InterruptedException e) {}
        return 42 * input.length();
    }
}
Listing 2: SlowWorker

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.

public class Main {

    public static void main(String[] args) {
        Worker worker = new SlowWorker();
        int result = worker.doWork(args[0]);
        System.out.println(result);
    }
}
Listing 3: Anwendung

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.

public class ProxyWorker implements Worker {

    private final Worker subject;

    public ProxyWorker(Worker subject) {
        this.subject = subject;
    }

    @Override
    public int doWork(String input) {
        long start = System.nanoTime();
        try {
            return subject.doWork(input);
        } finally {
            long duration = System.nanoTime() - start;
            System.err.println("doWork took: " + duration + "ns");
        }
    }
}
Listing 4: ProxyWorker

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.

public class Main {

    public static void main(String[] args) {
        Worker worker = new ProxyWorker(new SlowWorker());
        int result = worker.doWork(args[0]);
        System.out.println("result=" + result);
    }
}
Listing 5: Anwendung mit Verwendung des ProxyWorker

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.

public class TimingProxy implements InvocationHandler {

    private final Object subject;

    private TimingProxy(Object subject) {
        this.subject = subject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        long start = System.nanoTime();
        try {
            return method.invoke(subject, args);
        } finally {
            long duration = System.nanoTime() - start;
            System.out.println(method.getName() + " took: " + duration + "ns");
        }
    }
}
Listing 6: Dynamischer Proxy

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).

public class TimingProxy implements InvocationHandler {
    ...

    public static <T> T time(T instance) {
        return (T) Proxy.newProxyInstance(
            instance.getClass().getClassLoader(),
            instance.getClass().getInterfaces(),
            new TimingProxy(instance)
        );
    }
}
Listing 7: Erzeugung unseres dynamischen Proxys

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.

public class Main {

    public static void main(String[] args) {
        Worker worker = TimingProxy.time(new SlowWorker());
        int result = worker.doWork(args[0]);
        System.out.println("result=" + result);
    }
}
Listing 8: Anwendung mit Verwendung des dynamischen Proxys

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.

public class HttpRequestProxy implements InvocationHandler {

    private final HttpClient httpClient = HttpClient.newHttpClient();

    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        Request requestDefinition = method.getAnnotation(Request.class);
        HttpRequest request = HttpRequest.newBuilder()
            .method(requestDefinition.method(), noBody())
            .uri(URI.create(requestDefinition.uri()))
            .build();
        HttpResponse<String> response = httpClient.send(request, ofString());
        return response.body();
    }

    @Target(METHOD)
    @Retention(RUNTIME)
    public @interface Request {

        String method() default "GET";
        String uri();
    }

    public static <T> T create(Class<T> definition) {
        return (T) Proxy.newProxyInstance(
            definition.getClassLoader(),
            new Class<?>[] { definition },
            new HttpRequestProxy()
        );
    }
}
Listing 9: Verwendung des dynamischen Proxys als HTTP-Client
public class HttpRequestMain {

    public static void main(String[] args) {
        GoogleClient googleClient = HttpRequestProxy.create(GoogleClient.class);
        System.out.println(googleClient.google());
    }

    interface GoogleClient {

        @Request(method = "GET", uri = "https://google.de")
        String google();
    }
}
Listing 10: Verwendung des HTTP-Client-Proxys

Alle Code-Listings dieses Artikels sind auch auf GitHub verfügbar.

  1. S. Ruppert, Stellvertreterregelungen, in: JavaSPEKTRUM, 5/2019  ↩

  2. E. Gamma, R. Helm, R. E. Johnson, J. Vlissides, Design Patterns, Addison–Wesley, 2008  ↩

Fazit

In diesem Artikel haben wir das Dynamic Proxy Class API aus dem JDK kennengelernt. Dieses API existiert bereits seit JDK 1.3, ist jedoch vielen unbekannt.

Der Hauptanwendungsfall des APIs besteht darin, es uns zu ermöglichen, das Proxy-Muster für generische Funktionalität umzusetzen. Hierzu müssen wir das Interface InvocationHandler implementieren und über die statische Methode newProxyInstance der Klasse Proxy eine Instanz des Proxys erzeugen. Dabei ist zu beachten, dass dies nur für Interfaces funktioniert und wir je nach Anwendungsfall für die Methoden equals, hashCode und toString Ausnahmebehandlungen implementieren sollten.

Neben der Verwendung als Proxy lässt sich mit dem API auch ein weiterer Anwendungsfall lösen. In diesem definiert der Nutzende ein Interface und die Implementierung des dynamischen Proxys stellt die gesamte Funktionalität dar. Beispiele für dieses Muster sind Spring Data oder Feign.

TAGS

Kommentare

Um die Kommentare zu sehen, bitte unserer Cookie Vereinbarung zustimmen. Mehr lesen