Tools generieren eine gewaltige Masse von Infrastrukturcode. Ein Beispiel dafür ist der Compiler rmic, der Stubs und Skeletons zur Unterstützung von Remote-Proxys für RMI (Remote Method Invocation) in Java erzeugt. Erstaunlicherweise besteht rmic in modernen Java-Distributionen im Verzeichnis JDK/bin weiter.
Im Mai 2000 hat Sun Microsystems Java 1.3 und damit Dynamic Proxys herausgebracht.
Statische Factoring-Methoden erzeugen Interfaceinstanzen und leiten alle Methodenaufrufe über einen einzigen InvocationHandler weiter. Dieser wird vom RMI verwendet, um Stubs und Skeletons im Speicher anzulegen.
Großer Fortschritt
Dynamic Proxys erleichtern die Wartung von Code. Anstelle einer großen Anzahl von manuell erstellten Klassen schreiben wir einen einzigen Dynamic Proxy, getreu dem DRY-Prinzip (Don’t Repeat Yourself).
Ein Beispiel: Ein Unternehmen mit mehreren Hundert Business Entitys hatte ein Zaubertool geschrieben, um ihre proprietäre Sprache in Java zu übersetzen. Das Tool konvertierte jede Entity in mehrere Java-Klassen. Nach dieser ersten Generierung mussten die Programmierer den Code jedoch von Hand warten, was sehr aufwendig und fehleranfällig war.
Der generierte Code enthielt Factories, die ein wenig an Home Interfaces in EJB erinnerten. Das waren allein schon rund 600 000 Codeanweisungen. Ein einziger Dynamic Proxy ersetzte all das. Weniger Code, einfachere Wartung.
Dynamic Proxys werden von vielen der von uns täglich verwendeten Frameworks unter der Haube verwendet. So etwa im Spring Framework, WildFly, der NetBeans-Plattform, Apache Tomcat und dem OpenJDK, um nur einige zu nennen.
Aber Moment …
Dynamic Proxys sind nicht immer das beste Tool. Obwohl sehr praktisch, büßen Methodenaufrufe an einen Dynamic Proxy im Vergleich zu direkten Methodenaufrufen ein bisschen an Leistung ein. In dieser Artikelserie erfahren wir, wie wir diese Leistungseinbußen bei Verwendung Dynamic Proxys minimieren können.
Lernziel
Es gibt Fälle, in denen Dynamic Proxys die ideale Lösung für ein bestimmtes Problem sind. Sie werden Dynamic Proxys ganz genau kennenlernen und sie in einer Reihe von Szenarien sehen. Dadurch werden Sie einschätzen lernen, wann Dynamic Proxys sinnvoll sind und wann nicht.
Außerdem werden Sie ihre Stärken und Schwächen erkennen können, was die Fehlerbehebung in Systemen mit Dynamic Proxys erleichtert.
Java-Version
Der Code für diese Artikelserie wurde mit Java 13 erstellt. Wo es den Code klarer macht, verwenden wir das var-Schlüsselwort für lokale Variablen aus Java 10 und Textblöcke für mehrzeilige Strings aus Java 13. Beachten Sie, dass Textblöcke eine Vorschaufunktion in Java 13 sind und sich ändern können.
Obwohl wir in dieser Artikelserie Shallow Reflection nutzen, versuchen wir Deep Reflection zu vermeiden. Mark Reinhold definiert die Begriffe in der Mailingliste OpenJDK JPMS Spec Experts wie folgt:
- Ein Shallow Reflective Access ist der Zugriff zur Laufzeit über das Core Reflection API (java.lang.reflect) ohne setAccessible. Der Zugriff ist nur auf öffentliche Elemente in exportierten Paketen möglich.
- Der Deep Reflective Access ist der Zugriff zur Laufzeit über das Core Reflection API, bei dem die setAccessible-Methode verwendet wird, um auf nichtöffentliche Elemente zugreifen zu können. Der Zugriff ist auf jedes Element möglich, ob öffentlich oder nicht, in jedem Paket, ob exportiert oder nicht. Ein Deep Reflective Access impliziert einen Shallow Reflective Access.
Ein Architekt ist eine Person, die den Bau von Gebäuden plant, gestaltet und abnimmt. Das Endprodukt ist schwer zu verändern, da es buchstäblich in Beton gegossen wird. Software ändert sich ständig, auch noch viele Jahre nach der Installation. Es ist daher äußerst überraschend, wie sehr Christopher Alexanders 1977 erschienenes Buch „A Pattern Language. Towns, Buildings, Construction“ [1] die Softwareindustrie beeinflusst hat. Das Buch handelt von der zeitlosen Art des Bauens. Er beschreibt Lebensraummuster, die im Lauf der Geschichte immer wieder auftreten. So wird beispielsweise das Muster Nr. 159 als Light on two sides of every room (Licht auf zwei Seiten eines jeden Raumes) bezeichnet. Wir können uns dieses Muster aufgrund seines beschreibenden Namens leicht vorstellen (Kasten: „Nomen est omen“).
In den 90er-Jahren übertrug die Softwareindustrie die von Alexander vorgeschlagenen Prinzipien auf das Softwaredesign. Das Konzept der objektorientierten Entwurfsmuster (Kasten: „Entwurfsmuster“) wird in dem berühmten Buch „Design Patterns. Elements of Reusable Object-Oriented Software“ [2] von Erich Gamma et al. verewigt. Das Buch wurde von vier Autoren geschrieben, die als Gang of Four, kurz GoF, bezeichnet werden.
Entwurfsmuster
Ein Entwurfsmuster ist eine generelle Lösung für ein gängiges Problem, das bei der Computerprogrammierung auftritt. Es lässt sich in den meisten Sprachen anwenden und beschreibt sprachunabhängig die Verbindungen zwischen Objekten.
Nomen est omen
Einer der wichtigsten Identifikatoren eines Musters ist sein Name. In seinem Buch „Timeless Way of Building“ schreibt Alexander: „Die Suche nach einem Namen ist ein wesentlicher Bestandteil des Prozesses der Erfindung oder Entdeckung eines Musters. Wenn ein Muster einen schwachen Namen hat, heißt das, dass es sich um ein unklares Konzept handelt, und ich werde es, egal was man mir sagt, ganz bestimmt nicht umsetzen.“
Entwurfsmuster nutzen Namen für gängige Programmiertechniken auf ähnliche Weise, denn so wird es einfacher, den Verwendungszweck zu kommunizieren. Wenn wir die Muster kennen, können wir die Entwürfe schneller verstehen. Leider sind die Musterbezeichnungen der GoF nicht so fantasievoll wie die von Alexander. Ein Name wie Wrapper könnte schließlich alles heißen. Also Vorsicht bei der Namensgebung. Ein schwacher Name wie Wrapper wird unweigerlich Verwirrung stiften. GoF bezeichnen als Wrapper z. B. sowohl Adapter als auch Decorator. Wir stützen uns deshalb auch auf den Zweck, jedes Muster eindeutig zu identifizieren.
In dieser Artikelserie erläutern wir hauptsächlich vier Muster: Proxy, Adapter, Decorator und Composite. Diese Muster sind alle strukturell, d. h. sie beschreiben nicht das Verhalten oder die Erzeugung von Objekten, sondern wie Klassen und Objekte miteinander verknüpft sind.
Unsere vier Muster haben eine ähnliche Klassenhierarchie, sodass wir sie mit Hilfe von Dynamic Proxys mit einem geringen Aufwand implementieren können. Sie sind sich so ähnlich, dass wir sie Entwurfsmuster-Cousins nennen können.
Proxy Pattern
Der Zweck von Proxy Pattern ist die Bereitstellung eines Stellvertreters oder Platzhalters für ein anderes Objekt, um die Zugriffe darauf kontrollieren zu können [3]. Der Client kommuniziert über einen Stellvertreter (den Proxy) und verhält sich dabei ein wenig wie die Puppe eines Bauchredners.
Die Standardstruktur nach GoF ist in Abbildung 1 dargestellt. Der Client verweist auf die Schnittstelle (interface) Subject, die entweder ein RealSubject oder ein Proxy ist. Der Client weiß das nicht – und es ist ihm auch egal. In der Standardstruktur nach GoF verweist Proxy auf RealSubject. Eine Variation der Proxystruktur ist, dass Proxy auf Subject verweist. Damit können wir Proxys kaskadieren. Kaskadierende Proxys erläutern wir am Ende des nächsten Kapitels.
Der Proxy hat mehrere Anwendungsfälle:
- Ein virtueller Proxy erzeugt teure Objekte auf Verlangen. Beispiel: Im Spring Framework erstellt LazyConnectionDataSourceProxyProxy die reale JDBC DataSource „lazy“, wenn die erste Anweisung erstellt wird.
- Ein Remote-Proxy stellt einen lokalen Stellvertreter für ein Objekt in einem anderen Adressraum dar, z. B. auf einem Remote-Computer. Beachten Sie, dass der Remote-Proxy nicht wirklich auf das Remote-Objekt verweist, sondern die Methoden über RPC, RMI, Thrift, ProtoBuf, gRPC, REST, HTTP, TCP/UDP oder einen anderen Remoting-Mechanismus aufruft.
- Der Schutzproxy kontrolliert den Zugriff auf das Originalobjekt, z. B. zur Zugriffkontrolle oder Threadsicherheit. Beispiel: Collections.synchronizedCollection(), gibt eine neue Collection zurück, die vor Race Conditions geschützt ist.
Adapter Pattern
Der Zweck der Adapter Patterns ist wie folgt definiert: „Die Schnittstelle einer Klasse soll in eine andere Schnittstelle konvertiert werden, die Clients erwarten. Mit dem Adapter wird eine Kooperation von Klassen möglich, die sonst aufgrund inkompatibler Schnittstellen nicht möglich wäre“ [4].
Das Ziel des Adapter Pattern ist es also, dass Objekte auch mit ansonsten inkompatiblen Schnittstellen kooperieren können. Dieses Muster ist der Diplomat unter den Mustern. Das Adapter Pattern kann entweder als Objektadapter unter Verwendung der Komposition oder als Klassenadapter unter Verwendung der Vererbung implementiert werden.
In Abbildung 2 implementiert der Adapter die Target-Schnittstelle und delegiert an die Klasse, die wir anpassen wollen, in unserem Fall nennen wir diese Adaptee. Da unser Adapter ein Objekt innerhalb unseres Adapters ist, nennen wir dies einen Objektadapter. Der Objektadapter verwendet die Komposition zur Anpassung des Adaptee.
Eine weniger gebräuchliche Form des Adapters tritt auf, wenn unser Adapter den Adaptee erweitert und die Target-Schnittstelle implementiert. Da Adaptee eine Basisklasse unseres Adapters ist, nennen wir ihn einen Klassenadapter, siehe Abbildung 3. Der Klassenadapter verwendet die Vererbung, um den Adaptee anzupassen.
Ein Vorteil des Klassenadapters ist, dass Teile des Adaptee einfacher zu modifizieren sind. Leider ist es schwieriger, eine Hierarchie von Objekten anzupassen. In einem späteren Kapitel schauen wir uns an, wie man die Erweiterbarkeit des Klassenadapters und die Flexibilität des Objektadapters mit Hilfe eines Dynamic Proxys erhält.
Warum ist ein Objektadapter wie ein Proxy?
Im Proxy implementiert RealSubject die Subject-Schnittstelle. Ist es immer noch ein Proxy, wenn wir die Vererbungsbeziehung wegnehmen, oder wird er dann zu einem Adapter? Oder wird das Muster bei einem Adapter zu einem Proxy, wenn der Adaptee eine Target-Schnittstelle implementiert?
Das hängt vom Verwendungszweck ab, wie am Beispiel der Session-Beans in EJB klar wird: Die Implementierungs-Bean erbt nicht direkt von der Remote-Schnittstelle, da die Methoden sonst einen Kompatibilitätsfehler ausgeben. Die Remote-Schnittstelle würde RemoteException ausgeben, während die Implementierungs-Bean eine Business Exception ausgeben würde.
Ein Objektadapter hat eine ähnliche Struktur wie der Proxy. In beiden Fällen verwendet der Client die Toplevelschnittstellen Target oder Subject. Implementierungen dieser Schnittstellen erfolgen entweder im Adapter oder im Proxy.
Decorator Pattern
Der Zweck des Decorator Patterns definiert sich wie folgt: „Ein Objekt soll dynamisch um Zuständigkeiten erweitert werden. Decorators bilden eine flexible Alternative zur Unterklassenbildung, um die Funktionalität der Klasse zu erweitern“ [5].
Wenn wir eine Klasse mit neuen Methoden erweitern möchten, ist es der einfachste Ansatz, sie einer Unterklasse hinzuzufügen. Das kann jedoch zu unvorteilhaften Klassenhierarchien führen. Es besteht die Gefahr der Duplizierung von Code aufgrund der Unfähigkeit, mehr als eine Basisklasse zu erweitern. Ein besserer Ansatz ist das Decorator Pattern (Abb. 4), bei dem wir ein Objekt mit einer umfangreicheren Klasse dekorieren und damit zusätzliche Verantwortlichkeiten hinzufügen. Wir können dieses Muster auch verwenden, um Methoden zu entfernen, die wir nicht unterstützen möchten. Dies wird als Filterung bezeichnet.
Ein bekanntes Beispiel für einen Decorator ist der java.io.InputStream und zugehörige Klassen, wobei das Design aus C++ kopiert wurde. Wir haben einige konkrete Komponenten wie den FileInputStream und den Socket.getInputStream(). Wir haben auch eine Myriade Decorators, wie den BufferedInputStream zum Hinzufügen von I/O-Pufferung, den DataInputStream zum Lesen primitiver Daten im Little-Endian-Format und den ObjectInputStream zum Lesen von Java-Objekten mittels Serialisierung. Es gibt sogar einen ProgressMonitorInputStream der einen Swing ProgressDialog öffnet, wenn das Lesen der Eingabe länger als zwei Sekunden dauert.
Unter Listing 1 erstellen wir einen FileOutputStream zur Datei data.bin.gz. Wir dekorieren das mit dem GzipOutputStream, dem BufferedOutputStream und dem DataOutputStream. Jedes Glied in der Kette fügt mehr Funktionen hinzu. Und dann schreiben wir zehn Millionen zufällige Integer zwischen null und tausend.
try ( var out = new DataOutputStream( new BufferedOutputStream( new GZIPOutputStream( new FileOutputStream( "data.bin.gz"))))) { ThreadLocalRandom.current().ints(10_000_000, 0, 1_000) .forEach(i -> { try { out.writeInt(i); } catch (IOException e) { throw new UncheckedIOException(e); } }); out.writeInt(-1); // our EOF marker }
In Listing 2 lesen wir aus einem DataInputStream, der wiederum aus dem BufferedInputStream, dem GzipInputStream und dem FileInputStream liest.
try ( var fis = new FileInputStream("data.bin.gz"); var in = new DataInputStream( new BufferedInputStream( new GZIPInputStream( fis)))) { long total = 0; int value; while ((value = in.readInt()) != -1) { total += in.readInt(); } System.out.println("total = " + total); }
Der Grund, warum wir den FileInputStream separat deklariert haben ist, dass der GzipInputStream den Dateiheader liest, um sicherzustellen, dass es sich tatsächlich um eine gzip-Datei handelt. Wenn der Header falsch ist, gibt der Constructor von GzipInputStream eine Fehlermeldung aus. Der FileInputStream wird in diesem Fall dann nicht automatisch geschlossen, es sei denn, wir deklarieren ihn separat.
Warum ist ein Decorator wie ein Proxy?
Der Decorator ist in seiner Struktur dem Proxy am ähnlichsten. Component verhält sich wie Subject, Decorator wie Proxy und ConcreteComponent wie RealSubject. Ein Unterschied besteht in den Klassen, die der Client in der Regel sieht. Im Proxy verwendet der Client die Schnittstelle von Subject, während er im Decorator die konkreten Decorator-Instanzen verwenden kann.
Composite Pattern
Zweck des Composite Patterns ist es, Objekte zu Baumstrukturen zusammenzufügen, um Part-whole-Hierarchien zu repräsentieren. Das Composite Pattern ermöglicht es Clients, sowohl einzelne Objekte als auch Kompositionen von Objekten einheitlich zu behandeln [6].
Teil/Ganzes-Hierarchie?
Nutzer, deren Muttersprache nicht Englisch ist, haben oft Probleme mit dem Begriff Part-whole-Hierarchie. Wörterbuchdefinitionen von „das Teil“ und „das Ganze“ sind:
- Das Teil: ein Stück oder Teil eines Ganzen
- Das Ganze: die Gesamtheit von Teilen oder Elementen, die zu einer Sache gehören
Ein Objekt ist entweder ein Teil eines Ganzen oder es ist das Ganze, das aus Teilen besteht. Beispielsweise ist eine Datei Teil eines Verzeichnisses und ein Verzeichnis enthält Dateien und möglicherweise andere Verzeichnisse. Eine der Einschränkungen von Part-whole-Hierarchien ist, dass der Objektgraph seiner Definition nach nicht kreisförmig sein darf. Das ist aber keine starke Einschränkung. Durchläufe bei kreisförmigen Datenstrukturen können zu Endlosschleifen führen oder in einem StackOverflow-Fehler enden.
Die Struktur ist wiederum ähnlich wie beim Proxy und Objektadapter. Der Unterschied besteht darin, dass es eine Eins-zu-viele-Beziehung zwischen dem Composite und der Komponente gibt. Der Client spricht mit der Toplevelkomponentenklasse.
Warum ist ein Composite wie ein Proxy?
Sowohl im Composite als auch im Proxy würde der Client die Toplevelschnittstellen von Subject oder Component verwenden. Das Kompositum hat jedoch eine Eins-zu-viele-Beziehung zwischen seiner Composite- und Component-Schnittstelle, während das Proxy-Muster eine 1:1-Verknüpfung zwischen seiner Proxy- und der Subject-Schnittstelle hat.