4.16.5 | Dynamische Proxies |
Der Begriff Proxy dürfte vielen Lesern vom Internetzugang her bekannt sein. Es handelt sich hierbei um einen zentralen Server, der Anfragen von Clients entgegennimmt und dann delegiert.
Im Kontext der objektorientierten Programmierung hat der Begriff Proxy eine durchaus ähnliche Bedeutung: Ein Proxy ist eine Art Stellvertreterobjekt, das Methodenaufrufe an ein oder mehrere nachgeordnete Objekte delegiert. Daher ergibt sich auch ein Kapselungseffekt: Die Aufrufer »sehen« nichts davon, dass die Anfragen nicht vom Proxy selbst, sondern von den nachgeordneten Objekten bearbeitet werden. Auch brauchen die Proxy-Aufrufer die Klassen und Schnittstellen dieser nachgeordneten Objekte nicht zu kennen.
Ein Proxy kann in Java seit jeher durch eine Klasse realisiert werden, die alle Interfaces implementiert, deren Aufrufe delegiert werden sollen. Allerdings muss die Proxy-Klasse bei diesem Ansatz zur Laufzeit bereits existieren. Seit Version 1.3 können Proxy-Klassen auch dynamisch erzeugt werden. Damit ist es möglich, die Proxy-Schnittstelle erst zur Laufzeit festzulegen. Dynamische Proxies können somit überall da eingesetzt werden, wo die Schnittstelle erst zur Laufzeit festgelegt werden kann oder die Delegation an nachgelagerte Objekte völlig dynamisch erfolgen soll.
Dynamische Proxies bestehen aus einem Exemplar der Klasse Proxy sowie einem damit assoziierten Exemplar von InvocationHandler. Beide Klassen sind im Paket java.lang.reflect definiert.Der InvocationHandler erhält die an den Proxy gerichteten Methodenaufrufe und bearbeitet sie je nach Implementierung entweder selbst oder delegiert sie an andere Objekte. Hierzu ruft das Proxy-Objekt die Methode invoke() des InvocationHandlers auf. Diese Methode hat drei Parameter:
Abbildung 4.7 stellt den Zusammenhang von Proxy und InvocationHandler noch einmal dar.
- Den Proxy, über den der Aufruf kommt. Mit diesem Parameter kann ein InvocationHandler gegebenenfalls eine Unterscheidung treffen, falls er bei mehreren Proxies registriert ist.
- Die aufgerufene Methode in Form eines Method-Exemplars.
- Die der aufgerufenen Methode übergebenen Parameter in Form eines Object-Arrays. Gemäß den üblichen Konventionen des Reflection-API werden einfache Datentypen hier mit der entsprechenden Wrapper-Klasse dargestellt.
Die Klasse Proxy kann mit ihren statischen Methoden getProxyClass() und newProxyInstance() aus einer Liste von Interfaces eine Klasse bzw. ein Objekt erzeugen, das alle diese Interfaces implementiert.
Schematisch sieht die Erzeugung eines Proxy-Objekts folgendermaßen aus:// Erzeugung des anwenderdefinierten InvocationHandlers InvocationHandler handler = new MyInvocationHandler(); // Referenz auf System-ClassLoader holen ClassLoader sysClassLoader = ClassLoader.getSystemClassLoader(); // Array mit Class-Objekten für die Proxy-Interfaces Class[] proxyInterfaces = new Class[]{java.awt.event.ActionListener.class}; // Erzeugung des Proxy-Objekts Proxy proxy = Proxy.newInstance(sysClassLoader, proxyInterfaces, handler);Als erster Parameter muss Proxy.newInstance() der ClassLoader übergeben werden, über den die Interface-Klassen geladen werden können. Dies ist in der Regel der System-ClassLoader. Die Interfaces, die der Proxy implementieren soll, werden als Array mit den entsprechenden Class-Objekten spezifiziert. Im Beispiel enthält dieses Array nur ein Element, nämlich das Class-Objekt für ActionListener. Der letzte Parameter ist schließlich der InvocationHandler, an den alle Aufrufe weitergeleitet werden.
Die wichtigsten Merkmale einer so erzeugten Proxy-Klasse sind:Eine hilfreiche Eigenschaft zum Debuggen ist die Tatsache, dass die Aufrufe aller Proxy-Methoden in der invoke()-Methode des InvocationHandlers zusammengeführt werden. Dies könnte z. B. nützlich sein, um Oberflächen-Events zu protokollieren. Hierzu kann man einen InvocationHandler erstellen, der in seiner invoke()-Methode sämtliche Ereignisse protokolliert:
- Eine Proxy-Klasse ist stets Unterklasse von Proxy.
- Eine Proxy-Klasse wird stets als public final definiert. Normalerweise erfolgt keine Zuordnung zu einem Paket, es sei denn, eines der bei der Erzeugung angegebenen Interfaces ist nicht als public vereinbart. In diesem Fall wird die Proxy-Klasse in dem betreffenden Paket definiert.
- Falls eine bestimmte Methode in mehr als einem der Proxy-Interfaces definiert ist, kann der Proxy nicht unterscheiden, über welches der betreffenden Interfaces der Aufruf erfolgte.
- Neben den in den Interfaces enthaltenen Methoden delegiert Proxy auch die von Object geerbten Methoden an den zugehörigen InvocationHandler. Hierzu zählen insbesondere toString() oder auch equals(). Falls diese Methoden auf das Proxy-Objekt selbst angewendet werden sollen, muss dies im InvocationHandler entsprechend implementiert werden.
public class EventLogger implements InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws IllegalArgumentException { if (args == null) throw new IllegalArgumentException("Missing event object argument"); if (! (args[0] instanceof AWTEvent)) throw new IllegalArgumentException("Expecting event object"); System.err.println("EVENT: "+args[0]); return null; } }Bei der Erzeugung des zugehörigen Proxies gibt man im Konstruktor einfach die benötigten Listener-Interfaces an.// InvocationHandler erzeugen InvocationHandler logger = new EventLogger(); // Array mit den Interfaces erzeugen, // die der Proxy haben soll Class[] listenerInterfaces = new Class[] { java.awt.event.ActionListener.class, java.awt.event.WindowListener.class}; // Erzeugung des Proxy-Objekts Object proxy = Proxy.newProxyInstance( ClassLoader.getSystemClassLoader(), listenerInterfaces, logger);Anschließend wird der Proxy bei allen Komponenten registriert, für die eine Protokollierung erfolgen soll (in diesem Fall ein Frame, der einen Button enthält):JFrame frame = new JFrame("Proxy Demo"); JButton button = new JButton("OK"); frame.getContentPane().add(button); ... // Proxy als Event-Listener registrieren button.addActionListener((ActionListener)proxy); frame.addWindowListener((WindowListener)proxy);Es ist zu beachten, dass in den letzten beiden Zeilen ein Cast in den jeweiligen Listener-Typ durchgeführt werden muss, da proxy für den Compiler eine Referenz auf die Klasse Proxy ist. Erst zur Laufzeit referenziert proxy ein Objekt, das die entsprechenden Listener-Interfaces implementiert. Bei einem statisch (also zum Zeitpunkt des Kompilierens) definierten Proxy für die Listener-Interfaces wären die Casts dagegen nicht nötig.
In diesem Beispiel delegiert der InvocationHandler zwar keine Aufrufe, es wird aber deutlich, wie die Handhabung der Methodenaufrufe zahlreicher Listener-Interfaces mit einem dynamischen Proxy sehr elegant gelöst werden kann, da eine Abbildung auf eine einzige Methode im InvocationHandler möglich ist.
Die »statische« Alternative dazu wäre, eine Klasse zu erstellen, die alle erforderlichen Interfaces implementiert. Diese müsste allerdings jedes Mal neu kompiliert werden, wenn ein weiterer Listener-Typ unterstützt werden soll. In dem hier gezeigten Beispiel ist dagegen keine Neuübersetzung nötig. Außerdem ist eine Proxy-gestützte Lösung für diesen Zweck einfacher handhabbar, da beim herkömmlichen Ansatz schnell sehr viele Methoden zu implementieren wären.Material zum Beispiel
- Quelltexte: