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:
- Einen Class Loader (wo die synthetisierte Klasse definiert wird).
- Ein Array von Interfaces, die der Proxy implementieren soll.
- 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 Throwableproxy— die Proxy-Instanz selbst (selten verwendet; Vorsicht beim Aufrufen von Methoden darauf innerhalb voninvoke, da dies den Handler erneut eingibt und zu einer Endlosschleife führen kann).method— die aufgerufeneMethod;method.getName(),method.getReturnType(), ihre Annotationen usw. sind alle verfügbar.args— die Argumente alsObject[](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 eineClassCastException. Bei einervoid-Methode wirdnullzurü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.Proxyund implementiert Ihre Interfaces. - Hat einen generierten Namen wie
$Proxy0. - Leitet
equals,hashCodeundtoString(dieObject-Methoden) ebenfalls durchinvoke— 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.
Was man aus dem Ablauf mitnehmen kann:
repowar genau alsRepositorynutzbar —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 inLoggingHandler.invoke. Der Proxy ist ein echtesRepository(instanceofgabtruezurü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,@Cacheableund ähnliches für Interface-Beans implementiert. - Der Handler leitete jeden Aufruf mit
method.invoke(target, args)weiter, was bedeutet, dass einfind(99)-Fehler alsInvocationTargetExceptionzurückkam. Der Handler entpackte sie mitgetCause()und warf die echteNoSuchElementExceptionerneut, sodass der Aufrufer die natürliche Ausnahme abfing und nicht einen Reflection-Wrapper. Ein Proxy, der das Entpacken vergisst, gibtInvocationTargetExceptionan Aufrufer weiter. Object-Methoden werden ebenfalls durchinvokegeleitet, daher behandelte der Handlermethod.getDeclaringClass() == Object.classals Sonderfall und leitete sie einfach weiter. Ohne diesen Guard würdentoString/equals/hashCodeebenfalls protokolliert (störend) oder, wenn Sie Strings vom Proxy innerhalb voninvokekonstruierten, könnten sie rekursiv werden. DieObject-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$Proxy0zeigt, dass sie generiert und nicht geschrieben wurde. Da die API einClass<?>[]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ünscht →
java.lang.reflect.Proxy. Eingebaut, einfach, nur für Interfaces. - Konkrete Klasse proxyen erforderlich → ByteBuddy oder CGLIB (subklassenbasiert). Erforderlich, weil
Proxydas 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.