W3docs

Java Dynamic Proxies

Proxy-Implementierungen von Interfaces zur Laufzeit in Java mit java.lang.reflect.Proxy und InvocationHandler erstellen.

Ein dynamischer Proxy ist ein Objekt, das ein oder mehrere Interfaces implementiert, bei dem jedoch jeder Methodenaufruf — zur Laufzeit — durch einen einzigen von Ihnen geschriebenen Handler geleitet wird. Die JVM synthetisiert die Proxy-Klasse on the fly; Sie schreiben die Implementierung nie selbst. Dies ist der mächtigste Bereich von java.lang.reflect, und so funktionieren AOP, transparentes Logging, Lazy Loading, RPC-Stubs und Mocking-Bibliotheken. Dieses Kapitel zeigt, wie Proxy.newProxyInstance und InvocationHandler zusammenpassen und was sie können und nicht können.

Die zwei Bausteine: Proxy und InvocationHandler

Ein dynamischer Proxy benötigt drei Eingaben:

  1. Einen Class Loader (wo die synthetisierte Klasse definiert wird).
  2. Ein Array von Interfaces, die der Proxy implementieren soll.
  3. Einen InvocationHandler — die einzige Methode, die jeden Aufruf empfängt.
InvocationHandler handler = (proxy, method, args) -> {
  // called for EVERY method invoked on the proxy
  return ...;   // becomes the method's return value
};

MyService svc = (MyService) Proxy.newProxyInstance(
    MyService.class.getClassLoader(),
    new Class<?>[]{ MyService.class },
    handler);

svc ist jetzt ein echtes Objekt, das MyService implementiert. Der Aufruf von svc.doThing(x) führt keinen doThing-Rumpf aus — es gibt keinen — sondern ruft handler.invoke(proxy, <Method doThing>, [x]) auf. Der Handler entscheidet, was zu tun ist und was zurückgegeben wird.

Die invoke-Signatur

Object invoke(Object proxy, Method method, Object[] args) throws Throwable
  • proxy — die Proxy-Instanz selbst (selten verwendet; Vorsicht beim Aufrufen von Methoden darauf innerhalb von invoke, da dies den Handler erneut eingibt und zu einer Endlosschleife führen kann).
  • method — die aufgerufene Method; method.getName(), method.getReturnType(), ihre Annotationen usw. sind alle verfügbar.
  • args — die Argumente als Object[] (null, wenn die Methode keine nimmt); Primitiven werden geboxt.
  • Rückgabe — was der Aufrufer erhalten soll; muss zuweisungskompatibel mit method.getReturnType() sein, oder Sie erhalten eine ClassCastException. Bei einer void-Methode wird null zurückgegeben.

Ein häufiges Muster ist die Weiterleitung an ein echtes "Ziel"-Objekt: method.invoke(target, args) — diesen Aufruf mit Logging, Timing, Transaktionen oder Wiederholungsversuchen verkleiden. Dieser Method.invoke-Aufruf ist dasselbe reflektive Dispatching, das in Java Reflection: Methods behandelt wird; hier wird er vollständig durch die Method gesteuert, die die JVM an Ihren Handler übergibt. Diese Weiterleitungsform ist das Decorator-via-Proxy-Idiom, und es ist die Grundlage von Spring AOP.

Nur Interfaces

Die größte Einschränkung: java.lang.reflect.Proxy proxyt Interfaces, keine Klassen. Sie können mit dieser API keine konkrete Klasse dynamisch proxyen. Wenn Sie eine Klasse proxyen müssen, greifen Sie auf eine Bytecode-Bibliothek (CGLIB, ByteBuddy) zurück, die stattdessen eine Unterklasse generiert — deshalb liefern Frameworks diese mit. Für interface-basierte Designs reicht das eingebaute Proxy aus und benötigt keine zusätzliche Abhängigkeit.

Die synthetisierte Proxy-Klasse:

  • Erweitert java.lang.reflect.Proxy und implementiert Ihre Interfaces.
  • Hat einen generierten Namen wie $Proxy0.
  • Leitet equals, hashCode und toString (die Object-Methoden) ebenfalls durch invoke — Ihr Handler sollte also bereit sein, sie zu behandeln oder sinnvoll weiterzuleiten.

Ein durchgearbeitetes Beispiel: ein Logging- und Timing-Proxy

Das Programm definiert ein Repository-Interface und eine echte Implementierung, dann verpackt es die Implementierung in einen dynamischen Proxy, dessen Handler jeden Aufruf protokolliert, zeitlich erfasst, an das echte Objekt weiterleitet und das Ergebnis protokolliert — und so übergreifendes Verhalten hinzufügt, ohne die Implementierung zu berühren.

