Dynamische Proxies in Java

Dynamische Proxies in Java

1. Einführung

In diesem Artikel geht es umJava’s dynamic proxies - dies ist einer der primären Proxy-Mechanismen, die uns in der Sprache zur Verfügung stehen.

Einfach ausgedrückt sind Proxys Fronts oder Wrapper, die den Funktionsaufruf über ihre eigenen Einrichtungen (normalerweise auf reale Methoden) weiterleiten und möglicherweise einige Funktionen hinzufügen.

Dynamische Proxys ermöglichen es einer einzelnen Klasse mit einer einzelnen Methode, mehrere Methodenaufrufe für beliebige Klassen mit einer beliebigen Anzahl von Methoden durchzuführen. Ein dynamischer Proxy kann als eine ArtFacade betrachtet werden, der jedoch vorgeben kann, eine Implementierung einer beliebigen Schnittstelle zu sein. Unter dem Deckmantelit routes all method invocations to a single handler - die Methodeinvoke().

Während es kein Tool für alltägliche Programmieraufgaben ist, können dynamische Proxys für Framework-Writer sehr nützlich sein. Es kann auch in Fällen verwendet werden, in denen konkrete Klassenimplementierungen erst zur Laufzeit bekannt sind.

Diese Funktion ist in das Standard-JDK integriert, sodass keine zusätzlichen Abhängigkeiten erforderlich sind.

2. Aufruf-Handler

Lassen Sie uns einen einfachen Proxy erstellen, der nichts anderes tut, als zu drucken, welche Methode aufgerufen werden soll, und eine fest codierte Nummer zurückzugeben.

Zuerst müssen wir einen Subtyp vonjava.lang.reflect.InvocationHandler erstellen:

public class DynamicInvocationHandler implements InvocationHandler {

    private static Logger LOGGER = LoggerFactory.getLogger(
      DynamicInvocationHandler.class);

    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
      throws Throwable {
        LOGGER.info("Invoked method: {}", method.getName());

        return 42;
    }
}

Hier haben wir einen einfachen Proxy definiert, der protokolliert, welche Methode aufgerufen wurde, und 42 zurückgibt.

3. Proxy-Instanz erstellen

Eine Proxy-Instanz, die von dem soeben definierten Aufrufhandler bedient wird, wird über einen Factory-Methodenaufruf für die Klassejava.lang.reflect.Proxyerstellt:

Map proxyInstance = (Map) Proxy.newProxyInstance(
  DynamicProxyTest.class.getClassLoader(),
  new Class[] { Map.class },
  new DynamicInvocationHandler());

Sobald wir eine Proxy-Instanz haben, können wir ihre Interface-Methoden wie gewohnt aufrufen:

proxyInstance.put("hello", "world");

Wie erwartet wird eine Nachricht über die aufgerufene Methode vonput()in der Protokolldatei ausgedruckt.

4. Invocation Handler über Lambda Expressions

DaInvocationHandler eine funktionale Schnittstelle ist, ist es möglich, den Handler inline mithilfe des Lambda-Ausdrucks zu definieren:

Map proxyInstance = (Map) Proxy.newProxyInstance(
  DynamicProxyTest.class.getClassLoader(),
  new Class[] { Map.class },
  (proxy, method, methodArgs) -> {
    if (method.getName().equals("get")) {
        return 42;
    } else {
        throw new UnsupportedOperationException(
          "Unsupported method: " + method.getName());
    }
});

Hier haben wir einen Handler definiert, der für alle get-Operationen 42 zurückgibt und für alles andereUnsupportedOperationException auslöst.

Es wird genauso aufgerufen:

(int) proxyInstance.get("hello"); // 42
proxyInstance.put("hello", "world"); // exception

5. Beispiel für ein dynamisches Timing-Proxy

Lassen Sie uns ein potenzielles reales Szenario für dynamische Proxys untersuchen.

Angenommen, wir möchten aufzeichnen, wie lange die Ausführung unserer Funktionen dauert. Insofern definieren wir zunächst einen Handler, der in der Lage ist, das „echte“ Objekt zu verpacken, Timing-Informationen zu verfolgen und den Aufruf zu reflektieren:

public class TimingDynamicInvocationHandler implements InvocationHandler {

    private static Logger LOGGER = LoggerFactory.getLogger(
      TimingDynamicInvocationHandler.class);

    private final Map methods = new HashMap<>();

    private Object target;

    public TimingDynamicInvocationHandler(Object target) {
        this.target = target;

        for(Method method: target.getClass().getDeclaredMethods()) {
            this.methods.put(method.getName(), method);
        }
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
      throws Throwable {
        long start = System.nanoTime();
        Object result = methods.get(method.getName()).invoke(target, args);
        long elapsed = System.nanoTime() - start;

        LOGGER.info("Executing {} finished in {} ns", method.getName(),
          elapsed);

        return result;
    }
}

Anschließend kann dieser Proxy für verschiedene Objekttypen verwendet werden:

Map mapProxyInstance = (Map) Proxy.newProxyInstance(
  DynamicProxyTest.class.getClassLoader(), new Class[] { Map.class },
  new TimingDynamicInvocationHandler(new HashMap<>()));

mapProxyInstance.put("hello", "world");

CharSequence csProxyInstance = (CharSequence) Proxy.newProxyInstance(
  DynamicProxyTest.class.getClassLoader(),
  new Class[] { CharSequence.class },
  new TimingDynamicInvocationHandler("Hello World"));

csProxyInstance.length()

Hier haben wir eine Karte und eine Zeichenfolge (String) als Proxy angegeben.

Aufrufe der Proxy-Methoden werden an das umschlossene Objekt delegiert und erzeugen Protokollanweisungen:

Executing put finished in 19153 ns
Executing get finished in 8891 ns
Executing charAt finished in 11152 ns
Executing length finished in 10087 ns

6. Fazit

In diesem kurzen Tutorial haben wir die dynamischen Proxys von Java sowie einige seiner möglichen Verwendungen untersucht.

Wie immer ist der Code in den Beispielenover on GitHub.