java— editable, runs on the server

Was man aus dem Ablauf mitnehmen kann:

  • repo war genau als Repository nutzbar — repo.save(...), repo.count(), repo.find(...) kompilierten und liefen alle — doch keine Klasse namens "Logging-Repository" existiert im Quellcode. Die JVM generierte eine $Proxy0-Klasse, die das Interface implementiert, und jeder Aufruf landete in LoggingHandler.invoke. Der Proxy ist ein echtes Repository (instanceof gab true zurück).
  • Jede Geschäftsmethode erhielt automatisch ein Eintritts-/Austrittsprotokoll und Timing ohne Änderungen an InMemoryRepository. Diese Trennung — die Implementierung bleibt ahnungslos, der übergreifende Aspekt lebt im Handler — ist der gesamte Sinn von AOP, und dynamische Proxys sind der Mechanismus, mit dem Spring @Transactional, @Cacheable und ähnliches für Interface-Beans implementiert.
  • Der Handler leitete jeden Aufruf mit method.invoke(target, args) weiter, was bedeutet, dass ein find(99)-Fehler als InvocationTargetException zurückkam. Der Handler entpackte sie mit getCause() und warf die echte NoSuchElementException erneut, sodass der Aufrufer die natürliche Ausnahme abfing und nicht einen Reflection-Wrapper. Ein Proxy, der das Entpacken vergisst, gibt InvocationTargetException an Aufrufer weiter.
  • Object-Methoden werden ebenfalls durch invoke geleitet, daher behandelte der Handler method.getDeclaringClass() == Object.class als Sonderfall und leitete sie einfach weiter. Ohne diesen Guard würden toString/equals/hashCode ebenfalls protokolliert (störend) oder, wenn Sie Strings vom Proxy innerhalb von invoke konstruierten, könnten sie rekursiv werden. Die Object-Methoden bewusst zu behandeln ist ein Standardteil des Schreibens eines Proxy-Handlers.
  • Proxy.isProxyClass(repo.getClass()) bestätigte, dass die Klasse JVM-synthetisiert ist, und ihr Name $Proxy0 zeigt, dass sie generiert und nicht geschrieben wurde. Da die API ein Class<?>[] von Interfaces entgegennimmt, kann ein Proxy mehrere gleichzeitig implementieren — so kann ein einzelner Mock oder Stub mehrere Verträge gleichzeitig erfüllen.

Wann was verwenden

  • Interface, keine Abhängigkeit gewünschtjava.lang.reflect.Proxy. Eingebaut, einfach, nur für Interfaces.
  • Konkrete Klasse proxyen erforderlich → ByteBuddy oder CGLIB (subklassenbasiert). Erforderlich, weil Proxy das nicht kann.
  • Nur Interfaces in Tests stubben → eine Mocking-Bibliothek (Mockito), die auf diesen Mechanismen aufbaut — nicht selbst schreiben.

Dynamische Proxys schließen den Reflection-Teil ab: vom Inspizieren eines Class-Objekts, über das Lesen und Schreiben von Feldern, Aufrufen von Methoden, Erstellen von Instanzen über Konstruktoren, Lesen von Annotationen bis hin zum Synthetisieren ganzer Implementierungen zur Laufzeit. Zusammen bilden sie das Toolkit, das Frameworks ermöglicht, generisch über Typen zu arbeiten, gegen die sie nie kompiliert wurden — sparsam und hinter sauberen Abstraktionen eingesetzt, machen sie Javas Ökosystem von Containern, Mappern und Runnern möglich.

Übungen

Übung
Sie möchten einen Service, der durch ein Interface 'PaymentGateway' definiert ist, so verpacken, dass jeder Methodenaufruf protokolliert wird, ohne die echte Implementierung zu ändern. Sie rufen 'Proxy.newProxyInstance(...)' auf und übergeben 'new Class<?>[]{ PaymentGateway.class }' und einen Handler. Wie lautet der Standardweg innerhalb des 'invoke' des Handlers, um das tatsächliche Ergebnis der Methode zu erzeugen?
Sie möchten einen Service, der durch ein Interface 'PaymentGateway' definiert ist, so verpacken, dass jeder Methodenaufruf protokolliert wird, ohne die echte Implementierung zu ändern. Sie rufen 'Proxy.newProxyInstance(...)' auf und übergeben 'new Class<?>[]{ PaymentGateway.class }' und einen Handler. Wie lautet der Standardweg innerhalb des 'invoke' des Handlers, um das tatsächliche Ergebnis der Methode zu erzeugen?
Was this page helpful?