Lars Wunderlich AOP
Lars Wunderlich
AOP Aspektorientierte Programmierung in der Praxis
Lars Wunderlich : AOP Aspektorientierte Programmierung in der Praxis Frankfurt, 2005 ISBN 3-935042-74-4
© 2005 entwickler.press, ein Imprint der Software & Support Verlag GmbH
http://www.entwickler-press.de http://www.software-support.biz Ihr Kontakt zum Verlag und Lektorat:
[email protected] Bibliografische Information Der Deutschen Bibliothek Die Deutsche Bibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.ddb.de abrufbar.
Korrektorat: Petra Kienle Satz: text & form GbR, Carsten Kienle Umschlaggestaltung: Melanie Hahn Belichtung, Druck und Bindung: M.P. Media-Print Informationstechnologie GmbH, Paderborn. Alle Rechte, auch für Übersetzungen, sind vorbehalten. Reproduktion jeglicher Art (Fotokopie, Nachdruck, Mikrofilm, Erfassung auf elektronischen Datenträgern oder andere Verfahren) nur mit schriftlicher Genehmigung des Verlags. Jegliche Haftung für die Richtigkeit des gesamten Werks kann, trotz sorgfältiger Prüfung durch Autor und Verlag, nicht übernommen werden. Die im Buch genannten Produkte, Warenzeichen und Firmennamen sind in der Regel durch deren Inhaber geschützt.
Inhaltsverzeichnis 1
AOP
Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Von OOP nach AOP – Evolution der Programmierung . . . . 1.1 Die 1:1-Mapping-Vision . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 Einführung in AOP-Begriffe . . . . . . . . . . . . . . . . . . . . . . 1.3 Architektur- und Designvorgehen durch „Prismabildung“ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.4 Evolution der Softwareentwicklungsparadigmen . . . . . . 1.4.1 Vom prozeduralen Erwachen .... . . . . . . . . . . . . 1.4.2 ... zum objektorientierten Dilemma . . . . . . . . . . 1.5 AOP als Rettungsanker!? . . . . . . . . . . . . . . . . . . . . . . . . 1.6 Fortschritte von AOP gegenüber OOP . . . . . . . . . . . . . . 1.7 AOP-Techniken von morgen?! . . . . . . . . . . . . . . . . . . . . 1.7.1 Separation of Concerns versus Separation of Aspects . . . . . . . . . . . . . . . . . . . . 1.7.2 Multidimensionales Separation of Concerns. . . 1.7.3 Multidimensionale Interfaces . . . . . . . . . . . . . . 1.7.4 Anwendung von Vererbung im multidimensionalen Aspektuniversum . . . . . . . 1.7.5 Orthogonale bzw. nichtorthogonale Aspekte und Konfliktsituationen . . . . . . . . . . . . . . . . . . . 1.7.6 Symmetrische und asymmetrische AOP-Lösungen . . . . . . . . . . . . . . . . . . . . . . . . . 1.7.7 Weaving in dynamischem und statischem AOP . . . . . . . . . . . . . . . . . . . . . . . . . 1.8 Wann ist der Einsatz von AOP sinnvoll?. . . . . . . . . . . . . 1.8.1 Auswirkung von Softwareentwicklungsverfahren auf Projektmanagement. . . . . . . . . . . 1.8.2 Wiederverwendbarkeit – ein Aberglaube?. . . . . 1.8.3 AOP – ein Ansatz zur Lösung? . . . . . . . . . . . . . 1.9 Versuch einer Einschätzung von AOP. . . . . . . . . . . . . . . 1.9.1 AOP zum Einsatz bringen . . . . . . . . . . . . . . . . . 1.10 Zusammenfassung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
9 19 19 20 30 33 33 35 38 39 47 48 53 54 55 56 62 64 69 70 74 78 80 83 85
5
Inhaltsverzeichnis
2
3
6
AOP-Frameworks in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.1 AspectJ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.1.1 Die AspectJ-Sprache . . . . . . . . . . . . . . . . . . . . . 2.1.2 Beispiel mit AJDT in Eclipse . . . . . . . . . . . . . . 2.2 JBoss AOP. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.1 JBoss-IDE AOP am Beispiel . . . . . . . . . . . . . . . 2.3 JBoss AOP vs. AspectJ . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4 AspectWerkz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.5 Weitere Java-AOP-Tools . . . . . . . . . . . . . . . . . . . . . . . . . 2.6 Frameworks? – Eine Stellungnahme. . . . . . . . . . . . . . . . Architekturen mit AOP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1 Theme Approach – Analyse und Design in AOP . . . . . . 3.2 Architektonische Grundlagen . . . . . . . . . . . . . . . . . . . . . 3.2.1 Pointcutwahl – eine Frage der Modellierung . . 3.3 Technische Grundlagen AOP . . . . . . . . . . . . . . . . . . . . . 3.3.1 Java Reflection. . . . . . . . . . . . . . . . . . . . . . . . . . 3.3.2 Dynamic Proxies – erster Schritt Richtung AOP? . . . . . . . . . . . . . . . . . . . . . . . . . 3.3.3 Bytecodemanipulation . . . . . . . . . . . . . . . . . . . . 3.3.4 Java Standard Edition mit AOP versus Java Enterprise Edition . . . . . . . . . . . . . . . . . . . 3.3.5 Meta-Tags für die Realisierung von AOP . . . . . 3.4 Auswirkungen von Refactorings auf AOP . . . . . . . . . . . 3.5 Performancekiller AOP? . . . . . . . . . . . . . . . . . . . . . . . . . 3.5.1 Performance anders verstanden . . . . . . . . . . . . . 3.6 Anwendungsgebiete . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.6.1 Design by Contract . . . . . . . . . . . . . . . . . . . . . . 3.6.2 Singletons mit AOP realisieren . . . . . . . . . . . . . 3.6.3 Lazy creation/initialization . . . . . . . . . . . . . . . . 3.6.4 Instanzen und Ergebnisse cachen . . . . . . . . . . . 3.6.5 Exception-Softening . . . . . . . . . . . . . . . . . . . . . 3.6.6 Coverage-Tests . . . . . . . . . . . . . . . . . . . . . . . . . 3.6.7 Asynchrone Aufrufe realisieren. . . . . . . . . . . . . 3.6.8 Mocktests und Implementierungen mit AOP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
87 88 90 102 110 123 128 129 130 131 133 143 152 153 155 155 159 169 188 197 204 208 212 213 213 231 236 243 248 250 251 257
Inhaltsverzeichnis
3.7 Beschwörung der Dämonen? . . . . . . . . . . . . . . . . . . . . . 3.8 Sinnvolle Heirat – MDA/MDSD und AOP? . . . . . . . . . . 3.9 Zusammenfassung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Nachwort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Literaturverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Stichwortverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Autor. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
AOP
272 276 277 279 281 283 285
7
Einleitung Als ich vor einigen Jahren zum ersten Mal von aspektorientierter Softwareentwicklung hörte, schien es mir ein Hype zu sein wie viele andere auch. Mal versprach die Model-Driven-Architecture, ein Meilenstein in der Kunst der Programmierung zu sein, dann rief wieder jemand nach AOP und wurde von einem Verfechter für Webservices und SOA (Service-oriented Architecture) wiederum übertroffen. Ich war davon überzeugt, AOP wäre irgendwie so eine fixe Idee einer anderen Aufteilung von Software und AspectJ, irgendein dazugehöriges komisches Tool für Java. Diese Meinung hielt sich, bis ich das erste Mal Ramnivas Laddads (seines Zeichens u. a. Buchautor von „AspectJ in Action“) Ausführungen zum Thema AOP las, was für mich bedeutete, meine bisherige Denkweise über objektorientierte Softwareentwicklung in Java vollkommen in Frage zu stellen. Hiermit sind Sie eingeladen, diese Erfahrung zu teilen ... In Enschede in den Niederlanden fand Ende April 2002 die erste „Conference on Aspect-Oriented Software Development (AOSD)“ statt. Damals war unter den großen Softwareherstellern als einziger Sponsor IBM mit dabei, was dem Event an sich und dem damals so langsam stärker aufkeimenden, neuen Entwicklungsvorgehen ein Nischendasein bescherte. Auch heute muss man nach dem Begriff „AOP“ (aspect-oriented programming) noch immer suchen, um echte Neuerungen oder Nachrichten zum Thema zu finden. Indirekt ist AOP heute aber mehr denn je in aller Munde. Auch wenn man den Namen z.B. im Bereich der Java Enterprise Edition (Java EE) faktisch zurzeit kaum findet, so werden wir doch im Verlaufe des Buchs sehen, dass AOP ein ganz wesentlicher Antriebsmotor für die in den letzten Jahren erfolgten Entwicklungen im Java-Markt ist. Es gibt Hersteller, die sich heute mehr oder minder deutlich in diese Richtung orientieren. Die meisten Bücher am Markt sind in fester Hand der Produkte und Produktlinien. Java, C# oder .NET werden in Entwicklerkreisen oftmals nicht nur als Synonyme einer Programmiersprache oder Plattform, sondern vor al-
AOP
9
Einleitung
lem auch für objektorientierte Vorgehensmodelle, Entwicklungs- und Modellierungstechniken gesehen. Genau an dieser Stelle setzt AOP auch an. Es handelt sich um kein Produkt, kein Framework oder einen Unternehmensnamen, sondern um eine Idee, eine Vision, wie man Software entwickeln und designen könnte, wie man sie revolutionär ändern sollte. Für Entwickler kann die reine AOP-Lehre ein ähnlicher „Kulturschock“ sein wie der Umstieg von prozeduraler zu objektorientierter Softwareentwicklung. Viele Entwickler sind heute noch der Meinung, schon die reine Verwendung von Java als Programmiersprache würde automatisch zu einer signifikanten Verbesserung der Sourcecodequalität, deren Aussagekraft und Entwicklungsgeschwindigkeit gegenüber prozeduralen Ansätzen führen. Diese Haltung ist vermutlich ähnlich falsch wie die Erwartung, der reine Kauf eines Automobils erspare die Notwendigkeit, einen Schein zum Führen eines solchen Gefährts zu machen. Nicht umsonst schmücken Regale voller Bücher Bibliotheken und Buchläden mit dem Thema „Wie programmiere ich richtig?“ und „Nicht der gesamte Sourcecode eines Programms gehört in eine Main-Methode!“. Mit Pattern und UML-Diagrammen bewaffnet, den halben Schreibwarenladen hinter sich herziehend, muss der Java-Entwickler heute die Begrifflichkeiten von „separation of concerns“ bis „dependency injection“, von „extreme programming“ und „refactoring“ bis „service-oriented architecture“ mit der Muttermilch eingeimpft bekommen haben, um im täglichen Kampf mit den nur 42 Java-Schlüsselwörtern siegreich zu sein. Der gemeine OO-Entwickler wird zum Kostolany des JRE-Imperiums, um besser als sein Kollege Übersicht über Soll und Haben des Source-Chaos zu behalten. Interessanterweise stößt aber die reine Lehre des „Separation of Concerns“, also die Trennung von Konzepten, Aufgaben und Aspekten in unterschiedliche Teile des Sourcecodes, schnell an ihre Grenzen. Separation of Concerns heißt von der Ursprungsidee her, Klassen Verantwortungen für bestimmte Teilbereiche der Software zu geben, ihnen eine klare, wohl definierte Aufgabe zu verpassen. Dabei werden in einzelnen Klassen oder Klassenverbünden (also Packages oder sogar Jar-Files) Modelle und Aufgaben identifiziert, die fachlich bzw. technisch zusammengehören.
10
Einleitung
Java eignet sich an vielen Stellen gar nicht so recht dazu, Aspekte in Klassen vernünftig aufzuteilen. Oft steht man vor der Entscheidung, ob eine Funktionalität nun mehr in die eine oder mehr in die andere Komponente gehört und welche dann welche andere aufrufen darf, um nicht zirkuläre Beziehungen zwischen beiden aufzubauen. Schnell ist man dann verleitet, aus reiner Verzweiflung und weil es dem Stil des Programms scheinbar nicht schlechter bekommen kann, Codestellen n-fach zu kopieren und über den Source zu verteilen. Dies ist der Beginn von Redundanz. Selbst wer Martin Fowlers Refactoring-Handbuch auswendig gelernt hat, wird merken, dass Redundanz im Sourcecode ein unvermeidliches Problem zu bleiben scheint. So gebe auch ich zuweilen aus Architektensicht gerne die Parole aus, dass die Erstellung von Klassen, Methoden und Assoziationen unter diesen JavaElementen nicht so sehr der Vermeidung von Redundanz als vielmehr der Verbesserung, Vereinfachung und Klarheit der Strukturen dient. Notfalls nehme ich dazu auch manchmal Redundanz und Copy&Paste – wenn auch oft mit Missfallen – in Kauf. Sollte man nicht aber trotzdem versuchen, beiden sich scheinbar widersprechenden Zielen (Redundanzvermeidung und klaren Softwarestrukturen) gerecht zu werden? Frustration ist unvermeidlich, denn bestimmte Sourcecode-Zeilen, scheinen sich oft wie ein Geflecht durch Hunderte von Klassen zu ziehen. Immer wieder – zuweilen ein wenig unterschiedlich – sind sie der Horror für jeden Entwickler, der nachträglich daran etwas ändern muss, obwohl es sich oft nur um wenige Befehle handelt. Wie oft haben Sie die gleichen Algorithmen wieder und wieder programmiert? Wie oft haben Sie sich Generatoren gewünscht, die nicht nur einmalig Teile von Klassen erzeugen, sondern sich immer wieder mit Ihren eigenen Modifikationen am Sourcecode mischen lassen? Wie viele Stunden Zeit verschwenden Sie pro Tag auf redundante, wenig zielführende Kodierung? Das OO-Denken so zu erweitern, dass man beim Design der Anwendung auch AOP berücksichtigt, ist nicht allein der Schritt, ein Produkt durch ein anderes zu ersetzen. Es ist nicht allein die Frage, ob Sie gestern Java 1.3 programmierten und heute Java 1.4, es ist nicht die Frage, ob man besser NetBeans oder Eclipse als IDE benutzen sollte. Es ist eine Frage der Effektivität
AOP
11
Einleitung
und nicht allein der Effizienz, nicht der Frage, ob man die Dinge „richtig tut“, sondern ob man die „richtigen Dinge tut“. Zudem hängt die Wahl zwischen OO und OO mit AOP davon ab, bis zu welchem Grad Sie Aspekte in Ihrer Software zulassen möchten. Die Grenze verschwimmt schnell und gleichzeitig wird Ihnen eine weite Variabilität geboten – angefangen von „ich brauche nur mal kurzfristig eine kleine Lösung“ bis hin zu „Aspekte sind ein wichtiger Grundpfeiler meiner Architektur“. Im letzten Fall hat AOP das OO-Vorgehen fast vollkommen überholt oder zumindest ergänzt. AOP zu benutzen, kann Ihren bisherigen Programmierstil, Ihre bisherigen Design- und Architekturlösungen, Ihre Teamstrukturen und Ihre Vorgehensmodelle ändern. 쐌 쐌 쐌 쐌 쐌 쐌
AOP ändert den Ausdruck Ihrer Programme. AOP verringert die Menge an Redundanzen. AOP erhöht die Entwicklungsgeschwindigkeit. AOP erhöht das Abstraktionsniveau in der Kodierung. AOP verringert die Menge an notwendigem Sourcecode. AOP verringert die Menge an entstehenden Fehlern durch extreme Reduktion von Redundanz.
Da sind die Logging-Ausgabe, die aufgrund eines neuen Frameworks jetzt anders lauten muss, der Datenbankzugriff, der den Lookup auf den Datasource jetzt anders zu verwalten hat, die Security-Prüfung, in der neben User und Passwort jetzt noch drei weitere Parameter einzubauen sind, oder die fest-kodierte Performancemessung, die, einmal in den Sourcecode eingebracht, nur mühsam versetzt oder entfernt werden kann. Das sind alles Alltäglichkeiten, die einem über den Weg laufen, die wir geblendet von Kodierungsstilen und Designbüchern, von Tutorials und Einführungsseminaren in den letzten Jahren und Jahrzehnten in Java, aber auch in anderen Programmiersprachen, einfach hingenommen haben. Wie viel Zeit verbringen Sie tatsächlich mit effektivem Arbeiten am Source und wie viel Zeit vergeht, weil Software nicht 1:1 so programmiert werden kann, wie es im natürlichen Ausdruck möglich wäre? Wie oft suchen Sie nach Nullpointern im laufenden Programm und wie schnell finden Sie diese? Wie intelligent verhält sich Software heute eigentlich und wie aussage-
12
Einleitung
kräftig sind Fehlermeldungen? Wie lange braucht es, eine Architektur neu zu strukturieren, und wie oft entwickeln wir Programme, die so weit degenerieren, dass sie nach einigen Jahren komplett durch andere ersetzt werden müssen, und wie viel können wir dann eigentlich wiederverwenden? In den letzten Jahren gab es nicht wenige Produkte und Leitmotive, die immer wieder auf diese Frage eine andere Antwort hatten. So stellt sich auch heraus, dass am Markt eine ganze Reihe von Vertretern zu finden sind, die mal das eine, mal das andere Paradigma beschwören. An dieser Stelle gilt es, selbst zu entscheiden, ob AOP die für Sie beste Lösung darstellt oder zumindest eine hilfreiche Ergänzung ist. In vielen Projekten und Entwicklungsteams dürften diese Querschläger, diese zeitraubenden Suchen nach Problemen und Fehlerquellen zwischen 50 und 90% der Zeit aufwiegen. Schulnotentechnisch müssten sich einige Entwickler schon Fehlverhalten diagnostizieren, wenn es nicht zum guten Ruf gehören würde, so zu arbeiten und zu entwickeln, wie es viele Applikationsentwickler und -Designer heute einfach tun. Eigentlich haben wir es bei AOP mit einer ganz einfachen Weisheit zu tun: „Je weniger Zeilen Sourcecode notwendig sind, um eine Tatsache prägnant auszudrücken, umso weniger Fehler werden entstehen.“ Auf der Webseite der AOSD von 2002 findet sich der bemerkenswerte Satz: „Die Forscher im Bereich des AOSD werden primär von dem fundamentalen Ziel bewegt, das Prinzip des 'Separation of Concerns' zu verbessern.“ Das nun folgende Buch versucht, genau diese Vision umzusetzen, Ihnen als Leser aufzuzeigen, wo und wie und in welchem Fall aspektorientiertes Vorgehen die Qualität und Wartbarkeit des entstehenden Sourcecodes verbessern helfen könnte. Ebenso wie der Sprung von prozeduralen zu objektorientierten Ansätzen ist und bleibt AOP zunächst einmal ein geistiger Sprung, den es zu machen gilt. Was danach bleibt, ist die Anwendung dieses erlernten Prinzips auf Ihre tägliche Arbeitswelt. Ich werde im Folgenden nicht versuchen, Ihnen ein anderes Bild der Programmierwelt in Java zu offenbaren oder Ihnen zu zeigen, dass objektorientiertes Vorgehen oder OO-Entwicklung an sich falsch oder gar überflüssig ist. Basis dieses Buchs ist das Bemühen um die Beantwortung der Frage, wie Sie Ihre Konzentration bei der Entwicklung weg von Technologien und
AOP
13
Einleitung
Frameworks (also den statisch verknüpften Anteilen der Software) zurück in die Bahnen der abzubildenden Softwarelogik lenken können. AOP wird von manchen Kritikern – auch diesem Thema muss man sich der Ehrlichkeit halber stellen – nicht als Meilenstein in der Frage des Entwicklungsfortschritts gesehen, sondern eher als der Beginn des totalen Chaos, einem anarchisch anmutenden Verhalten des Sourcecodes. Diese Ambiguitätstoleranz gilt es hinzunehmen. Das reine Wissen von AOP führt nicht automatisch zu einem perfekten Separation of Concerns, genauso wenig wie die reine Kenntnis von Java nicht automatisch zu guter Objektorientierung führt. Beides ist und bleibt die Kunst der Entscheidung zwischen sinnvoll und nicht sinnvoll, zwischen Gut und Böse, wenn Sie so wollen. Lassen Sie mich Sie mitnehmen auf die Reise vom prozeduralen Vorgehen bis zur aspektorientierten Entwicklung und entscheiden Sie, wenn Sie die Grundkonzepte verstanden haben, selbst, ob es sich lohnt, sich mit Produkten auseinander zu setzen, die Sie bei der Arbeit in diesem Sektor unterstützen. Nichts kann falscher sein, als das unreflektierte Übernehmen einer fremden Ideologie. Zeigen Sie gesunde Skepsis, aber lassen Sie auch den Ihnen innewohnenden Forscherdrang wirken, bei der Frage, ob mit AOP nicht auch Ihre Bauchschmerzen in Sachen Software an manchen Stellen verschwinden. Dabei wünsche ich Ihnen viel Spaß und viel Erfolg! Über Lob, Anregungen, aber auch Kritik und Verbesserungsvorschläge Ihrerseits würde ich mich sehr freuen. Lars Wunderlich, 10.09.2005
[email protected] www.lars-wunderlich.de
Was Sie als Leser mitbringen sollten ... Das Allerwichtigste, was ein Leser mitbringen sollte – eigentlich für jedes Buch – ist Neugierde. Neugierde, Neues zu erfahren, Neugierde, den eigenen Standpunkt zu hinterfragen, Neugierde, nicht alles zu glauben und hinzunehmen und mehr erfahren zu wollen.
14
Einleitung
Ganz ohne Vorwissen geht es bekanntermaßen allerdings auch nicht und in diesem Fall kümmert sich dieses Buch primär um Java-Belange. Einerseits finden sich aufgrund der Objektorientierung von Java für diese Sprache eine ganze Reihe von Lösungsalternativen, andererseits scheint eben auch deshalb in Java der Gedanke von AOP heute oft am einfachsten realisierbar. Grundsätzlich ist AOP aber keine java-spezifische Antwort auf eine ungelöste architektonische Frage. In anderen Sprachen werden Sie ebenfalls Lösungen für AOP finden und auch hier empfehle ich Ihnen „Neugierde“. Wenn wir über Java sprechen, gehe ich davon aus, dass Ihnen die Grundprinzipien der Sprache bekannt sind und Ihnen mit Java das ein oder andere Programm unter Zuhilfenahme des Java Runtime Environment, des Java Compilers und gegebenenfalls der ein oder anderen Entwicklungsumgebung (wie z.B. JBuilder, NetBeans oder Eclipse) schon geglückt ist. Sollten Sie bereits mit UML-Diagrammen und Begrifflichkeiten der Java Enterprise Edition vertraut sein, dürfte dies die Einarbeitung in bestimmte Problematiken erleichtern; dies ist allerdings keine unbedingte Voraussetzung. Innerhalb dieses Buchs werde ich auch auf einige Produkte eingehen. Ein recht populäres und daher auch hier nicht unerwähnt bleibendes Produkt ist AspectJ, das unter Eclipse-Regie entwickelt und unter der Eclipse-Webseite beworben wurde und sich besonders gut in eben diese kostenlose Entwicklungsumgebung einfügt. Insofern werden Sie sehen, dass die Beispiele – soweit notwendig – in Eclipse entwickelt und vorgestellt wurden. Es macht daher Sinn, als Leser eine Internetverbindung und einen entsprechenden Rechner zur Hand zu haben, um in den Beispielen das ein oder andere selbst herunterladen und ausprobieren zu können.
Gliederung des Buchs ... Einleitung: Diese haben Sie derzeit vor sich, sie schließt im Anschluss mit einem Kurzporträt von AOP. Woher stammt der Begriff, wie lange gibt es ihn schon und wer hat sich das alles ausgedacht? ;-) Kapitel 1: Im ersten Kapitel nähern wir uns den Grundbegriffen von AOP, wir lernen Pointcuts und Joinpoints ebenso kennen wie Problematiken der
AOP
15
Einleitung
Mehrdimensionalität von Sourcecode und asymmetrischer Entwicklungsweise. Warum entwickeln wir so, wie wir es tun? Was ist AOP und wohin entwickelt es sich, wozu könnte es werden? Kapitel 2: Das zweite Kapitel setzt die Kenntnisse über AspectJ und JBoss AOP auf festere Füße und beschreibt in einer kurzen Referenz die Sprachelemente beider Produkte und deren Einbettung in die Eclipse IDE als Plugins. Kapitel 3: Nach der Klärung der Grundbegriffe und der zur Verfügung stehenden Tools beginnt nun ein Ritt durch die unterschiedlichen Anwendungsgebiete. In AspectJ und JBoss AOP-Beispielen sehen wir uns Ausdrucksmöglichkeiten der Java-Sprache ebenso an wie Möglichkeiten und Unmöglichkeiten von AOP. Den Abschluss bilden ein Nachwort und eine kurze Literaturauflistung.
Wie alles anfing – AOP Da es sich bei AOP im Gegensatz zu zahlreichen Softwareapplikationen nicht um ein Produkt, sondern eher um eine Erkenntnis oder ein Forschungsergebnis handelt, ist eine klare Zuordnung schwierig. Auch heute fällt eine deutliche Trennung zwischen generativer Programmierung, Bytecode-Enhancing-Methoden, Metadatennutzung und dem Thema AOP immer wieder schwer. Bestimmte Ideen wie beispielsweise die Ausführung einzelner Sourcecodestücke unter bestimmten Bedingungen (so genannte „demons“) finden sich z.B. schon 1959 bei Oliver Selfridge. Erst gut sieben Jahre später entsteht allerdings basierend auf dieser Idee eine erste Implementierung im Projekt PILOT, beschrieben von Warren Teitelman. Der Begriff der „aspektorientierten Programmierung“ (AOP) ist allerdings wesentlich jünger und stammt aus der Mitte der 90er Jahre des letzten Jahrhunderts von der später entstehenden AspectJ-Gruppe bei Xerox PARC. „AOP“ wurde dabei durch Chris Maeda geprägt. Später (2001) spricht man nicht nur von AOP als reiner Entwicklung, sondern wie im OO-Kreise ebenfalls von aspektorientierter Entwicklung. Un-
16
Einleitung
terschiedlichste Strömungen kommen mit den Jahren unter dem Begriff AOP zusammen, darunter IBMs HyperSpaces, Composition Filters und Adaptive Programming. Heute im Jahre 2005 – also gut rund zehn Jahre, seit der Begriff AOP geprägt wurde – hat die um AspectJ versammelte Gruppe von Visionären mit dem inzwischen bei Eclipse beheimateten Projekt noch immer einen deutlichen Meilenstein in der Geschichte von AOP gesetzt. In den Universitäten scheint in Fragen von AOP zurzeit immer noch eine deutliche Tendenz in Richtung AspectJ, aber auch des ursprünglichen HyperSpaces-Projekts, vorhanden zu sein. Erste Realisierungen mit JBoss AOP machen deutlich, dass man hier einen dritten, sehr erfolgversprechenden Vertreter dieses Bereichs in Java antrifft. Tiefergehend werden wir die unterschiedlichen Produkte in den späteren Kapiteln noch einmal anschauen.
Danksagung Für die konstruktive und positive Zusammenarbeit möchte ich mich bei der Lektorin Christiane Auf und bei Alexander Neumann vom Verlag bedanken. Eberhard Wolff gebührt mein Dank, der bei der fachlichen Durchsicht immer wieder Verbesserungspotenziale aufzeigte und mich an vielen Stellen aber auch gleichzeitig in meinem Eindruck von den Möglichkeiten von AOP bestärkte. Besonderer Dank gilt aber vor allem meiner Frau Melanie und meinem Sohn Pascal, die immer wieder fast übermenschliches Verständnis aufbrachten, wenn Papa bis tief in die Nacht vorm PC saß und morgens kaum aus dem Bett zu kriegen war.
AOP
17
1
Von OOP nach AOP – Evolution der Programmierung
Nach einer Kurzeinführung in die Begriffe von AOP enthält das nachfolgende Kapitel einen Entwicklungsabriss der Programmiersprachen und zeigt auf, wie AOP mit OOP in Zusammenhang steht. Neben Anwendungsgebieten und AOP-Begriffen stellt es zusätzlich Entwicklungsmethoden vor, die den Einsatz von AOP begünstigen. Es zeigt den Raum der Möglichkeiten, aber auch der offenen Fragen in Bezug auf die Zukunft von AOP, OO und Programmierung an sich.
1.1 Die 1:1-Mapping-Vision Adrian Colyer, Projektleiter des AspectJ-Projekts bei Eclipse.org und inzwischen Wegbereiter im Springframework-Team, begann seinen Vortrag auf der JAX 2005 mit der Vorstellung der Vision eines „1:1-Mapping“. Gemeint ist damit die Idee, jede Ausprägung eines natürlichen und intuitiven gedanklichen Konzepts problemlos 1:1 in Software umsetzen zu können. Jeder Teil der Software sollte für sich separat designt, entwickelt, modifiziert oder gegebenenfalls auch entfernt werden, ohne dass sich dadurch umfangreiche Änderungsanforderungen für andere Aspekte der Zielsoftware ergeben. Diese Vision scheint gleichzeitig so einfach wie genial. Allein die Umsetzung wirft jedoch eine Menge Fragen auf. Wie soll ich die unterschiedlichen Aspekte von Konzepten miteinander verheiraten? Wie soll es gehen: Gleichzeitig jede Perspektive auf die zu erstellende Software so zu konstruieren, dass sie „radikal einfach“ ist und auf der anderen Seite mit anderen Teilen der Software korrespondieren und im Einklang funktionieren kann? Es ist die Forderung nach einer möglichst simplen Produktion von Software, die Zeit spart und die Nerven schont. Eigentlich müsste dies eine Selbstverständlichkeit in moderner Softwareentwicklung sein. Die Antwort auf die Frage, wie dies möglich sein soll, kann und will aspektorientierte Herangehensweise durch Trennung dieser Aspekte liefern.
AOP
19
1 – Von OOP nach AOP – Evolution der Programmierung
Leider wird Software heute noch an vielen Stellen nach dem 1:n-Prinzip entwickelt. Selbst wenn es gelingt, ein Konstrukt wie „Kunde“, „Kreditkarte“ oder „Bankverbindung“ in genau eine Klasse oder eine Gruppe von Klassen zusammenzuführen, so werden wir auch andere Aspekte vorfinden. Dazu gehören Konfiguration, Logging oder Transaktionsmanagement. Sie tauchen in Methoden oder zumindest Randbereichen der Klassen immer und immer wieder auf. Sie werden sich im Verhältnis zur 1:1-Abbildung der natürlich anmutenden Konzepte „Kunde“ oder „Kreditkarte“ in ihrer Anwendung delokalisieren und beginnen, sich über den kompletten Sourcecode zu verteilen. Dies widerspricht der ursprünglichen „1:1-Vision“. Zunächst müssen wir die Gründe verstehen, warum sich Design von Software so verhält, bevor wir uns daran machen, die Chancen von aspektorientierter Herangehensweise einschätzen zu können und die Vision Stück für Stück Realität werden zu lassen. Im nachfolgenden Kapitel lernen wir gemeinsam Schritt für Schritt, was AOP ist, wie weit der Horizont an Möglichkeiten für AOP ist und welche Auswirkungen dies wiederum auf unseren Umgang mit Softwareentwicklung haben kann.
1.2 Einführung in AOP-Begriffe „[...] important steps of aspect-oriented software development: modularize, relate and compose.“ - [Clarke05] Beginnen wir zunächst damit, einige Grundbegriffe von Aspekten zu erörtern und bestmöglich zu beschreiben, bevor wir in die Varianten und die Komplexität des Themas eintauchen. Ein Aspekt ist – im aspektorientierten Softwareentwicklungssinne (AOSD) – ein Bereich der Software, der sich mit einer ganz bestimmten Fragestellung (concern) auseinander setzt und dafür eine Lösung bzw. Implementierung anbietet. Die Fragestellung rankt sich allgemein um 쐌 die Erreichung eines bestimmten Ziels, 쐌 die Erstellung oder das Angebot eines bestimmten Konzepts 쐌 oder die Realisierung eines bestimmten Themengebiets.
20
Einführung in AOP-Begriffe
Innerhalb typischer Softwareapplikationen finden sich normalerweise eine ganze Reihe solcher meist voneinander unabhängiger concerns, die durch technische AOP-Aspekte realisiert werden können. Ein Aspekt kann – rein technisch gesehen – die Wirkungsweise von JavaStatements zwischen Kommunikationspartnern innerhalb eines Java-Programms ändern oder bei Bedarf Aussehen und Struktur eines Objekts wandeln. Aspekte werden daher seltener zur Beschreibung von Geschäftslogik als vielmehr zur Implementierung technischer Aspekte der Software genutzt. Typische technisch orientierte Aspektbereiche könnten sein: 쐌 Implementierung von Sicherheitsprüfungen und Transaktionskontexten 쐌 Einstreuung fremder Geschäftslogik außerhalb des Standardsystemverhaltens 쐌 Tracing, Logging und Performanceprüfungen und Abfragen 쐌 qualitätssichernde Maßnahmen (z.B. Exceptionprüfung, Datentyp- und Inhaltsprüfungen/Validierungen, JUnit-Tests und -Tracings) 쐌 Umleitung von Methodenaufrufen an fremde Remoteinstanzen oder vorübergehende Deaktivierung von Systemkomponenten 쐌 Ereignisgesteuerte Programmierung durch Überwachung von Instanzen und Klassen ohne deren Wissen 쐌 Initialisierungs- und Erzeugungsmechanismen (lazy initialization/loading/creation beispielsweise) 쐌 Caching und Pooling-Mechanismen 쐌 Überwachung der Einhaltung von Design-/Architekturrichtlinien und Verträge zwischen Aufrufer und Aufgerufenem (Design by Contract) 쐌 Umsetzung von Designpatterns Derartige Belange einer Software, die keine direkte Auswirkung auf das Verhalten der Geschäftslogik, sehr wohl aber auf die Wirkungsweise der Gesamtapplikation haben, nennt man auch cross cutting concerns. Sie verlaufen „quer“ zur restlichen Software und werden an vielen Stellen immer wieder in den Code „eingestreut“ bzw. sind ohne AOP über den Sourcecode „wild“ verteilt (scattered and tangled).
AOP
21
1 – Von OOP nach AOP – Evolution der Programmierung
Immer wieder finden sich z.B. bei Verwendung von Jakarta Commons Logging folgende Zeilen in Hunderten von Klassen: public class ABC { private static final Log LOGGER = LogFactory.getLog(ABC.class); [...] if (LOG.isDebugEnabled()) { LOG.debug(...); } [...] if (LOG.isDebugEnabled()) { LOG.debug(...); } }
Typische nicht AOP-zentrierte Softwareentwicklung beschäftigt sich zu weiten Teilen mit der Implementierung und gegebenenfalls Reorganisation technischer Aspekte und erzeugt dabei eine große Menge Redundanz im Code. Die Notwendigkeit solcher cross cutting concerns führt somit zu Designs, die in ein starr architektonisches „Korsett“ eingebettet sind. Typische Beispiele waren bisher J2EE und EJB-Konzepte der ersten und zweiten Generation (vor Einführung von Metadaten und EJB 3.0 in Verbindung mit Java 5). Bei J2EE findet sich eine Mischung aus noch recht klassischer OO-Denkweise1 und schon aspektorientierter Ansätze2. Hier sehen Sie ein Enterprise Java Bean-Beispiel, angelehnt an das Sun J2EE 1.4-Tutorial. Eingefärbt sind die bestehenden J2EE-Abhängigkeiten, die weitestgehend durch Annotations in EJB 3.0 ersetzt werden konnten.
1. Vererbungshierarchien und zu implementierende Interfaces 2. Deploymentdeskriptoren, die Bezug nehmen auf das Verhalten einzelner Klassen
22
Einführung in AOP-Begriffe import javax.ejb.EJBObject; import java.rmi.RemoteException; import java.math.*; public interface CurrencyConverter extends EJBObject { public BigDecimal dollarToEuro(BigDecimal dollars) throws RemoteException; } import java.rmi.RemoteException; import javax.ejb.CreateException; import javax.ejb.EJBHome; public interface CurrencyConverterHome extends EJBHome { CurrencyConverter create() throws RemoteException, CreateException; } import import import import
java.rmi.RemoteException; javax.ejb.SessionBean; javax.ejb.SessionContext; java.math.*;
public class CurrencyConverterBean implements SessionBean { BigDecimal euroRate = new BigDecimal("1.23456"); public BigDecimal dollarToEuro(BigDecimal dollars) { BigDecimal result = euroRate.multiply(yenRate); return result.setScale(2,BigDecimal.ROUND_UP); } public public public public public public
CurrencyConverterBean() {} void ejbCreate() {} void ejbRemove() {} void ejbActivate() {} void ejbPassivate() {} void setSessionContext(SessionContext sc) {}
}
AOP
23
1 – Von OOP nach AOP – Evolution der Programmierung
Besonders die klassische Lösung von Vererbungshierarchien und Interfaces tat und tut sich schwer damit, evolutionär weiterentwickelt zu werden. Der Aspekt selbst ist wie jeder andere Teil der Software auch bei Anwendung auf die Java-Sprache ebenfalls in Java implementiert. Dies ist kein Zwang, aber typisch. Die einzige Schwierigkeit besteht darin, die Stellen zu identifizieren, an denen der Aspekt in den Sourcecode integriert werden soll. Die Aspekte müssen also quer über den Code eingestreut werden. Die eigentliche Ausführung übernimmt dann der Aspektcompiler/Weaver. Er ist in der Lage, den Aspekt an den definierten Stellen einzuweben und somit die wiederkehrenden Sourceabschnitte über die Software zu verteilen. Vom Prinzip her ähnelt der Weaver einer großen Nähmaschine, deren Faden sich später durch den Softwarestoff zieht. Die Menge der Stellen, an denen der Aspekt wirken soll, wird als „pointcut“ bezeichnet1. Es handelt sich dabei um die definierten „Einstichpunkte“, bei deren Identifikation der Aspektcompiler den Aspekt in den Sourcecode „weben“ könnte. Eine Stelle, an der ein solches Einweben tatsächlich vorgenommen wurde, bezeichnet man als Verbindungspunkt (Joinpoint). Das exakte Verhalten des mit AOP-Mitteln modifizierten Quellcodes ergibt sich aus der Definition des Joinpoint und dem festgelegten Kontext, in dem er angesprochen werden soll (advice). Ein wenig abstrakter und weniger Sourcecode-behaftet kann man sich einen Joinpoint auch als eine Art möglichen Event innerhalb vorstellen. Der Advice ist ein Verweis auf den Teil des Sourcecodes, der ausgeführt werden soll, wenn ein spezifischer Event auftritt. Versuchen wir diese Menge an Informationen noch mal mit einem etwas realeren Beispiel zu untermauern. Nehmen wir an, Sie seien Parkwächter auf einem großen Parkplatz und teilen sich diesen Job mit ein paar Kollegen.
1. Da es leider trotz diverser Bemühung bis heute keine allgemeine AOP-Standardisierung gibt, stützen sich die im Folgenden verwendeten Begriffe auf die AspectJ-Sprachspezifikation, die für den Umgang mit AOP in Java eine sehr große Verbreitung genießt.
24
Einführung in AOP-Begriffe
Zunächst einmal muss grob festgelegt werden, wer was überhaupt überwacht. Alle arbeitsteiligen Spielarten sind denkbar. Angefangen von: „ich überwache den linken, mein Kollege den rechten Teil“ über „ich überwache alle roten und blauen Autos, mein Kollege den Rest“ bis hin zu „ich überwache Autos beim Einfahren auf den Parkplatz und kontrolliere Papiere und mein Kollege die Ausfahrt“. Das heißt, Sie verständigen sich erst einmal auf eine Menge möglicher Events, die da kommen könnten. Diese Menge bezeichnet man bei AOP als „Pointcut“. Nachdem diese grobe Einteilung vorgenommen ist, müssen Sie nun überlegen, was Sie bei Eintritt eines solchen Ereignisses tun wollen. Wenn also z.B. jemand sich einem Auto nähert, beobachten Sie, was er macht. Wenn er ins Auto eingestiegen ist, könnten Sie ihn über eine Überwachungskamera aufzeichnen oder in einer Verbrecherkartei suchen. Das heißt, neben dem reinen Ereignis (Person steigt in Auto) müssen Sie entscheiden, ob Sie sich für den Moment vor dem Ereignis, für den Moment danach oder für den ganzen Prozess interessieren. Dies ist der „Advice“ zum Ereignis. Die Art des Advice hängt zum einen von der Art der definierten Beobachtungsmenge (Pointcut) ab, zum anderen von der Art der Aktionen, die Sie planen. Wenn Herr Müller sich nun tatsächlich abends um 22.30 Uhr einem blauen Opel Corsa nähert und schließlich einsteigt, ist dies ein so genannter „Joinpoint“. In diesem Moment greift Ihre Überwachung tatsächlich. Ein Joinpoint ist als das tatsächliche Auftreten eines zu überwachenden Pointcut unter einer Advice-Bedingung zu verstehen. Der Joinpoint muss dabei Eigenschaften aufweisen (Herr Müller, 22.30 Uhr, blauer Opel Corsa). Für unser Autobeispiel könnte man beispielsweise annehmen, der „Aufrufer“ sei Herr Müller, das Ereignis sei „Öffnen des Fahrzeugs und Einsteigen“ und der Parameter sei „ein Autoschlüssel, ein Metalldraht oder ein Vorschlaghammer“. Wenn Sie sich für den kompletten Prozess interessieren, ist es ein around()-Advice. Normalerweise dürften Sie sich als Parkplatzwächter wohl nicht aussuchen können, wann Sie was überwachen wollen und was im konkreten Fall zu tun
AOP
25
1 – Von OOP nach AOP – Evolution der Programmierung
ist. Es wird vermutlich entsprechende Vorschriften geben, nach denen Sie sich richten müssen. Eine solche niedergeschriebene Vorschrift, die genau Auskunft gibt über die möglichen Pointcuts, den zeitlichen Advice und welche Joinpoint-Aktionen für welchen konkreten Fall gelten sollen, nennt man auch ein „Binding“. Im übertragenen Sinne könnten wir folgenden Metacode schreiben: pointcut autoUeberwachung(): public Auto.openAndGettingIn(); around() : autoUeberwachung() { beobachten(joinPoint.getSourceObject()); }
Nach so viel Theorie ein etwas praktischeres Beispiel. Für jene, die mit AOP-Produkten bisher keinerlei Berührung hatten, ist ein Hello-WorldSourcecode sicherlich am verständlichsten. Im Folgenden sehen Sie eine typische, sehr einfache Java-Klasse, die „Hello“ auf die Console ausgeben wird und sich dann beendet. Hello-World-Beispiel-Klasse: package test; public class HelloWorld { private void sayHello() { System.out.println("Hello"); } public static void main(String[] args) { HelloWorld hw = new HelloWorld(); hw.sayHello(); } }
Bei einem Blick auf den Sourcecode könnte man nun aber stutzig werden, schließlich ist das versprochene Literal „World“ nicht im Sourcecode zu sehen. Dafür müssen wir uns mit einem weiteren Begriff vertraut machen, dem des Interceptors.
26
Einführung in AOP-Begriffe
Der Interceptor (zu Deutsch „Abfänger“) soll in unserem Beispiel den Aufruf der sayHello()-Methode abfangen. Mit dem Begriff Interceptor belegen zahlreiche Produkte eine Klasse, die Aspekte definiert. Die Begriffe Aspekt und Interceptor können wir hier für den Moment als synonym betrachten. Die Aspekt-Implementierung entscheidet, was anstelle des Aufrufs der sayHello()-Methode geschehen soll und ob sie überhaupt zum Zuge kommt. Schauen wir uns dazu eine Beispielimplementierung mit dem JBoss AOPProdukt an. Zur Implementierung realisiert die Beispiel-HelloWorldInterceptor-Klasse das Interceptor-Interface. Wird die sayHello()-Methode aufgerufen, wird diese Methoden-„Execution“ an die invoke-Methode weitervermittelt. Im einfachsten Fall wird die Ausführung der Standardmethode sayHello() aufgerufen. Danach wird „World“ auf die Console geschrieben. Streng genommen erfolgt die Durchführung des Aspektteils (das Drucken des Worts „World“) also nach der Ausführung der Basismethode. In diesem Fall ist der Pointcut also definiert als der Aufruf der sayHello()-Methode. Der Advice wird durch die invoke()-Methode beschrieben und der Joinpoint ist der tatsächliche Aufruf der sayHello()-Methode auf dem HelloWorld-Objekt in der main-Methode. Es handelt sich im Beispiel um einen so genannten „Around“-Advice. Er umfasst die ursprüngliche Methodenausführung und ist in der Lage, sowohl davor als auch danach weitere Befehle abzusetzen. JBoss AOP-Interceptor: package test; import org.jboss.aop.advice.Interceptor; import org.jboss.aop.joinpoint.Invocation; public class HelloWorldInterceptor implements Interceptor { public String getName() return "Hello World Interceptor"; }
AOP
27
1 – Von OOP nach AOP – Evolution der Programmierung
public Object invoke(Invocation arg0) throws Throwable { Object o = arg0.invokeNext(); System.out.println("World"); return o; } }
Es stellt sich nun die Frage, wie man die Hello-World-Klasse und ihren Interceptor zueinander in Beziehung setzen kann. Im konkreten Fall geschieht dies durch eine Verbindungsvorschrift (binding), in der steht, dass bei Ausführung der Methode sayHello() aus der Klasse test.HelloWorld der Interceptor angesprungen werden soll. Die Formulierung der betroffenen Elementmenge des Aspekts übernimmt der Pointcut: execution(private void test.HelloWorld->sayHello())
Bei JBoss AOP legt man für seine Bindevorschriften eine eigene jbossaop.xml-Datei mit folgendem Inhalt an:
IDE-Produkte wie beispielsweise die JBoss AOP-IDE-Erweiterung für Eclipse helfen schnell bei der Definition solcher Pointcuts. Wir sehen hier eine klare Trennung zwischen Weaving-Regeln (der Definition des Pointcut) und der Implementierung des späteren Aspekts. Das
28
Einführung in AOP-Begriffe
Binding findet deskriptiv in der XML-Datei statt, die Implementierung des Aspekts folgt wie gewohnt in einer Java-Klasse. Um sich nicht zu sehr an eine JBoss AOP-Implementierung zu klammern, sei alternativ einmal exakt dieselbe Realisierung mittels AspectJ, einem weiteren AOP-Produkt, kurz skizziert. Sie werden darin begriffliche Ähnlichkeiten in der Syntax, aber auch leichte Abweichungen vorfinden. AspectJ-Aspekt: package test; public aspect HelloWorldAspect { pointcut sayPC() : execution(private void test.HelloWorld.sayHello()); after(): sayPC() { System.out.println("World"); } }
Der AspectJ-Aspekt mutet ein bisschen wie eine Mischung aus Java-Klasse und aspektorientierter Erweiterung an. Der Advice (after()) ist hier weitaus deutlicher erkennbar als bei JBoss, dafür vermischen sich Bindevorschrift und Aspektimplementierung. Wobei man in diesem Fall fairerweise sagen muss, dass man durch eine Delegation des „World“-Aufrufs mit AspectJ das gleiche Verhalten hätte realisieren können. Der AspectJ-Aspekt befindet sich in einer Datei mit der Endung „.aj“. Sie erlaubt es, Java-Schlüsselwörter und Syntax mit AspectJ-Elementen zu mischen. Eine separate build.ajproperties-Datei entscheidet bei AspectJ, welche Klassen innerhalb des Klassenpfads überhaupt potenziell zum Einweben eines Aspekts herangezogen werden können. Beide Beispielapplikationen erzeugen die erwarteten HelloWorld-Zeilen zur Ausführung. Wie die Applikationen allerdings dafür gestartet bzw. die
AOP
29
1 – Von OOP nach AOP – Evolution der Programmierung
Java-Dateien kompiliert werden müssen, ist von Produkt zu Produkt verschieden.
1.3 Architektur- und Designvorgehen durch „Prismabildung“ Der Einsatz von Aspekten setzt zunächst die Kenntnis und Akzeptanz der Tatsache voraus, dass es redundante Bereiche in Software gibt, die sich mit AOP lösen lassen. Das Wissen alleine reicht allerdings noch nicht aus. Danach gilt es, sich mit dem Design solcher Aspekte auseinander zu setzen. Dann ist es möglich, diese auch bereits in der Analyse- und Designphase eines Projekts bzw. Softwarevorhabens zu berücksichtigen und in die spätere Gesamtarchitektur einzubringen. Adrian Colyer, seines Zeichens derzeitiger AspectJ Project Leader, empfiehlt, Aspekte zunächst zur Prüfung und Durchsetzung von Designrichtlinien innerhalb der Realisierungsphase zu verwenden. AspectJ erlaubt hierbei die Definition von Ausnahmen, die bereits zur Compiletime1 bestimmte Designmuster prüfen. Diese Checks dienen praktisch nicht dem fertigen Endprodukt und gehen daher auch nicht in eine lauffähige Version ein. Der Aspekt agiert in diesem Fall rückendeckend für Architekten und Designer zur Stärkung und Qualitätssicherung der Softwarestruktur. Diese Herangehensweise bedeutet für die Implementierung das geringste Entwicklungsrisiko, da der Aspekt nicht im endgültigen Bytecode enthalten ist. Colyer empfiehlt, erst im zweiten Schritt, nachdem man sich mit Tool und dem eigentlichen Denkmodell auseinander gesetzt hat, jene cross cutting concerns zu implementieren, die für die Integration bestimmter Utilityfunktionen notwendig sind (wie Logging oder Security). Dies ist der Versuch, die Idee des Separation of Concerns auf die Trennung zwischen Geschäftslogiken und technischen „Services“ zu konzentrieren und mittels AOP greifbarer zu gestalten. Hier fließen die durch Aspekte 1. Compiletime bedeutet in diesem Fall, dass Pre- oder Postcompileläufe Source- oder Bytecode in einem Weavertool nochmals modifizieren.
30
Architektur- und Designvorgehen durch „Prismabildung“
wirksam gewordenen Softwareteile erstmalig auch in die Erstellung eines produktiven Systems ein. Somit wird die Aspektorientierung ab diesem Zeitpunkt essentieller Bestandteil des Designs, aber auch der Entwicklungs-, Build- und Deploymentumgebungen. Je nachdem, wie und wann der Aspekt zur Ausführung kommt bzw. in den Source übergeht, verschmelzen die Hauptachse der Geschäftslogik und die Aspekte (siehe Abbildung 1.1).
Abb. 1.1: Prisma-Entwicklungseffekt bei aspektorientiertem Vorgehen
Ein sehr ähnlicher Ansatz findet sich u.a. in Ausführungen von Ramnivas Laddad wieder. Bei der Aufnahme der Anforderungen an die spätere Software müssen bereits frühzeitig die Aspekte identifiziert werden, die sich jenseits der Hauptlogik befinden. In einer Art Prismazergliederung werden sie bewertet und gegebenenfalls mit bereits vorhandenen oder implementierten Aspekten ausgefüllt. Jene Aspekte, die als neu bewertet realisiert werden müssen, können mehr oder minder unabhängig von anderen programmiert und im Weaving-Prozess wieder zusammengeführt werden. Man benutzt in diesem Kontext auch die Begrifflichkeiten der „Decomposition“, also der Zergliederung des Problemumfelds, und der „Recomposition“, dem erneuten Zusammenführen (siehe Abbildung 1.1). Die Modularisierung der primären Geschäftslogik anstelle der Auftrennung von Java-Klassen auf n Packages ist der letzte Schritt in der Kette der Weiterentwicklung der Fertigkeiten im AOP-Bereich. Entwickler und Architek-
AOP
31
1 – Von OOP nach AOP – Evolution der Programmierung
ten versetzen sich in die Lage, faktisch nahezu unabhängig voneinander Geschäftsmodule zu implementieren und erst zur Ausführung wieder zusammenzusetzen. Diese Ausführung lässt den Schluss zu, dass es sich hierbei aus Designsicht um das komplexeste und am schwierigsten zu beherrschende Anwendungsthema von AOP handelt, wobei die Grenzen von AOP dabei oftmals weniger durch Produktfertigkeiten als vielmehr durch die Vorstellungskraft aller Beteiligten gesetzt sein dürften. Es geht um die Frage, wie man anhand von Geschäftsvorfällen Software unterteilen kann und ob ein homogenes Gesamtkunstwerk am Ende entstehen kann. Wir werden einen möglichen Analyse/Design-Weg, der eine solche Modellierung unterstützen soll, in Kapitel 3 als „Theme Approach“ wiederfinden. Tritt man noch einmal von dem sicherlich sehr theoretischen AOP-Entwicklungsansatz zurück und überträgt man ihn auf die Projektalltagssituation, lässt sich festhalten, dass in einem entsprechenden Entwicklerteam wie beim Einsatz fast jeder Technologie das notwendige Know-how für AOP oftmals erst reifen muss. Bei der Umsetzung des Projektvorhabens gibt es dabei also wichtige Erfolgskriterien zu beachten: 쐌 Wie verbreitet ist das AOP-Know-how im Entwicklerumfeld? (Dies gilt einerseits bezogen auf den aspektorientierten Ansatz an sich, andererseits in Bezug auf die eingesetzte Softwarelösung.) 쐌 In welcher Entwicklungsphase eines Projekts befinden wir uns? Konnten die nötigen Aspekte bereits identifiziert werden? Ist rein von Umgebung und Plattform her eine Integration von Aspekten überhaupt (noch) möglich? Wie sehen politische Gremien den Einsatz eines solches Verfahrens, besonders wenn sie bisher mit AOP keinerlei Berührung hatten oder intensiv in die Belange und Entscheidungen Einfluss nehmen können? 쐌 Wie gut lassen sich die Produkte in den Entwicklungsalltag integrieren? Wie verändert das Weaving die Ergebnisse von Debug-Aktivitäten und Runtime-Verhalten? Wie steht es mit der Nachvollziehbarkeit von durch Aspekte initiiertem Softwareverhalten? Wie integrieren sich die AOPTools in andere Build-Produkte wie Ant oder Maven, um eine reibungs-
32
Evolution der Softwareentwicklungsparadigmen
lose und sichere Softwareerstellung zu gewährleisten? Wie passt das Thema der Aspektorientierung z.B. in die Verwendung eines Obfuscator1? Bisher haben wir in den Ausführungen primär über AOP in seiner Wirkung auf Arbeitsteams und Vorgehensmodelle gesprochen. Woher kommt nun aber die Notwendigkeit, sich von gewohnten Entwicklungskonzepten abzuwenden? Wieso ist AOP der logisch gesehen nächste Schritt in der Weiterentwicklung von Programmiersprachen?
1.4 Evolution der Softwareentwicklungsparadigmen An dieser Stelle nehme ich Sie mit auf einen kurzen Ausflug von prozeduraler Programmierung in Richtung AOP. Viele Leser dürften erste Erfahrungen im Programmierbereich noch mit Sprachen wie C, COBOL, Pascal oder Basic gemacht haben. Recht typisch für solche Sprachen ist, dass z.B. Speicherbereiche noch explizit allokiert und vor allem auch freigegeben werden müssen. Es sind Sprachen, die außer primitiven Datentypen oft nur riesige Arrays zu bieten haben, um Sachverhalte darzustellen. Auch ich gehörte zu den Assemblerprogrammierern, die in vielen Bereichen auf dem Rückzug waren und sich nach neueren und effektiveren Sprachen umsahen.
1.4.1
Vom prozeduralen Erwachen ...
Die große Herausforderung, die sich dem Softwareentwickler in prozeduralen Sprachen stellt, besteht in der Fertigkeit, den Abstraktionssprung zwischen einer fachlich gewünschten Realität und einer technischen Realisierung zu schaffen. Typisches Beispiel könnte die Verwaltung von Telefonbucheinträgen sein. Namen, Vornamen, Adressen, Telefonnummern ... diese allgemein gültigen 1. Ein Produkt, das die Methoden- und Klassenbenennungen zwecks Verhinderung einer Dekompilierung ohne Zutun des Entwicklers modifiziert und so ein nachträgliches AspectWeaving verhindern könnte.
AOP
33
1 – Von OOP nach AOP – Evolution der Programmierung
Konzepte verschmelzen nicht selten zu endlosen Arraylisten von Strings und Integern. Sie müssen mit speziellen Sortier- und Suchalgorithmen ausgestattet werden, um dem Benutzer einen einfachen und schnellen Zugriff auf die Daten zu ermöglichen. In solch prozeduralen Programmen verbinden sich sehr schnell die fachlichen Objekte (Personen, Adressen) und die technischen Belange der Syntax der Sprache. Nur mit Mühe gelingt es, in einigen dieser Sprachen überhaupt die Daten und typische auf ihnen operierende Aktionen1 in Form von Bibliotheken voneinander zu trennen. Dies verfolgt das Ziel, nicht in jeder Applikation jeden benötigten Algorithmus erneut zu implementieren. Übrig bleibt oft ein endlos scheinender Codestrang, dessen Struktur und Ablaufsequenz mal mehr, mal weniger klar strukturierbar erscheint. Gegen GOTOs und extensive Pointerarithmetik ist aber oft wenig Kraut gewachsen. Immerhin warten einige Sprachen schon mit Funktionen oder Methoden auf. Dennoch reicht die Bandbreite der syntaktischen Fähigkeiten von globalen Variablen bis hin zu feingranularen Sichtbarkeiten und Pass-by-reference/Pass-by-value-Datenübergaben. Mit dem Schlüsselwort struct in C oder Satzstrukturen in COBOL finden sich in den Sprachen erste Ansätze, auch mit Datencontainern komplexere Gruppierungen vorzunehmen. Es gibt die ersten wiederverwendbaren Elemente – oder, um auf das Beispiel zurückzukommen, aus Character-Arrays und Integern werden Adress- und Telefonbucheinträge. Rückblickend betrachtet tat und tut sich die rein prozedurale Lehre schwer: 쐌 Mit Fragen der Wiederverwendung von Sourcecode jenseits eines Cut&Paste&Adapt-Ansatzes, solange die Sprache keine Möglichkeit zur Verwendung und Auslagerung von Prozeduren oder Modulen in Bibliotheken ermöglicht 쐌 Mit der Aufgabe der Aggregation von Daten zu komplexeren Gebilden jenseits der inhärenten primitiven Datentypen 쐌 Mit der Trennung von Anwendungscode, der Daten manipuliert und Sourceteilen, die Eigenschaften und Verhalten von Daten repräsentieren
1. wie Sortierung, Filterung, Suche oder Speicherung
34
Evolution der Softwareentwicklungsparadigmen
쐌 Mit der Klarheit des beim Programmieren entstehenden Sourcecodes 쐌 Und letztendlich mit einer Abstraktionslücke. Sie entsteht zwischen fachlich gewünschtem Verhalten einer Software, den dafür notwendigen fachlichen Konzepten und den syntaktischen Möglichkeiten. Wie gut diese Lücke zu meistern ist, ergibt sich aus der semantischen Ausdrucksfähigkeit der Sprache.
1.4.2
... zum objektorientierten Dilemma
Wirft man einen Blick in die Historie, so finden sich schnell prozedurale Sprachderivate wie C++ oder Visual Basic, die dem Entwickler zunehmend ein höheres Sprachniveau anbieten. Plötzlich finden sich Begrifflichkeiten, die man eher im Biologiewörterbuch eines zwölfjährigen Schülers erwarten würde, denn in einem Softwarereferenzwerk. Jetzt gibt es Klassen, Objekte und Instanzen, die in Form von Vererbung Eigenschaften und Verhalten von Elternteilen verliehen bekommen. Es finden sich Sichtbarkeiten, Kapselung und Freundschaftsbeziehungen und Assoziationen. Der manchmal sehr sanfte Übergang von harten Binärtypen1 zu weicheren Alternativen wie „Variant“ oder „Object“, die man explizit vergibt oder implizit vom Compiler vergeben bekommt, machen den Übergang zu einer neuen Sprache für den Entwickler erträglich. Daten und Algorithmen werden erstmals nicht horizontal (hier die Daten, da die Operationen), sondern vertikal eingeteilt (Daten und Operationen von A gegenüber Daten und Operationen von B). Dies scheint zunächst der viel natürlichere Ansatz zu sein. Objekte gehören einer bestimmten Klasse an, verfügen über von außen sichtbare und unsichtbare Eigenschaften und Verhaltensmuster. Was solche objektorientierten Sprachen wie Java allerdings nicht beantworten, ist die Frage, wie mit der reinen Erkenntnis, dass ein Objekt Eigenschaften und Verhalten aufweist und in eine Vererbungshierarchie eingebettet ist, umgegangen werden soll. Der Besitz eines Metallstabs und eines Gummiballs macht einen schließlich auch noch nicht zum Golfprofi.
1. z.B. byte, short, int, float, char
AOP
35
1 – Von OOP nach AOP – Evolution der Programmierung
Dafür bedarf es jetzt wiederum Algorithmen. Sie werden gerne als Pattern bezeichnet, da sie statt einzelner Statementsequenzen globalere Klassenbeziehungen beschreiben helfen. Sie erläutern dem Laien, wie mit der „neu gewonnenen Freiheit“ umzugehen ist. Ein wenig fühlt man sich an Immanuel Kant erinnert – Aufklärung, wie man programmiert, wird offenbart – vielleicht in Büchern wie diesem. ;-) Für einige Entwickler endet die vertiefte Kenntnis der Programmiersprache allerdings doch in einer gewissen Frustration. Sie ist mit der Enttäuschung verbunden, dass es sich auch bei Java nur um eine Insel auf einer „scheinbar flachen Scheibe“ handelt. Zugegebenermaßen ist die Darstellung ein wenig überspitzt und ketzerisch. Dennoch bemüht sich die Softwareentwicklung seit Jahren in allen Bereichen darum, eine natürlichere, einfachere und somit leichter verständliche Sprache zwischen Anwender und Maschine zu finden. Beispiele sind die lachende Büroklammer der Textverarbeitung, die nervtötend bei jedem Return-Klick eine fast besserwisserische Ader an den Tag legt, die OCR-Software, die Briefe in gebrochenem Deutsch vorlesen kann, der Hilfeassistent, der durch die Analyse des Festplattencrashes führt, selbst wenn er nicht helfen kann, oder der „Papierkorb“ auf dem „Desktop“, der eine weit plastischere Aussagekraft als ein „copy to nul device“ oder ein „delete aus der FAT-Tabelle“ hat. Der Versuch, realitätsnahe Software zu schreiben Beim Schreiben von Java-Applikationen bzw. von Software überhaupt haben viele von uns sich in den letzten Jahren angewöhnt, einen hohen Realitätsbezug zu suchen. Er erscheint naheliegend bei Aufteilung und Design eines Businessmodells mit Beziehung zu natürlich vorhandenen Elementen. Die Syntax der Java-Sprache selbst (do, while, if, case, …) assoziiert eine natürlichsprachige Semantik und die JRE-Klassennamen (Date, Point, Number, List, Currency, …) verstärken den Eindruck, dass man den JavaCode bei guter Namenswahl leicht lesen und verstehen kann. Die Abstraktionslücke zwischen Umwelt und Code scheint sich zu schließen.
36
Evolution der Softwareentwicklungsparadigmen
Entitäten wie Autos, Häuser, Züge oder Tapetenrollen sind schnell modelliert, da man sie aus der Realität heraus kennt. Schwieriger wird es schon mit Quasientitäten wie abstrakten Listen und Gruppen, Helpern, Controllern, Listenern, Komparatoren. Haben Sie schon mal mit einem Listener gesprochen, nachdem Ihnen der Controller Befehle gab? Hier fallen der Vergleich und die Vorstellungskraft schon schwerer und es gibt erste Diskussionen, wo Attribute denn nun hingehören. Gibt es hier eigentlich noch sachliche Logikzwänge oder nur gefühlsorientiertes Handeln beim Design? Kann man das irgendwo nachlesen oder studieren oder hat da schon mal jemand was darüber geschrieben, braucht man einen Architektenrat dafür und hilft der? Ganz schwer ist es mit Bereichen wie Transaktionsmanagement, Security, Designrichtlinien, Performance und qualitätssichernden Maßnahmen – Themen, die sich nicht mit Geschäftsmodellen im eigentlichen Sinne beschäftigen, sondern als „non-functional requirements“ daher kommen. Irgendwie betreffen diese „übergreifenden“ Konzepte der nichtfunktionalen Anforderung alles: die Controller, die Helper, das Modell, die Persistenz usw. Die Aufteilung wird damit kein Stück einfacher, im Gegenteil. Der wesentliche Punkt ist: Software und die ihr zugrunde liegenden Sprachen drängen förmlich in Richtung Vereinfachung und Verminderung der Abstraktionslücke zwischen natürlich anmutenden Konzepten und programmatischer Syntax. Vereinfachung heißt aber auch Verringerung von Komplexität oder anders formuliert: Verbesserung der Les-, Strukturier- und Wiederverwendbarkeit. Als Entwickler in einer Programmiersprache wie Java erkennen manche sehr schnell, andere erst nach Jahren, dass dem formulierten Anspruch aber auch in dieser Sprache Grenzen gesetzt sind. Ein typisches Beispiel ist die wesentlich einfacher als in C++ wirkende Vererbungshierarchie. Jede Klasse darf nur von genau einer Superklasse erben. Dies ist allerdings keine Vererbungslehre im biologischen Sinne mehr, sondern eher ein Delegationsprinzip. Ungeliebte Aufgaben werden einfach weitergegeben oder mit Polymorphismus hin und her gereicht. Insofern ist es fraglich, ob man bestimmte Sachverhalte und Beziehungen der Realität be-
AOP
37
1 – Von OOP nach AOP – Evolution der Programmierung
zogen auf Vererbungshierarchie in Java nicht zwangsläufig anders ausdrücken muss – jedenfalls anders, als das menschliche Gefühl spontan assoziieren würde. Das heißt, man ist gezwungen, in Java für bestimmte Beziehungen und Verhaltensweisen abweichende Ausdrucksformen zu suchen, da Java hier begrenzt ist. All das hört sich plötzlich doch recht realitätsfern und so gar nicht mehr nach einer modernen Programmiersprache an. Mit der zuvor beschriebenen „1:1-Mapping-Vision“ hat dies scheinbar gar nichts mehr zu tun. Ein Extrembeispiel: Ein Maulesel, also eine Kreuzung aus Pferd und Esel, muss in Java einer Superklasse zugeordnet werden. Es bleibt keine andere Wahl, als das Tier entweder der Gattung des Pferds oder der des Esels zuzuordnen. Das ähnelt der Definitionsfrage, was ein Multifunktionsgerät ist. Ist es ein Drucker, ein Scanner und ein Kopierer? Ist es in der heimischen Küche ein Häcksler, ein Mixer und ein Rührbesen? Ist ein Handy mit Kamera nun ein Fotoapparat oder ein Telefon? Man sieht, dass Objektorientierung in einigen Bereich Schwächen zeigt. Dies geschieht dort, wo man versucht, reale Dinge unverändert in Sourcecode zu gießen. Nicht nur dieses Ziel wird verfehlt, das Ergebnis der Entwicklung wird zudem noch mit hässlichen Redundanzen gekrönt.
1.5 AOP als Rettungsanker!? Wer an Java herumkritisiert, muss auch skizzieren, wie die Lösung oder Alternative aussieht, die es zu betrachten gilt. Einen Teil der eigentlich attestierten Mächtigkeit eines objektorientierten Konzepts auch tatsächlich zu erlangen, setzt in Java den Einsatz von AOP voraus. Es gibt Kritiker, die AOP vorwerfen, Java als Sprache selbst mehr zu verkomplizieren, denn zu vereinfachen. Eine ähnliche Anamnese kann man aber auch dem Sprung von COBOL zu Java zusprechen. Seit die Variablen über Hunderte von Klassen verteilt sind, ist die Suche nach Deklarationen und Werten deutlich schwerer geworden als zu Zeiten, als noch alle Variablen an einer Stelle des Programms definiert wurden. Oder nicht?
38
Fortschritte von AOP gegenüber OOP
In diesem und den folgenden Kapiteln werde ich auf die Konzepte von aspektorientierter Programmierung speziell im Bereich Java eingehen. Gleichzeitig gilt es dabei im Hinterkopf zu behalten, wo die Entwicklung der Programmiersprachen hergekommen ist und wie das Ziel aussieht. Auf einer ungefähren Geraden zwischen monolithisch rechnernahen Sourcecodestrukturen hin zu einfacher, verständlicher und realitätsnaher Softwareentwicklung bewegen wir uns dabei vorwärts. Die Frage, ob AOP der logische Folgeschritt nach OOP auf diesem Weg ist, beantwortet sich daraus, wie es angewendet wird. Dabei ist zu klären, warum und ob eine Erreichung des Primärziels1 damit gewährleistet ist oder zumindest in Teilbereichen machbar erscheint.
1.6 Fortschritte von AOP gegenüber OOP Wer frisch aus einer Java-Einsteiger-Schulung oder einem Seminar zu OOAD (Objektorientierte Analyse und Design) kommt, kann den Himmel leicht voller Objekte, Referenzen und Vererbung hängen sehen. Trivial eigentlich, dass man mittels Polymorphismus eine Abweichung vom Standardverhalten eines Elternteils implementiert und so extreme „Wiederverwendung“ betreiben kann. Nach den ersten 100.000 Zeilen Sourcecode in Java, die man geschrieben hat, dürfte sich aber auch Ernüchterung breitmachen. Die tolle Wiederverwendung und Vermeidung von Redundanzen trägt nur bedingt Früchte. Klar gibt es Klassen und Methoden, die man erneut benutzen kann. Dennoch finden sich auch innerhalb der Instanzmethoden sehr viele wiederkehrende Algorithmen und Muster. Das scheint einem objektorientierten Ansatz zuwider zu laufen. Schließlich bedeutet Objektorientierung doch, Verantwortungen aufzuteilen und Klassen und Methoden zuzuweisen. Programme, in denen bestimmte Teile immer wieder vorkommen, müssen also in der Entwicklung fehlgeleitet sein oder einem unglücklichen Design unterliegen. Die Konsequenz ist für Einsteiger in Java klar: „Sehr wahrscheinlich habe ich das Prinzip der Objektorientierung noch nicht gut genug verstanden, um
1. einer intuitiveren und einfacheren Softwareentwicklung
AOP
39
1 – Von OOP nach AOP – Evolution der Programmierung
Redundanz zu vermeiden, bzw. das von mir gewählte Design ist unzureichend.“ Auf der Suche nach Verbesserungspotenzial trifft man immer wieder auf den Begriff des Refactoring, der Restrukturierung des Sourcecodes nach der Erstellung. Hier wird im Nachhinein Stück für Stück das Design der Anwendung sinnvoll modifiziert, um einen neuen Zielzustand zu erreichen, den man aufgrund sich ändernder Rahmenbedingungen zu Beginn nicht abschätzen konnte. Eigentlich hätte dieser gedachte Zielzustand die Ausgangsbasis sein sollen, aber dies war scheinbar nicht vorhersehbar. So sind manche Architektur- und Designentscheidungen im Nachhinein unbrauchbar oder unpassend bzw. sie müssen gar revidiert werden. Zerstreuung des Sourcecodes (Scattering) Als Beispiel sei hier eine ganz normale Business-Logik-Klasse gezeigt und die Implementierung einer Geschäftsmethode inhaltlich angerissen. public class BusinessLogicA extends AbstractBusinessClass { private static Log LOGGER = LogFactory.getLog(BusinessLogicA.class); public Object executeA(Param a1) { // Berechtigungen prüfen if (!user.isInRole(“Admin“) { throw SecurityException(); } // Vorbedingungen prüfen if (a1 == null) { throw IllegalArgumentException(); } if (LOGGER.isDebugEnabled()) { LOGGER.debug(“START executeA“); } // Ausführung der BusinessLogic executeB(a1); Object result = X.calculateResult(a1);
40
Fortschritte von AOP gegenüber OOP if (LOGGER.isDebugEnabled()) { LOGGER.debug(“FINISH executeA“); } return result; } private void executeB(Param b1) { // Berechtigungen prüfen if (!user.isInRole(“Admin“) { throw SecurityException(); } // Vorbedingungen prüfen if (b1 == null) { throw IllegalArgumentException(); } // ggf. Synchronisationsmechanismus aktivieren if (LOGGER.isDebugEnabled()) { LOGGER.debug(“START executeB“); } // Ausführung der BusinessLogic doIt(b1); if (LOGGER.isDebugEnabled()) { LOGGER.debug(“FINISH executeB“); } } }
Die markierten Zeilen erweisen sich zweifelsohne als redundant. Schlimmer noch, durch den Import des Beispielloggers (LogFactory.getLog()) schafft die Klasse eine Abhängigkeit von einer applikationsfremden Komponente (hier Apache Commons Logging). Das steht dann für die Ausimplementierung der Berechtigungsprüfung und des Synchronisationsmechanismus vielleicht auch zu befürchten. Nur mit großer Mühe ist nun eine Third-Party-Abhängigkeit (z.B. der Logger) durch eine andere zu ersetzen. Hunderte von Klassen müssen modifiziert werden. Dabei helfen auch die Prinzipien von Kapselung oder Inversion of Control/Dependency Injection (Einbringen von Abhängigkeiten in
AOP
41
1 – Von OOP nach AOP – Evolution der Programmierung
den Source von außen) nur wenig weiter. Mindestens eine Zeile Sourcecode bleibt an dieser Stelle für jeden Aspekt übrig. Eine mögliche Lösung besteht darin, alle Aufrufe auf den Instanzen der Klasse BusinessLogicA zu wrappen und so z.B. vor und nach dem Aufruf der Methode executeA() die Aspekte anzuwenden. Für eine solche Lösung bietet der Dynamic Proxy, dem wir im dritten Kapitel begegnen, eine probate Antwort. Leider hilft er bei der Umsetzung im Falle von executeB() überhaupt nicht, da diese Methode privat ist. Mit Hilfe von AOP ließe sich die Klasse auf folgende Größe reduzieren: public class BusinessLogicA extends AbstractBusinessClass { public Object executeA(Param a1) { executeB(a1); return X.calculateResult(a1); } private void executeB(Param b1) { doIt(b1); } }
Könnte man die Implementierung auf dieses Minimum reduzieren, enthielte sie nur noch die reine Geschäftslogik und würde von den technischen Aspekten entbunden. Dies führt nicht nur zu einer deutlichen Verkleinerung des Source und dient somit der Übersichtlichkeit des Designs, unnötige Dependencies zu fremden Komponenten verschwinden ebenfalls und der Source kompiliert auch ohne sie. Fast perfekt. Letztendlich führt dies zu der Frage: „Warum sind Redundanzen und zunehmende Komplexität in wachsenden Java-Systemen ohne AOP scheinbar unvermeidbar?“ Stellen Sie sich Java wie ein großes Blumenbeet vor. Im Hinterkopf hat der Landschaftsarchitekt eine erste grobe Planung, wie der endgültige Garten aussehen soll. Während die Wege, Zäune und ersten Bäume gepflanzt und gesetzt werden, nimmt die Flexibilität immer weiter ab. Jeder falsch verlegte
42
Fortschritte von AOP gegenüber OOP
Pfad, jedes falsch gesetzte Stück Rasen muss mit großer Mühe wieder entfernt werden. Nachdem der Garten eine erste Struktur angenommen hat, beginnen sich die Wurzeln und das Unkraut unkontrolliert auszubreiten. Ganze Netzwerke von Beziehungen und Geflechten entstehen. Die größten und am tiefsten verwurzelten Bäume und Pflanzen sind nur wieder zu verpflanzen oder austauschbar, indem man andere um sie herumliegende in Mitleidenschaft zieht (siehe Abbildung 1.2).
Abb. 1.2: Natürliche Verwurzelungsstrukturen zentraler Bäume
Java bewegt sich ähnlich wie ein solches Pflanzenbeet auf einer primär zweidimensionalen Ebene. Alternativ können Sie sich objektorientierte Programmierung auch wie eine Tischdecke vorstellen, auf der Kreise und Verbindungslinien aufgemalt sind. Zentrale Komponenten weisen unzählige Beziehungen zu Aufrufern auf. Ändert sich nun die Schnittstelle zu einer zentralen Komponente wie z.B. Securityprüfung oder Loggingmechanismus, müssen Dutzende oder sogar Hunderte von Stellen modifiziert werden.
AOP
43
1 – Von OOP nach AOP – Evolution der Programmierung
Im übertragenen Sinne könnte man für das Bild des beforsteten Waldes oder eines entsprechenden Grundstücks behaupten, dass immense Schäden verursacht und Schneisen ins Holz getrieben würden (siehe Abbildung 1.3). Ähnlich verhält es sich dann auch mit der Softwarestruktur.
Abb. 1.3: Weitflächige Auswirkungen durch die Änderung zentraler Komponenten
Dieser Vorgang, durch Umbenennung oder Änderung von Schnittstellen zahlreiche Referenzstellen zusätzlich modifizieren zu müssen, nennt sich Refactoring. Refactoring ändert die Struktur unseres Beziehungsgeflechts und trägt normalerweise zu dem Ziel bei, die Menge der Beziehungen zwischen Klassen/Komponenten zu reduzieren, um Änderungsrisiken zu minimieren. Moderne IDEs unterstützen den Entwickler bei diesen immer wiederkehrenden „Aufräumaktionen“. In der Literatur findet sich diesbezüglich auch der Begriff der „zunehmenden Degenerierung“. Software verfällt aufgrund der zunehmenden Größe und damit einhergehenden Komplexität einem wachsenden Chaos. Softwareentwicklung und das Aufsetzen von Architektur- und Designrichtlinien ist der Versuch, gleichzeitig dem Zielsystem eine einheitliche Struktur zu verleihen und diese andererseits bestmöglichst zu bewahren.
44
Fortschritte von AOP gegenüber OOP
Abb. 1.4: Blatt mit angedeuteten Löchern
Versuchen Sie nochmals, sich Java und objektorientiertes Vorgehen als eine zweidimensionale Tischdecke vorzustellen. Nehmen wir weiterhin an, dass Sie die Aufgabe hätten, in gleichmäßigen Abständen ein immer gleiches Loch hineinzuschneiden (siehe Abbildung 1.4). Java-Programmierung ähnelt nun dem Griff zur Schere. An zig Stellen muss ein Kreis geschnitten werden, der nur mit Mühe wieder aus dem Programm zu entfernen ist, geschweige denn z.B. durch eine andere Figur wie ein Dreieck ersetzt werden kann. Bezogen auf das Tischdeckenbeispiel könnten wir die Tischdecke nun mehrfach falten und mit einer Schere einschneiden. In mehreren Schichten würden nun dieselben Figuren wie in einer Art Kopie entstehen. Man spricht in diesem Fall auch vom „Einweben“ (weaving). Massenerzeugung von immer gleichartigen Sourceelementen bezeichnet man als generative Programmierung, die automatische Erzeugung einer Menge von Codezeilen nach einem bestimmten Muster. Der Clou bei AOP ist nun, dass Sie das ausgeschnittene Muster in der Tischdecke hinzufügen und entfernen können, ohne dass Sie den Originalsource (die Tischdecke) dafür ändern müssen. Das mutet an wie ein Zaubertrick, solange wir es aus Sicht einer Tischdecke betrachten. Anders sieht es aus, wenn wir dem Problem Dreidimensionalität verleihen und eine Schere und Klebstoff benutzen. Dann ist es weit weniger magisch. So ist es auch mit AOP. Das Problem von Java besteht im übertragenen Sinne darin, ein n-dimensionales Problemumfeld auf eine zweidimensionale Oberfläche zu projizieren. Das ähnelt einem Foto oder einer Videoaufnahme einer dreidimensionalen Umgebung. Sie können in einer solch zweidimensionalen Darstellung nur mit großer Mühe die notwendige Klarheit und Trennung von Geschäfts-
AOP
45
1 – Von OOP nach AOP – Evolution der Programmierung
logik und Aspekten wie Security, Logging und Transaktionsmanagement erreichen. AOP verhält sich plastisch gesprochen nun wie die dritte (bis n-te) Dimension auf der Suche nach der Lösung für eben dieses zweidimensionale Problem. Sie haben eine Menge Sourcecode, in dem bestimmte alltägliche Aspekte fehlen. Um nicht den Sourcecode an Hunderten von Stellen anpassen zu müssen (was die zweidimensionale Lösung wäre), wählen Sie einen Generator1. Er modifiziert den Bytecode vor der Ausführung so, dass der Aspekt zum Einsatz kommt, ohne dass der ursprüngliche Source geändert werden muss. Wir kommen im späteren Kapitel noch einmal auf die konkrete Implementierung in Java zurück. Um ein wenig mehr zurück ins Java-Umfeld zu wechseln: Ein Aspekt ändert – wie wir bereits sahen – das Verhalten eines Methodenaufrufs. Stellen Sie sich vor, ein Objekt A ruft auf einem Objekt B eine Methode auf, die nach Ausführung mit der Rückgabe eines Returnwerts antwortet (siehe Abbildung 1.5).
Abb. 1.5: Aspekt-Intercepting
1. den Aspekt-Compiler oder genauer gesagt einen Weaver
46
AOP-Techniken von morgen?!
Außer über Tricks ist es normalerweise nicht möglich, den Aufruf von B umzuleiten oder die Parameter oder gar den Rückgabewert zu ändern. Ein Aspekt setzt sich nun wie eine Art Proxy zwischen beide Kommunikationsrichtungen. Er leitet im einfachsten Falle die Information von A nach B und umgekehrt nur durch. Im Gegensatz zum Proxy1 ist der Aspekt aber keine typische Java-Klasse. Er muss nicht explizit instanziiert werden und kann gegebenenfalls auch dem Sourcecode von A oder B dynamisch hinzugefügt werden. Abhängig vom eingesetzten Tool kann der Aspekt dabei als Java-Klasse entwickelt werden oder aber auch syntaktisch vollkommen anders aussehen. Ein Aspekt modifiziert normalerweise die Kommunikation zwischen zwei Endknoten eines Methodenaufrufs. Seltener verändert er A oder B selbst, z.B. indem er ihre Struktur oder sogar ihre Vererbungshierarchie nachhaltig modifiziert. A und B wissen nichts von der Existenz des Aspekts. Somit sind sie oft unvorbereitet auf die Modifikationen, die der Aspekt durchführt.
1.7 AOP-Techniken von morgen?! Dieser Kapitelabschnitt beschäftigt sich primär mit fortgeschrittenen Techniken, wie man AOP teilweise schon heute und teilweise erst morgen anwenden kann und vielleicht wird. AOP ist nüchtern betrachtet eine in der Entwicklung befindliche Praktik. Sie finden sowohl Elemente, die man leicht und einfach anwenden kann, als auch solche, bei denen es erste Vorstellungen und Ideen, aber wenig reale Tools oder Vorgehensweisen gibt. Wenn Sie neu in AOP sind, empfiehlt es sich, in das Kapitel 2 zu wechseln und mit den dort beschriebenen Tools ein paar Tests durchzuführen. Alternativ können Sie der Theorie von AOP in Kapitel 3 etwas mehr praxisorientiertes Futter hinzufügen. Wenn Sie der Meinung sind, bis hierhin alles bereits gekannt oder mühelos verstanden zu haben, folgen Sie mir ... zum AOP von morgen.
1. Das Prinzip des Proxy werden wir uns als Einarbeitung ins Thema später noch einmal ansehen.
AOP
47
1 – Von OOP nach AOP – Evolution der Programmierung
1.7.1
Separation of Concerns versus Separation of Aspects
Der Begriff der Separation of Concerns (SOC), also der Trennung der Zuständigkeiten, und das Konzept der Modularisierung gehen miteinander Hand in Hand. Diese Concerns sind die gleichzeitige Hauptmotivation für die Organisation und Strukturierung von Applikationen. Concerns bestimmen, wie verwaltbare Module und Elemente entstehen und nach welchen Strukturierungsmerkmalen. Der Begriff „Objektorientierung“ bedeutet implizit selbst „Separation of Concerns“, denn Objekte sind Kombinationen von Eigenschaften und Verhalten. Sie werden nach Zuständigkeiten aufgeteilt und gegenüber anderen abgeschottet. Das Problem der Objektorientierung ist dabei genau diese Regel: Zerteilung von Zuständigkeiten nach einem bestimmten Prinzip. Es ist in der Objektorientierung trotz Interfaces und Vererbung nur sehr schwer möglich, ein Objekt so zu gestalten, dass es aus mehreren Richtungen unterschiedlich aussieht. Eine Klasse mit ihren öffentlichen Methoden und Feldern erlaubt es aber immer nur, eine Sicht auf einen Datencontainer zu skizzieren. Ein bisschen erinnert die Zuständigkeitsthematik an die Organisation eines Unternehmens. Einzelne Abteilungen werden aufgegliedert nach fachlichen Themenschwerpunkten. Sie interagieren miteinander und lösen unabhängig voneinander ihre jeweiligen Aufgaben. Im Bereich der Betriebswirtschaftslehre und der Unternehmensformen findet sich u.a. die typisch hierarchische Baumstruktur. Sie beginnt von oben mit der Unternehmensleitung, darunter folgen die Abteilungsleiter und unterschiedliche Bereiche. Ganz am Ende stehen dann die Sachbearbeiter. Jeder von ihnen ist disziplinarisch und fachlich in der Hierarchie einsortiert. So ähnlich fühlt sich Java in der Benutzung auch an. Die Applikation zerfällt in Teilbereiche unterschiedlicher Zuständigkeiten. Sie werden gemeinhin gruppiert in JARs (Java Archive), die untereinander gekoppelt und gegebenenfalls ineinander geschachtelt sind. Darunter findet sich die typische, allerdings auch definitionsbedürftige Form von Packages. Sie separieren
48
AOP-Techniken von morgen?!
anhand von Namenskonventionen Aufgabenbereiche (eben Separation of Concerns). Darin wiederum finden wir als Quasi-Miniaufgabenverteilung Klassen und Methoden, die gegenseitiges Information Hiding betreiben. Der eine darf vom anderen nichts wissen – nicht, wie er was macht, und vor allem oft auch nicht, mit welchen Mitteln und Werkzeugen. Je kleiner die Schnittstellen zwischen diesen Informations- und Verarbeitungsträgern, umso einfacher ist die scheinbare Ersetzbarkeit der einzelnen Einheit. Wirft man einen Blick zurück in die Realität und auf die Organigramme von Unternehmen, fallen allerdings auch Matrixorganisationen auf. Kernkompetenzen lassen sich zuweilen nur schwer einem bestimmten thematischen Cluster zuordnen. Eingesetztes Personal ist vielseitig und muss zur Verrichtung seiner Arbeit zusätzlich bestimmte Fähigkeiten erlernen bzw. mitbringen (Lesen, Schreiben, Rechnen, PC bedienen, Formulare ausfüllen können, Essensmarken der Kantine verwenden etc.), weiterentwickeln und verbessern können. Viele Unternehmen befinden sich aufgrund wandelnder Umweltbedingungen und der Änderung von geschäftspolitischen Feldern in einem ständigen Wandel der Organisations- und Zuständigkeitsstrukturen, häufig im Interesse der Agilität von Projekttätigkeiten. Das heißt, Separation of Concerns, das einem Unternehmen eine Primärstruktur1 gibt, bildet nur eine von vielen gleichzeitig koexistierenden Aufgabenstrukturen und Fertigkeiten der Angestellten (Teilelemente) ab. Sprechen wir von einer Primärstruktur in Sachen Software, so macht ein Blick auf heutige Softwarearchitekturen deutlich, dass besonders dort, wo eine Software von einer Onetier- zu einer Multitier-Umgebung übergeht2, das Prinzip SOC zu verwischen beginnt. In einer Fat-Client-Anwendung sind Packages oft fachlich geprägt (Kalkulation, Einkauf, Verkauf, ...), manchmal auch mehr architektonisch geclustert (Präsentation, Persistenz, Domain-Modell). In einer N-Tier-Umgebung tritt der Aspekt der Remotefähigkeit, das heißt das Überschreiten von Tiergrenzen und damit zusammenhängende Aspekte von Transformation und 1. disziplinarische Zuordnung von Arbeitgeber, Angestellter, Chef, Untergebener 2. Das heißt, die Einzelplatzapplikation wird zur geclusterten Netzwerkanwendung.
AOP
49
1 – Von OOP nach AOP – Evolution der Programmierung
Datenübertragung im Netzwerk, immer stärker zu Tage. Eine klare Konzeption oder Linie in der Art der Modularisierung, die allgemeingültig zu nennen wäre, scheint sich nicht mehr oder nur noch schwer zu finden. Module verteilen sich über mehrere Tiers. Businesslogik und Technologieaspekte beginnen miteinander zu verschmelzen. Die Tragik dieser Mischung von vielen Aspekten in einer unzureichenden Abbildung bezeichneten Peri Tarr, H. Ossher, W. Harrison und S. M. Sutton, Jr. in ihrem Vortrag zum Thema „N degrees of separation: Multi-dimensional separation of concerns“ als „the tyranny [or hegemony] of dominant decomposition“. Es geht effektiv um das Problem, dass bei einer Dekomposition, d.h. einer Zerlegung eines Gesamtsystems in Teile (eben separation of concerns), nur wenige und sogar meistens nur eine Dimension bzw. ein Prinzip die Oberhand behält. Andere Sichtweisen oder Zerteilungsprinzipien werden vernachlässigt. Anders formuliert: Die heutige objektorientierte Vorgehensweise erlaubt zwar eine Unterstützung des Separation of Concerns, aber eben nur in einer Dimension. Wenn Sie eines der Softwareentwicklungsvorgehen in einem Projekt verfolgen sollten, das eine strenge Aufteilung zwischen objektorientierter Analyse (OOA) und objektorientiertem Design (OOD) vorschreibt, so könnte der Begriff „Use Cases1“ bereits bekannt sein. In einem Use Case wird in relativ strukturierter Form das Verhalten der Anwendung zu einem bestimmten Zeitpunkt innerhalb eines definierten Workflows beschrieben. Use Cases finden sich in Diagrammform sowohl im Bereich der Unified Modelling Language (UML) zur Visualisierung von Anwendungszusammenhängen als auch als Standardartefakt der Analysephase im Rational Unified Process (RUP). Legt man nun viele dieser Use Cases nebeneinander, so werden sich die typischen cross cutting concerns in einer ganzen Reihe von ihnen wiederfinden (z.B. „Vorbedingung: Der Benutzer hat die Berechtigung zur Ausführung von ...“).
1. Anwendungsfälle
50
AOP-Techniken von morgen?!
Abb. 1.6: Artefakttypen über die OO-Phasen hinweg
In Literatur und Realität wird nun der Schritt von diesen Anwendungsfalldiagrammen zu Klassen-, Konzept- oder Komponentenmodellen als schwierigste Entscheidung für Architekten/Designer und Entwickler beschrieben. Dafür gibt es unterschiedlichste Methoden (z.B. CRC-Karten), die zumeist darauf abzielen, „Substantive“ und „Verben“ – also Sprachelemente innerhalb der Use Case-Dokumentationen – voneinander zu trennen und daraus Konzepte und Anwendungsaufgaben herauszufiltern. Dies entspricht dem OO-Paradigma der Aufteilung nach Eigenschaften und Verhalten von Objekten. Es passt somit bestmöglich zum zu erreichenden Zielzustand, z.B. einem Klassendiagramm in der UML. Da eine Reihe von UML-Notationen relativ nahtlos in Java-Source zu überführen sind (forward engineering) und wiederum Java-Source in Diagramme zurückgewandelt werden kann (reverse engineering), entspricht der Schritt vom Use Case zum Klassendiagramm der direkten Überführung von Anforderungen (business requirements) zum „fertigen“ Java-Modell. Bei dieser Umsetzung wird in seiner klassischen Form einem oder mehreren do-
AOP
51
1 – Von OOP nach AOP – Evolution der Programmierung
minanten Aufteilungskriterien der Vorrang eingeräumt. Dies ist ein Unterschied zum Theme/UML-Ansatz, den wir später noch im Bereich von aspektorientierter Analyse und Design (AOAD) kennen lernen werden. Sie werden in fertigen Softwareanwendungen Bereiche finden, die nach fachlichen Kriterien aufgegliedert sind. Andere Teile werden in Anpassung an technische Umgebungen konstruiert. Aber in bestimmten Bereichen werden auch beide Systematiken miteinander verschmelzen. Die Trennung der Überschneidungsbereiche stellt sich oft als die schwierigste Aufgabe heraus. Sie ist in diesem Fall auf einer Dimension1 nicht hinreichend lösbar. Dieses Problem stellt sich in der Designphase, beim Aufbau von Architekturen und sogar im kompletten Softwareentwicklungszyklus. Es führt zur Notwendigkeit von massivem Refactoring in bestimmten Zeitabschnitten, wenn die Aufteilung zu Designbeginn nicht gut genug durchdacht war. Es können eben nicht alle Details beim Schritt von Anforderungsanalyse zu Verantwortungstrennung vorausschauend berücksichtigt werden. Etwas ketzerisch könnte man die Behauptung aufstellen, dass viele Architekturen sich deshalb nicht über einen langen Zeitraum als tragfähig erweisen, weil die Anpassungen an sich ändernde Rahmenbedingungen und Umwelteinflüsse zu enormen Zeitaufwänden und Wartungsrisiken führen. Die reine Betreuung und Anpassung einer Softwarelogik an sich macht dabei nur 50% des Problems aus. It is, therefore, impossible to encapsulate and manipulate, for example, features in the object-oriented paradigm, or objects in rule-based systems. Thus, it is impossible to obtain the benefits of different decomposition dimensions throughout the software lifecycle. Developers of an artifact are forced to commit to one, dominant dimension early in the development of that artifact, and changing this decision can have catastrophic consequences for the existing artifact. - Peri Tarr, HyperJ Dokumentation
1. oder, wenn Sie das Beispiel mit der Tischdecke oder dem Foto bevorzugen, auf zwei Dimensionen, was faktisch hier keinen Unterschied macht
52
AOP-Techniken von morgen?!
1.7.2
Multidimensionales Separation of Concerns
Der Begriff des SOA (Separation of Aspects) findet sich in Zusammenhang mit aspektorientierter Entwicklung kaum, dabei könnte man statt Separation of Concerns mit AOP-Mitteln auch von einem Separation of Aspects sprechen. Die Concerns werden hier nicht unwesentlich von den Aspekten getragen. Mit dem Begriff SOA verbindet man heute zumeist allerdings eher „service-oriented architecture“. Was objektorientierte Softwareentwicklung aber aufgrund des SOC-Problems faktisch „braucht“, ist ein multidimensionaler Ansatz. In ihm werden Applikationsbereiche und Zuständigkeiten voneinander getrennt entwickelt und erst zum Ausführungszeitpunkt miteinander verschmolzen. Dies bedeutet eine Lösung für Fragen der 쐌 Kapselung von Zuständigkeiten, 쐌 Trennung von Zuständigkeiten, 쐌 unabhängigen Evolution von Zuständigkeiten ohne Beeinträchtigungen anderer Dimensionen der Software, 쐌 Überlappung und Interaktion von Zuständigkeiten 쐌 und der problemlosen Restrukturierung/Rekomposition des Systems. Bei IBM findet sich im Jahre 2000, als Java gerade eben zum Sprung von einer 1.1er-Version in Richtung 1.2 (Java 2) ansetzte, bereits ein sehr interessantes Produkt mit dem ans Weltall erinnernden Namen „Hyperspace/ HyperJ“. Damals nicht unter dem Namen „Aspekt“ ausgeprägt, aber dennoch nicht weniger plausibel, kristallisiert sich die Idee eines „Multi-dimensional separation of concerns“ (kurz MDSOC) heraus, das, so wörtlich, „die fundamentalen Grenzen des Software Engineerings aufheben soll1“. Damals entstand auch der Begriff „subject-oriented programming“ (SOP). Er meint die separate Entwicklung von Anwendungsteilen und deren spätere Verschmelzung. Unter dem Namen „symmetrische AOP“ (siehe Diskussion weiter hinten in diesem Kapitel) finden wir einen solchen Ansatz wieder. Symmetrisch meint in diesem Fall, dass alle Sourcecodedimensionen einan1. http://www.research.ibm.com/hyperspace/index.htm
AOP
53
1 – Von OOP nach AOP – Evolution der Programmierung
der gleichgestellt sind. Die typischen Tools wie AspectJ und JBoss AOP weisen heute aber vorwiegend Lösungen für einen asymmetrischen Ansatz auf.
1.7.3
Multidimensionale Interfaces
Sicherlich ist Ihnen das Prinzip des Java-Interface bekannt, denn es beschreibt eine Schnittstellenvereinbarung zwischen dem Aufrufer und einer oder gegebenenfalls mehreren Implementierungen. Aus AOP-Sicht betrachtet ist die Dimension, in der sich diese typischen Java-Interfaces bewegen, die primäre oder Basisdimension. Interfaces kapseln und verbergen die Implementierungen der Kernrichtung. Gemeinhin finden wir in der Basisdimension die reine Geschäftslogik und -modellwelt. Im AOP-theoretischen Sinne jedenfalls keine weiteren Aspekte der Software. Die Frage ist, wie geht man mit Vereinbarungen oder Schnittstellenbeschreibungen um, die sich nicht in der Basisdimension, sondern in einem der anderen orthogonal dazu verlaufenden Aspekte befinden. Sie können mit Interfaces nur bedingt umgehen. Hier bieten die mit Java 5 eingeführten Metadaten eine für die Sprache sehr einfache und zugleich revolutionäre Lösung. Über Java-Annotations lassen sich Einstiegspunkte für Aspekte definieren. Gleichzeitig kann hierüber auch bestimmt werden, welches Verhalten einzelne Aspekte während ihres Ablaufs annehmen sollen. Andersherum ergibt sich die Frage, für welchen Anwendungsfall Annotations überhaupt Sinn machen, wenn nicht für die Verwendung von Aspekten. Generatoren könnten über festgelegte Annotations z.B. noch weitere Sourcecodebestandteile generieren. Sie würden aber primär immer Bezug nehmen auf die Sourcecodeteile, denen sie zugeordnet sind. Damit sind Annotations methoden-, klassen- bzw. instanzkontextbezogene Metadaten, also sehr spezielle Bindinginformationen, die als primären Verwender Aspekte haben. Ein typisches Beispiel kann die Festlegung einer Annotation für SecurityPrüfungen gepaart mit zusätzlichen Informationen über die aufzubietende Benutzerrolle sein.
54
AOP-Techniken von morgen?!
Nur dann, wenn der Benutzer zur Laufzeit nach seiner Authentifizierung, d.h. der Ausweisung seiner Identität z.B. über eine Kombination aus Namen und Passwort (Schlüssel), einer bestimmten Rolle als zugehörig zugeordnet werden kann, darf er bestimmte Methoden aufrufen. Im dritten Kapitel werden wir eine ganze Reihe von Möglichkeiten kennen lernen, um mit Hilfe von Annotations Pointcuts zu definieren. Die sich daraus ergebende Mächtigkeit der Java-Sprache ist neben den Java Generics sicherlich eine der wichtigsten Erweiterungen in Richtung Java 5 und 6.
1.7.4
Anwendung von Vererbung im multidimensionalen Aspektuniversum
Aspekte müssen und dürfen wir nicht komplett voneinander unabhängig betrachten, denn letzten Endes sind sie alle gemeinsam Puzzleteile des fertigen Bilds. Da überrascht es nicht, dass moderne AOP-Produkte nicht nur die Formulierung einzelner Aspekte erlauben. Aspekte können wie natürliche JavaKlassen auch voneinander erben und sich spezialisieren. Man könnte beispielsweise behaupten, aspektorientierte Vererbung würde Wurmlöcher erlauben, in denen die Paralleluniversen zueinander Verbindung halten. Dieses Bild passt natürlich nur, insofern Aspekte als so etwas wie n Dimensionen eines Raums verstanden werden. In der praktischen Programmierung stellen sich damit allerdings auch die typischen Probleme des polymorphen Verhaltens ein. Nehmen wir das Beispiel zweier Aspekte, die von einem dritten „Superaspekt“ erben. Die Aspekte würden sich grundsätzlich unabhängig voneinander in den Sourcecode weben lassen, so dass eine gegenseitige Wirkung untereinander nur begrenzt existiert. Der „Superaspekt“ beeinflusst nun aber bei Modifikation an gleich mehreren, scheinbar unabhängigen Dimensionen des Aspektuniversums. Das wäre, wie wenn sich die Gesetze der Physik für unterschiedliche Bereiche des Weltalls gleichzeitig ändern würden, während sie an anderer Stelle unverändert blieben.
AOP
55
1 – Von OOP nach AOP – Evolution der Programmierung
Ein praktisches Beispiel aus der Vererbungslehre. Gegeben sind zwei Klassen „Auto“ und „Flugzeug“, die beide von „Fahrzeug“ Eigenschaften erben. Definieren wir „Fahrzeug“ nun in „Haustier“ um, hat dies sehr extreme Auswirkungen auf das Verhalten von „Auto“ und „Flugzeug“. Das Auto würde nicht in der Garage, sondern vielleicht in einem Käfig gehalten und auch nicht auf Rädern fahren, sondern auf vier Beinen laufen. Das Flugzeug hingegen könnte zu einem Wellensittich mutieren und fortan durch Wohnung und Stadtpark statt über den Wolken fliegen. Vererbung ist bereits heute in Java ein sehr kniffliges Thema, obwohl die Begrenzung auf Einfachvererbung bei dieser objektorientierten Sprache assoziiert, man habe hier deutlich zum Verständnis des Verhaltens der erstellten Software beigetragen. Die exzessive Nutzung von Vererbung in Aspekten führt zu ähnlich verwirrenden und merkwürdigen Modellierungen wie die beschriebene intensive Nutzung von Vererbung im kleinen (objektorientierten) Rahmen in Java.
1.7.5
Orthogonale bzw. nichtorthogonale Aspekte und Konfliktsituationen
Aspekte verlaufen eigentlich grundsätzlich – da sie separate Applikationsdimensionen beschreiben sollen – unabhängig voneinander. Projiziert man sie mittels eines Weaver zurück auf die eine Dimension der zur Laufzeit ausgeführten Java-Applikation, stehen sie mehr oder minder stark in Bezug zu ihr und sogar zueinander in Konkurrenz. Wie gut also die Applikation ohne den Aspekt und der Aspekt ohne Wissen über die Applikation „existieren“ können, ist abhängig vom Design des Aspekts. Im besten Falle wissen beide (Basissource und Aspekt) nichts voneinander und man spricht von einem „orthogonalen“ Aspekt. Er steht faktisch rechtwinklig zur Businesslogiklaufrichtung und Basisdimension und überquert sie so (cross-cutting) in seiner eigenen Dimension (siehe Abbildung 1.7).
56
AOP-Techniken von morgen?!
Abb. 1.7: Orthogonale und nichtorthogonal positionierte Aspekte
Ändert sich der Winkel geometrisch gesehen im Schenkel zur Basislogik1, wird eine evolutionär unabhängige Weiterentwicklung beider Richtungen deutlich schwieriger. Die Abhängigkeit nimmt zwischen beiden deutlich zu, ihr Verhalten „bedingt einander“. Bei echt orthogonal verlaufenden Aspekten ist die Reihenfolge der Aspekte, in denen sie durch den Weaving-Prozess zu dem einen Sourcecode zusammengesetzt werden, faktisch irrelevant. Echt orthogonale Aspekte „berühren“ sich daher – wenn überhaupt – nur in der Nutzung des gleichen Aufsetzpunkts, in dem sie in den Source eingehen (Joinpoint). Der Umkehrschluss muss lauten, dass die Reihenfolge bei nichtorthogonalen Aspekten untereinander eine größere Rolle spielt, je spezifischer der Aspekt auf die Basisdimension des Java-Source angepasst ist. Je größer die Affinität zwischen Aspekten untereinander oder zur Basisdimension ist, umso komplexer wird der Build- und Architekturprozess, um die korrekte Abarbeitung zu garantieren.
1. was übersetzt eine höhere Abhängigkeit und Adaption an spezifische Businesslogikteile bedeutet
AOP
57
1 – Von OOP nach AOP – Evolution der Programmierung
Das bedeutet, dass die „zwanghafte oder undurchdachte“ Modellierung und Arbeit mit Aspekten zu genau dem chaotischen und strukturell gescheiterten SOC-Ergebnis führen kann, das Kritiker AOP gerne im Vorweg anlasten. Ein relativ komplexes Szenario ist hier schnell konstruiert, in dem die Anwendung eines Aspekts auf den Basis-Java-Code die Existenz eines anderen Aspekts bzw. die durchgeführte Bytecodemodifikation eines Fremden voraussetzt. Aspekt A führt eine Instanzvariable/Methode oder eine Klassenhierarchie ein, die im Ursprungscode nicht gegeben war. Dies kann wiederum die Voraussetzung für die Anwendung von Aspekt B sein. Versuchen wir es an dieser Stelle nochmals mit einem realitätsnäheren Beispiel. Nehmen wir an, Sie wollten einen Supermarkt eröffnen. Ein entsprechendes Gebäude steht zur Verfügung, Regale und Produkte sind vorhanden. Alles ist bestens aufgebaut, als Ihnen einfällt, dass Strom jetzt nicht schlecht wäre, um Kühltruhen und Lampen zu betreiben. Also sorgt ein Aspekt für Licht in den Gängen. Da es außerdem sinnvoll ist, hin und wieder in den Gängen feucht durchzuwischen, beauftragen Sie eine Reinigungfirma, die nach Ladenschluss die Gänge wischen soll. Auch dies ließe sich durch einen Aspekt umsetzen. Da die Putzfrau sich nun ausbittet, wenn sie schon arbeiten muss, dies bei Licht zu tun, müssen Sie also immer erst für Strom sorgen, wenn Sie es sauber haben wollen. Andersherum macht es wenig Sinn. Schlimmstenfalls verleitet die Abhängigkeit von Aspekten dazu, künstlich weitere Einsprungsstellen oder Klassenstrukturen einzubauen, um das nachträgliche Einweben von Aspekten zusätzlich zu erleichtern. Dies ähnelt der Ansicht, Java nur deshalb zu verwenden, weil es gerade „in“ ist. Solche Voraussetzungen sind schlecht für die Einführung eines neuen Entwicklungsparadigmas wie AOP. Statt Verbesserung der Trennung der Verantwortlichkeiten nährt dies hingegen oftmals den Boden für das Gegenteil. Beantworten Sie – falls Sie bereits mit dieser Aufgabe betraut waren – für sich einmal ernsthaft die Frage, was mit Ihrer Software passiert, wenn Sie die typischen Aspekte Security-, Transaktionsmanagement und Logging
58
AOP-Techniken von morgen?!
ohne Anpassungen des restlichen Codes rückstandslos entfernen würden. Grundsätzlich dürfte sich an den meisten Stellen bei einem Durchlauf nichts ändern bzw. es dürfte zumindest zu keinen Compilefehlern kommen. Aber ist das tatsächlich der Fall? Und wenn dem nicht so ist, liegt es dann daran, dass der Aspekt nicht „aspektorientiert“ entwickelt wurde, entwickelt werden konnte oder dass er nicht wirklich „orthogonal“ ist? Gibt es denn vollkommen unabhängige Dimensionen, also orthogonale Aspekte überhaupt? Meine Behauptung an dieser Stelle ist: Ja, es gibt sie. Aber es sind wenige und die, die es sind, tragen oftmals faktisch nichts zum Grundverhalten einer Software bei. Vielmehr lassen sich im Vorhinein bestimmte Designvorgaben mit AOP prüfen oder nachträglich bestimmte Verhaltensmuster in der Software leicht ergänzen. Sie haben aber oft nur vorübergehenden Charakter. Stellen Sie sich einen Logging-Aspekt vor, der den Eintritt und den Austritt einer Methode mitloggt. Ein solches Interceptor-Beispiel sehen Sie hier als JBoss AOP-Source: public class LoggingAspect implements Interceptor { public String getName() { return "Logging interceptor"; } public Object invoke(Invocation arg0) throws Throwable { MethodInvocation mi = (MethodInvocation) arg0; try { System.out.println("START - " + mi.getTargetObject() .getClass().getName() + "." + mi.getMethod().getName()); return arg0.invokeNext(); } catch (Exception e) { throw e;
AOP
59
1 – Von OOP nach AOP – Evolution der Programmierung } finally { System.out.println("END - " + mi.getTargetObject() .getClass().getName() + "." + mi.getMethod().getName()); } } }
Solange niemand aus dem Inhalt des Logs tiefgreifende Konsequenzen zieht, verhält er sich faktisch orthogonal. Man kann den Aspekt zu jedem Zeitpunkt installieren und auch wieder entfernen und er arbeitet auch unabhängig vom Basissource. Der Loggingaspekt „stört nicht weiter“ – vielleicht abgesehen von einem leichten Performancenachteil. Lässt man ihm aber weiterführende Bedeutung innewohnen, kombiniert ihn mit Security und Transaktionsmanagement, setzt Performancemesspunkte in der Software an, ist er nicht mehr irrelevant. Jetzt kommt es darauf an, in welcher Reihenfolge er zu den anderen in den Source gewoben wird, um sowohl das eigene als auch deren Ergebnisse nicht zu verfälschen. Auch kann man den Aspekt möglicherweise nicht mehr entfernen, ohne automatische Log-Parser oder Administratoren zu verwirren. J2EE-Entwickler dürften mit den über Jahre üblichen Deploymentdeskriptoren vertraut sein und wissen, dass es von den Aspekten Transaktionsmanagement und Persistenz von Objekten in J2EE jeweils zwei Varianten gibt: eine komplett Container-überlassene (CMT: Container-managed Transactions, CMP: Container-managed Persistence) und eine „selbst gestaltete“, „manuelle“ (BMT: Bean-managed Transactions, BMP: Beanmanaged Persistence). Erstgenannte entspricht dem „das macht jemand für mich“-Ansatz und funktioniert, solange die Konfiguration des Aspekts auch feingranular genug gesteuert werden kann. Dadurch entsteht gegebenenfalls aber auch eine hohe Affinität zwischen Aspekt und Basissource. Die zweite Variante („du darfst es gerne machen, aber ich sage dir, wann du was machst“) ist die althergebrachte und leider oft immer noch notwendige. In etwas komplexerer
60
AOP-Techniken von morgen?!
Software, bei der beispielsweise mehrere unterschiedliche Transaktionen geschachtelt werden müssen oder sich überschneiden, kann man zuweilen die „Bean-managed“ Variante an diversen Stellen der „Container-managed“ vorziehen. Es ist eben bei weitem nicht so einfach, echte Orthogonalität zu erreichen, wie es wünschenswert wäre. Modularisierung und Verantwortungszuweisung an Klassen sind Grenzen gesetzt. Auch der Einsatz eines Aspekts wie Logging für Ein- und Austritt von Methoden macht doch das grundsätzliche Loggen von Objektinhalten oder Programmzuständen und Fehlerkonstellationen nicht komplett obsolet. Auch wer einen Loggingaspekt verwendet, wird deshalb oft nicht auf die Implementierung von Log-Ausgaben im Basissource verzichten wollen. Andererseits stellt sich die ernsthafte Frage, ob das Festhalten an eigenen Debug- und Log-Statements einem vordefinierten und flexiblen Aspekt-Pattern vorzuziehen ist. Aspekte sind und bleiben in gewisser Weise interdependent. Sie hängen voneinander mal mehr, mal weniger ab und ihre Reihenfolge ist mitnichten irrelevant, weil echte Orthogonalität nur selten vorherrscht. Um die Auswirkungen von Aspektbeziehungen zueinander absehen zu können, stellen sich vier konkrete Fragen: 쐌 Wie orthogonal ist das Verhalten, das der Aspekt in die Software einbringt? Ist die Software problemlos weiterverwendbar, wenn der Aspekt nicht eingewoben wurde? 쐌 Wie steht der jeweilige Aspekt zu anderen Aspekten in Beziehung? Setzt ein Aspekt z.B. auf Joinpoints eines anderen Aspekts auf? Befinden sich mehrere Aspekte in Aufruf- oder Vererbungshierarchien zueinander? 쐌 Modifiziert der Aspekt Teile des Basissource oder setzt er nur auf ihm auf? Greift der Aspekt auf Ressourcen oder Speichermedien zu, die mit anderen Clients oder Aspekten genutzt werden. Führen somit das Verhalten und die Modifikationen, die der Aspekt durchführt, automatisch zu Interdependenzen?
AOP
61
1 – Von OOP nach AOP – Evolution der Programmierung
Je deutlicher die Beantwortung dieser Fragen in Richtung spezifisches und fremdabhängiges Verhalten weist, umso mehr dürften sich die Risiken im endgültigen Sourcecode niederschlagen.
1.7.6
Symmetrische und asymmetrische AOP-Lösungen
Die meisten AOP-Produkte basieren auf einer klaren Trennung zwischen Basissource und Aspekt . Die Aspekte sind oftmals mit wenig Mühe zu entfernen. Wird allerdings der Basiscode entfernt, ist das Ergebnis oft nicht mehr lauffähig. Etwas überspitzt ließe sich also behaupten, AOP sei die Krücke für die Krücke. Denn OO vermag das Paradigma des Separation of Concerns nicht en detail perfekt umzusetzen. AOP verbessert in Teilbereichen zwar diesen Umstand, kann aber ohne einen OO-Basissource1 nicht existieren. Selbst wenn die AOP-Aspekte in OO realisiert werden, kommt AOP in den meisten Produkten nicht ohne einen Basissource aus, in den hinein es seine Aspekte projiziert. Heraus kommt durch den Webprozess eine Art AOP-„Fake“. Es verbleibt eine OO-Anwendung, diesmal ohne Aspekte, sondern mit „normalem“ Redundanzinhalt nach dem Compile. Betrachtet man die Aspekte wie auch den Basissource als die Dimensionen der Anwendung, so wird einer Dimension ein deutlicher Vorrang oder zumindest eine außergewöhnliche Stellung eingeräumt. Während die Entwicklung noch relativ symmetrisch erfolgt (Aspekte können sich auf andere Aspekte oder den Basissource beziehen), ergibt sich durch das Weaving eine asymmetrische Bedeutung, weshalb diese (vermutlich derzeit häufigste) Anwendungsvariante als asymmetrisches Paradigma bezeichnet wird. Bezieht sich Aspekt A z.B. auf den Basissource B und Aspekt C auf Aspekt A, so ist A für C der Basissource. Die Beziehung zwischen A und C verhält sich allerdings anders als zwischen A und B, da A ein anderer Vorzug beim Weaving eingeräumt wird. 1. auch als core oder base model bezeichnet
62
AOP-Techniken von morgen?!
Dennoch ist auch ein vollkommen symmetrischer Ansatz theoretisch möglich, der auch den Basissource mit als Aspekt integriert. Damit dies möglichst gut funktioniert, wird der Basissource nochmals modularisiert, das heißt in Einzelteile zerlegt. Die fachlichen Zerlegungselemente werden beim symmetrischen Paradigma1 mit den orthogonal verlaufenden, mehr technisch orientierten Cross-Cutting Concerns zueinander in Beziehung gesetzt. Der symmetrische Ansatz ist der mit Sicherheit schwieriger zu realisierende. Er unterscheidet sich noch deutlicher vom bisher bekannten reinen objektorientierten Verhalten. Durch die Modularisierung entstehen mehrere Kleinstapplikationen, die zueinander bis zum Weaving keine Verbindung besitzen. Hier braucht es entsprechende Denk- und Designmodelle, Tools und Prozessunterstützung. Nicht zuletzt stellt sich dann auch die Frage, ob Java als Sprache und die virtuelle Maschine als Laufzeitumgebung hier heute den richtigen Weg weisen.
Abb. 1.8: Vergleich zwischen symmetrischem und asymmetrischem Ansatz
1. William Harrison, Harold Ossher and Peri Tarr. Asymmetrically vs. symmetrically organized paradigms for software composition. Technical report, IBM, 2002.
AOP
63
1 – Von OOP nach AOP – Evolution der Programmierung
Welches Paradigma verfolgbar ist, hängt von den persönlichen Architekturvorstellungen, aber auch vom eingesetzten Werkzeug ab. IBMs HyperJ versucht sich am nahezu vollkommen symmetrischen Ansatz. Hingegen dürften AspectJ, AspectWerkz und JBoss AOP die Realisierung nach asymmetrischem Paradigma in der Nutzung favorisieren, obwohl z.B. AspectJ auch die Verbindung von Aspekten untereinander unterstützt. Dem symmetrischen Ansatz wird zudem nachgesagt, zwar in Forschung und Theorie denk- und machbar zu sein, aber in realen Applikationen nicht häufig genug angewendet zu werden, um einen flächendeckenden Beweis für die Anwendbarkeit zu liefern. Die Beispiele in diesem Buch konzentrieren sich aus den genannten Gründen auf den asymmetrischen Ansatz. Ein symmetrischer Ansatz ist vor allem sinnvoll, wenn man versucht, die in Software gegossenen Geschäftsvorfälle und Modellentitäten voneinander zu trennen. In den meisten Fällen nutzt man AOP heute aber hauptsächlich noch für technische Aspekte, bei denen asymmetrisches Entwickeln typisch ist. So bleibt denn AOP ein Hilfsmittel für OO. Damit lassen sich mindestens die geschätzten 15% des Source kanalisieren und modularisieren, die im herkömmlichen OO-Geschäft unzureichend handhabbar sind. Sie führen gemeinhin immer wieder zu zerstreuten (scattered) und ineinander komplex verzahnten, verwirrenden (tangled) cross cutting concerns im OO-Core. Muss man fachliche Elemente ebenfalls dekomponieren und in Aspekte zerlegen, ergeben sich auch schnell 50–90% Abdeckung durch Aspekte. Der Wert hängt allerdings auch sehr stark von der Art der Zählweise ab, schließlich finden sich in den Aspekten wiederum Objekte, die objekt- und nicht aspektorientiert entwickelt wurden.
1.7.7
Weaving in dynamischem und statischem AOP
Das Standardverhalten vieler Weaver ist heute, den Aspekt und die für das Einweben notwendigen Metadaten als statisch zu behandeln. Das bedeutet, dass Joinpoints, Advices und Verbindungen zwischen beiden zwar zum Designzeitpunkt, nicht aber zur Laufzeit modifizierbar sind.
64
AOP-Techniken von morgen?!
Grund dafür ist, dass die meisten Werkzeuge die Aspekte vor der Ausführung im asymmetrischen Paradigma in den Core weben. Es gibt vier typische Ansätze, um Aspekte in den Basissource zu integrieren: 쐌 Modifikation des Java-Source vor der Ausführung des Compiles und der Umwandlung in Bytecode (source weaving) 쐌 Bytecodemanipulation nach der Generierung des Cores zur Integration der Aspekte vor dem Laden des Bytecodes durch den Classloader der Anwendung (siehe Kapitel 2) (bytecode weaving) 쐌 Bytecodemanipulation beim Laden des class-File durch einen Classloader zur Integration der Aspekte (load-time weaving). Dies kann durch einen eigenen Classloader, eine gepatchte Variante des bestehenden Classloader oder durch ein Classloader-Plugin in Java 5 geschehen. 쐌 Virtuelle Anwendung von Aspekten auf einzelne Instanzen im Gegensatz zur kompletten Klasse z.B. durch Nutzung von dynamischen Proxies (behandeln wir noch in Kapitel 2 und 3) (runtime or continous weaving)
Abb. 1.9: Weaving-Varianten im Vergleich
Die beiden erstgenannten Varianten schließen eine Änderung des Bytecodes ohne Restart der Applikation faktisch aus. Sie zeigen also klar eine statische Weaving-Lösung auf. Besonders zweitere ist typisch für die meisten AOPWerkzeuge heutzutage. Oftmals kann man in beiden Varianten auch nicht
AOP
65
1 – Von OOP nach AOP – Evolution der Programmierung
mehr unterscheiden, ob der Aspekt nun durch ein AOP-Tool oder den Entwickler selbst in den Bytecode kam. Die dritte Variante (load-time-weaving) würde es durch Löschen des Classloader zur Laufzeit1 erlauben, dass die gesamte Applikation oder Teile von ihr (abhängig von der Art der Abschottung der Module voneinander) in gewisser Weise ein Reset der Weaving-Regeln ermöglichen. Dies sieht dann auf den ersten Blick wie ein „dynamischer“ Ansatz einer Rekonfiguration zur Laufzeit aus. Die letzte Variante (run-time-weaving) wird von Kritikern zu Recht als keine echte AOP-Lösung bezeichnet, obwohl sie dafür hohe Dynamik anbietet, da jede neue Instanziierung eines Objekts ein und derselben Klasse zu einem potenziell anderen Verhalten der Instanz führen kann. Problematisch wirkt sich hier vor allem der definierte Umgang der virtuellen Maschine mit Klassen aus, die sich mit Änderung von geladenen Klassen sehr schwer tut. Jedes Tool, das Sie derzeit am Markt finden, versucht, die Modifikation der Klassenstruktur vor der Injektion in die VM durchzuführen. Danach sind zwar auch immer noch unterschiedliche Verhaltensweisen mehrerer oder einzelner Instanzen denkbar, dennoch ist eine Änderung von Vererbungshierarchien oder existierenden Feldern oft unmöglich. Nur die letzte Variante, also die Änderung und Einführung von Aspekten zur Laufzeit, kann als „dynamisches AOP“ bezeichnet werden. Alle anderen Einsatzbereiche führen zu statischem AOP, was aufgrund seiner einfacheren Realisierbarkeit überwiegt. Genaugenommen finden wir in heutigen Architekturen und Tools also noch eine starke Tendenz zu statischen, asymmetrischen Lösungen vor. Im dynamischen AO-System spielt die Ersetzbarkeit der Aspektlogik eine entscheidende Rolle. Dabei darf sich die Ersetzbarkeit nicht ausschließlich auf die dem Aspekt inhärente Logik beschränken, sondern sie ist vor allem auf die Modifizierbarkeit der Weaving-Metadaten (advices/ pointcuts/ joinpoints) gemünzt.
1. was im J2EE-Bereich z.B. einem Redeployment eines Anwendungsteils, beispielsweise eines Enterprise Java Beans (EJB) oder einer Webapplikation, entspräche
66
AOP-Techniken von morgen?!
In diesem Zusammenhang fällt auch der Begriff des morphing aspect, eines Softwareaspekts, der sein Verhalten zur Laufzeit anpasst und sich an unterschiedlichen Stellen in den Programmstack einbringt. Er kann und sollte gegebenenfalls auch dynamisch wieder entfernt werden können. Der Aspekt kann zudem1 einer separaten Kompilierung unterliegen und echt-symmetrisch ohne den Core abgelegt werden (structure-preserving compilation (SPC)). Wunderwelt Dynamic AOP Insgesamt erweitert die Nutzung dynamischer Aspekteinbringung die Fähigkeiten von AOP nochmals deutlich. Dies ist besonders in den Softwarebereichen zu spüren, wo laufende Anwendungen nicht gestoppt werden können, um Software längeren Wartungszyklen zu unterziehen oder Fehlersuche zu betreiben. In solchen Applikationen wäre die dynamische Einstreuung von Aspekten hilfreich, um Fehlersituationen zu analysieren. Eine ähnliche Lösung bietet JBoss AOP mit dem dynamischen Wrappen von Objektinstanzen an, die in einem Cluster synchronisiert oder auf verschiedenen Maschinen gecacht werden sollen. Kombinieren Sie einmal für sich die Ideen von Java Management eXtensions (JMX2) mit Remote Attaching an laufende Anwendungen3 und dem dynamischen Einweben von Aspekten. Alle drei Teilbereiche überlappen sich in gewissen Bereichen. Sie könnten in Kombination dazu verwendet werden, Applikationen zur Laufzeit so zu manipulieren, dass sie für einen wohl definierten Zeitraum ihr Verhalten ändern, ohne den Basis-Bytecode, auf dem die Startsequenz der Applikation aufbaute, damit zu behelligen. Zunächst hören sich die Möglichkeiten naturgemäß etwas atemberaubend an. Es ist förmlich der letzte Schritt vor der Vision, ein Programm vollständig zur Laufzeit umschreiben zu können. Auch Hotcode-Replacement4, also
1. so er denn echt-orthogonal angelegt ist, was eine relativ wichtige Voraussetzung für eine Dynamisierung ist, da er nur so „universell“ einsetzbar erscheint 2. Management der Anwendung zur Laufzeit von außen 3. Debuggen von produktiven und laufenden Java-Anwendungen über das Netzwerk 4. auch als Hot swapping bezeichnet
AOP
67
1 – Von OOP nach AOP – Evolution der Programmierung
das Ersetzen von Bytecode durch einen Debugger zur Laufzeit in der VM über JPDA1 bzw. JVMTI2, in Java 5.0 unterstützt dies leider nicht in Gänze. Damit dieser Ansatz auch performant durchführbar ist, müsste entweder ein hoch generischer Ansatz her, bei dem jedes Objekt von jedem anderen, zu dem es in Korrelation steht, abgeschottet wird (wie im Dynamic ProxyFall möglich). Oder bereits die virtuelle Maschine müsste mit dem Einweben von Aspekten direkt vertraut sein, um die Strukturen von Klassen bzw. Event/Aufrufbeziehungen zur Laufzeit ändern zu können (custom VM weaving). Eine dritte Möglichkeit bietet die Nutzung der Debugfähigkeiten einer Standard-VM. Im Debugmodus gestartet, gestattet sie es, an Breakpoints, die zur Laufzeit setzbar sind, fremden Source auszuführen, bevor die Programmausführung fortgesetzt wird. Es finden sich am Markt in diesem Bereich auch tatsächlich einige Werkzeuge, die mittels der genannten Verfahren das Bild einer dynamischen AOP-Implementierung ermöglichen. Darunter befinden sich beispielsweise Produkte wie 쐌 WOOL, das mit dem Debuginterface Hotswap-Aktionen durchführt, 쐌 PROSE 1 (ebenfalls Angriff auf das Debuginterface), 쐌 SteamLoom3, eine Erweiterung für das Caesar-AOP-Tool, das aufsetzend auf der IBM Jikes VM direkte Manipulation mittels „customer VM“ durchführt. Nichtsdestotrotz bleibt bei Dynamic AO die Frage, ob die Erreichung und Umsetzung zu einem sinnvollen, architektonisch vertretbaren Ergebnis führen kann. Sind das erreichte Resultat und dessen Design und Verhalten der Anwendung auch zur Laufzeit noch nachvollziehbar? Weitere Kriterien bei dieser Entscheidung bringt auch die Frage nach Security hier mit ein. Schnell hebelt man doch in Dynamic AOP durch Class-
1. Java Platform Debugging Architecture 2. Java Virtual Machine Tool Interface 3. http://www.st.informatik.tu-darmstadt.de/static/pages/projects/AORTA/Steamloom.jsp#dl
68
Wann ist der Einsatz von AOP sinnvoll?
Code-Modifikation in der VM Policies und Schutz, der durch signierte JARFiles erreicht werden soll, wieder aus. Ein Programm, das zur Laufzeit seine Struktur verändert, ähnelt einem mutierenden Virus. Es ist fraglich, ob dies erstrebenswert ist, und noch fraglicher, ob es auch performant implementiert werden kann.
1.8 Wann ist der Einsatz von AOP sinnvoll? AOP ist – mit dem richtigen Werkzeug und dem dafür notwendigen Verständnis für die Anwendung von AOP – oft nur unwesentlich komplexer anzuwenden als ein reines Java-Programm1. Man kann bei der Diskussion um AOP schnell zu dem Urteil gelangen, dass besonders große Applikationen mit Tausenden von Klassen vom Ansatz des AOP profitieren, weil gerade hier die Redundanz und Zerstreuung der Aspekte verteilt über den kompletten Sourcecode am deutlichsten zu Tage tritt. Bedenkt man, dass besonders Unternehmensanwendungen, bei denen viele Entwickler parallel beteiligt sind und nur wenig Zeit für architektonische Gesamtlösungen und Redesigns übrig bleibt, einer zunehmenden Degenerierung unterliegen, ist die Anwendung von AOP ein probates Mittel. Degenerierung heißt letztendlich, dass die Applikation aufgrund zunehmender Komplexität nicht mehr beherrschbar wird. Codereduktion bedeutet einen wesentlichen Schritt in Richtung Komplexitätsreduktion. Komplexitätsreduktion wiederum bedeutet Verbesserung der Wartbarkeit und Qualität sowie Wiedererlangung von Beherrschbarkeit des Source. Betrachten wir zudem, dass sich die Stellen, an denen ein Aspekt in der Applikation zum Tragen kommen soll, sehr gut von der eigentlichen Implementierung trennen lassen, so erleben Aspekte schnell eine hohe Wiederverwendbarkeit. Dies gilt sowohl für kleine wie auch für Großprojekte, die im Bereich der cross-cutting concerns oft eine höhere Ähnlichkeit aufweisen als in Betrachtung der reinen Geschäftslogik.
1. Die Möglichkeiten von voll dynamischem AOP und einem symmetrischen Ansatz blenden wir an dieser Stelle dabei allerdings aus, da er heute nicht oft im Einsatz ist.
AOP
69
1 – Von OOP nach AOP – Evolution der Programmierung
Wurde z.B. eine J2EE-Applikation mit Bedacht mit AOP-Elementen durchsetzt, sind diese ohne größere Schwierigkeiten auf andere Applikationen übertragbar – und umgekehrt. Denn auch bei stabiler Applikationslogik sind die weiteren Aspekte evolutionär weiterentwickelbar, ohne die Haupt-JavaApplikation dabei zu berühren. Dass dies möglich ist, beweist JBoss mit der Implementierung ihres Applikationsservers in Version 4.0. Der Trend in der Java Enterprise Edition, zu den POJOs (Plain Old Java Objects) zurückzukehren, zeigt, wie man aus den Schwierigkeiten vergangener Jahre gelernt hat. Mittels Aspektanreicherung über Java 5-Annotations wirkt der Source nicht nur aufgeräumter, er befreit auch die Geschäftslogik von lästigen Aspektnebenschauplätzen. Java EE 5 greift genau diese Annotationsfähigkeiten mit EJB 3.0 und JSR 181 (Webservice Metadata) auf. In den weiteren Kapiteln dieses Buchs werden noch einmal detaillierter die verschiedenen Anwendungsmöglichkeiten beschrieben, in denen AOP schon heute Früchte trägt. Darin gehen wir dem Thema Annotations nochmals genauer auf den Grund.
1.8.1
Auswirkung von Softwareentwicklungsverfahren auf Projektmanagement
Welche Auswirkungen hat die Art und Weise, wie wir Software entwickeln, auf unsere Kommunikation und Arbeitsweise im Team? Welche Bedeutung hat die Entscheidung für eine objektorientierte Herangehensweise und welchen Einfluss nehmen aktuelle moderne Architekturen auf den Erfolg eines Projekts? Rein gefühlsmäßig würde ich zunächst behaupten, dass die Programmiersprache und das Entwicklungsvorgehen1 eines Softwaresystems nur begrenzte Auswirkungen auf die Organisation des Projekts haben, in dem die Realisierung stattfindet. Dafür müssen entsprechende Wissensträger in allen relevanten Gebieten vorhanden sind. Genauer betrachtet könnte sich diese Vermutung aber auch als Irrtum herausstellen.
1. vor allem OO/RUP, weniger hingegen z.B. XP
70
Wann ist der Einsatz von AOP sinnvoll?
Abb. 1.10: Logische Module vs. Architekturschichtenmodell
Stellt man sich ein Team von Entwicklern vor, die gemeinsam an einer Anwendung schreiben, so werden sich zumeist Aufgabenbereiche herauskristallisieren, in denen jeweils ein Teammitglied arbeitet. Innerhalb einer Multitier-Architektur könnten dies zum Beispiel Verantwortliche für Client/ Präsentationsschicht, Server/Business-Logik, Backend-/Datenbankmanagement sein (siehe Abbildung 1.10). Nehmen wir an, wir wählen diese Aufteilung, weil technologisch das Knowhow in der Entwicklermannschaft entsprechend verteilt ist. So kennt sich der eine z.B. sehr gut mit Oberflächenelementen (z.B. JSPs oder Portlets) aus, während der andere über umfangreiches Datenbank-Know-how (in Sachen Structured Query Language, SQL) verfügt. Dennoch sind entsprechende Reibungsverluste unvermeidlich. Während wir vielleicht laut entsprechender Architekturkonzepte die Notwendigkeit zur Trennung von Software in Layer und Tiers als sinnvoll erkannt haben, passt diese Sicht faktisch nicht zu einer Modularisierung, die sich an fachlichen Aspekten ausrichtet. Der Anwendungsfall „Informationen auf der Oberfläche suchen und anzeigen“ durchläuft plötzlich alle Schichten der Architektur. Jeder Mitarbeiter ist für einen Teilbereich der zugrunde liegenden Logik zuständig: der Datenbankadministrator für die Tabellen und Dateninhalte, das Teammitglied in der Businesslogik für die Zusammenstellung oder Filterung der Daten, der Oberflächenentwickler für die Anzeige der Daten, die letztendlich aber aus der Datenbank stammen.
AOP
71
1 – Von OOP nach AOP – Evolution der Programmierung
Wir erhalten bei dieser Aufteilung also eigentlich zur Schichtenarchitektur orthogonal verlaufende Zuständigkeitsbereiche (siehe Abbildung 1.10). Zusätzlich mag es vielleicht einen Mitarbeiter geben, der sich um das Thema der Authentifizierungen und Autorisation, also der Berechtigungsprüfungen, kümmern soll. Seine Aufgaben sind sowohl modulspezifisch und fachlich getrieben als auch verteilt über n-Layer der Architektur. Ergebnis ist: Während das Know-how-Trägertum zumeist in Teams technisch orientiert vorliegt, erfordert die Umsetzung ein fachlich strukturiertes Wissen und Vorgehen. Die Auswirkung auf Projektteams zeigt sich in der Zersplitterung von Mitarbeitern in einer Matrixorganisation. In jeder technisch orientierten Schicht sollte potenziell ein Mitarbeiter sein, der sich wiederum um modulspezifische Belange kümmert (siehe Abbildung 1.11).
Abb. 1.11: Aufteilung der Projektaufgaben nach Schichtenmatrix
Zumeist sind aber gar nicht genügend Mitarbeiter vorhanden oder die Auslastung auf die unterschiedlichen Modulbereiche verzerrt sich aufgrund des notwendigen Arbeitsaufkommens mit der Zeit des Fortschritts des Projekts (siehe Abbildung 1.12).
72
Wann ist der Einsatz von AOP sinnvoll?
Abb. 1.12: Verzerrung der geplanten Zuständigkeiten durch den Projektfortschritt
Um eine Lösung für das Problem der gleichzeitigen Arbeit an einem gemeinsamen System zu finden, beginnen Großprojekte damit, klare Trennungen zwischen Applikationsteilen einzuführen. Diese Teilbereiche werden dann oft mit dem Begriff „Komponente“ geschmückt. Es gibt Komponentenverantwortliche und -entwickler. Die Arbeit scheint so deutlich besser strukturierbar als zuvor. Dafür müssen Schnittstellen abgesprochen werden und Interna von öffentlichen Teilen abgetrennt werden, so dass die Kommunikation zwischen den beteiligten Entwicklungspartnern nur über die Grenzbereiche der Schnittstellen erfolgen muss. Ziel dabei ist die Reduktion des Kommunikationsbedarfs, da sich alle Beteiligten auf die gemeinsamen Grenzen der Komponenten konzentrieren können. Tatsächlich funktioniert dieses Prinzip aber nur so gut, wie die Einteilung der Software in Komponenten gelingt. Aufgrund der orthogonal verlaufenden architektonischen und fachlichen Dimensionen der Software wird der Abstimmungsbedarf im Team allerdings sehr hoch bleiben. Jeder, der an einer Seite des berühmten „Softwaretischtuchs“ zieht, kann, obwohl es nur seine Seite war, andere mit beeinflussen – bewusst oder unbewusst.
AOP
73
1 – Von OOP nach AOP – Evolution der Programmierung
AOP kann an dieser Stelle helfen, die Aufteilung der Applikationsbereiche zu verbessern und damit die Interdependenzen und Reibungsverluste zwischen Entwicklern und Sourcecode zu verringern.
1.8.2
Wiederverwendbarkeit – ein Aberglaube?
Die Auswirkungen komplexer Anwendungssysteme beschränken sich nicht nur auf die organisatorischen Teamaspekte. Typisches Beispiel ist die Testbarkeit separater Modul- oder Architekturteile. Allein zum Testen der Oberflächenanzeige muss manchmal die komplette Persistenzschicht aktiviert, die Datenbankverbindung aufbaubar und der User in Datenbanken autorisiert sein (LDAP-Einträge, Datenbankmappings, Property-Dateien, XMLKonfigurationen ...). Leider ist der Fall nicht selten, dass man stunden- oder sogar tagelang nicht zu seinem eigentlichen Problemfeld vordringen kann. Gründe können sein, dass Komponenten anderer Entwickler nicht laufen, nicht in der richtigen Version vorliegen oder die Gesamtapplikationen mit all ihren Teilen ein vertieftes Wissen um die Belange und Probleme fremder Anwendungsbereiche benötigen. Teile dieser Problematiken sind mittels Mocktests, das heißt dem Einsatz von Stubs, überbrückbar. Mocks sind allerdings oft aufwändig in Handhabung und Pflege, denn sie müssen möglichst repräsentative Testfälle unterstützen. Besonders zu Beginn einer Entwicklung, wenn sich bestimmte Komponenten noch in einer frühen Entwicklungsphase befinden, sind Mocktests machbar. Anschließend, wenn eine stärkere Integration gefordert ist, um die Stabilität der Realisierung zu testen, häufen sich die Probleme. Das Spezialwissen über den eigenen Arbeitsbereich reicht schnell nicht mehr aus. Dann ist mehr übergreifendes Allgemeinwissen in Bezug auf die Anwendung notwendig als die reine Konzentration auf modulspezifische oder fachlich orientierte Belange. Eine andere typische Forderung an objektorientierte Softwareprogramme klingt mir persönlich noch gut im Ohr. Besonders in der Anfangsphase von Java, als die Sprache so langsam Schritt für Schritt vom Image eines HTMLGimmicks zu einer ernstzunehmenden Richclient-Plattform-Anwendung zu migrieren begann, wurden auf die Fassaden der Aktienmärkte immer wieder „neue Technologien“ gemalt. Ein solcher Jump-Start ist nicht ohne Prob-
74
Wann ist der Einsatz von AOP sinnvoll?
leme und anstatt die Entwicklungszeiten zu verkürzen, verlängerten sich einige Projekte. Resultat ist ein Aufwand weit über den erwarteten Rahmen hinaus, weil fehlende oder mangelhafte Prozesse und Tools die Arbeit behindern. Gleichzeitig konnte und kann gegebenenfalls an vielen Stellen heute die Wiederverwendbarkeit von Modellen in Frage gestellt werden. Wie viele der ehemals entwickelten Klassen finden sich in neuen Programmen und modernen Produkten heute tatsächlich noch wieder? Das Ziel, auch einfache Konzepte wie Klassenentitäten (oben war das Beispiel eines Kunden oder einer Bankverbindung gegeben) wiederzuverwenden, bedeutet, diese Entitäten zunächst in die eigene Anwendung zu migrieren. Das heißt, eine 1:1-Übernahme von bereits fertigen Sourcecodeteilen in neue Kontexte fremder Applikationen fällt schwer oder ist zuweilen mit erheblichem Aufwand verbunden. Oft stimmt die Benennung der Packages nicht, die Klassen weisen Abhängigkeiten zu fremden Third-Party-Jars auf, die Implementierung der Konfigurationsabfrage ist anders, usw. Wiederverwendung basiert zumeist auf der Frage, wie man ein kontextfremdes Klassenmodell (zumeist Geschäftsmodellklassen) so in den eigenen Kontext versetzen kann, dass es ohne weiteres brauchbar ist. Dabei stellt sich die Frage: „Was zeichnet ein Modell eigentlich aus?“ Ein Modell ist eine Vereinfachung der Wirklichkeit, eine Abstraktion von Details zum Erkennen grundsätzlicher Mechanismen und Strukturen. Wenn man von Vereinfachung und Abstraktion von Details spricht, bedeutet das im gleichen Schritt allgemein, dass Modelle für einen bestimmten Kontext und eine bestimmte Applikation geschrieben werden. Sie lassen Eigenschaften und Verhalten von Klassen weg, die für andere Zwecke brauchbar wären. Das heißt, es ist schwer oder nur mit Mühe vorstellbar, ein und dieselbe Klasse in unterschiedlichen Zusammenhängen gleich zu betrachten. Das Ergebnis wird sein, dass eine Klasse bei der Modellierung zu unterschiedlichen Lösungsvarianten führt, die jede für sich in ihrem Zusammenhang richtig sein können.
AOP
75
1 – Von OOP nach AOP – Evolution der Programmierung
Ein typisches Beispiel könnte ein Baumarkt sein, in dem ein Kunde einen Lattenzaun kauft. Für die Einkaufsabteilung ist der Artikel mit Dingen wie einer Artikelnummer, einem Namen, einem Stückpreis und einem Großhändler verknüpft (eine Art finanztechnische Sicht). Für die Marktleitung und die Verkäufer ist es wichtig, wo sich der Artikel befindet, wie der Kunde verschiedene Produkte vergleichen kann und gegebenenfalls wie die Ware eingepackt ist oder wie die Vermarktung funktioniert. Die Marktleitung nimmt also eine logistische, aber auch marketingorientierte Sicht ein. Den Kunden interessiert am Lattenzaun vielleicht die Haltbarkeit, die Farbe, die Maserung, das Gewicht und die Größe des Produkts. Das wiederum sind logistische, finanztechnische und individuelle Designaspekte. Leicht ist erkennbar, dass sich eine Diskrepanz zwischen diesen Gesichtspunkten auftut, dass es mehr als eine Perspektive für unseren Beispiel-Lattenzaun gibt. Suchten wir also ein Modell, das all diese Sichten in sich vereint, um individuell verwendbar zu sein, entstünde immer ein entweder für die jeweiligen Perspektiven überfrachtetes, erklärungsbedürftiges oder unzureichendes Modell eines Lattenzauns. Die Lösung liegt also zwischen einem sehr abstrakten Modell eines solchen Zauns und einer sehr speziellen Lösung für genau einen Einsatzzweck. Bestimmte Details (z.B. Preis oder Farbe) wegzulassen, weil sie in einem anderen Kontext nicht benötigt würden, ist also genauso problematisch, wie sie im Modell zu behalten und gleichzeitig darauf hinzuweisen, dass sie sehr speziell sind. Das bedeutet, der Glaube an einfache Wiederverwendbarkeit stößt an Grenzen: 쐌 architektonische Grenzen (In welcher Schicht setzt man ein Element wie ein?) 쐌 aspektorientierte Grenzen (Wie ist die Verbindung zu anderen Elementen realisiert?) 쐌 fachliche/perspektivische/kontextabhängige Grenzen (Passen die Attribute, Eigenschaften und das Verhalten eines Modells ohne weitere Übertragung in einen anderen Kontext?)
76
Wann ist der Einsatz von AOP sinnvoll?
쐌 erkenntnistheoretische, organisatorische Grenzen (Wer weiß überhaupt davon, dass jemand so etwas schon einmal programmiert hat und wie es eingesetzt werden muss, damit es funktioniert?) Man kämpft also an der „Softwarefront“ mit unterschiedlichsten Imponderabilien, von denen sich einige (mitnichten aber alle) durch AOP lösen lassen. Die Gründe, warum sich in mittleren und größeren Projekten ein hoher Abstimmungsbedarf in Sachen Zusammenarbeit im Team und Projektmanagement und ein nur bedingt zu minimierendes Risiko verbirgt, finden sich letztendlich oftmals in der Softwarestruktur selbst, aber auch in den Ideen, die in der Software versteckt sind. Wiederverwendung von Teilen scheitert oft auch an organisatorischen Problemen. Wer wartet die Software später? Wie kann ich ihm verständlich machen, welche Ideen darin stecken? Wie kann ich am besten begreifen, was jemand anderes mit seiner Klasse, seinen Attributen, seinen Methoden tatsächlich meinte? Heutige Software gibt oft vor, dass alle Aspekte, alle Perspektiven sich an einer Stelle in Form eines Sourcecodes manifestieren müssen. Dieser Source wird dann durch mehrere Threads sequentiell durchlaufen und muss für alle Softwareteile gleich aussagekräftig sein. Sie gibt auch vor, wie sich Menschen in Teams organisieren müssen, die gleichzeitig an einem Produkt arbeiten, wie Softwareentwicklung vonstatten geht. Wer hier deutliche Erleichterungen sucht, muss die Softwareentwicklung an sich revolutionieren. Software ist ein bisschen vergleichbar wie eine Fahrt mit einem Taxi durch eine Stadt. Das Taxi ist der laufende Thread, der sich nach bestimmten Regeln durch den Source und die statischen Elemente, die Häuser, bewegt. Im Vorbeifahren an den Gebäuden werden Sie von außen immer nur deren Fassaden sehen. Die Möglichkeiten des Fortgangs Ihrer Reise durch die Straßen orientieren sich an den Wegen, Kreuzungen und Hinweisschildern, die den Verkehrsfluss regeln. Insofern befinden Sie sich nicht nur zum Designzeitpunkt Ihrer Software, sondern vielmehr noch zur Laufzeit in einem relativ starren und unbeweglichen Konstrukt. Die Evolution der Software ist erschwert. Nur bedingt können Sie vorher nicht existierende Straßenzüge, neue Gebäude oder einen anderen Straßenbelag zur Laufzeit in die gepflas-
AOP
77
1 – Von OOP nach AOP – Evolution der Programmierung
terte Stadt einbringen. Stattdessen nutzten Sie die Idee des Redeployment von Software. Für das Stadtbeispiel bedeutet dies, alle Straßenzüge und Gebäude kurzzeitig wegzureißen und sie so wieder in den Ursprungszustand zu versetzen. Dies ist z.B. notwendig, um eine neue Straße zu bauen, die Grundwasserleitungen der Stadt auszutauschen oder einen U-Bahn-Bau zu beginnen. Man muss den Umgang mit und das Herangehen an die Softwareentwicklung ändern, um für diese Team- und Softwareprobleme eine Lösung zu bieten. So ließe sich auch die „1:1-Mapping-Vision“ Stück für Stück realisieren. Denn nur wenn es uns gelingt, Sachverhalte so abzubilden, wie es die Wirklichkeit auch tut – mehrdimensional, perspektivenbezogen und so speziell oder allgemein wie es jeweils notwendig ist –, können wir in Teams miteinander an gemeinsamen Lösungen intuitiv arbeiten. Softwareentwicklung ist trotz softwaretechnischer Errungenschaften der letzten zehn Jahre (grafische Oberflächen, Mausnutzung und Internet beispielsweise) noch immer nicht das, was sie sein sollte: kinderleicht. Die Sprachkonstrukte moderner Programmiersprachen wie C# oder Java wirken noch immer primitiv und weit weniger intuitiv als beispielsweise eine Kücheneinrichtung, ein Werkzeugschrank oder ein Legobaukasten. Man könnte sagen, wir seien als Programmierer zuweilen dem „Korsett der zeilenbasierten, sequentiell und algorithmisch redundanten Sourcecodeschnippselei“ schon so verhaftet, dass das Risiko besteht, blind dafür zu werden, welches ungeheure Potenzial noch brach liegt.
1.8.3
AOP – ein Ansatz zur Lösung?
Wenn es gelingt, Software nicht nur in einer Dimension, sondern in n Dimensionen zu zergliedern und zwischen diesen Dimensionen klare Schnittstellen zu definieren, können einige wesentliche Probleme heutiger Entwicklungsvorgehen gelöst werden. Zunächst muss es gelingen, die Softwareteile so aufzusplitten, dass jedes von ihnen eine wohl definierte Aufgabe ausführt und auch selbstständig ausführen kann. Dann gilt es, diese teilweise oftmals sehr simplen Teile wieder zu einem großen Ganzen, der Applikation, zusammenzubauen.
78
Wann ist der Einsatz von AOP sinnvoll?
Aber für effektive Lösungen bedarf es Tools und Visualisierungen, die über die in diesem Buch vorgestellten heutigen Produktlösungen noch deutlich hinausgehen müssen. Man benötigt Vorgehensmodelle, die beantworten, wie Designs entstehen sollen, und vor allem Wissen bei Projektverantwortlichen und Entwicklern, wie Aspekte ein- und umgesetzt werden können und sollen. In einem meiner letzten Projekte, die parallel zur Entwicklung dieses Buchs stattfanden, verwendeten wir Hivemind1, eine Microkernel-Container-Lösung, die Sie bei Jakarta vorfinden. Hivemind hilft, Komponenten auf OO-Basistechnik klarer voneinander zu trennen und miteinander in Beziehung zu setzen (Stichwort: Dependency Injection). Hivemind liefert dabei auch einen ersten, recht einfachen AOP-Lösungsansatz, den wir u.a. verwendeten, um Transaktionsmanagement aus dem Sourcecode herauszutrennen und unsichtbar zu nutzen. Trotz des architektonisch faszinierenden und mächtigen Ansatzes gab es auch Skeptiker, die vor allem die mangelnde Transparenz der damit realisierbaren Lösungen bemängelten. Es fehlt Hivemind derzeit an ausreichenden Visualisierungsmöglichkeiten, bestimmte Entscheidungen über riesige XML-Konfigurationsdateien hinaus verständlich zu erläutern. Den mit Hivemind möglichen AOP-Aspekten droht dabei ein ähnliches Schicksal, so dass viele technisch raffinierte Lösungen, die wir hätten realisieren können, der klassischen Variante hintenan gestellt wurden. Lieber etwas so implementieren, dass es alle verstehen, als dass es so genial ist, dass fast niemand es versteht2. Vergessen Sie bei „Spielwiesen“ wie AOP nie, welche Auswirkungen solche Produkte oder Ideen auf Gemüter haben können, die sich mit Mühe gerade einmal die „normalen“ Java-Klassenkonzepte angeeignet haben3.
1. http://jakarta.apache.org/hivemind/ 2. Siehe auch [Wunderlich05] – Kapitel 7.1 Gebote der Architektur 3. Dies ist zweifelsohne kein Buch über Projektmanagement oder Architekturen. Das will und kann es auch gar nicht sein. Aber bei der Frage der Migration von OO über OO mit AOP nach AOP (?) gilt es psychologische Auswirkungen nicht zu unterschätzen.
AOP
79
1 – Von OOP nach AOP – Evolution der Programmierung
Softwareentwicklung soll einfacher werden, das ist unbestritten. Aber für AOP braucht es mehr als einen Pointcut, ein paar Advices und Klassen. Es braucht eine Strategie, technisch wie menschlich, um AOP zu dem werden zu lassen, was es ist: eine geniale Lösung für ein ziemlich einfaches Problem. Was wir brauchen, sind Visualisierungstools, die jedem Entwickler verraten, an welcher Stelle des Sourcecodes er derzeit arbeitet und welche Auswirkungen seine Aktionen auf andere Teile haben werden. Stellen Sie sich einen Hausbau vor. Es macht wenig Sinn, die Strom- und Wasserleitungen darin zu verlegen, ehe das Baugrundstück gekauft ist oder der Rohbau steht. Und dennoch finden sich zum Ausführungszeitpunkt Handwerker unterschiedlichster beruflicher Richtungen mit den verschiedensten Aufgabenbereichen an einer definierten Stelle wieder. Solange sie alle ein gemeinsames architektonisches Ziel verfolgen, ist es problemlos möglich, den Dachausbau zu betreiben, während andere die Küche montieren, oder zu tapezieren, während in anderen Zimmern Fußboden verlegt wird. Ohne die architektonischen Grundlagen (z.B. Bauzeichnung oder Betongerüst) wird allerdings eine Parallelisierung dieser Arbeiten nur schwer möglich sein. Je besser wir alle Aktivitäten in definierte Einzelaufgaben auftrennen können, umso schneller kann die Entwicklung vonstatten gehen, umso weniger Reibungsverluste werden auftreten. Es ist eines der Hauptaugenmerke von AOP, wie die AOSD-Community selbst ausführt, genau diese Verantwortungsaufteilung deutlich zu verbessern. Deshalb ist AOP ein richtiger Schritt, auch Projektorganisationen von heute zu verändern.
1.9 Versuch einer Einschätzung von AOP Sollten Sie als Leser bei der Diskussion um mehrdimensionale Räume, Programmasymmetrien und Schwächen von objektorientiertem Vorgehen und objektorientierter Programmierung zu der Erkenntnis gekommen sein, dass in Sachen AOP viel im Fluss ist, teilen wir diese Meinung.
80
Versuch einer Einschätzung von AOP
Gleichzeitig ist es eine faszinierende Aufgabe, an den Möglichkeiten solcher Technologien teilzuhaben. Java wird stark und stärkt sich durch seine Community, die nicht aufgehört hat, in den vergangenen zehn Jahren immer wieder noch bessere, noch klarere Lösungen zu fordern. Sie können im aspektorientierten Bereich daran teilhaben und müssen sich nicht mit einem theoretischen Text und einer nicht realisierten Spezifikation zufrieden geben. Die auch in diesem Kapitel aufgezeigten Schwächen sind nicht wirklich unlösbare Aufgaben, sondern bieten im Gegenteil große Potenziale, die es zu nutzen gilt. Wir brauchen Tools, wir brauchen Prozesse, wir brauchen Entwickler, Designer und Architekten, die sich dieser Herausforderung stellen wollen. Bei AOP ist für jeden etwas dabei, angefangen vom Entwickler, der nur die Performance seiner Anwendung testen will, bis hin zum Architekten, der jede Richclient-Anwendung mit ein paar eingewobenen Aspekten zur skalierbaren Multitier-Applikation verzaubern kann. Besonders spannend ist dabei die Tatsache, dass Java 5 als allererste JavaVersion echten AOP-Schnittstellensupport integriert hat, was Java als AOPPlattform erstmals wirklich interessant macht. In vielen Foren werden Sie beim Stöbern vielleicht auf sehr kritische Stimmen stoßen. Dort hört sich AOP nach einem gefundenen Forschungsmittelding zwischen dem Rütteln an Grundfesten der Softwareentwicklungstheorie und dem Heiland leidgeplagter Stephen-Hawking-Weltraumabenteurer an. Spätestens nach der Sichtung diverser wissenschaftlicher Papiere weisen William Buhlmans Ausführungen im Buch „Out of body“ über die Grundlage von Astralreisen erstaunliche Parallelen zu Diskussionen um AOP auf. AOP scheint an vielen Stellen fast mehr Bezug auf wissenschaftliche Themen der Mathematik und Physik zu nehmen, als man gemeinhin von Programmierung erwarten mag. Als kritischer Leser kann man somit zur Auffassung kommen, hier gäbe es deutliche Hinweise auf einen unvollständigen, noch undurchdachten Programmieransatz. Er sei schön, aber wirkungslos, solange nicht andere sich die Hörner an den damit verbundenen Erkenntnistheorien abgestoßen haben. Einige in der AOP-Entwicklermannschaft erkennen am Horizont schon
AOP
81
1 – Von OOP nach AOP – Evolution der Programmierung
das nahe Ende objektorientierter Programmierung, während andere vom „Anfang vom Ende“ sprechen und eine nicht mehr beherrschbare Komplexität in die Softwareentwicklung Einzug nehmen sehen. Tatsache ist aber, dass aspektorientiertes Vorgehen schon heute gang und gäbe ist. Dabei reden wir weniger von der vergleichsweise noch geringen Schar der AspectJ- oder JBoss AOP-Anhänger sondern, vielmehr von der großen J2EE/Java EE-Gemeinde, die mit Deployment-Deskriptoren einen ähnlichen Ansatz heute schon verfolgt. Java EE 5.0 ist AOP. In Java 5 findet sich mit Annotations nun ein eigenes Aspektsprachmittel. Viele benutzen AOP schon heute wie selbstverständlich, ohne es zu wissen. Versteht man AOP mehr als Produkt, denn als Idee, bleiben Tools wie AspectJ, die die Implementierung von cross cutting concerns extrem erleichtern. Mit AOP lassen sich in Java viele „Hints“ und „Tricks“ einbauen, angefangen von der spontanen Überwachung bestimmter Aufrufe bis hin zur manipulativen Änderung des Softwareverhaltens. Ersteres dürfte besonders jene ansprechen, die heute noch mit AOP auf Kriegsfuß stehen und ein unkontrollierbares Verhalten der Java-Applikation erwarten. Am anderen Ende der Skala befinden sich jene Entwickler, die forschend der Überzeugung verbunden sind, die ganze Software ließe sich aus Aspekten kombinieren, Dimensionen, die in Vererbungshierarchien zueinander Beziehung aufnehmen. Das heißt nicht, dass dies kein lohnenswertes Ziel wäre, sondern lediglich, dass wir es auf der einfachen Basis mit fertigen Fakten und Produktunterstützung zu tun haben, während auf der anderen Seite immer wieder – auch mit Erfolg – Forschung betrieben und Neuland betreten wird und noch betreten werden muss. Jeder neue Entwicklungsansatz, jede neue Sprache, jedes neue Entwicklungsparadigma warf und wirft zunächst neue Fragen auf und bedingt das Finden neuer Lösungsansätze. Mit den inzwischen fast zehn Jahren, die AOP auf dem Buckel hat, ist man hier schon ein ganzes Stück vorangekommen, dennoch sprießen AOP-Pattern nicht so sehr wie OO-Design-Bücher aus dem Boden.
82
Versuch einer Einschätzung von AOP
Kein Paradigma konnte bisher „echte“ Realität in den Computer bringen, auch virtuelle Welten vermögen dies visuell nur scheinbar. Trotz Model Driven Architecture, OO und AOP bleiben Lücken zwischen Anforderungsdefinition und Softwareimplementierung. Zwischen AOP-Fans und Kritikern entbrennt noch heute der Streit, ob AOP nicht ein besserer Precompiler á la C++/CICS oder DB/2 sei, ob AOP nicht dem Namen „strukturierte Metaprogrammierung“ entspräche und ob AOP nicht ein besseres Vehikel eines verkorksten OO-Ansatzes sei, der Rettungsanker einer irreal wirkenden objektorientierten Welt. Der Ehrlichkeit halber muss man das ansprechen, schließlich versucht dieses Buch nicht, Ihnen AOP zu verkaufen, ohne nicht auch berechtigte Kritik zu erwähnen. Tatsache bleibt aber, dass AOP für Java richtig angewandt große Chancen bietet – Chancen, die Architektur zu verbessern, Redundanzen zu verringern und somit die Gesamtevolution und Stabilität des Systems zu verbessern. Die Integration des AOP-Gedankens mit Techniken wie Metatags, Application Servern, generativer oder regelbasierter Softwareentwicklung bietet oft unerkannte Möglichkeiten. Die Fähigkeiten dynamischer AOP, Bytecodemanipulation zur Laufzeit und die Einschleusung programmfremder Aspekte bieten besonders für Großapplikationen großes Potenzial. Heute etablierte Middleware-Applikationen könnten in vielen architektonischen Bereichen im nächsten Schritt ganz andere, vielleicht intelligente Techniken beschreiten, um Verhalten und Struktur zu beschreiben. Betrachtet man AOP als ersten Schritt in eine neue Entwicklungssystematik, kann und soll dieses Buch nur erste Hinweise, aber bei weitem keine endgültigen Lösungen anbieten. Welches sind nun die Schritte, in denen man sich von der heutigen objektorientierten Realität zur AOP-Programmierung herantasten sollte?
1.9.1
AOP zum Einsatz bringen
Eine gewisse Vertrautheit muss mit AOP erst aufkommen, um sich über Sinn, Nutzen und Ziele unterhalten zu können.
AOP
83
1 – Von OOP nach AOP – Evolution der Programmierung
Wie Adrian Colyer von AspectJ ausführt, beginnt der Einstieg in AOP-fremder Umgebung mit einer additiven, ja optionalen Nutzung von AOP. Es handelt sich um Bereiche, die für den produktiven Betrieb einer Software gegebenenfalls verzichtbar sind. Dort hinein gehören qualitätssichernde Maßnahmen wie die Prüfung von eingehaltenen Designpattern, die Messung von Codeabdeckung oder die Gesamtperformance der Anwendung. Hilfreich ist in diesem Fall zweifelsohne die Unterstützung von Projektleitung und Entscheidungsgremien gegebenenfalls nach einer entsprechenden Evaluierungsphase der verschiedenen Produkte und einer Einführung der Entwickler in die Thematik AO. Dann können Schritt für Schritt weitere Einsatzgebiete folgen, bis AO aus den Büros einen OO-Ansatz irgendwann verdrängen könnte. Der Umstieg auf ein AOP-orientiertes Paradigma dürfte indes für einige Entwickler auch mit einer ähnlichen gedanklichen Hürde verbunden sein wie der Umstieg von COBOL auf Java – oder allgemein formuliert von rein prozeduralem zu objektorientiertem Vorgehen. Jene Publizisten, die Aspekten gewidmete Werke verfassen, skizzieren denn auch gerne die Unausweichlichkeit von Aspekten an die Softwarezukunftswände. Sicherlich spielt AOP eine zunehmende Rolle. Ob man sich dessen allerdings in Entwicklerkreisen bei Technologieabstraktionen wie Annotations oder Model Driven Architecture bewusst wird, wenn man nur ein paar Tags im Source unterbringt oder ein paar Verzierungen an UML-Diagramme malt, die später zu einem aspektorientierten Realisierungsansatz führen, ist ungewiss. Vielleicht kann mit AO in kurzer Zeit wahr werden, wofür OO viele Jahre gebraucht hat. Durch die weitere Verbesserung der Wiederverwendbarkeit und die hohe Integration von IDEs und Werkzeugen bei Modellierung und Implementierung wird die Komplexität des AO-Ansatzes zumindest in Teilen vor dem Entwickler verborgen. Somit könnte für einfache und überschaubare, orthogonale Einsatzgebiete der AOP-Ansatz spielerisch zur Wirklichkeit werden. Die Java Enterprise Edition in Version 1.5/5.0, die in
84
Zusammenfassung
Kombination mit JDK 5.0 dafür wesentliche Grundlagen schafft, wird ein wichtiger Wegbereiter sein.
1.10 Zusammenfassung Aspektorientierte Programmierung hilft, die Modularisierung (Separation of Concerns) von Software deutlich zu verbessern, indem sie einen quasi ndimensionalen Raum der Entwicklung aufspannt, der in objektorientierter Entwicklung heute nicht denkbar ist. OO bevorzugt aufgrund seiner Basisidee und der bisher typischen Verfahren in Analyse und Design die Dominanz einiger weniger neuralgischer Aufteilungsverfahren. Sie bilden derzeit noch die Basis für die Dekomposition der Anforderungen in Richtung Klassen und Komponentenstrukturen. Man spricht von typischen 15–20% der OO-Software, die Redundanzen oder technische Querschnittsaufgaben widerspiegeln, die in aspektorientiertem Vorgehen vermeidbar wären. Teilt man auch die reine Geschäftslogik nochmals in Aspektbereiche auf und nutzt nicht nur die Chancen aspektorientierter Erweiterung im technischen Randbereich, wächst der Prozentsatz aspektorientierter Software dramatisch. In einer aspektorientierten Analysephase wird bei AOP das Problem- bzw. Aufgabenumfeld in separate Aspekte aufgeteilt, die nach ihrer Implementierung in einem Rekompositionsverfahren durch einen so genannten Weaver an markanten Stellen des Basis-Sourcecodes (joinpoints) wieder zusammengeführt werden müssen. Man unterscheidet asymmetrische und symmetrische Entwicklungsansätze. Der verbreitetste ist der asymmetrische, indem einem Core-Sourcezweig1 ein Vorrang vor orthogonal verlaufenden, oftmals technisch versierten cross cutting concerns gegeben wird. Beim symmetrischen Paradigma wird der komplette Source modularisiert und in unterschiedliche Aspekte eingeteilt, die später zusammengewoben werden sollen.
1. der gemeinhin die primäre Geschäftslogik enthält
AOP
85
1 – Von OOP nach AOP – Evolution der Programmierung
Des Weiteren unterscheidet man statische und dynamische AO. Bei der statischen können die Bezugspunkte des Aspekts auf den Kernsource zur Laufzeit nicht verändert werden – beim dynamischen hingegen sehr wohl. Hauptsächlich im Einsatz sind im Bereich AOP heute noch Entwicklungsszenarien mit asymmetrisch-statischem Vorgehen. Werkzeuge, Prozesse und Ergebnisqualität werden zukünftig darüber entscheiden müssen, ob durch AOP eine sinnvolle Ergänzung zu OOP oder sogar eine komplett separate oder tendenziell andere Softwareentwicklungsweise entsteht.
86
2
AOP-Frameworks in Java
Das nachfolgende Kapitel stellt einige bekannte Java-Produkte vor, die bei der Umsetzung aspektorientierter Softwareentwicklung behilflich sind. Zwei sehr bekannte Kandidaten werden wir in zahlreichen Beispielen im nächsten Kapitel beleuchten: JBoss AOP und AspectJ. Neben diesen beiden und ihrer Unterstützung in IBMs Eclipse IDE beschreibt das Kapitel kurz einige weitere Kandidaten. Der Schwerpunkt liegt jedoch auf einer vertieften Betrachtung der ersten beiden Produkte, da für sie eine recht gute IDE-Unterstützung und damit eine einfachere Einarbeitung gewährleistet scheint. Ohne Garantie auf Vollständigkeit versucht das Kapitel dabei, erste Anhaltspunkte für die Usability dieser Produkte und ihre Einbindung in moderne Entwicklungssysteme aufzuzeigen.
Ihre Berühmtheit erlangten AspectJ und JBoss AOP durch recht unterschiedliche Voraussetzungen. Auf der einen Seite steht die AspectJ-Crew, die den Begriff der aspektorientierten Programmierung formte und gleichzeitig inzwischen über Jahre hinweg – zuletzt in Kooperation mit IBMs IDE-Flagschiff – für die Java-Sprache wegweisende und innovative AOPLösungen entwickelte. Auf der anderen Seite findet sich das JBoss-Unternehmen, das mit seinem Open Source Application Server und einer inzwischen immer größer werdenden, darum herumrankenden Produktpalette großen Einfluss im Enterprise-Segment von Java gewann. JBoss AOP ist derzeit noch eine der neuesten Entwicklungslinien bei JBoss, hat allerdings aufgrund seiner innovativen Nutzbarkeit und der guten Einbindung in den Application Server schnell an Bedeutung gewonnen. Ein weiterer sehr wichtiger Produktname, den man im Kontext mit Java AOP nicht unerwähnt lassen darf, ist AspectWerkz. Die Entwicklermannschaften von AspectJ und AspectWerkz beschlossen vor einiger Zeit, ihre gemeinsamen Aktivitäten und Lösungsansätze zu koppeln und miteinander in Einklang zu bringen. So ist gut vorstellbar, dass sich in Zukunft die Menge der angebotenen AOP-Lösungen zugunsten eines, bezogen auf den Funktionsumfang, noch mächtigeren Tools verkleinern wird.
AOP
87
2 – AOP-Frameworks in Java
In der Reihe „AOP@work“ findet sich im Internet1 ein interessanter Vergleich der verschiedenen Technologien und Produkte sowie ihrer daraus resultierenden Vor- und Nachteile. Ein Blick ist hier sicherlich undokumentiert empfehlenswert.
2.1 AspectJ Bei AspectJ handelt es sich um eine Java-Spracherweiterung für AOP, die als Unterprojekt auf der Eclipse-Webseite2 zu finden ist. Das inzwischen gut zehn Jahre in Entwicklung befindliche Projekt startete in den Forschungslabors von Xerox Palo Alto und sein Sourcecode steht seit Version 1.1 unter Common Public License. Das Produkt ist somit kostenfrei herunterladbar. Neben dem Support für die Java-Sprache gibt es die Unterstützung für C (als AspectC), eine Erweiterung für .NET-Sprachen wie Visual Basic oder C# sind derzeit nicht Ziel des Projekts, aber denkbar. Neben der inzwischen rund 10 MB umfassenden AspectJ-Version gibt es eine Reihe von Plugins für bekannte Entwicklungsumgebungen, darunter Erweiterungen für Eclipse (derzeit noch in Version 3.1), aber auch Suns NetBeans3 oder Borlands Jbuilder4-Versionen. Weitere IDEs wie IDEA/IntelliJ oder Oracles JDeveloper werden teilweise durch zusätzliche, separate Produkte ebenfalls bereits mit AspectJ kombinierbar gemacht. Des Weiteren kann das Einweben von Aspekten in bestehenden und fremden Sourcecode über entsprechende Ant-Tasks in den Buildprozess integriert werden. Die AspectJ-Technologien bestehen aus zwei Hauptteilen, zum einen der aspektorientierten Sprachspezifikation, zum anderen aus einer Reihe von Tools, einem Compiler (ajc), einem Debugger (ajdb), einem Programmstrukturbrowser (ajbrowser) und einem Dokumentationsgenerator (ajdoc). Für die Einbindung in die Entwicklungsumgebung Eclipse gibt es beispielsweise bereits angepasste Compiler-, Debug- und Browsetoolelemente (visu1. 2. 3. 4.
88
http://www-128.ibm.com/developerworks/java/library/j-aopwork1/ http://eclipse.org/aspectj http://aspectj4netbean.sourceforge.net http://aspectj4jbuildr.sourceforge.net
AspectJ
alizer), die unter dem Namen AJDT1 ebenfalls auf der Eclipseseite bzw. über den Eclipse-Update- und -Installationsmechanismus in eine bestehende IDE-Version integrierbar sind. AspectJ arbeitet vorwiegend in einem Bytecode-zentrierten Weavingverfahren mit den erzeugten Java-Klassen. Das bedeutet, die Integration der Aspekte in den bestehenden Java-Programmablauf geschieht in einem an den eigentlichen Bytecode-Compile anschließenden Schritt. Der AspectJ Compiler ist aber auch in der Lage, Compile und Weaving in einem einzelnen Schritt zu unterstützen. Innerhalb der Integration in die aktuelle Eclipse IDE nutzt AspectJ bzw. AJDT die inkrementellen Buildoptionen, so dass nicht der komplette Sourcecode, sondern nur die durch die Anwendung eines Aspekts bzw. durch die Modifikation von Klassen betroffenen, abhängigen Java-Programmelemente neu kompiliert werden müssen. Allerdings sei an dieser Stelle angemerkt, dass der inkrementelle Compile unter Eclipse 3.1 in einigen früheren Versionen auch recht holprig verlief. Die Option zur vollständigen Kompilierung steht allerdings ohne weiteres stets offen, wenn sie auch mit erheblichem Zeitaufwand bei großen JavaProjekten verbunden sein dürfte. Damit ein mit Aspekten aufgewertetes Programm lauffähig ist, wird eine rund 100 Kbyte große zusätzliche AspectJ-Runtime-Bibliothek benötigt, die dem modifizierten Programm zur Ausführung im Klassenpfad beizulegen ist. AspectJ ist grundsätzlich in der Lage, auch mit älteren Java-Versionen ab Java 1.1 umzugehen, dennoch sollte AspectJ selbst unter Java 2 laufen. Es unterstützt ab seiner Version 5.0 natürlich auch die Spezifika und Erweiterungen der Java 5-Sprache, darunter Sprachelemente wie Generics oder die im vorherigen Kapitel angesprochenen Annotations.
1. http://eclipse.org/ajdt
AOP
89
2 – AOP-Frameworks in Java
2.1.1
Die AspectJ-Sprache
Wir haben bereits im vorangegangenen Kapitel eine ganze Reihe von AspectJ-Beispielen auf relative intuitive Art und Weise kennen gelernt. An dieser Stelle sei ein kurzer Überblick über die Sprache an sich gewährt. Für eine tiefergehende Einarbeitung wird ein Blick in den AspectJ-Programming Guide empfohlen1. Die AspectJ-Sprache unterscheidet Pointcuts und Joinpoints. Während ein Pointcut die Menge der möglichen Einstiegspunkte eines Aspekts benennen hilft, definiert der Joinpoint die tatsächlichen und mit Hilfe des Advice die geltenden Rahmenbedingungen. Aspekt-Klassendefinition Basissyntaxelemente Die grundsätzliche Syntax für einen Aspekt in AspectJ sieht wie folgt aus: [ privileged ] Modifizierer aspect [ extends ] [ implements ] [ PerClause ] { AspectBody }
Neben den typischen Syntaxelementen, wie man sie auch in Java-Klassen findet (Modifizierer, Name, extends, implements, Body), gibt es weitere Schlüsselwörter (privileged, aspect und perClause). Privileged erlaubt dem Aspekt den expliziten Zugriff auf nicht sichtbare Elemente. Aspect zeigt an, dass nachfolgend ein Aspekt definiert wird. PerClause definiert, wie eine Aspektinstanz zu bilden ist. Schauen wir uns dazu diese Elemente nochmals in einem kleinen Übersichtsbeispiel an. Beispiel: package test; public aspect SampleAspect issingleton() { pointcut classesToLog(): within(SampleClass);
1. http://eclipse.org/aspectj/doc/released/progguide/index.html
90
AspectJ pointcut outputIntercept(): classesToLog() && execution(* *(..)); pointcut pp2(): call(* *(..)) && within(B); before() : outputIntercept() || pp2() { System.out.println("Los geht's"); System.out.println( thisJoinPoint.toLongString()); } }
Das Schlüsselwort privileged erlaubt dem Aspekt den Zugriff auf private und ansonsten nicht sichtbare Elemente/Klassen. Für die meisten Anwendungsfälle dürfte es aber nicht notwendig sein. PerClause PerClause (im Beispiel isSingleton()) definiert, in welchem Kontext der As-
pekt wann instanziiert und wo assoziiert wird. Standardmäßig sind Aspekte Singletons, statische Daten sind also Classloader-weit gültig. Dies kann verändert werden, z.B. so, dass ein Aspekt sich auf eine bestimmte Klasse oder sogar einen bestimmten Joinpoint bezieht. Dies ist durch explizites Setzen des PerClause-Elements in der Syntax möglich. issingleton()
à Ein Aspekt pro Aspekt-Classloader. perthis()
à Der Aspekt ist mit jedem gemäß des Pointcut aufrufenden Element assoziiert und wird gemäß der Instanziierung dieses Pointcut erzeugt. pertarget()
à Der Aspekt ist mit jedem gemäß des Pointcut aufgerufenen Element assoziiert.
AOP
91
2 – AOP-Frameworks in Java percflow(Pointcut)
à Der Aspekt ist mit jedem Eintritt in den Programmkontrollfluss assoziiert, der gemäß Pointcut definiert wird. percflowbelow(Pointcut)
à Der Aspekt ist mit jedem Eintritt in den Programmkontrollfluss assoziiert, der gemäß Pointcut unterhalb des Pointcut definiert wird. Mittels des Schlüsselworts aspectOf() kann Zugriff auf eine bestimmte Aspektinstanz genommen werden. Pointcutdeklarationen Der Pointcut definiert ähnlich einem SQL-Select-Statement eine Menge von möglichen Joinpoints, die in einem Aspekt zum Tragen kommen können. Die möglichen Pointcutdeklarationen umfassen folgende Schlüsselwörter und deren Bedeutung: call(void B.doIt()) && within(A)
à A ruft eine Methode auf B auf. execution(void B.doIt())
à Eine Methode auf B wird aufgerufen. this(A)
à Die aktuelle Klasse ist von einem bestimmten Typ. target(B)
à Die Zielklasse des Aufrufs ist von einem bestimmten Typ. within(C)
à Auszuführender Code gehört zu einer bestimmten Klasse.
92
AspectJ handler(CException)
à Ausführung eines Exceptionhandlers cflow(call(void B.doIt()))
à Die Ausführung erfolgt innerhalb einer in der doIt()-Methode verschachtelten Methode. cflowbelow(call(void B.doIt()))
à Die Ausführung erfolgt innerhalb einer in der doIt()-Methode verschachtelten Methode, außer der Pointcutdefinition selbst. args (x)
à Das übergebene Argument an die Methode ist x. get(Type field)
à Referenz auf ein Feld mit der übergebenen Signatur set(Type field)
à Zuweisung eines Werts zu einem gegebenen Feld staticinitialization(Type)
à Ausführung des statischen Initializer für einen Typ Type. initialization(Type)
à Initialisierung einer Instanz durch Aufruf eines Konstruktors der Klasse Type preinitialization(Type)
à Vorinitialisierung einer Instanz durch Aufruf eines Konstruktors der Klasse Type adviceexecution()
à Eine beliebige Advice-Ausführung selbst
AOP
93
2 – AOP-Frameworks in Java
Innerhalb der Deklaration dieser Pointcuts besteht die Möglichkeit, einzelne Elemente mit Wildcards auszustatten und damit unvollständige Methodensignaturabgleiche zu ermöglichen. execution(* *(..))
à „* *“ definiert eine beliebige Methode unabhängig von Rückgabewert, Sichtbarkeit, Klassen- oder Methodenname. Die Punkte „(..)“ in den Klammern weisen auf eine beliebige Anzahl von Parametern der Methoden hin. call(*.new())
à Aufruf aller Default-Konstruktoren call(*Helper+.new())
à Aufruf eines Konstruktors einer Klasse, die mit dem Namen Helper endet, oder einer ihrer Unterklassen (definiert durch den Plusoperator). Mehrere Pointcutdefinitionen können miteinander über boolesche Logikelemente verbunden werden. Das erlaubt es, die Menge der betroffenen potenziellen Joinpoints zu vergrößern oder auch zu verkleinern, abhängig vom Sourcecode, auf den er angewendet wird. Als boolesche Operatoren stellt AspectJ UND („&&“), ODER („||“) und NICHT („!“) bereit. call(* *(..)) && within(A)
à Alle Methodenaufrufe aus der Klasse A heraus Syntaxbeispiele: pointcut testPointcut(int x): args(x) && call(static int Calculator.calc(int)) && if (x > 1); pointcut invocations(Caller c): this(c) && call(void Service.execute(String));
94
AspectJ pointcut workingPoints(Worker w): target(w) && call(void Worker.do(Task)); pointcut perCallerWork(Caller c, Worker w): cflow(invocations(c)) && workingPoints(w);
Benannte Pointcuts Pointcuts können benannt werden oder anoym bleiben. Die Benennung von Pointcuts hilft, ihren Inhalt klarer zu fassen und Pointcutdefinition und Aspekt-Advices voneinander zu trennen und wiederverwenden zu können. Zusätzlich ist es möglich, über entsprechende Deklarationen Bezug auf die aufrufenden bzw. aufgerufenen Objektinstanzen und übergebene Parameter zu nehmen: pointcut testPC(A a, B b): target(b) && args(a) && call(void doIt(A));
à Aufruf der Methode doIt() auf b mit Übergabe des Parameters a. Die Pointcutdefinition muss in Klammern die Elemente des Joinpoint benennen, die später in der Advice-Implementierung genutzt werden sollen. Kombinationen von Pointcutdefinitionen und Advices Der reine Pointcut beschreibt lediglich die möglichen Stellen des Sourcecodes, an denen Events von Interesse sein könnten. Der Advice kombiniert nun den Aspekt bzw. Aspektteile oder -bereiche mit definierten Eventtypen auf einer durch einen benannten oder anoymen Pointcut vorbeschriebenen Sourcecodeelementmenge. Hier sehen wir einen benannten Pointcut (testPC) in Kombination mit einem Advice:
AOP
95
2 – AOP-Frameworks in Java pointcut testPC(A a, B b): target(b) && args(a) && call(void doIt(A)); before(A a, B b) : testPC(a, b) { System.out.println("" + a + " is called on " + b); }
Im unbenannten Fall verschmelzen Advice und Pointcutdefinition: before(A a, B b) : target(b) && args(a) && call(void doIt(A)) { System.out.println("" + a + " is called on " + b); }
Folgende Advice-Typen sind vorgebbar: before() :
à Ausführung des Aspekts vor Erreichen des durch den Pointcut definierten Joinpoints ReturnType around() :
à Ausführung anstelle des im Joinpoint definierten Programmteils. Der tatsächlich anzusprechende Joinpoint kann über proceed() ausgeführt und der Rückgabetyp (ReturnType) per return an den Aufrufer zurückgegeben werden. after() :
à Ausführung des Aspekts nach Ausführung des Joinpoint, egal ob dieser normal erfolgreich (z.B. Erreichen des return-Statement einer Methode) oder durch eine Ausnahme (Exception/Throwable-Unterklasseninstanz) beendet wurde.
96
AspectJ after() returning [ () ] :
à Ausführung nach erfolgreicher Ausführung des Joinpoint und Verlassen des Joinpoint über return after() throwing [ (Type) ] :
à Ausführung ausschließlich, falls der Joinpoint bei seiner Ausführung eine Exception/Throwable-Unterklasseninstanz wirft. Bei Definition von Type muss der Typ der Exception entsprechend gelten. Schlüsselwörter in Aspektdefinitionen Innerhalb der Aspektmethoden kann Zugriff auf den Umgebungskontext, in dem der Aspekt ausgeführt wird, genommen werden. Hierfür definiert AspectJ die folgenden drei Variablen, die im Aspekt ansprechbar sind: thisJoinPoint
à Variable vom Typ org.aspectj.lang.JoinPoint, die eine Referenz auf die Beschreibung des aktuell ausgeführten Joinpoint beschreibt. Entspricht der Bedeutung des Java-Schlüsselworts this für eine Klasse, in diesem Fall allerdings für den Joinpoint. thisJoinPointStaticPart
à Entspricht dem Aufruf der Methode thisJoinPoint.getStaticPart(). thisEnclosingJoinPointStaticPart
à Statischer Teil des einschließenden Joinpoint Komplexe Deklarationsbeschreibungen in AspectJ declare parents : ImplType implements TypeList
à Die ImplType-Typen, die TypeList implementieren declare parents : SubType extends Type
à Die SubType-Typen, die von Type erben
AOP
97
2 – AOP-Frameworks in Java declare warning : :
à Falls sich unter der Pointcutdefinition ein Joinpoint im Programm findet, wirft der AspectJ-Compiler eine Warnung aus. Der Compile wird trotzdem fortgesetzt. Der Text wird hinter der Pointcutdefinition im Element definiert. declare error : :
à Falls sich unter der Pointcutdefinition ein Joinpoint im Programm findet, wirft der AspectJ-Compiler einen Fehler aus. Der Compile wird gegebenenfalls nicht fortgesetzt. Der Text wird hinter der Pointcutdefinition im Element definiert. declare soft : Type :
à Alle Exceptions vom Typ Type, die in einem Joinpoint auftreten, der durch den Pointcut definiert wird, werden in eine org.aspectj.lang. SoftException (Unterklasse von RuntimeException) überführt und geworfen. declare precedence : TypePatternList
à Bei jedem Joinpoint, auf den mehrere Aspekte zutreffen, ist die Reihenfolge durch die Typepatternliste zu definieren. Manuelle Compiles mit ajc ajc ist der Name des AspectJ-Compilers, der das Weaving von Aspekten und Klassen sowohl auf Java-Sourcen als auch auf bereits fertigem Bytecode oder JAR-Files durchführen kann.
Der ajc ist selbst eine Java-Klasse (org.aspectj.tools.ajc.Main) und kann daher sehr leicht zur Laufzeit aufgerufen und in eigene Anwendungen integriert werden, wobei Compilemessages über einen so genannten IMessageHolder weiter verwendet werden können. Seine Verwendung ähnelt sehr der des typischen Javac-Compilers, die Compilerparameter sind wie folgt:
98
AspectJ ajc [Options] [file... | @file... | -argfile file...] -inpath Path
Eingabepfad, der dem Klassenpfad ähnelt und all jene Dateien und Verzeichnisse umfasst, in die potenziell Aspekte durch den Compiler integriert werden sollen 쐌
-argfile File
Datei, die eine Liste mit Parametern für den Compiler enthält. Die Parameter sind durch Zeilenumbrüche voneinander getrennt. 쐌
-aspectpath JarList
Jar-Liste mit enthaltenen Aspekten im Binärformat 쐌
-bootclasspath Path
Typischer Bootstrap-Classpath-Override der VM 쐌
-classpath Path
Typischer Klassenpfad mit plattformspezifischem Delimiter 쐌
-d Directory
Outputdirectory für erzeugten Bytecode 쐌
-deprecation
Entspricht dem Parameter -warn:deprecation 쐌
-encoding format
Default Source Encoding-Format 쐌
-extdirs Path
Überschreiben des Extension-Verzeichnisses der VM durch Umleitung auf ein anderes Verzeichnis 쐌
-g:[lines,vars,source]
Definition der in den Bytecode einzukompilierenden Debugattribute -g:lines,vars,source: vollständiger Debugoutput -g:none: keine Debugattribute einpflegen -g:{items}: Debugdaten aus den folgenden Item-Typen: lines, vars, source
AOP
99
2 – AOP-Frameworks in Java 쐌
-help
Aufruf der Hilfe des Compilers zur Anzeige der Compile-Optionen 쐌
-incremental
Unterstützung für inkrementellen Compile. Kompiliert wird nur, wenn durch den Standard-Input Stream-Daten gesandt werden, wobei der Compiler darauf achtet, nur notwendige Files erneut zu kompilieren. 쐌
-log file
Logoutput-Datei für Compilernachrichten 쐌
-noExit System.exit() nach vollzogener Kompilierung nicht ausführen
쐌
-noImportError
Unterdrückt Fehler für nicht auflösbare Importstatements. 쐌
-nowarn
Unterdrücken von Warnungen beim Compilelauf, außer explizit deklarierten Warnings 쐌
-outjar output.jar
Erzeugte Zielklassen sollen in dieses Output-Jar einfließen. 쐌
-preserveAllLocals
Bewahrung der lokalen Variablen während der Codegenerierung 쐌
-proceedOnError
Fortsetzen des Compile im Fehlerfall 쐌
-progress
Ausgabe des Compilevorgangs 쐌
-referenceInfo
Ermittlung von Referenzinformationen 쐌
-repeat N
n-fache Wiederholung des Compileprozesses 쐌
-showWeaveInfo
Ausgabe von Weaving-Informationen
100
AspectJ 쐌
-source [1.3|1.4]
Sourcecodeversion der eingehenden Java-Dateien 쐌
-sourceroots DirPaths
Menge aller Verzeichnisse, die Java- oder .aj-Dateien enthalten, die kompiliert werden sollen. Dieser Parameter wird bei inkrementellem Compile benötigt. 쐌
-target [1.1 bis 1.5]
Classfile-Zielversion 쐌
-time
Anzeige der Verarbeitungsgeschwindigkeit 쐌
-verbose
Ausgabe aller betroffenen und bearbeiteten Klassen (compilation units) 쐌
-version
Compilerversionsausgabe 쐌
-warn: elementList
Ausgabe von Warnings bei Auftreten eines Verstoßes gegen die in der Elementliste aufgeführten Programmfehler. Mögliche Werte umfassen: constructorName: Methode trägt den Namen eines Konstruktors deprecation: Verwendung von deprecated Elementen maskedCatchBlocks: Nutzung versteckter Catch-Blöcke packageDefaultMethod: Überschreiben einer Package-default-Methode unusedArguments: ungenutzte Methodenargumente unusedImports: ungenutzte Importstatements unusedLocals: ungenutzte lokale Variablen none: Unterdrückung aller Warnungen 쐌
-Xlint:{level}
Default-Schweregrad für potenzielle Programmierfehler. Default ist Warning. 쐌
-Xlintfile PropertyFile
PropertyFile, der den Messagelevel spezifischer Crosscutting-Messages überschreibt
AOP
101
2 – AOP-Frameworks in Java 쐌
-XnoInline
Around-Advices nicht durch Sourcecodeinlining integrieren 쐌
-Xnoweave
Erzeugung von class-Dateien ohne eingewobene Aspekte 쐌
-Xreweavable[:compress]
Zwingt den Compiler, Klassen zu erzeugen, die erneut mit weiteren Aspekten in einem späteren Compilevorgang durchsetzt werden können. 쐌
-XserializableAspects
Unterdrückung des Fehlers, dass Aspekte nicht java.io.Serializable implementieren dürfen 쐌
-1.3
Java 1.3-Kompatibilität 쐌
-1.4
Java 1.4-Kompatibilität Weitere AspectJ-Tools Neben dem AspectJ-Compiler selbst liefert das Produkt noch einen ajdocJavadoc-Generator, einen AspectJ-Browser zur Anzeige der in einen Sourcecode gewobenen Aspekte und der dafür verwendeten Joinpoints sowie einen AspectJ Ant Task zur Integration des Aspektweaving in den Buildprozess (iajc).
2.1.2
Beispiel mit AJDT in Eclipse
AJDT, das AspectJ Developers Toolkit, ist eine Eclipse-Erweiterung, die auf der IDE-Webseite als Subprojekt erhältlich ist. AJDT unterstützt die Entwicklung von Java-Aspekten in der Eclipse-IDE mit AspectJ und bringt dafür intern das AJDE (AspectJ Developers Environment) mit, das die dafür notwendigen AspectJ-Bibliotheken und Anteile enthält. Das AJDT besteht aus dem AJDE sowie einem Kern (core) und einem Userinterface (UI-)Teil sowie einigen Beispielen und einer AspectJ-Runtimeumgebung. Zusammen umfasst der Download aus Eclipse heraus runde 16 MB,
102
AspectJ
allerdings auch abhängig von der gewählten Version und den selektierten Bestandteilen. Bei Erstellung dieser Dokumentation war AJDT in Version 1.3.0 vom 25.08.2005 erhältlich, was dem Milestone 3 der Version AspectJ Version für 1.5 entspricht. Zusätzlich kommt das AJDT-Paket mit einer Visualizer-Erweiterung daher, die eine Anzeige der Codeeinwebungen der Aspekte in die Klassen erlaubt. AJDT im Einsatz Von der Eclipse-Webseite können Sie sowohl die freie Entwicklungsumgebung1 als auch das AJDT herunterladen. Für das nachfolgende Beispiel schauen wir uns Installation und Nutzung von AspectJ in Eclipse 3.1 an. Werfen Sie bei der Installation einen Blick auf die derzeit aktuelle EclipseVersion und schauen Sie sich gleichzeitig nach einer passenden AJDT-Version um. Zumeist wird die AJDT-Erweiterung erst mit zeitlichem Versatz herausgebracht, weshalb Sie durchaus auf neue Eclipse-Versionen (z.B. zum Erstellungszeitpunkt Eclipse 3.2) treffen können, die vom AJDT gegebenenfalls noch nicht unterstützt werden. Nach dem Download des 103 MB großen Pakets von der Eclipse-Seite und einer passenden JDK Java-Version in Größe von ca. 60 MB z.B. von Sun2 (in den jeweiligen Windows-Versionen) kann es bereits losgehen. Nach dem Start wird über die Updatefunktion von Eclipse (Help | Softwareupdates | Find and install... | Search for new features to install) eine Verbindung zur Eclipse/AJDT-Seite aufgebaut, indem die Updateseite3 für AJDT neu erfasst wird. Danach kann eine entsprechende Version heruntergeladen und für Eclipse installiert werden (siehe Abbildung 2.1). Ein Neustart integriert die AspectJSoftware in die Entwicklungsumgebung.
1. http://www.eclipse.org/downloads/index.php 2. http://java.sun.com/j2se/1.5.0/download.jsp 3. http://download.eclipse.org/technology/ajdt/31/dev/update
AOP
103
2 – AOP-Frameworks in Java
Abb. 2.1: AJDT Update Site – Auswahl der zu installierenden Features
Nach der Installation macht es Sinn, die Einstellungen für die AspectJ-Unterstützung nochmals im Dialog Window | Preferences anzupassen. Wer an der Eclipse-Debugunterstützung verstärktes Interesse zeigt, sollte in den Advanced-Eigenschaften das Inlining, also die Verschmelzung von Aspekten und Basissource, deaktivieren (siehe Abbildung 2.2). Unter der Other-Lasche findet sich auch die Einstellung für das CompileVerhalten. Sie können zwischen inkrementellem Compile und vollständigem Kompilat wählen. Obwohl dem AspectJ Compiler eine Unterstützung für inkrementellen Compile zugesagt wird, hatten einige AJDT-Versionen hiermit immer wieder Probleme. Grundsätzlich sollten Sie diese Funktion aktivieren, solange Sie nicht definitiv sicher sind, dass sie in der konkreten Umgebung nicht zum Laufen zu bringen ist. Im zweiten Fall hilft häufig nur ein manueller Compile auf dem jeweiligen Eclipse-Projekt (über ein explizites Clean).
104
AspectJ
Abb. 2.2: AspectJ-Eigenschaften in Eclipse
Nach der Einrichtung von AspectJ starten wir nun eine erste einfache Implementierung mittels eines Aspekts. Dafür verwenden wir die klassische Aspektdefinition in AspectJ. Über das Projekt-Menü (New | Project | AspectJ Project) kann man ein neues, vorkonfektioniertes AspectJ-Projekt anlegen lassen. Dabei werden eine build.ajproperties-Datei, die konfiguriert, welche Dateien potenziell vom Weavingprozess berührt werden können, und die aktuelle AspectJ-RuntimeBibliothek bereits zu Bestandteilen des Projekts (siehe Abbildung 2.3). Etwas versteckt verbirgt sich hinter dem Eclipse-AspectJ-Projekt auch eine andere Buildeinstellung, die sich über die Properties des Projekts anzeigen lässt. Über die Projektproperties lassen sich die Eigenschaften und das Verhalten des Compilers nochmals individuell für dieses Projekt definieren, darunter auch, wo gegebenenfalls neue JAR-Files erzeugt werden und welche weiteren Bibliotheken beim Buildprozess mit einzubinden sind.
AOP
105
2 – AOP-Frameworks in Java
Abb. 2.3: Angelegtes AspectJ-Projekt
Die Umrüstung bestehender Java-Eclipse-Projekte ist ebenso leicht vollbracht wie die Entfernung der Aspektunterstützung. Über die rechte Maustaste auf dem Projekt lässt sich die so genannte AspectJ-Erweiterung (AspectJ nature) aktivieren und deaktivieren. Über das Menü File | New | Other... kann man nun klassische Java-Klassen und Interfaces im AspectJ-Beispielprojekt anlegen, neue Aspekte definieren oder bestehende als Beispiele integrieren.
Abb. 2.4: Anlegen eines neuen Aspekts 106
AspectJ
Nach dem Anlegen des Aspekts über den Next-Button und der Auswahl von Finish testen wir ihn mit einem sehr einfachen Basisaspekt mit folgendem Inhalt: package test; public aspect SampleAspect issingleton() { pointcut classesToLog(): within(SampleClass); pointcut outputIntercept(): classesToLog() && execution(* *(..)); before() : outputIntercept() { System.out.println("Los geht's"); System.out.println( thisJoinPoint.toLongString()); } }
Hier werden zwei Pointcuts definiert, die aufeinander Bezug nehmen, so dass der zweite die Definition des ersten ergänzt. Der anschließende before()-Advice schreibt einmal den „Los geht's“-String und den Namen der Methode als Langtext der intercepteten Methode auf die Konsole.
Abb. 2.5: AspectJ-Aspekt-Editor
AOP
107
2 – AOP-Frameworks in Java
Fehlt schließlich noch die zugehörige Klasse, auf die sich der Aspekt bezieht. package test; public class SampleClass { public static void main(String[] args) { SampleClass sc = new SampleClass(); sc.testMethod(); } private void testMethod() { System.out.println("Method"); } }
Hier zeigen sich jetzt schnell die Vorteile der Integration in die IDE. Spezielle Pfeile im Sourcecode, aber auch Cross-Reference-Anzeigen weisen den Weg, wo Aspekte in den Source eingebunden sind und um welche Aspekte es sich dabei handelt (siehe Abbildung 2.6).
Abb. 2.6: View-Unterstützung
108
AspectJ
Die entwickelte AspectJ-Applikation wird nun über Run As... bzw. Debug As... AspectJ/Java-Application gestartet. Ist das Inlining von Aspekten wie oben beschrieben deaktiviert, gelingt auch das Debuggen in die Aspekte hinein (siehe Abbildung 2.7).
Abb. 2.7: Debugunterstützung im AJDT
Über eine derartige Softwareunterstützung ist die Erstellung und Erweiterung aspektorientierter Softwareapplikationen sehr leicht machbar. Für den Überblick, wo Aspekte überall in den Sourcecode eingreifen, bringt das Toolpaket zusätzlich den so genannten Visualizer mit. Über eigene Views und die AspectJ Visualizer-Perspektive in Eclipse bleibt auch die Übersicht in vielen Klassen und Packages nicht auf der Strecke. Für eine detaillierte Auflistung aller Features (z.B. auch der Erweiterungen für Java 5.0/AspectJ 5.0 in Richtung Annotations) wird ein Blick in die Hilfedokumentation empfohlen, die standardmäßig mit dem AJDT mitgeliefert wird. Darunter findet sich auch eine sehr ausführliche Anleitung zum Anlegen erster Aspekte, aber auch zur AspectJ-Sprachspezifikation samt Beispielen für die Deklaration neuer Aspekte.
AOP
109
2 – AOP-Frameworks in Java
2.2 JBoss AOP JBoss, bekannt geworden durch die Herstellung eines Open Source Java Enterprise Edition Servers, bietet innerhalb ihres Produktportfolios mittlerweile auch eine AOP-Erweiterung namens JBoss AOP1 an. JBoss AOP ist dabei nicht nur eines von vielen JBoss-Produkten, sondern gleichzeitig wesentlicher Wegbereiter für die Umsetzung von Java EE 5 und EJB 3.0. Die JBoss-Variante eines AOP-Tools bringt ähnlich wie AspectJ einen Weaver, entsprechende Frameworkklassen, eine Integration in die Eclipse IDE und Support für Java 5 Annotations mit. Darüber hinaus enthalten die JBoss AOP Framework-Bibliotheken aber auch bereits fertige Aspekte für Themenbereiche wie Transaktionsmanagement, Security oder Caching. Die Entwicklung neuer Aspekte ähnelt – wie wir später sehen werden – in vielen Punkten einem Mittelding aus der Entwicklung dynamischer Proxies (sie verläuft im Wesentlichen Java-Syntaxbehaftet, ohne Integration eigener Schlüsselwörter wie bei den alten AspectJ-Versionen) in Kombination mit typischem Pointcut-Binding. JBoss AOP bringt Fähigkeiten eines dynamischen AOP-Systems mit, indem es in der Lage ist, auch einzelne Objektinstanzen ad hoc in ihrem Verhalten zu modifizieren. Dazu kommt noch die Unterstützung für speziell eingeführte Vererbungen in Java (mixins) als derzeit herauszustellende Merkmale. JBoss AOP Weaving-Strategien Im vorangegangenen Kapitel wurden die verschiedenen Weavingstrategien bereits angesprochen. JBoss AOP unterstützt dabei sowohl das Compiletime-Weaving als auch Classloader-Weaving wie auch Hotswap-Weaving, also Einführung von Binding und die Modifikation von Klassen oder sogar einzelnen Instanzen zur Laufzeit.
1. http://www.jboss.org/products/aop
110
JBoss AOP
Letzteres ist allerdings im Vergleich zu einer vollkommenen Bytecode-Engineering-Fähigkeit, wie wir sie z.B. beim BCEL-Tool gesehen haben, funktional eingeschränkt, trotzdem aber nicht minder interessant. Beim Compiletime-Weaving werden die Aspekte bereits im PrecompileProzess eingebracht. So eingewobene Aspekte müssen nachträglich zur Laufzeit nicht mit dem Code verknüpft werden und führen so zu potenziell schneller laufenden Anwendungsstartups. Diese Form der Aspekte sind zur Laufzeit dann aber auch nicht mehr modifizierbar. Beim Loadtime-Weaving über den Classloader hängt sich JBoss AOP in jeden beliebigen Classloader ein und modifiziert dessen Verhalten bzw. den Bytecode. Das Hotswapping und die Per-Instance-Interception beschäftigen sich mit dem Einbringen von Aspekten für alle Klassen oder spezielle Instanzen innerhalb der VM zur Laufzeit. Das Thema wird im entsprechenden Kapitelabschnitt später noch angesprochen. Interceptoren und Invocation-Typen Ein JBoss-Einführungsbeispiel haben wir im vorangegangenen Kapitel bereits kennen gelernt. Zur Erinnerung hier ein sehr einfach strukturierter Aspekt, der vor der Joinpoint-Ausführung „Hallo Welt!“ auf die Konsole schreibt. public class HelloInterceptor implements org.jboss.aop.advice.Interceptor { public Object invoke(Invocation invocation) throws Throwable { System.out.println("Hallo Welt!"); return invocation.invokeNext(); } }
AOP
111
2 – AOP-Frameworks in Java
Dieses Beispiel wurde mit einem Interceptor erzeugt. Das ist grundsätzlich aber kein Zwang für Aspekte. Die Invocation-Instanz entspricht in etwa der thisJoinPoint-Variable aus AspectJ und kapselt den Joinpoint selbst. Im Gegensatz zum proceed()Aufruf von AspectJ, in dem die eigentliche Joinpoint-Primärlogik aufgerufen wird, führt der Aufruf von invocation.invokeNext() bei JBoss AOP zur Ausführung. Sie müssen an dieser Stelle selbst entscheiden, welche Syntax Ihnen nun intuitiver erscheinen mag. Der konkrete Typ von invocation hängt von der Art des Joinpoint bzw. der Pointcutdefinition ab. Einige Informationen (z.B. bei Methoden über exakte Signaturen und Parameter) sind erst nach einem Typecasting auf den konkreten Typ erreichbar. Folgende Invocationtypen sind in JBoss AOP definiert: 쐌
org.jboss.aop.joinpoint.MethodInvocation
Methoden-Joinpoint beim Intercepten einer Methode 쐌
org.jboss.aop.joinpoint.ConstructorInvocation
Konstruktor-Joinpoint beim Intercepten eines Konstruktors 쐌
org.jboss.aop.joinpoint.FieldInvocation
Abstrakter Feld-Joinpoint beim Intercepten eines Felds 쐌
org.jboss.aop.joinpoint.FieldReadInvocation
Lesender Zugriff auf eine Instanzvariable 쐌
org.jboss.aop.joinpoint.FieldWriteInvocation
Modifizierender Zugriff auf eine Instanzvariable 쐌
org.jboss.aop.joinpoint.MethodCalledByMethod
Method-Caller Pointcut zur Referenzierung von Aufrufendem und Aufgerufenem 쐌
org.jboss.aop.joinpoint.MethodCalledByConstructor
Method-Caller Pointcut zur Referenzierung von Aufrufendem und Aufgerufenem
112
JBoss AOP 쐌
org.jboss.aop.joinpoint.ConstructorCalledByMethod
Konstruktor-Caller Pointcut zur Referenzierung von Aufrufendem und Aufgerufenem 쐌
org.jboss.aop.joinpoint.Constructor.CalledByConstructor
Konstruktor-Konstruktor-Call-Joinpoint Ein Aspekt ist in JBoss AOP ganz allgemein als einfache (plain) Java-Klasse realisierbar, die eine Menge von Advices, Pointcutdefinitionen und weitere Konstrukte kapselt. public class TracingAspect { public Object trace(Invocation invocation) throws Throwable { System.out.println("Start execution"); return invocation.invokeNext(); } }
Die Methode trace() entspricht in diesem Fall der Advicedefinition. Wichtig ist die Einhaltung des Signaturpatterns: Object methodName(Invocation object) throws Throwable
Der Methodennamen ist dabei frei wählbar, die Parameter dürfen auch die oben aufgeführten Untertypen von Invocation sein, um auf unterschiedliche Aufrufe unterschiedliche Advices anzuwenden. Im Gegensatz zur Implementierung des Interceptor-Interface muss in dieser Implementierungsvariante aber der Methodenname im späteren Binding explizit angegeben werden. Bindings Um einen Aspekt auf den Hauptsourcestrang anzuwenden, wird ein Binding, also eine Zuordnung zwischen Aspekt und Basissource, benötigt. Diese wird bei JBoss z.B. in XML definiert:
AOP
113
2 – AOP-Frameworks in Java
Binding-Varianten:
Die erste Variante definiert einen Pointcut für den Aufruf der Methode doIt() auf der Klasse A.
Variante 2 beschreibt einen Pointcut, der auf alle Klassen abzielt, die eine Methode doIt() mit int-Parameter besitzen:
Die dritte Vairante unterbricht einfach alle Methodenaufrufe, unabhängig von den Parametern:
Die drei gezeigten Varianten zeigen von einer sehr konkreten Signaturzuordnung bis zu einer starken Variablen die Möglichkeiten des Binding. Das letzte Beispiel webt den Aspekt gemäß Pointcutdefinition in alle möglichen Klassen und Methoden ein. „*“: ist ein Wildcardsymbol für 0 bis n Zeichen, das in Typausdrücken, Methodennamen oder Annotationsausdrücken benutzt werden kann.
114
JBoss AOP
„..“: zeigt eine beliebige Anzahl von Parametern für einen Konstruktor oder eine Methode an. Für die Definition von Klassen bzw. Typen im Binding können ebenfalls Wildcards verwendet werden. Beispiele: test.Model
Meint den vollqualifizierten Namen des Typs Model im Package test. test.*
Meint alle Typen innerhalb des Package test sowie deren Unter-/Innerklassen. @model.TestElement
Meint alle als model.TestElement per Annotations getaggten Klassen/Methoden. $instanceof{test.Model}
Meint alle Klassen vom Typ test.Model, entspricht in seiner Logik dem Java-Schlüsselwort instanceof. Wird der Aspekt ohne Interceptor erzeugt, muss beim Aspect-Binding der Name der Methode (hier trace) mitgegeben werden.
AOP
115
2 – AOP-Frameworks in Java
Aspektdefinition im Binding-XML Das Aspect-XML-Element enthält den Nanem des Aspekts (class-Attribut) und den scope, also den Gültigkeitsbereich des Aspekts. Er entscheidet darüber, wie viele Instanzen von der genannten Aspektklasse erzeugt werden und wann. Dass dies wichtig ist, liegt daran, dass Aspekte im Gegensatz zu normalen Klassen nicht vom Entwickler, sondern vom jeweiligen Aspektprodukt erzeugt werden. Dafür muss diese Software allerdings wissen, wann und wie oft sie Instanzen des Aspekts bilden soll. Dies ist interessant, wenn man innerhalb des Aspekts bestimmte Zustandsfelder plant. Stellen Sie sich beispielsweise ein Caching von Anfrageparametern vor, das sinnvollerweise pro Instanz, wo dieses Zwischenspeichern geplant ist, benötigt wird. Hier macht eine einzelne Aspektinstanz pro VM wenig Sinn. Beispiel:
Das Aspektattribut scope kann folgende Ausprägungen annehmen: 쐌
PER_VM
Eine einzige Instanz innerhalb der virtuellen Maschine 쐌
PER_CLASS
Jede Klasse, an die aufgrund eines Joinpoint die Ausführung eines Advice gebunden wird, erhält eine einzelne Aspektinstanz. 쐌
PER_INSTANCE
Jede durch einen Advice betroffene Klasseninstanz erhält eine eigene Aspektinstanz. 쐌
PER_JOINPOINT
Pro Joinpoint wird eine Aspektinstanz erzeugt.
116
JBoss AOP 쐌
PER_CLASS_JOINPOINT
Für einen zugewiesenen Joinpoint wird eine Aspektinstanz erzeugt. Der Aspekt wird über alle Instanzen der Klasse hinweg für diesen Joinpoint gemeinsam verwendet. Der Aspektdefinition lassen sich weitere Attribute beimischen, die zur Laufzeit ausgewertet und bei der Ausführung des Aspekts genutzt werden können. 11
Für eine ausführliche Erläuterung der Verwendung von Attributen zur Konfiguration der Aspekte einschließlich Nutzung von Aspektfabrikfunktionen sei hier der Blick in die JBoss-AOP-Referenzdokumentation empfohlen. Binding-Pointcut-Definitionen Bisher haben wir das Aspect-Element gesehen. Darin eingeschachtelt sind die Pointcutinformationen, die die Menge möglicher Joinpoints definieren. Die Methodenpattern bilden dabei das Hauptattribut innerhalb des Pointcut. Der Aufbau typischer Methodenpattern sieht wie folgt aus: [Modifier] [Rückgabetyp] [Klassenname] -> [Methodenname] (Parametertypen)
Der Aufbau typischer Fieldpatterns sieht wie folgt aus: [Modifier] [Feldtyp] [Klassenname] -> [Feldname]
à Die Angabe der Modifier (public, private, static) erfolgt optional. Attribute können mit dem negierenden NICHT-Operator explizit ausgeschlossen werden.
AOP
117
2 – AOP-Frameworks in Java public model.Test->doIt(java.lang.String)
à doIt()-Methode der Klasse model.Test mit Parameter java.lang.String public java.lang.String model.Test->myField
à Feld mit dem Namen myField in der Klasse model.Test vom Typ String !static * $instanceof{model.Test}->*(..)
à Meint alle nichtstatischen Methoden der Klasse model.Test oder einer Unterklasse. void @aspects.Singleton->getInstance()
à Meint alle mit der @aspects.Singleton-Annotation markierten Klassen bzw. deren parameterlose getInstance()-Methode. public model.Test->new(java.lang.String)
à Öffentlicher String-Konstruktor der Klasse model.Test. * *->@aspects.Asynchronous(..)
à Alle mit der @aspects.Asynchronous-Annotation getaggten Methoden aller Klassen @model.PersistentType *->save(@model.EntityType)
à Meint alle save-Methoden aller Klassen, die eine mit @model.PersistentType getaggte Klasseninstanz zurückgeben und eine mit @model. EntityType getaggte Parameterklasseninstanz entgegennehmen. Pointcuts Die folgende Auflistung zählt die innerhalb eines Pointcut erlaubten Advices: 쐌 all() Alle Methoden, Konstruktoren und Felder des spezifizierten Typs
118
JBoss AOP
쐌 call() Call-Aufruf einer Methode oder eines Konstruktors 쐌 execution() Execution-Ausführung einer Methode oder eines Konstruktors Beim Call wird der Aspekt als dem Aufrufer zugehörig erachtet, bei der Execution dem ausgeführten Element. Dies ist besonders dann wichtig, wenn entschieden werden muss, wie viele gegebenenfalls unterschiedliche Aspektinstanzen gebildet werden müssen. 쐌 field() Zugriff auf ein Feld einer Klasse 쐌 set() bzw. get() Schreibender bzw. lesender Zugriff auf ein Feld einer Klasse 쐌 within() Meint jeden Joinpoint (Methoden- oder Konstruktor-Call) innerhalb des spezifischen Aufrufs. 쐌 withincode() Meint jeden Joinpoint (Methoden- oder Konstruktor-Call) innerhalb der spezifischen Methode oder des Konstruktors. 쐌 has() bzw. hasfield() UND-verknüpfte Bedingung, dass eine Klasse über eine(n) bestimmte(n) Methode/Konstruktor oder ein bestimmtes Feld verfügen muss. Alle Joinpointteilelemente lassen sich über die booleschen Operatoren NICHT („!“), UND („AND“) und ODER („OR“) miteinander in so genannten Expressions verknüpfen. Introductions und Mixins JBoss AOP erlaubt das Verändern der Vererbungshierarchie von Klassen. Für die Einführung neuer Interfaces (z.B. Tagginginterfaces wie java.io.Serializable), die ohne Methodenimplementierung auskommen, ist diese Introduction sehr einfach in XML zu realisieren:
AOP
119
2 – AOP-Frameworks in Java java.io.Serializable
Soll eine Klasse nicht nur ein Interface implementieren, sondern muss sie auch neue Methoden realisieren, wird dies über einen Mixin ermöglicht. model.NewInterfaceToImplement model.InterfaceRealizationMixin new model.InterfaceRealizationMixin(this)
Neben der Hauptklasse model.TestClass, die erweitert werden soll, gibt es eine Hilfsklasse model.InterfaceRealizationMixin, die mit der Hauptklasse instanziiert die über model.NewInterfaceToImplement an die Instanz gerichteten Aufrufe entgegennimmt und abarbeitet. public class InterfaceRealizationMixin implements NewInterfaceToImplement { TestClass instance; public InterfaceRealizationMixin( TestClass instance) { this.instance = instance; } // Methode, die implementiert werden muss: public void doIt(String param) { //... } }
120
JBoss AOP
Mixins sind – wie bereits angesprochen – dort von Interesse, wo spontan Vererbungshierarchien geändert werden sollen. Wahrscheinlicher ist allerdings oftmals, dass eine Klasse ein Interface implementieren muss, das sie zuvor nicht kannte, oder um Funktionen erweitert werden muss, die es zuvor nicht gab. Soll z.B. ein Controller gleichzeitig Listener für bestimmte Events werden, er implementiert aber das Listenerinterface nicht, kann dies in JBoss über eine Introduction dennoch möglich gemacht werden. Declarations Über Declarations können, ähnlich wie mit dem Schlüsselwort declare in AspectJ, auch in JBoss AOP Warningmessages und Fehler erzeugt (Werfen von Exceptions) werden, wenn einzelne Typen nicht den Vorgaben entsprechen. Beispiel: All persistent model objects must implement a store() method.
Dynamic AOP JBoss AOP erlaubt es, zur Laufzeit neue Aspekte in die in der VM befindlichen Klassen einzubringen und somit deren Verhalten zu verändern. Auch bei anderen Applikationstypen ohne AOP ist dies über ein Redeployment von Klassen durch Ersetzen einer ClassLoader-Instanz, die diese Klassen geladen hat, zur Laufzeit grundsätzlich möglich. Diese Lösung ist technisch bei JBoss natürlich auch machbar, wenn auch nicht besonders elegant. Klassen, deren Instanzen zur Laufzeit modifiziert werden sollen, müssen dafür im XML vorbereitet werden (prepare-Aufruf im XML) und ein spezielles Interface implementieren (org.jboss.aop.Advised). Zur Laufzeit können dann neue Interceptoren in die bestehenden Instanzen eingebracht werden. Dies ist z.B. dann sinnvoll, wenn eine Instanz sich
AOP
121
2 – AOP-Frameworks in Java
spontan remotefähig verhalten und/oder Aufrufe auf ihr an eine fremde Instanz weiterleiten soll. Advised advised = (Advised) existingObject; advised._getInstanceAdvisor().insertInterceptor( new TestInterceptor());
Dieses Verhalten, Interceptoren zur Laufzeit an eine Instanz zu binden, nennt man Per Instance Interception. Daneben gibt es in JBoss AOP die zusätzliche Möglichkeit, weitere Bindings im Hot-Deployment-Verfahren zur Laufzeit einzubringen, indem man diese dem AspectManager bekannt gibt: org.jboss.aop.advice.AdviceBinding binding = new AdviceBinding( "execution(model.Test->doIt(..))", null); binding.addInterceptor(TestInterceptor.class); AspectManager.instance().addBinding(binding);
Weitere JBoss AOP-Features JBoss bringt im AOP-Produkt weitere, interessante Features mit, darunter die Fähigkeit, Annotations als Javadoc-Elemente bereits in Java 1.4 zu verwenden, sowie die Unterstützung für Ant-Tasks im Build-Prozess. Der Ant-Task aopc (AOP Compiler) von JBoss AOP erlaubt es, Klassen bereits vor der Ausführung auf die Aspekte vorzubereiten. Auch der Aufruf von der Kommandozeile unter Übergabe der diversen JBoss-Bibliotheken und der Aufruf der Klasse org.jboss.aop.standalone.Compiler sind ohne weiteres möglich. Für weiterführende Informationen zu Features von JBoss AOP (z.B. Annotations, Metadaten oder bereits vordefinierte Aspekte sowie deren Reihenfolge (precedences)), die an dieser Stelle in aller Ausführlichkeit nicht beschrieben werden können, sei ein Blick auf die JBoss-AOP-Webseite bzw. die kostenlos verfügbaren Dokumentationen in Form von PDFs empfohlen.
122
JBoss AOP
2.2.1
JBoss-IDE AOP am Beispiel
Die Installation der JBoss AOP verläuft wie bereits im AJDT-Abschnitt besprochen. Nach dem Setup der Eclipse 3.0.2 IDE (im Test lief meine eigene JBoss AOP-Erweiterung noch nicht problemlos mit 3.1 zusammen) kann über die Konfigurations- und Installationswizards von Eclipse von der JBoss AOPSeite1 eine aktuelle Version der IDE-Erweiterung heruntergeladen werden.
Abb. 2.8: JBoss-IDE AOP – Auswahl in Eclipse
Wählen Sie dann zum Download die IDE AOP aus. Die Standard-JBossIDE umfasst die Unterstützung des JBoss Application Servers und die Entwicklung entsprechender Java-EE-konformer Anwendungen, die außerhalb des Themenbereichs dieses Buchs liegen. Wir wollen uns an dieser Stelle auf die reine Application-Server-unabhängige AOP-Entwicklung konzentrieren.
1. http://jboss.sourceforge.net/jbosside/updates
AOP
123
2 – AOP-Frameworks in Java
Die JBoss-IDE AOP besteht beim Download aus einem Core und einem UIBestandteil. Die Parallelinstallation mit anderen AOP-Werkzeugen wie AspectJ (z.B. zum Zwecke des Vergleichs beider Produktlinien) ist möglich, die gleichzeitige Verwendung beider Konzepte in einem einzelnen Java-Projekt ist allerdings nicht empfehlenswert. Nach der Installation legen wir über das Menü File und den Menüpunkt New | Other ein JBoss AOP-Projekt an. Dies hat den Vorteil, dass automatisch im Projekt die JBoss Libraries mit eingebunden und das Buildverhalten des JBoss Projekts um den JBoss Weaver ergänzt wird.
Abb. 2.9: New Wizard
Das neu generierte Projekt enthält auch das File jboss-aop.xml, in dem ab dem Zeitpunkt des Binding spätestens erste Weavinginformationen hinterlegt werden (siehe Abbildung 2.10). Zum Test verwenden wir drei Klassen, die hier wie folgt konstruiert in unseren Beispielen bereits vorkamen:
124
JBoss AOP
Abb. 2.10: JBoss-Beispielprojekt ModelA-Klasse, ihre doIt()-Methode wird später durch den Aspekt unterbrochen: package model; public class ModelA { public void doIt() { System.out.println("Test"); } }
Dann benötigen wir noch die Aspektklasse selbst: package model; import org.jboss.aop.joinpoint.Invocation; public class TracingAspect { public Object trace(Invocation invocation) throws Throwable { System.out.println("Start execution"); return invocation.invokeNext(); } }
AOP
125
2 – AOP-Frameworks in Java
Die Klasse implementiert keine Interfaces und bietet auch nur die selbst definierte trace-Methode an. Schließlich folgt noch eine Testklasse, um die Aspekteinwebung auszuprobieren: package model; public class TestClass { public static void main(String[] args) { ModelA a = new ModelA(); a.doIt(); } }
Sind die drei Klassen in der IDE erfasst, kann man nun über die rechte Maustaste auf der doIt()-Methode dieser ein Binding für den TracingAspect verpassen, indem man im JBoss AOP-Untermenü Apply Advice... auswählt. Die IDE-Erweiterung sucht dann nach potenziellen Aspektklassen und zeigt sie samt Methoden an. In diesem Fall wählen wir die Trace-Methode der Aspektklasse (siehe Abbildung 2.11). Die JBoss-IDE AOP zeigt nach Auswahl des Advice mittels kleiner Pfeile und eines eigenen AspectManagers an, welche Pointcuts und Aspekte definiert sind (siehe Abbildung 2.12). Leider lassen die Modifikationsmöglichkeiten der in der jboss-aop.xml-Datei definierten Elemente z.B. mit Wildcards in der getesteten Version zu wünschen übrig. Um schließlich die Testklasse TestClass auch starten zu können, wählen wir über die rechte Maustaste auf der Testklasse die Funktion Debug | Debug... und richten eine JBoss AOP-Launch Configuration ein (andere funktionieren normalerweise nicht!) (siehe Abbildung 2.13).
126
JBoss AOP
Abb. 2.11: Auswahl passender Advices
Abb. 2.12: Aspektanzeige in der Eclipse IDE
AOP
127
2 – AOP-Frameworks in Java
Abb. 2.13: JBoss AOP Application – Launch configuration
Beim Starten wird dann der neue Aspekt berücksichtigt und kann auch gedebuggt werden.
2.3 JBoss AOP vs. AspectJ Bezüglich einer Entscheidung für das eine oder andere Tool tut man sich zuweilen etwas schwer. AspectJ überzeugt mit der aus meiner Sicht umfangreicheren und besser konfigurierbaren Aspektlösung in Eclipse. Der Editor ist für AspectJ-Zwecke angepasst, zahlreiche Einstellungen in den Projekteigenschaften des Programms zeigen die Mächtigkeit der Lösung. Allerdings „humpelte“ die IDE über viele Versionen auch vor sich hin. Oftmals ist man hilflos, wenn Pointcuts nicht greifen, Advices nicht angesprochen werden. Liegt es am Compiler? An der IDE? An der Pointcutdefinition?
128
AspectWerkz
Hier trifft man mit JBoss AOP auf eine supersaubere und maximal Javanahe Lösung. Die Aspekte selbst sind echte Java-Klassen, lediglich das Binding ist in einer externen Datei hinterlegt. Für einfache Fälle ist die JBossIDE-AOP schlichtweg genial und liefert gute Wizards. Wenn es aber an die Erzeugung komplexer Pointcutdefinitionen geht, erweist sich die IDE in der aktuellen Version oft noch als Bremsklotz. Statt Editoren oder XML-Helper zur Verfügung zu stellen, löscht sie nicht selten händische Modifikationen beflissentlich oder sie doppelte in meinen Tests einzelne Pointcuts unbeabsichtigt. Jede Lösung hat ihre eigenen Probleme. Für den Einstieg eignet sich JBoss AOP sicherlich eher als AspectJ, das mit den zuweilen umfangreicheren Lösungen daherkommt. Primär sollte sich die Entscheidung für oder gegen ein Produkt aber am benötigten Funktionsumfang ausrichten.
2.4 AspectWerkz Unter LGPL-Lizenz wird auf der Webseite von AspectWerkz1 ein neben AspectJ weiteres, bereits lange erhältliches AOP-Werkzeug entwickelt, das sowohl für private als auch für kommerzielle Zwecke eingesetzt werden darf. Die Definition der Aspekte bzw. ihrer Einwebpunkte kann über die mit Java 5 verfügbaren Annotations, custom-doclet-Tags in Java 1.3/1.4 oder per XML erfolgen. Die Bytecode-Manipulation kann sowohl zur Compile- als auch zur Loadund Runtime erfolgen, wobei AspectWerkz für sich proklamiert, sich dabei in alle ClassLoader außer dem Bootstrap-Classloader einhängen zu können. Zudem gibt es Erweiterungen in Richtung Dynamic AOP, also der oben beschriebenen Fähigkeit, zur Laufzeit Aspekte zu aktivieren und zu deaktivieren. Außerdem ist AspectWerkz in der Lage, Aspekte für fremde AOP-Lösungen in begrenztem Maße zu adaptieren und funktionsfähig zu machen.
1. http://aspectwerkz.codehaus.org/
AOP
129
2 – AOP-Frameworks in Java
One world one tool ... In einer gemeinsamen Erklärung haben die Projektteams von AspectJ und AspectWerkz sich zu Beginn des Jahres 2005 dazu bereiterklärt, ihre Aktivitäten zu bündeln und eine gemeinsame AO-Programming-Plattform zu erschaffen, die dann unter dem Namen AspectJ 5 auftreten soll. Zum Erstellungszeitpunkt dieses Buchs gab es leider noch keinen finalen Release, sondern lediglich eine Milestoneversion von AspectJ 5.0. Die Version 2.0, die hier im Folgenden beschrieben wird, könnte also voraussichtlich die letzte eigenständige AspectWerkz-Version sein, wobei das gemeinsame Entwicklungsteam angehalten ist, einen Migrationsweg aufzuzeigen. Die bislang unterstützten Doclet-Tags werden im Gegensatz zu den AspectWerkz-Annotation-basierten Aspektdeklarationen allerdings nicht länger unterstützt.
2.5 Weitere Java-AOP-Tools Unter dem Namen Nanning1 findet sich auf einer Codehaus-Seite ein weiteres AOP-Tool. Leider scheint die Weiterentwicklung seit fast zwei Jahren eingestellt zu sein, so dass Nanning mit den Funktionalitäten heutiger AOPTools speziell in Richtung Java 5.0 nicht mithalten kann. Auf der Webseite CaesarJ2 findet sich eine neue AOP-Lösung, von Mitgliedern der TU Darmstadt entwickelt, die zum Erstellungszeitpunkt dieser Dokumentation Version 0.7.0 erreicht hatte und damit noch nicht als finale Version zu betrachten war. Insgesamt ähnelt die Syntax in einigen Teilen sehr der von AspectJ. CaesarJ bringt bereits eine erste Eclipse-Integration für die Version 3.0.1 mit. Auf einer schweizer Webseite3 findet sich mit PROSE eine ebenfalls sehr bekannte und beliebte AOP-Lösung. PROSE bedeutet „PROgrammable extenSions of sErvices“ und umfasst ein Dynamic Weaving Tool, das es er-
1. http://nanning.codehaus.org/overview.html 2. http://caesarj.org/ 3. http://prose.ethz.ch/Wiki.jsp?page=Prose
130
Frameworks? – Eine Stellungnahme
laubt, ad hoc Aspekte in den Source zur Laufzeit einzubringen oder aus diesem zu entfernen. EAOP1 heißt ein Tool, dessen Entwicklung leider 2002 endete und das für Event-based Aspect-Oriented Programming steht. Da seit Version 1.0 keine Neuerungen in das Produkt einflossen, sollte es für eine aktuelle Problemstellung vermutlich unzureichend sein. In der Java-EE-Konkurrenz Spring2 findet sich auch eine AOP-Lösung namens „Spring AOP“ wieder. Im Gegensatz zu vielen anderen Produkten beschränkt man sich hier auf die reine Arbeit in Java, weshalb es keinen separaten Compile-Schritt und kein Bytecode-Enhancing gibt. Spring beeinflusst die Classloaderhierarchie nicht und beschränkt sich bei AOP-Mitteln derzeit auf das Intercepten von Methoden. Leider gibt es aktuell keine adäquate IDE-Unterstützung, die über die reine Editierung von Konfigurationsdateien hinausgeht.
2.6 Frameworks? – Eine Stellungnahme Auch wenn es dieses Buch nicht schafft, alle Produkte mit allen Syntaxvariationen vorzustellen, sollten Sie bereits einen ersten, recht guten Überblick gewonnen haben, welche Eigenschaften die unterschiedlichen Produkte ausmachen. Unabdingbar ist heutzutage nicht nur eine möglichst nahtlose Integration in bestehende VMs und ClassLoader, sondern auch eine Nutzung der Java 5Annotationsphilosophie und eine gute Integration in bestehende Entwicklungsumgebungen. Besonders Letzteres ist und bleibt ein Stolperstein, denn gleichzeitig soll sich AOP faktisch nahtlos und unsichtbar in die Software zur Laufzeit einbetten, andererseits sollen die Auswirkungen dieses Weaving dem Benutzer nicht verborgen bleiben. Dazu kommt das Problem des Refactoring am Sourcecode und der Verschiebung von Methoden, Klassen und Sourceartefakte. 1. http://www.emn.fr/x-info/eaop/tool.html 2. http://www.springframework.org/docs/reference/aop.html
AOP
131
2 – AOP-Frameworks in Java
Als sehr nützlich dürften sich die fertigen Aspekte/Interceptoren erweisen, die in Produkten wie JBoss AOP bereits mitgeliefert werden. Überhaupt täte es Java als Sprache und JRE gut – was in Teilen auch schon passiert –, nicht nur mit Annotations ein Sprachelement bereitzustellen, das das Einfügen von Metadaten ermöglicht, sondern auch gleichzeitig fertige Implementierungen anzubieten. Davon sind wir in einigen Bereichen noch recht weit entfernt. Auch die Unterstützung einiger VMs für spezifische Erweiterungszwecke zwecks Dynamic AOP weist Wege jenseits des Standards und der Spezifikation auf. AOP und AOP-Tools leiden unter einer fehlenden Spezifikation und einem mangelnden Bewusstsein für sie in Richtung gemeinsamer Community. Die Zusammenführung von AspectJ und AspectWerkz zeigt aber immerhin den Trend von vielen vereinzelten Lösungen zur Bemühung um gemeinsame Standardprodukte. Der weitere Ausbau von Annotations und Implementierungen in Java 5.0 und Java 6.0 dürfte an vielen Stellen dramatisch zu einer Verbesserung und Vereinfachung der Ausdrucksfähigkeit der Java-Sprache führen. Ein Feature, das bei Bekanntgabe in vielen Foren und von zahlreichen Programmierern nur müde belächelt wurde, könnte AOP den Durchbruch und die Stabilität verschaffen, dieses Programmiervorgehen zunehmend in „real world“Applikationen auch jenseits der Java Enterprise Edition zum Einsatz zu bringen. Bleiben Sie neugierig.
132
3
Architekturen mit AOP
Das nachfolgende Kapitel beschäftigt sich mit Architektur- und Designvorschlägen in aspektorientierter Programmierung und enthält dazu eine Reihe von Beispielen in AspectJ und JBoss AOP. Da es sinnvoll ist, sich vor dem Steuern eines Fahrzeugs mit den Grundlagen von Gaspedal und Bremse kurz zu beschäftigen, wirft dieses Kapitel einen Blick hinter die Kulissen von Produkten wie BCEL, Janino oder Javassist. Wir schauen uns das Theme Approach-Vorgehensmodell für Analyse und Design von Aspekten an und werden unterschiedlichste Einsatzmöglichkeiten und -gebiete aufdecken, in denen AOP ein nützlicher Begleiter sein kann oder wo es die tägliche Arbeit sogar revolutionieren hilft.
In den letzten Kapiteln wurden bereits Einsatzgebiete und Varianten von AOP angesprochen. Oliver Böhm geht in seiner Präsentation „AOP mit AspectJ1“ von einer realisierbaren Codereduktion von 30–95% aus, primär beeinflusst durch Einsatzgebiet und Entwickler-Know-how. Ebenfalls steht eine 20–40% schnellere Entwicklung oder zumindest eine Verbesserung der Sourcecodequalität im Raum. Sicherlich lässt sich über den prozentualen Gewinn streiten. Grundsätzlich sollte aber schon eine gewisse Einigkeit darüber bestehen, dass, wenn die AOP-Mechanismen in sinnvoller Art und Weise in der bestehenden oder neuen Software zum Einsatz kommen, sich als Mindestmaß eine bessere Modularisierung ergibt. Somit wäre zumindest eines der Hauptziele dieses Vorgehens erfüllt. Durch die Trennung der Aspekte vom ursprünglichen Source ergibt sich ein theoretisch sehr hoher Wiederverwendungsgrad. Beim JBoss Server bzw. JBoss AOP findet man dies bereits sehr beispielhaft in Form von Aspektbibliotheken. In ihnen finden sich fertige cross cutting-Lösungen oft in Form von so genannten Interceptoren wieder. Die Entwicklung der Software in dem in Kapitel 1 beschriebenen n-dimensionalen Raum ermöglicht es nun, ganz andere Architekturen und Pattern 1. http://www.agentes.de/artikel/aspectj.html
AOP
133
3 – Architekturen mit AOP
anzugehen. In ihnen können z.B. Komponenten, die zueinander in keiner Beziehung stehen, nun einem gemeinsamen Eventhandling unterworfen werden. Typisches Beispiel ist das Observer-Pattern. Ändert sich der Zustand einer Modellklasse, können entsprechende Listener, d.h. Klassen, die ein bestimmtes Interface implementieren, über die Zustandsänderung informiert werden.
Abb. 3.1: Modellstruktur
Eine Implementierungsvariante soll an dieser Stelle nochmals gezeigt werden, wie sie OO-typisch wäre. Abbildung 3.1 zeigt in UML-Form das nun folgende Softwarebeispiel. Eine ExampleClass ist aus Gründen der Einfachheit Modellerzeuger und gleichzeitig Eventlistener. Das gemeinsam zwischen beiden vereinbarte Interface ist ModelListener. Letztendlich wird beim Aufruf von setValue() im ConcreteModel lediglich in der ExampleClass die recalculate()-Methode aufgerufen. Das bedeutet, ändert sich der Datenzustand im Modell, startet dies eine Neuberechnung im angemeldeten Listener. Obwohl das Beispiel sehr einfach klingen mag, braucht man für das reine Eventhandling bereits etliche Klassen und Methoden (ModelListener, addListener()-Methode etc.).
134
Architekturen mit AOP
Hier die abstrakte Modellklasse zum Management von Listenern: package observer; import java.util.ArrayList; import java.util.Iterator; import java.util.List; public abstract class AbstractModel { private List listeners = new ArrayList(); public void fireModelChangedEvent(Object value){ for (Iterator iter =this.listeners.iterator(); iter.hasNext();) { ModelListener listener = (ModelListener) iter.next(); listener.modelChanged(this, value); } } public void addListener(ModelListener listener) { if (!listeners.contains(listener)) { listeners.add(listener); } } }
Dazu die konkrete Modellklasse für den Aufruf des Events: package observer; public class ConcreteModel extends AbstractModel { private Object value = null; public void setValue(Object newValue) { this.value = newValue; this.fireModelChangedEvent(newValue); } }
AOP
135
3 – Architekturen mit AOP
Und das Modellistener-Interface: package observer; public interface ModelListener { public void modelChanged(Object source, Object newValue); }
Schließlich folgt die Ziel-Listenerklasse, die gleichzeitig Erzeuger des Modells ist. Sie enthält in diesem Fall dafür eine entsprechende Main-Methode: package observer; public class ExampleClass implements ModelListener { public static void main(String[] args) { ExampleClass instance = new ExampleClass(); ConcreteModel modelInstance = new ConcreteModel(); modelInstance.addListener(instance); modelInstance.setValue("Hallo Welt"); System.exit(0); } public ExampleClass() { } public void modelChanged( Object source, Object newValue) { System.out.println("New event received:" + source + " value:" + newValue); recalculateState(); } public void recalculateState() { System.out.println("---"); } }
136
Architekturen mit AOP
Der folgende Output ist bei Ausführung des Programms auf der Konsole sichtbar. New event received:observer.ConcreteModel@defa1a value:Hallo Welt ---
Es gibt nun eine Reihe von Implementierungslösungen, die für eine AOPVariante möglich wären; angefangen vom einem sehr abstrakten Observerpatternansatz, in dem jede Modifikation der ConcreteModel-Klasse zu einem Aufruf in ExampleClass führt. In diesem Fall habe ich einmal die in den klassischen Beispielen fett markierten Klassenelemente genutzt, um den Event aufzufangen und weiterzureichen. Der JBoss AOP-Interceptor mit dem Namen EventInterceptor wird beim Erzeugen der ExampleClass()-Instanz aufgerufen und registriert die Instanz aufgrund ihrer Modellistener-Eigenschaft als Listenerkandidat. Der Aufruf der setValue()-Methode benötigt nun kein Event-Feuern mehr, auch die abstrakte Modelloberklasse ist nicht mehr nötig und die Registrierung von ExampleClass gelingt nun implizit. Einzig das ModelListener-Interface blieb unverändert. package aopobserver; public class ExampleClass implements ModelListener { public static void main(String[] args) { ExampleClass instance = new ExampleClass(); ConcreteModel modelInstance = new ConcreteModel(); // Registrierung entfernt modelInstance.setValue("Hallo Welt"); System.exit(0); } public ExampleClass(){// Interceptor greift hier } public void modelChanged( Object source, Object newValue) {
AOP
137
3 – Architekturen mit AOP System.out.println("New event received:" + source + " value:" + newValue); recalculateState(); } public void recalculateState() { System.out.println("---"); } } package aopobserver; // Superklasse entfernt, Listenerlogik entfernt public class ConcreteModel { private Object value = null; public void setValue(Object newValue) { this.value = newValue; } } package aopobserver; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.jboss.aop.advice.Interceptor; import org.jboss.aop.joinpoint.Invocation; import org.jboss.aop.joinpoint.MethodInvocation; public class EventInterceptor implements Interceptor { private static List listeners = new ArrayList(); public String getName() { return "Eventbridge"; }
138
Architekturen mit AOP public Object invoke(Invocation arg0) throws Throwable { if ((arg0.getTargetObject() instanceof ModelListener) || (arg0.getTargetObject() == null)) { // REGISTRIERUNG: Object o = arg0.invokeNext(); Object listener = arg0.getTargetObject()!= null ? arg0.getTargetObject():o; if (!listeners.contains(listener)) { listeners.add(listener); } return o; } else { // EVENT AUSFÜHRUNG: Object o = arg0.invokeNext(); MethodInvocation mi = (MethodInvocation) arg0; Object[] arguments = mi.getArguments(); for (Iterator iter = listeners.iterator(); iter.hasNext();) { ModelListener listener = (ModelListener) iter.next(); listener.modelChanged( arg0.getTargetObject(),arguments[0]); } return o; } } }
Über arg0.getTargetObject() kann das Zielelement unseres Aufrufs bestimmt werden. Da der Interceptor sowohl neue Listener registrieren als auch den setValue()-Aufruf in Empfang nehmen soll, wird hier zwischen ModelListener-Registrierungen und setValue()-Aufruf unterschieden. Der erste Teile des Source – hier mit dem Kommentar Registrierung überschrieben – tritt in Aktion, wenn über new ein neues Objekt vom Typ Model-
AOP
139
3 – Architekturen mit AOP Listener erzeugt wird. In diesem Fall werden all diese Neuerzeugungen in einer Liste gespeichert.
Gegebenenfalls muss das Zielobjekt erst erzeugt werden (vor dem Konstruktoraufruf ist ExampleClass() eben nicht vorhanden, um registriert zu werden). Die Methode arg0.invokeNext() führt die interceptorte Methode aus (hier den Konstruktor) und liefert das Ergebnis zurück. Der zweite Teil des Source – mit dem Kommentar Eventausführung – reagiert auf Events und steuert diese den Listenern zu. Der Else-Zweig führt die Methode setValue() über invokeNext() aus und füttert den Listener samt seiner Argumente und dem Zielobjekt1. Die Verbindung der Klassen bzw. der Aufruf des Interceptors wird über die jboss-aop.xml-Datei gesteuert. Hier der Beispielinhalt:
1. Ganz sauber ist diese Implementierung freilich nicht. Größtes Problem ist die statische Liste von Eventlistenern, die nicht klassenorientiert reagiert, sondern alle Listener für alle Events hier akzeptiert. Außerdem stellt die Deregistrierung von Listenern ein Problem dar. Da es keine Weakreferences auf Listener gibt, wird die Liste der Listener immer größer, aber nie kleiner. Da es sich hier nur um ein einfaches Übungsbeispiel handeln soll, wollen wir diese Probleme außer Acht lassen.
140
Architekturen mit AOP
Er zeigt die zwei Einstiegspunkte: die Ausführung der setValue()-Methode und die Konstruktion einer ExampleClass-Instanz. Zu der tiefergehenden Bedeutung jeder einzelnen Zeile und den Schlüsselwörtern kommen wir im Verlauf des Buchs noch. An dieser Stelle gilt es lediglich zu wissen, dass das Binding (bind) definiert, dass bei Ausführung (execution) der entsprechenden Methoden der Interceptor mit dem Namen aopobserver.EventInterceptor aktiv wird. Nach einer großen Verschlankung des Sourcecodes sieht es hier zunächst nicht aus. Bedenkt man jedoch, wie abstrakt ein solcher Aspekt gehalten werden könnte und dass in diesem Fall die Observer-Subscriber-Logik tatsächlich in eine Bibliothek auslagerbar wird – was in der klassischen Variante kaum denkbar ist –, ergeben sich schon deutliche Vorteile. Das in verschiedenen Büchern beschriebene Pattern lässt sich also nicht nur immer wieder in unterschiedlichen Kontexten als eine Art Handlungsanweisung einsetzen. Im Klassiker für Entwurfsmuster [Gamma04] werden Sie – falls Sie dieses Buch schon einmal in Händen hielten – immer wieder UML-Diagramme und abstrakte Beispiele finden, wie Sie Pattern für sich im Sourcecode nutzen können. Es gibt allerdings dabei drei wesentliche Probleme: 1. Sie müssen zunächst einmal verstehen, welche Lösung für welche Art von Problemen das Pattern überhaupt bietet. 2. Sie müssen das Pattern auf Ihren eigenen Sourcecode transponieren, also übertragen, können. 3. Es bedarf Aufwand, das Pattern zum Einsatz zu bringen. Denn Sie werden es nicht einmal, sondern n-fach tun müssen. Jedes Mal wieder, wenn Sie es brauchen, werden Sie es neu bauen, neu konstruieren, neu programmieren müssen. Die hier kurz angerissene AOP-Variante weist in eine komplett andere Richtung und verspricht, das fertige Pattern aus dem Schrank zu ziehen. Die Probleme 2 und 3 entfallen. Selbstverständlich müssen Sie noch immer wissen, wo sich das Pattern als nützlich erweisen könnte, aber es dann einzusetzen, ist nahezu ohne Aufwand möglich.
AOP
141
3 – Architekturen mit AOP
Wenn wir über Separation of Concerns reden, dann muss nochmals deutlich gesagt werden, dass es hier nicht nur um die Aufteilung rein fachlicher Themenkomplexe geht. Das Beispiel zeigt, dass man auch funktionales Verhalten recht einfach auslagerbar und wiederverwendbar machen kann. Dies stellt nicht grundsätzlich die in [Gamma04] besprochenen Pattern in Frage, sondern versetzt Sie vielmehr in die Lage, diese problemlos einsetzen zu können. Im Beispiel sollte klar werden, dass Eventerzeuger (Modell) und Eventconsumer (ExampleClass) nichts voneinander wissen und erst zum Buildzeitpunkt zusammengeführt werden. Die Ausgabe dieser Implementierung ist erwartungsgemäß dieselbe wie in dem rein objektorientierten Beispiel zuvor. Wenn es auf diese Art und Weise gelingt, Sourcecodeteile voneinander abzuschotten und dazu in Strukturen und private-Elemente fremder Komponenten einzudringen, löst sich ein Teil der Probleme, die Java Reflection1 mit in die Javasprache bringt. Trennt man Sourcecodeteile weitmöglichst voneinander, entfallen auch zahlreiche Compileabhängigkeiten. Denkt man nur an die unzähligen ThirdParty-Libraries in großen Applikationen. Oft kann das Deployment nicht durchgeführt werden, wenn nur eine einzige Klasse solch eine Fremdbeziehung zu einem Produkt eines Dritten aufweist. AOP separiert hier sinnvoll die Aspekte voneinander und reduziert so die direkten Abhängigkeiten, die in einem monolithisch großen Source auftreten würden. Überhaupt streben große Anwendungen durch Komponententrennung das an, was AOP inhärent ausmacht: 쐌 쐌 쐌 쐌 쐌
eine Aufteilung der Zuständigkeiten eine Reduktion von Abhängigkeiten eine Verbesserung der Softwarestruktur eine Minimierung der Entwicklungsrisiken eine Parallelisierung des Entwicklungsprozesses
1. http://java.sun.com/docs/books/tutorial/reflect/
142
Theme Approach – Analyse und Design in AOP
Besonders der letzte Punkt, die Parallelisierung von Prozessschritten, bedeutet aber auch die Notwendigkeit, bereits in der Analyse- und Designphase erste Schritte in diese Richtung zu machen. Die heute üblichen Vorgehensweisen unterstützen zwar eine modularisierte Aufteilung der Anwendung und Vergabe von Zuständigkeiten, wie gut dies allerdings zu einem AO-Ansatz passt, ist fraglich. Hierbei soll z.B. der so genannte „Theme Approach“ eine Lösung aufzeigen, die wir uns im Folgenden anschauen wollen.
3.1 Theme Approach – Analyse und Design in AOP Gehen wir einmal von der Prämisse aus, die Entwicklung von Java-Applikationen mittels AOP würde tatsächlich zu einer deutlich veränderten Anwendungsarchitektur führen. Dann bräuchte man dafür entsprechende Zusatzprodukte, also Tools, um die Programmierphase bestmöglich zu bewältigen. So ist der Rückschluss ebenso zulässig, dass die Berücksichtigung dieses „anderen“ Ansatzes bestenfalls sehr frühzeitig, bereits während der Analyse- und Design-Phasen, angegangen werden sollte. Für genau diesen Ansatz fehlt es jetzt an einem notwendigen Vorgehensmodell und einer dazu passenden Notation zur Visualisierung der Gedanken und Ideen auf dem Weg hin zum fertigen Programm. In Kapitel 1 haben wir festgestellt, dass die Überführung von OOA nach OOD, also die Transformation der Anforderungen (requirements) in das fertige Design, eine besondere Herausforderung ist. Dies liegt daran, dass hier entschieden wird, nach welchen Prinzipien die aus den Anforderungen identifizierten Konzepte aufgeteilt werden. In Kapitel 1 fiel in diesem Zusammenhang der Begriff der „dominanten Dekomposition“. Wenn die Zielsetzung nun aber kein dominanzgetriebenes Modell sein soll, sondern ein Bauplan, der zunächst alle Dimensionen der Anforderungen bestmöglich wiedergeben soll, so geschieht die Überführung von OOA nach OOD mit heutigen Mitteln vielleicht zu schnell. Sie ist möglicherweise zu endgültig, was die Frage des Reverse Engineering von OOD nach OOA betrifft, oder geschieht zuweilen unvorteilhaft.
AOP
143
3 – Architekturen mit AOP
Ich möchte an dieser Stelle daran erinnern, dass man sich besonders im Bereich von Analyse und Design zwar einer Methodik- und Toolunterstützung bedienen kann, dass für viele dieser Prozessschritt aber weiterhin um einiges komplexer erscheint als das reine „Heruntertippen“ von Sourcecode. Wenn also Kritik am bisherigen Vorgehen geäußert wird, muss man fairerweise sagen, dass es sich hier auch um eine sehr schwierige Aufgabe und eine etwas andere Zielsetzung handelt. Die Aufgabe ist schwieriger, da sie die Teilungsdominanz nur in einer Richtung berücksichtigt, und sie ist anders, da ihre Zielsetzung bisher ein rein objektorientiertes System ist. Aber eins nach dem anderen. Erstmals hörte ich vom „Theme Approach“ durch ein Buch von Siobhán Clarke [Clarke05] und Elisa Baniassad. Beide Autorinnen stellen in ihrem Werk Modellierungs- und Erarbeitungsmethodiken vor, um einem aspektorientierten Entwicklungsansatz besser gerecht zu werden. Ganz allgemein gesprochen wird die Phase zwischen OOA und OOD dabei nochmals aufgespalten und mit zusätzlichen Modellen untermauert. Der Name „Theme Approach“ ist sicherlich deshalb sehr gerechtfertigt, da die Einteilung der Anforderungen zunächst nach Themen oder „Leitmotiven“ erfolgt. Wir reden dabei aber mitnichten von einem vollständigen Entwicklungsprozess, sondern vielmehr von einem Ansatz. Er hilft innerhalb einer bestimmten Prozessstelle bzw. er verbessert die Näherung beim Thema Design. Im Theme Approach, dessen Ausgangsmaterial je nach Vorgehensmodell sehr unterschiedliche Anforderungsdokumentationen sein können1, werden die verschiedenen Themenbereiche nach Aufnahme aller bisher bekannten Anforderungen2 „geclustert“. Das heißt, es werden größere und kleinere Leitmotive identifiziert und gegeneinander abgegrenzt. Dies muss man sich vorstellen wie einen Umverteilungsprozess, bei dem zunächst versucht wird, die Essenz des jeweiligen Themas herauszufinden, was sicherlich ein wenig einer Verantwortungszuordnung in einem Klassenmodell ähnelt. 1. Use Cases oder XP Stories z.B. 2. also nicht zur Analyse, sondern eigentlich direkt danach
144
Theme Approach – Analyse und Design in AOP
Abb. 3.2: Beliebiger Wechsel zwischen Analyse und Designphasen
Während dieser Analysephase werden die Themen selbst auch daraufhin geprüft, in welchem Verhältnis sie wiederum zu den Anforderungen stehen; das heißt, wo werden durch die Existenz eines Themas viele Anforderungen abgedeckt und wer stößt die Ausführung des Themeninhalts aktiv an. Theme/Doc Ein kleines Beispiel dazu. Zu erstellen sei eine einfache Software, die Kostenvoranschläge erzeugen soll. Aufgelistet sind ein paar allgemein formulierte, spontan aufgelistete Anforderungen des Auftraggebers. Anforderungen: A1: Die Daten müssen von autorisierten Benutzern auf einer Oberfläche erfasst und angezeigt werden können.
AOP
145
3 – Architekturen mit AOP
A2: Die nach der Eingabe gespeicherten Daten müssen nun im Kalkulationsmodul zu den Kostenvoranschlagszahlen umgerechnet werden. A3: Nach Eingabe der entsprechenden Benutzerkennung kann der Anwender nun das gespeicherte Datenmaterial ausdrucken. A4: Die durchgeführten Aktionen im Programm müssen auch im Nachhinein noch durch Log-Einträge nachvollzogen werden können. Es finden sich in diesem kurzen Text eine Reihe von Anforderungen wieder, die bei einer Themenbereichssammlung nützlich sein können. Im obigen Beispiel könnte man also beispielsweise die Themenbereiche „Dateneingabe“, „Datenanzeige“, „Datenspeicherung“, „Kostenermittlung“, „Angebotserstellung“, „Berechtigungsprüfung“ und „Logging“ ermitteln. Eigentlich gibt es noch weitere (z.B. ein impliziter Workflow, der durch die Auflistung und Reihenfolge bzw. den Tenor im Text herauszuhören ist).
Dateneingabe
Kostenermittlung
A2 A1
A3
Angebotserstellung
Datenanzeige
Datenspeicherung
A4 Logging
Abb. 3.3: Aspekt-Anforderungszuordnung
146
Berechtigungsprüfung
Theme Approach – Analyse und Design in AOP
Im so genannten Theme-Relationship-View können wir uns nun die Anforderungen und die identifizierten Themes anschauen. Hierbei zeigt sich bereits, dass die Anforderung A4 (Logging), die in der Beschreibung zunächst separat benannt wurde, sich eigentlich auf alle Anforderungen bezieht. Das heißt, statt des A4 wären die Anforderungen A1–A3 korrekterweise mit dem Theme Logging verbunden. Nun wissen Sie bereits aus der Erfahrung aus Kapitel 1, dass es sich bei Logging normalerweise um ein sehr klassisches, potenzielles cross cutting concern handelt, weshalb man das an dieser Stelle auch gar nicht zu verheimlichen braucht. Die Modellierung hier, das Theme an A4 zu binden, lässt das Modell aber wesentlich einfacher erscheinen, als Logging über Beziehungslinien mit den anderen drei Anforderungen zu verbinden. Worin bestehen nun die Eigenschaften von Logging in diesem Kontext? Logging ist hier zunächst ein Theme, das sich nicht zerteilen (splitting) und anderen Anforderungen unterordnen lässt. Es gehört zu keinem anderen Themenbereich so in Gänze dazu, dass man es praktisch darin unter- bzw. aufgehen lassen könnte. Zum anderen bezieht sich dieses Theme nicht wirklich auf eine einzelne konkrete Anforderung, sondern mehr auf andere Themes. Andere initiieren durch ihre Aktionen das Logging-Theme (trigger). Die eben genannten Eigenschaften – keine sinnvolle Aufsplittung möglich, Dominanz eines anderen Theme und ein zugehöriges Trigger-Verhalten – können auf einen Aspekt hinweisen. Der Prozess der Analyse bzw. Identifikation ist im Theme Approach wie gesehen also mehrstufig. Vor der Modellierung von Klassen oder Methoden versucht man intensiv, die Schlüsselthemen und Beziehungen zueinander zu erarbeiten und dabei jene, die Aspekte sind, herauszufinden. Im Beispiel ist die Berechtigungsprüfung ein technisch gesehen zweiter Aspekt. Auch hier dominieren die tatsächlichen Anwendungsfälle (Dateneingabe, Datenanzeige, Datenausgabe) vor dem Aspekt der Berechtigungsprüfung. Die Prüfung kann nicht sinnvoll auf mehrere Aspekte aufgeteilt werden und es sind tatsächlich die Themes selbst, die den Aspekt antriggern.
AOP
147
3 – Architekturen mit AOP
Da in einem umfangreichen Anforderungsdokument sehr wahrscheinlich viele auch sehr heterogene Aufgabenstellungen gleichzeitig zu einer Unmenge von Themes und Aspects führen, antwortet der Theme Approach auf dieses Problem mit Cross cutting relationship- und Individual theme-Modellen (views). Inhaltlich sehen diese Modelle dann aber dem oben dargestellten Relationship-Modell ähnlich, nur dass sie sich jeweils auf andere Teile des gesamten Anforderungsmodells konzentrieren. Die Idee hinter drei verschiedenen Sichten auf Themes innerhalb der Analysephase ist die Betrachtung der 쐌 Erarbeitung und Abgrenzung der verschiedenen Themes untereinander und in Bezug auf die Anforderungen, die sie erfüllen (relationship view), 쐌 Analyse und Erstzuweisung von Verantwortlichkeiten des Theme (cross cutting view), 쐌 Designplanung für den späteren Übergang zum UML-Modell (individual view). Theme/UML Der nächste Schritt nach dieser übergreifenden Trennung der Themes und der Identifikation von Schnittstellen unter ihnen, sieht vor, jedes Theme einzeln zu modellieren – und darin besteht schon ein ziemlich gravierender Unterschied zu den bisher üblichen Verfahrensweisen. Das bedeutet, der Workflowschritt der Dateneingabe wird beispielsweise von Klassen und Modellen her zunächst komplett unabhängig von dem der Datenausgabe, z.B. auf einen Drucker, betrachtet. Jedes Theme wird separat auf seine Anforderungen hin designt. Dabei bleibt nicht aus, erneut weitere Themes (quasi Subthemes) zu identifizieren oder auch zu erkennen, dass die Einteilung aus der Analysephase eventuell an einigen Ecken und Enden klemmt oder noch nicht vollständig genug ausfiel. Wichtig ist, dass man sich zwischen den einzelnen Theme-Approach-Phasen frei hin und her bewegen kann, ohne einem strengen Pfad folgen zu müssen. Dies lässt die Freiheit, Entscheidungen vom Beginn der Analyse wieder zu revidieren und andere Einteilungen oder Lösungswege zu suchen. Den-
148
Theme Approach – Analyse und Design in AOP
noch ist der Übergang von der Analyse hin zum Design natürlich schon ein relativ harter Bruch und eine qualitativ hochwertige Analyse nimmt hier viel überflüssige Nacharbeit ab,
Abb. 3.4: Theme-Beziehungsübersicht
Spinnt man das Spiel der Themes aus dem Anfangsbeispiel weiter, so würde man nun die Themes „Dateneingabe“ und „Datenanzeige“ unabhängig voneinander modellieren. Im Endeffekt bedeutet dies sogar, potenziell mit mehreren Personen parallel in die UML-Arbeit1 einsteigen zu können, ohne das Gesamtmodell von Anwendung und Architektur im Auge behalten zu müssen. Dabei würden sicherlich Konzepte wie „Kunde“, „Auftrag“ oder „Bestellposition“ in beiden Themes redundant vorkommen, was jetzt zunächst ein1. sprich: Erstellung von Klassen-, Sequenz- oder z.B. Interaktionsdiagrammen
AOP
149
3 – Architekturen mit AOP
mal widersinnig erscheinen mag, da Redundanzvermeidung u. a. eines der präferierten Ziele von AOP ist. Tatsächlich geschieht aber etwas anderes. Innerhalb des Prozessschritts der Datenaufnahme/-eingabe würden sich gegebenenfalls ganz andere Attribute als zweckmäßig erweisen als beim Druck oder im Layout einer GUI. Möglicherweise werden einerseits Daten erfasst (wie Adresse und Erfassungsuhrzeit), die später nicht mehr relevant sind, oder im weiteren Verlauf werden Layoutdaten wie die Position des Fensters, in dem die Daten auf dem Bildschirm stehen sollen, benötigt. Es ist hilfreich, für jeden Prozessschritt zunächst einmal ein Idealmodell an die Wand zu werfen, ohne sich darüber Gedanken zu machen, ob physisch darunter alle Modelle in einem einzigen münden oder nicht. – [Wunderlich05] In der Realität müssen schließlich aber wieder alle Teilmodelle sinnvoll komponiert werden – solange wir denn einen redundanzvermeidenden, asymmetrischen Architekturansatz wählen. Im Beispiel ist dies mittels eines Basismodells, das als neues Theme in unser Modell Einzug hält, realisiert. Im Basismodell werden die unabhängigen Teilmodelle nun möglichst optimal zusammengefasst, um die Core-Dimension – oder etwas lapidarer ausgedrückt: die „Standard-Javaklassen“– zu bilden. Kritiker könnten zu Recht sagen: „Wo ist jetzt der Gewinn gegenüber einem bisherigen Vorgehensmodell?“ Sowohl im „Standardmodell“ als auch beim „Theme Approach“ kommen schließlich Modelle und Klassen schlimmstenfalls wieder alle zusammen in einem großen „Kloß Software“. Wenn Sie einen Blick auf heutige Klassenmodelle werfen, dann wird Ihnen auffallen, dass je nach Modellierung zwar die Konzepte (was ist ein Auto, ein Flugzeug, ein Schornstein, ein Hochhaus) gut im objektorientierten Sinne in einem Modell voneinander zu separieren sind, dass diese Modelle jedoch auch verwaschene Lifecycleübergänge aufweisen. Das heißt, viele Elemente weisen z.B. ein Statusattribut auf. Bei einem Flugzeug könnte das z.B. „im Bau“, „verkauft“, „in Betrieb“, „ausrangiert“, „zerlegt“ sein. Bis hierhin ist das Modell noch einfach und der Übergang oft noch sauber modelliert. Wohin aber mit den Metaattributen, die man zusätzlich benötigt, wie „Betriebsstunden“, „Kaufpreis“, „Entsorgungszeitpunkt“ usw.?
150
Theme Approach – Analyse und Design in AOP
Sie beziehen sich wiederum darauf, in welchem Lebenszyklusabschnitt sich das Objekt befindet. Der Kaufpreis kann erst sinnvoll gefüllt werden, wenn das Flugzeug auch gebaut wurde, der tatsächliche Entsorgungszeitpunkt steht gegebenenfalls erst fest, wenn das Flugzeug nicht mehr in Betrieb ist. Diese voneinander zu trennen und nicht nur im Javadoc darauf hinzuweisen, dass einige „Felder“ erst „später irgendwann“ gefüllt werden, ist schwierig. Oftmals werden zur Lösung Zustandsübergangsdiagramme gewälzt und kleine Häkchen neben jene Werte geschrieben, die dann gerade wieder relevant sind. In der klassischen Modellierung, in der eine Klasse nur durch eine Perspektive beschrieben wird, ist diese Darstellung sehr unvollkommen und missverständlich. Zurück zum Theme Approach. Der Ansatz hat nicht nur den Beigeschmack, eine Menge Diagramme zu erzeugen, nein, er dokumentiert auch auffallend ausführlich den Weg zum endgültigen Modell. Wozu ist die Aufbewahrung dieser Informationen nun sinnvoll? Viele Diagramme werden doch als Schrank-Ware im Aktenordner verschwinden, oder? Wenn sich die Anforderungen an die Software ändern – und das ist eine der typischen Schwierigkeiten bei der dominanten Dekomposition –, können Sie jetzt ganz in Ruhe das ein oder andere Theme ändern oder ergänzen, ohne sich darauf zu konzentrieren, das Gesamtgebäude mit dem Refactoring nicht zum Einsturz zu bringen. Dies hört sich nach einem typischen Forward Engineering-Prozess an, bei dem aus den Design-Diagrammen faktisch 1:1 das größtenteils fertige Programm entsteht. Ein Vorgang, der sehr gut mit Generatoren und UML-Diagrammen zu harmonieren scheint. Tatsächlich finden sich am Markt in diesem Bereich auch erste MDA (Model Driven Architecture)-Ansätze, die einen solchen Entwicklungsprozess unterstützen. Neben den Basiselementen enthielt das Beispielmodell noch drei AspektThemes. Auch sie werden zunächst ganz unabhängig voneinander modelliert1. 1. Über die Problematik von nichtorthogonalen Aspekten, die dennoch Beziehungen zueinander aufweisen, haben wir in diesem Zusammenhang in den vorherigen Kapiteln schon einmal gesprochen.
AOP
151
3 – Architekturen mit AOP
Sie könnten gegebenenfalls gemeinsame neue Subthemes aufdecken, in denen Klassen zusammenzuführen sind. Wahrscheinlicher ist allerdings, dass die Themes separat entstehen. Bliebe das Thema der Aufrufschnittstelle zwischen ihnen bzw. zwischen dem AOP- und dem OOP-Teil. Hierzu werden entsprechende Binding-Metainformationen (in der Grafik als gestrichelte Linien) eingezogen. Eine Bindung „*.execute(..)“, wie sie in Abbildung 3.4 zu sehen ist, könnte dabei bedeuten, dass alle execute()-Methoden unabhängig von ihrer Klassenzugehörigkeit bei Aufruf ein Trigger für die Ausführung eines Aspekts sind. Leider scheinen hier Ausdrucksstärke und Spezifikation noch zu ungenau, um jede AOP-Operation ausreichend beschreiben zu können. Die im Theme/UML gesammelten Informationen sollten dennoch zunächst ausreichen, um mit der Programmierung und der Komposition in einem entsprechenden AOP-Produkt beginnen zu können. Für eine weiter- und tieferführende Dokumentation zum Theme Approach empfiehlt sich ein Blick in [Clarke05] oder auf die Webseite1. Nach der Diskussion um Vorgehensmodelle ist es nun sinnvoll, die innerhalb der Analyse/Design-Phase erarbeiteten Aspekte auch in Sourcecode umzusetzen. Ein Teil dieser Aufgabe besteht in der Beschreibung der Bindings, also der Frage, wo Aspekte aneinander grenzen.
3.2 Architektonische Grundlagen Dieser Abschnitt beginnt mit einer allgemein theoretischen Einführung in die Anwendung von Aspekten und Überlegungen zur Verbindung von Aspekten und Kernsource. AOP verhält sich in der Realisierung wie OOP oder weniger programmatische als vielmehr reale Herausforderungen wie beispielsweise Autofahren. Die reine Kenntnis über die Bedieninstrumente eines Fahrzeugs und die ungefähre Richtung führen nicht zwangsläufig zu einem guten Ergebnis. Daher werden im Folgenden einige Hauptüberlegungen zum Einsatz von AOP
1. http://www.thethemeapproach.com/
152
Architektonische Grundlagen
angestellt, die man bei der praktischen Nutzung nicht aus den Augen verlieren sollte.
3.2.1
Pointcutwahl – eine Frage der Modellierung
In den bislang vorgestellten Programmierbeispielen von Aspekten stellte sich die Implementierung des Aspekts als relativ trivial dar. Oft bildet ein vorhandener Sourcecode die Basis, der an einzelnen Stellen um das ein oder andere Statement „ergänzt“ werden soll. Kritiker sprechen hier von einem nichtdeterministischen Laufzeitverhalten. Denn nach Einweben des Aspekts reagiert das Programm nach dem AOP-Weaving womöglich anders als erwartet. Ganz von der Hand zu weisen ist dieser Einwand nicht, solange die Stellen, an denen Aspekte in den Sourcecode gewoben werden, dem Entwickler nicht transparent gemacht werden können. Es bedarf also für die Anwendung von Aspekten 쐌 des Wissens um das Vorgehen zur Erstellung von Aspekten, 쐌 des Vorhandenseins entsprechender Toolunterstützung um die Joinpoints des Programms zu visualieren, 쐌 der Erstellung bzw. Verabredung entsprechender Designrichtlinien, wie Aspekte wo eingewoben werden sollen und dürfen. Mit aspektorientierter Programmierung faktisch jeden Methoden- und Klassenaufruf ins Gegenteil verkehren zu können, klingt oft weit weniger charmant als das eigentlich ursprünglich geplante Ziel der Verbesserung der Sourcecodestruktur und Lesbarkeit des Programms. Der Entwickler eines Aspekts muss bei der Definition des Joinpoint sicherstellen können, dass der Aspekt überall dort angreift, wo es für sinnvoll erachtet wird. Andererseits darf er aber eben auch nur dort und nicht an anderer Stelle Auswirkungen auf das Programmverhalten haben. Für die anderen Sourcecodefragmente, die von einem Aspekt berührt werden könnten, sind nicht selten auch andere Entwickler(gruppen) zuständig. Benötigt wird also neben dem reinen Verständnis über die Funktionsweise von Aspekten auch ein gemeinsames „Wertesystem“, das definiert, 쐌 in welchen Bereichen Aspekte für welchen Zweck verwendet werden,
AOP
153
3 – Architekturen mit AOP
쐌 wer über die Einführung neuer Aspekte entscheidet und wie über die Einführung neuer Aspekte informiert wird, 쐌 auf welche Weise Pointcuts definiert werden (auf welchen Elementen des Source dürfen sie wie aufsetzen), 쐌 für welche Aktivitäten Aspekte nicht eingesetzt werden dürfen. Zunächst erscheint die Reglementierung des Einsatzumfangs von Aspekten einer freien Meinungsäußerung in der Mehrdimensionalität des Sourcecodes zu widersprechen. Gerade die durch AOP entwickelbaren Designkonstrukte schaffen ja eine ganz neue „Programmbewusstseinsebene“. Dafür müssen sie allerdings auch verstanden werden. Die Manipulation fremder privater Methoden und Instanzvariablen schlimmstenfalls von Third-Party-Jars ist sicherlich mehr als „Hack“ oder „Patch“ zu bezeichnen denn als sinnvolle Designstrategie. All zu leicht werden diese Aspekte in kommenden Versionen z.B. durch Umbenennung interner, privater Methoden nicht oder nicht mehr anwendbar sein. Aspektorientiertes Vorgehen wird so schnell zur Farce, zu dem tödlichen Versuch, den Änderungen des Sourcecodes schnell genug zu folgen, um das Verhalten des Programms noch „einigermaßen“ vorhersehen zu können. Setzen Sie auf bestimmte Einstiegspunkte in fremden Programmen auf, laufen Sie schnell den Änderungen hinterher. Tatsächlich wird aber andersherum ein sinnvolles Vorgehen daraus. Man definiert erst, welche Grundbedingungen geschaffen sein müssen, um die Software möglichst klar in ihrer einen Dimension zu strukturieren. Auf den allgemein definierten Strukturen lässt es sich dann oftmals besser aufsetzen, als mit der Gefahr zu leben, Aspekte nicht mehr oder an den falschen Stellen einzuweben. In gewisser Weise offenbart sich mit der Einführung von Annotations in Java 5 eine Lösung dafür, Aspekteinsprungspunkte Anwendern und Entwicklern der Aspekte gleichermaßen deutlich zu machen.
154
Technische Grundlagen AOP
Dem Pointcut1 fällt die Aufgabe zu, die Eigenschaften von Programmelementen zu definieren, die sie aufweisen müssen, um in den Genuss eines Aspekts zu kommen. Den Pointcut korrekt und bestmöglich zu formulieren, ist eines der größten Geheimnisse oder besser der immanentesten Herausforderungen auf dem Gebiet aspektorientierter Programmierung. Ein weiterer Teil des aspektorientierten Vorgehens bedarf der Betrachtung der Aspekte an sich und ihrer Beziehungen zueinander. Wann wird welcher Aspekt gegebenenfalls in welcher Reihenfolge aktiv? Welche Gemeinsamkeiten (in Form von Bibliotheken bzw. Klassen) weisen sie auf? Lassen sich abstrakte Aspekte gleicher bzw. ähnlicher Verhaltensweisen gemeinsam faktorisieren? Wie ist es dem Aspekttool rein technisch nun möglich, die Bindevorschrift zwischen Pointcut, Joinpoint und der Advice-Aspekt-Zuordnung einzuhalten? Wie manipuliert man eigentlich bestehenden Bytecode?
3.3 Technische Grundlagen AOP Primäre Kandidaten für das Einweben von Aspekten in Java sind 쐌 die Modifikation von Bytecode nach oder bei dessen Erstellung2, 쐌 die generische Erzeugung von Klassen oder die Nutzung von Dynamic Proxies. Dies wird gepaart mit einem kurzen Ausflug in Richtung Classloader-Mechanismen. In diese unterschiedlichen Richtungen will dieses Unterkapitel einen groben Blick werfen, um zu verstehen, was unterhalb der „Wundermaschine Weaver“ so alles los ist, damit genau diese Varianten machbar werden.
3.3.1
Java Reflection
Java Reflection hat zunächst nur wenig mit AOP zu tun, dennoch ist es Basisbestandteil der erweiterten Funktionalitäten von Java und wichtige Voraussetzung für das Verständnis einiger Konzepte, die im Folgenden besprochen werden. 1. also faktisch der SELECT-Bedingung des Programms 2. Ich gehe davon aus, dass die Modifikation von Sourcecode weniger spannend sein dürfte.
AOP
155
3 – Architekturen mit AOP
An dieser Stelle soll Java Reflection nur kurz angerissen, nicht aber weiter vertieft werden. Weiterführende Informationen finden Sie beispielsweise im Java Tutorial unter dem Unterpunkt „Reflection“1. Die Reflection API2 dient der Widerspiegelung des Aussehens und des Zustands von Klassen/Interfaces und der innerhalb der VM befindlichen Objekte. Zum einen gibt Reflection Auskunft über die statische Struktur eines Objekts bzw. der Klassen, denen das Objekt angehört (welche Konstruktoren, Felder und Methoden mit welchen Sichtbarkeiten bietet das Objekt an?). Zum anderen erlaubt Reflection das Auslesen der Eigenschaften (Lesen von Datenzuständen), die Modifikation von Feldinhalten (Schreiben von Datenzuständen) und den Aufruf von Methoden und Konstruktoren. Im nachfolgenden Beispiel wird die Klasse ReflectionExample zunächst auf ihre Feld- und Methodenstruktur hin ausgelesen, anschließend wird der String-Konstruktor gesucht und auf ihm eine Instanz erzeugt. Das Beispiel schließt mit dem Aufruf der execute-Methode unter Übergabe eines entsprechenden Strings. import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; public class ReflectionExample { private String inputValue = null; public static void main(String[] args) { try { Class reflectionClazz = Class.forName("ReflectionExample"); // Felder dieser Klasse Field[] fields = reflectionClazz.getDeclaredFields();
1. http://java.sun.com/docs/books/tutorial/reflect/ 2. zu Deutsch Widerspiegelung, lat. reflectere: „zurückbeugen, -biegen, -krümmen“
156
Technische Grundlagen AOP for (int i = 0; i < fields.length; i++) { Field f = fields[i]; System.out.println(f); } // Methoden dieser Klasse Method[] methods = reflectionClazz.getDeclaredMethods(); for (int i = 0; i < methods.length; i++) { Method m = methods[i]; System.out.println(m); } // Konstruktor suchen Constructor constructor = reflectionClazz.getConstructor( new Class[] { String.class }); // Instanz erzeugen, Parameter übergeben Object instance = constructor.newInstance( new Object[] { "Hallo" }); // Methode suchen Method method = reflectionClazz.getDeclaredMethod( "execute", new Class[] { String.class }); // Private-Status aufheben method.setAccessible(true); // Methode aufrufen method.invoke( instance, new Object[] { " Welt" }); } catch (Exception e) { e.printStackTrace(); } } public ReflectionExample(String input) { inputValue = input; }
AOP
157
3 – Architekturen mit AOP private void execute(String appendix) { System.out.println( this.inputValue + appendix); } }
Zur erfolgreichen Ausführung muss bei Java Reflection der Security Manager deaktiviert oder über Policies so eingestellt sein, dass der Zugriff auf die Datenstrukturen erlaubt wird. Der Konsolen-Output für das obige Beispiel könnte abhängig vom Compiler wie folgt erscheinen: private java.lang.String ReflectionExample.inputValue static java.lang.Class ReflectionExample.class$0 public static void ReflectionExample.main(java.lang.String[]) private void ReflectionExample.execute(java.lang.String) Hallo Welt
Reflection ist der Einstieg in die Untersuchung von Java-Programmstrukturen und die dynamischere Handhabung von Java. Es ist somit auch ein erster Schritt in Richtung der Manipulation des Verhaltens eines Programms. Die reine Anzeige (oder eben Reflexion) der Klassenstruktur ist oftmals weit weniger interessant als die daraus resultierende Änderung von Objektzuständen oder die Erzeugung von Instanzen. Java Reflection ermöglicht eine ganze Reihe von Unsauberkeiten im Sourcecode. Das Problem bei Reflection ist dabei, dass die Anwendung von Reflection zu 95% auf Stringliteralen arbeitet und somit zu einem ungetypten und syntaktisch nicht prüfbaren Sourcecode führt. Der Compiler kann beim Compile die Korrektheit der Statements nicht prüfen, was zu einem potenziellen Risiko vor allem in umfangreichem Sourcecode führt. Spätere Refactoringmaßnahmen, in denen Klassen oder Methoden umbenannt werden könnten, werden schnell zum „Eiertanz“ auf dem Code. Einerseits ist dieses Verhalten richtig, da Reflection ermöglichen soll, das zu tun, wozu man im normalen Sourcecode nicht im Stande wäre (z.B. ad hoc Ermittlung, Laden und Instanziieren fremder Klassen). Gleichzeitig be-
158
Technische Grundlagen AOP
zeichnete James Gosling, der Java-Ur-Vater, auf der OOP 2003 „Reflection als die größte Fehlerquelle in der Java-Nutzung“. Einem JBoss AOP-XML-File oder einer AspectJ-Pointcutdeklaration ließe sich hier natürlich der gleiche Vorwurf einer deklarativen, syntaktisch ungeprüften Konfigurationsvariante machen. Auch sie setzen auf Stringliteralen auf, werden erst beim Weaving aktiv und bringen daher ein hohes Risiko in AOP. Wir sehen später, wie sich auch das Problem sich ändernder Signaturen und nicht geprüfter Pointcuts minimieren lässt.
3.3.2
Dynamic Proxies – erster Schritt Richtung AOP?
Im Folgenden beschäftigen wir uns mit einer AOP-Variante, die ganz ohne fremde Produkte oder Weaver auskommt und ab Java 1.3 verfügbar ist. Der Begriff des Proxy beschreibt ein Stellvertreterobjekt, das Methodenaufrufe an ein oder mehrere dahintergeschaltete Instanzen delegiert. Dem aufrufenden Client ist ohne weiteres normal nicht bekannt, mit welcher Instanz er arbeitet bzw. dass die Originalinstanz über einen Proxy abgeschirmt wurde. Bereits vor der Version 1.3 von Java, mit der dynamische Proxies eingeführt wurden, konnte man natürlich Stellvertreterobjekte erzeugen, indem man die Aufrufe an eine Instanz A zunächst an ein Objekt B geroutet hat, das seinerseits wieder A aufrief. Seit Version 1.3 lässt sich B nun dynamisch erstellen, es muss also vorher nicht definiert werden. Somit ist es möglich, die Proxyschnittstelle erst zur Laufzeit festzulegen. Die Einsatzmöglichkeiten dynamischer Proxies sind relativ vielfältig. In unserem AOP-Kontext können solche Proxies nun verwendet werden, um in einer oder sogar mehreren hintereinander geschalteten Stellvertreterinstanzen Aspekte, die vor oder nach der Zielmethode angewandt werden sollen, zu verbergen. Dynamische Proxies bestehen aus einer Instanz der Klasse Proxy sowie einer damit assoziierten Instanz der Klasse InvocationHandler. Beide Proxyelemente finden sich seit JDK 1.3 im Package java.lang.reflect. Eine InvocationHandler-Instanz empfängt die an einen Proxy gerichteten Methodenaufrufe und kann selbst entscheiden, ob sie die Aufrufe an die ei-
AOP
159
3 – Architekturen mit AOP
gentliche Zielinstanz delegiert oder abweichend ein anderes Verhalten vorgibt bzw. ergänzt. Im einfachsten Falle ruft das Proxyobjekt die Methode invoke() des InvocationHandler auf.
Abb. 3.5: Dynamic Proxy für AOP-Zwecke
Eine typische Erzeugung eines Proxy zu AOP-Zwecken sieht wie folgt aus: // Instanziierung des selbstdefinierten Handlers InvocationHandler handler = new AnInvocationHandler(); // Referenz auf den System-ClassLoader besorgen ClassLoader sysClassLoader = ClassLoader.getSystemClassLoader(); // Array mit Class-Objekten für Proxy-Interfaces Class[] proxyInterfaces = new Class[]{test.MyInterface.class}; // Erzeugung des Proxy-Objekts Proxy proxy = Proxy.newInstance(sysClassLoader, proxyInterfaces, handler);
An dieser Stelle sei dazu auch ein zusätzliches sehr einfaches Implementierungsbeispiel gegeben, bei dem wir dem Sinn der Realisierung einmal weniger Beachtung schenken wollen als dem Einsatz des Proxy:
160
Technische Grundlagen AOP package test; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class AnInvocationHandler implements java.lang.reflect.InvocationHandler { private Object obj; public static Object newInstance(Object obj) { return java.lang.reflect. Proxy.newProxyInstance( obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), new AnInvocationHandler(obj)); } private AnInvocationHandler(Object obj) { this.obj = obj; } public Object invoke( Object proxy, Method m, Object[] args) throws Throwable { Object result; try { System.out.println( "before executing method " + m.getName()); result = m.invoke(obj, args); } catch (InvocationTargetException e) { throw e.getTargetException(); } catch (Exception e) { throw new RuntimeException( "unexpected exception: " + e.getMessage()); } finally { System.out.println( "after executing method " + m.getName()); } return result; } }
AOP
161
3 – Architekturen mit AOP
Die invoke-Methode empfängt die auszuführende Nachricht, druckt eine Informationszeile (before execution/after execution) jeweils vor und nach Ausführung der eigentlich beim Invoke aufzurufenden Aktion auf die Konsole und führt ihrerseits beim Invoke die Zielmethode selbst aus. Im aspektorientierten Sinne würde hier der Begriff des around()-Advice passen, der Proxy legt sich um die auszuführende Methode herum, daher around. Blieben noch ein Beispiel für das Interface und eine Implementierung der Vollständigkeit samt einem Aufrufbeispiel übrig. package test; public interface MyInterface { Object execute(Object input) throws Exception; } package test; public class MyInterfaceImpl implements MyInterface { public Object execute(Object input) throws Exception { if (input == null) { throw new Exception( "Input object must not be null"); } return input.toString() + input.hashCode(); } }
Der nachfolgende Aufruf würde sinnvollerweise in einer Factory gekapselt, so dass der das MyInterface nutzende Client von der Nutzung eines Proxy im Hintergrund nichts mitbekommen würde. package test; public class ExampleClass {
162
Technische Grundlagen AOP public static void main(String[] args) { MyInterface anInstance = (MyInterface) AnInvocationHandler.newInstance( new MyInterfaceImpl()); try { System.out.println( anInstance.execute(new String( "Hallo AOP!"))); } catch (Exception e) { e.printStackTrace(); } } }
Auf der Ausgabekonsole kommt schließlich folgendes Ergebnis zustande: before executing method execute after executing method execute Hallo AOP!-972986071
Wirft man einen kritischen Blick auf die Implementierung der MyInterfaceImpl-Klasse, so fällt die Nullprüfung bereits zu Beginn auf, die in dieser sehr einfachen Lösung wiederum einziger Grund für das Werfen von Exceptions ist (was man in einem vernünftigen Design normalerweise tunlichst vermeiden sollte). Die Prüfung auf null des übergebenen Objekts soll hier repräsentativ für umfangreichere oder komplexere Prüfungen der Eingabeobjekte verstanden werden. Hier macht sie nicht so wahnsinnig viel Sinn. Dennoch kann die Klasse AnInvocationHandler nun beides (Logging und Prüfung) enthalten. Sinnvoller wäre auch, diese beiden Aspekte noch einmal voneinander zu trennen. Leider bedarf das Handling und „Ineinanderschachteln“ mehrerer Proxies dann aber auch schon eines etwas komplexeren Designs, an dessen Ende vermutliche eine AOP-typische Lösung stehen würde. Idealerweise wüsste die Factory, die MyInterfaceImpl und MyInterface zueinander über Proxies in Beziehung setzt, von beiden Klassen lediglich per Konfiguration. Die zu instanziierenden Proxies und ihre Reihenfolge würden ebenfalls dynamisch per Konfiguration übergeben. AOP
163
3 – Architekturen mit AOP
Das Thema „Dynamic Proxy“ soll mit der Erweiterung um eine Factory abgeschlossen werden. Dazu ergänzen wir der Einfachheit halber ein neues Interface, das wir von der Klasse AnInvocationHandler implementieren lassen. Alle Proxies, die wir zukünftig schachteln wollen, müssen dieses Interface realisieren. package test; public interface InterceptorInterface { Object getInterceptor(Object obj); }
Die Klasse AnInvocationHandler wird nun erweitert: [...] public class AnInvocationHandler implements java.lang.reflect.InvocationHandler, InterceptorInterface { public AnInvocationHandler() { } public Object getInterceptor(Object obj) { return AnInvocationHandler.newInstance(obj); } [...] }
Und als Zugabe folgt abschließend eine simple Factory, die später noch die Konfiguration zu einer Implementierungsklasse extern einlesen müsste. Ein komplexes Fehlerhandling ist an dieser Stelle der Einfachheit halber nicht aufgezeigt.
164
Technische Grundlagen AOP package test; import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class SampleFactory { public static Object getInstance( Class targetInterface) { String targetImplName = targetInterface.getName() .replaceAll(".class","") .concat("Impl"); try { Class clazz = Class.forName(targetImplName); Object implInstance = clazz.newInstance(); List handlers = getHandlers(clazz); for (Iterator iter = handlers.iterator(); iter.hasNext();) { InterceptorInterface handler = (InterceptorInterface) iter.next(); implInstance = handler.getInterceptor(implInstance); } return implInstance; } catch (Exception e) { throw new IllegalArgumentException( "Instanciation failed: " + targetImplName); } } /** * gibt eine Liste aller Interceptoren zurück */ private static List getHandlers(Class clazz) { List list = new ArrayList(); list.add(new AnInvocationHandler()); return list; } } AOP
165
3 – Architekturen mit AOP
Das verkürzt den späteren Aufruf und die spätere Nutzung des Proxykonzepts auf die folgenden Statements: MyInterface anInstance = (MyInterface) SampleFactory.getInstance(MyInterface.class); try { System.out.println(anInstance.execute( new String("Hallo AOP!"))); } catch (Exception e) { e.printStackTrace(); }
Es gibt einige Produkte am Markt, die diese Erzeugung von Implementierungsklasse in Kombination mit Interceptoren (ein anderer Begriff für die Anlage von Proxies) bereits recht gut perfektioniert haben. Neben Java EE Application-Servern, die aufgrund der Trennung von Interfaces und Implementierungen von Beans bereits entsprechende Angriffsflächen bieten, findet sich ein solches Interceptorkonzept beispielsweise auch in LightweightContainern wie Apaches Hivemind1 oder dem Springframework wieder. Je nach Variante mit und ohne Interfaces. AOP für Einsteiger? Beim Einsatz von Dynamic Proxies handelt es sich um eine sehr Java-nahe und aus Debugsicht recht gut unterstützte Variante, Aspekte und Cross-Cutting Concerns in separate, wiederverwendbare Klassen auszulagern. Jene, die diesem Dynamic Proxy-Konzept die Medaille „AOP für Arme“ aufdrücken, haben insofern Recht, als der Ansatz sehr eingeschränkt ist. Interceptor-Methoden müssen in Interfaces deklariert sein (womit sie wiederum public sein müssen) und fühlen sich nur bei einer Trennung zwischen Interface und Implementierung „zu Hause“. Von anderer Seite wird dieser Ansatz allerdings auch befürwortet, lässt er doch beim Konstruieren jeder Instanz zu, andere Interceptoren ad hoc zur Laufzeit zur Anwendung zu bringen.
1. http://jakarta.apache.org/hivemind
166
Technische Grundlagen AOP
Verlagert man das Proxy-Chaining selbst in einen Proxy, erlaubt das, bei jeder gewrappten Instanz zur Laufzeit dynamisch neue Interceptoren in einer Art Kette (chaining) hinzuzufügen oder dynamisch zu entfernen, weshalb in diesem Zusammenhang auch gerne der Begriff „Dynamic AOP“ als Werbemittel verwendet wird. Obwohl recht dynamisches Verhalten möglich ist, ist der Dynamic Proxy-Ansatz eben leider auf eine bestimmte Designgrundlage gestützt. Die Schlussfolgerung, jede nur erdenkliche Klasse nun mit Interfaces auszustatten und über Fabriken künstlich, statt über „new“ zu erzeugen, wird sehr wahrscheinlich sowohl beim Übertritt von einer virtuellen Maschine auf ein skalierbares n-Tier-System mit vielen verschiedenen Prozessen als auch aufgrund des Umfangs von Klassen und Interfaces ad absurdum geführt. Dynamic Proxies an neuralgischen Stellen der Architektur (z.B. an Sessionfassaden, Controllern oder Adaptern) eingesetzt, können allerdings sehr gute Hilfestellung liefern, Robust- und Wiederverwendbarkeit von Aspektimplementierungen zu erreichen. Jakartas Hivemind verwendet eine solche Dynamic Proxy-gesteuerte Variante. Pflicht ist die Verwendung solcher Proxies für ein vernünftiges Design natürlich nicht. Im Anwendungsdesign finden sich immer wieder einige typische Stellen, an denen die Anwendung von Transaktionsauf- und -abbau, Sicherheitsprüfungen oder Tracing sinnvoll ist. Diese gilt es zu identifizieren und über Dynamic Proxies oder auch Dependency Injection in Sachen Konfiguration und Verhalten zu flexibilisieren. Dynamic Proxy-Alternativen Die Anwendung von Dynamic Proxies hat ihren Preis. Der Proxy muss instanziiert, Anfragen müssen umgeleitet werden. Laut einiger Studien ist die Verwendung des Standard Java DP (Dynamic Proxy) rund 40x langsamer als der direkte Aufruf der Instanz in JDKs bis Version 1.4. Ab Java Version 5 ergibt sich noch ein Performancenachteil von 1,5.
AOP
167
3 – Architekturen mit AOP
Neben dem Standard-Java DP gibt es auch noch andere Implementierungsvarianten wie z.B. den CGLIB1 Dynamic Proxy. Da Letzterer aber eine echte Klasse generiert, kostet bereits der erstmalige Aufruf einiges an Geschwindigkeit. Ebenfalls noch relativ frisch am Markt wird unter Apache Commons Regie eine Commons Proxy-Bibliothek2 entwickelt, die zum Erstellungszeitpunkt allerdings noch in der Sandbox in Version 0.1 vorlag. In den CGLIB-Bibliotheken findet sich auch folgendes Beispiel für die Implementierung eines Delegate. Ein Delegate delegiert (wie der Name schon sagt) einen Aufruf an eine fremde Objektinstanz. In diesem Fall delegiert er einen Methodenaufruf an die Startklasse selbst. public interface TestDelegate { int main(String[] args); }
Zur Erzeugung des Delegate wird über die Klasse MethodDelegate künstlich ein Delegate erzeugt und diesem die Zielmethode mitgegeben: public class Main { public static int main( String[] args ) { Main newMain = new Main(); TestDelegate start = (TestDelegate) MethodDelegate.create(newMain, "alternateMain", TestDelegate.class); return start.main( args ); } public int alternateMain( String[] args ) { for (int i = 0; i < args.length; i++) { System.out.println( args[i] ); } return args.length; } } 1. http://cglib.sourceforge.net 2. http://jakarta.apache.org/commons/sandbox/proxy/
168
Technische Grundlagen AOP
Um solcherlei Tricks mit Klassen vollführen zu können, bedarf es allerdings der Bytecodemanipulation oder der Erzeugung von neuen Klassen zur Laufzeit1, womit wir beim direkt anschließenden Thema angelangt wären.
3.3.3
Bytecodemanipulation
Beginnen möchte ich mit der simplen Frage: „Was ist eigentlich Bytecode“? Jeder, der ein bisschen mit Java zu tun hatte, wird antworten, es handele sich um den kompilierten Java-Sourcecode, der der virtuellen Maschine übergeben wird, um Instruktionen auszuführen. Dies assoziiert zunächst, dass es sich bei Bytecode um etwas in Beton Gegossenes handelt, ähnlich wie ein Buchdruck der frühen Tage oder ein Gipsabdruck. Eigentlich ist es aber nicht mehr als die transformierten Befehlsanweisungen aus dem Javasource in die Sprache des Compilers. Reversibel, also umkehrbar in den Javasource in Sachen Dekompilierung, und genauso modifizierbar – für uns Menschen allerdings mit größerer Mühe – wie der Javasource auch. Das bedeutet also: Alles, was ich mit Javacode machen kann, kann ich mit Bytecodemanipulation auch machen. Gilt das immer? Nein, denn ich kann auch Javacode schreiben, der nicht in Bytecode kompilierbar, also überführbar ist und umgekehrt. Normalerweise spricht man aber nicht von Bytecodemanipulation, sondern benutzt – besonders, was Technologien wie JDO anbelangt – den Begriff des „Bytecodeenhancing“, also der Erweiterung, der Verbesserung des Codes über seine bisherigen Stärken hinaus. Nochmals anders gesagt, der Bytedatenstrom wird so verändert, dass er sich tatsächlich nicht unbedingt so verhält, wie der Entwickler in seinem Sourcecode formuliert hat. Wer über Bytecode redet, muss auch über Classloader sprechen. Der Classloader ist so ein bisschen das Gegenstück zum Compiler. Er führt zwar nicht den Bytecode wieder zurück in Javasource, aber er lädt ihn, prüft seinen Inhalt gegen diverse Checksummen und baut Referenzen zu anderen Klassen (fremder Bytecode) auf, um Klassen der VM zur Verfügung zu stellen. 1. in diesem Fall dem Delegate
AOP
169
3 – Architekturen mit AOP
Classloader befinden sich zur Laufzeit in einer Classloader-Hierarchie. Das heißt, es kommen potenziell beim Laden einer Klasse mehrere Classloader zum Einsatz. Jeder Classloader befragt per Definition seinen „parent“, was normalerweise dem Classloader entspricht, von dem er selbst geladen wurde, ob dieser etwas über eine zu ladende Klasse weiß. Erst wenn dieser die Kenntnis verneint, wird der eigene Classloader selbst aktiv. Ziel dieser Aktion ist primär die Minimierung von redundant geladenen Klassen und die Unterstützung von Sicherheitsmechanismen. Um Klassen wie z.B. die Klasse java.lang.String nicht selbst redefinieren zu können, sind alle Classloader angewiesen, zunächst Bootstrap-Classloader abwärts bis zum konkretesten Classloader nach der java.lang.String-Klasse zu fragen. Der Bootstrap-Classloader wird die Klasse stets nur aus dem JRE bzw. den entsprechenden Bibliotheken lesen (siehe Abbildung 3.6).
Abb. 3.6: Classloader-Hierarchie
170
Technische Grundlagen AOP
Ein Überschreiben dieses Verhaltens und ein damit einhergehendes Ignorieren der Classloader-Hierarchie oder einer abgewandelten Baumstruktur ist zwar möglich, allerdings unerwünscht. Wenn AOP in Java die Kunst beschreibt, das Verhalten eines Programms zu beeinflussen, so gibt es für AOP verschiedene Ansatzpunkte, um dies zu realisieren: die Manipulation des Javasource, des Bytecodes, des Classloader oder der Durchleitung von Aufrufen durch Proxyinstanzen und die Manipulation des Verhaltens dort. Letzte Eingriffsmöglichkeit besteht in der Änderung der virtuellen Maschine, sprich der Eigenentwicklung einer Lösung, die Aspekte zur Laufzeit in den Bytecode integrieren kann. Es lässt sich aber vortrefflich darüber streiten, ob man in diesem Fall wirklich noch von einer Java-VM sprechen kann oder ob das Produkt nicht längst ein Derivat ist. Aus dynamischen Gesichtspunkten heraus – also der Modifizierbarkeit von Aspekten oder Aspektverhalten zur Laufzeit – sind Javasource- und Bytecodemanipulation eigentlich am uninteressantesten. In beiden Fällen ist das Verhalten der Klasse schon vorgeprägt, bevor der Classloader die Klasse in die VM einsetzt. Insofern ist es bei diesen Varianten zumindest aus Laufzeitumgebungssicht vollkommen irrelevant, ob hier überhaupt eine Bytecodemanipulation stattgefunden hat oder ob der Benutzer selbst durch Programmierung in Java ein solches Zielverhalten definiert hat. BCEL Die Bytecode Engineering Library (BCEL) ist eine der ältesten Open-SourceBibliotheken, die man im Bereich der Manipulation vorfinden kann. Der Bytecode einer Klasse ist in mehrere Segmente unterteilt (siehe Abbildung 3.7). Hinter einem allgemeinen Header-Bereich findet sich der so genannte Constant Pool. Er liefert ein wenig simpel formuliert lediglich eine Menge von Strings. Hier finden sich Literale, die im Code benutzt werden, ebenso wie Importstatements, Parametertypen oder Superklassennamen – ein wilder Wust aus Zeichenketten, der ohne Pointer und Verweise faktisch wenig sinnvolle Informationen enthält.
AOP
171
3 – Architekturen mit AOP
Abb. 3.7: Class-Bytecode-Aufteilung
Ihm folgen – schon Javasource-ähnlicher – Blöcke, die das Zugriffsverhalten, die Felder der Klasse sowie Methoden und Klassenattribute beschreiben. Sie sind im Wesentlichen Mischungen aus definierten Byte-Befehlen und Pointern in den Konstantenpool. So werden Verhalten und Daten zueinander wieder in Beziehung gesetzt. Der Compiler entscheidet relativ selbstständig, ob er den Javasource 1:1 oder in modifizierter Form transformieren soll. Besonders statische und finale Elemente von Klassen sowie private Felder und Methoden eignen sich hervorragend, damit aus Performancegründen Teile des Sourcecodes entgegen den Vorschriften aus der Javaklasse wieder „geinlinet“, also redundant kopiert werden. BCEL ist ein Produkt, mit dem man eine solche Klassenstruktur aus existierenden .class-Files auslesen, gegebenenfalls ändern und die Änderungen neu speichern kann. Bestehende Klassen lassen sich ebenso ändern wie auch neue auf binärer Basis erzeugt werden können. Selbstverständlich kann erzeugter Bytecode auch in Zusammenhang mit dem aktuellen Classloader benutzt werden.
172
Technische Grundlagen AOP
BCEL eignet sich gut, um einen Einblick in die Java Bytecode-Strukturen zu gewinnen. Das Jakarta-Produkt ist dabei Bestandteil zahlreicher kommerzieller wie auch Open-Source-Produkte und kommt in Compilern, Optimizern, Obfuscatorn, aber auch Sourcecodegeneratoren und Analysetools zum Einsatz. Auf der oben beschriebenen Begrifflichkeitsebene (also Fields, Constant Pool, Bytebefehle) arbeitet BCEL, was den Umgang damit zuweilen etwas schwerfällig macht. Auch ist es „leider“ relativ unproblematisch möglich, den Bytecode so zu manipulieren, dass der Classloader das Laden bzw. die spätere Ausführung ablehnt, weshalb bei Manipulationen mit Vorsicht vorzugehen ist. An dieser Stelle tiefer in BCEL einzutreten, würde bedeuten, die Aufgabe der Bytecodemanipulation (eigentlich Teil des AOP-Weaver) dem Problem des AOP vorziehen. Dennoch ist ein Blick auf die BCEL-Seite1 durchaus empfehlenswert, um sich der Möglichkeiten, aber auch der Komplexität der Modifikationen bewusst zu werden. Janino Auf der Janino-Webseite2 findet sich ein weiteres, sehr interessantes OpenSource-Produkt, das zunächst einmal mit der Bezeichnung „Embedded Java Compiler“ daherkommt und verspricht, einzelne Expressions, Blöcke, Klassenkörper bis hin zu Javaklassen oder Mengen von Sourcefiles zu kompilieren, um daraus sofort lad- und ausführbaren Sourcecode zu erzeugen. Obwohl Janino nicht direkt mit Bytecodemanipulation, sondern mehr mit Ad-hoc-Bytecodeerstellung zu tun hat, ist ein Blick auf das Produkt an dieser Stelle durchaus gerechtfertigt. Dazu sei an dieser Stelle ein Sourcecodebeispiel mit Janino in Version 2.3.7 präsentiert.
1. http://jakarta.apache.org/bcel/manual.html 2. www.janino.net
AOP
173
3 – Architekturen mit AOP
Ziel des Compile soll die Überführung des folgenden, einfachen Klassencodes in Bytecode sein: public class TestClass { public String testMethod() { return "Hallo Welt"; } }";
Diese bis dato der VM vollkommen unbekannte und weder in Bytecode noch als .class-File vorliegende Klasse soll jetzt ad hoc kompiliert und geladen werden und die Methode testMethod() soll ausgeführt werden. Notwendig ist dazu, den Sourcecode einem Scanner zu übertragen, der den Sourcecode vor dem Compile parst und dann dem Compiler zur Überführung bereitstellt. Mittels einer kleinen Hilfsklasse überführt der Beispielcode dafür den in einem String vorliegenden Source in einen Eingabestrom für den Scanner. Der Compiler bekommt für den Compile zusätzlich eine Referenz auf den aktuellen Classloader mit (hier über einen Trick durch eine selbst erzeugte Instanz geladen), um weitere gegebenenfalls existierende Klassenreferenzen auflösen zu können. Über den eigenen Classloader des Compilers (den ByteArrayClassLoader) lässt sich nun Zugriff auf die neue Klasse nehmen, die über Java Reflection die Methode testMethod aufruft und das Ergebnis „Hallo Welt“ ausgibt. import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; import import import import
org.codehaus.janino.ByteArrayClassLoader; org.codehaus.janino.Scanner; org.codehaus.janino.SimpleCompiler; org.codehaus.janino.Scanner.ScanException;
public class ClassBuilder {
174
Technische Grundlagen AOP public static void main(String[] args) { // zu kompilierender Sourcecode String classSource = " public class TestClass { " + "public String testMethod() " + "{return \"Hallo Welt\";}}"; // Sourceparser Scanner scanner = null; try { scanner = new Scanner(null, new ClazzContentInputStream( classSource), null); } catch (ScanException e2) { e2.printStackTrace(); } catch (IOException e2) { e2.printStackTrace(); } try { // Kompilierung SimpleCompiler compiler = new SimpleCompiler( scanner, new ClassBuilder().getClass() .getClassLoader()); scanner.close(); ByteArrayClassLoader cl = ( ByteArrayClassLoader) compiler.getClassLoader(); // Laden der Klasse Class clazz = cl.loadClass( "TestClass"); // Instanziierung und Ausführung Object o = clazz.newInstance(); Method m = clazz.getMethod("testMethod", new Class[]{}); String returnValue = (String) m.invoke(o,new Object[]{}); System.out.println(returnValue);
AOP
175
3 – Architekturen mit AOP } catch (Exception e) { e.printStackTrace(); } } // String => InputStream-Umwandlung private static class ClazzContentInputStream extends InputStream { private byte[] arr = null; private int pointer = 0; public ClazzContentInputStream( String content) { arr = content.getBytes(); } public int read() throws IOException { if (pointer>=arr.length) { return -1; } return arr[pointer++]; } } }
Wie bereits angemerkt, kann Janino sowohl für Teilelemente von Sourcecode Scriptvaluierung durchführen als auch ganze Klassen(verbände) kompilieren. Dieser Compiler eignet sich somit im Gegensatz zum Javac vor allem für die Runtime-Kompilierung und Syntaxprüfung von Sourcecodeausdrücken. Für die recht überschaubare Größe des Compilers gibt es allerdings auch einige Einschränkungen, die man in puncto Flexibilität oder Aussagekraft von Compilefehlern hinnehmen muss. Stellen Sie sich einfach einmal für den Moment vor, die Fähigkeiten von Java Reflection und Ad-hoc-Bytecodeerzeugung miteinander zu kombinieren. Da zum Entwicklungszeitpunkt die Klassen, mit denen Sie im späteren Sourcecode umgehen wollen, gegebenenfalls noch gar nicht existieren, ist es schwer, für diese Klassen Methodenaufrufe zu konstruieren. Entweder Sie lassen die später erzeugten Klassen bestimmte Interfaces implementie-
176
Technische Grundlagen AOP
ren, die Sie zuvor schon statisch definiert haben, oder Sie machen sich Reflection zunutze. Für Einsteiger in Java macht Reflection auf den ersten Blick nur wenig Sinn. Schließlich ist das Aussehen der Klassen in der IDE, im Editor, doch problemlos ablesbar. Wozu sollte man da Reflection brauchen, um von Klassen deren Methoden oder Konstruktoren zu erfahren? Nun, Reflection macht gerade dann Sinn, wenn wir nicht wissen, wie die Klassen aussehen. Dies ist im Umgang mit unbekannten Klassen ein wichtiger Faktor, wenn wir nicht wissen, welche Methoden Klassen überhaupt haben, und dennoch mit ihnen umgehen müssen. Oder wir kennen sehr wohl deren ungefähre Struktur, aber nicht ihren Namen oder haben bei der Entwicklung (noch) keinen Zugriff auf sie. So entsteht zwischen beiden scheinbar fremden Technologien ein großes Synergiepotenzial. Wir werden uns dem Potenzial aus der Kombination von AOP-Ideen und Bytecodeerzeugung/-manipulation zum Ende dieses Kapitels hin noch einmal nähern. Javassist Unter dem Namen Javassist1 (Java Programming Assistant) findet sich unter der Obhut von JBoss ein weiteres interessantes Open-Source-Produkt zur Manipulation von Bytecode. Javassist bietet die bereits von BCEL und Janino her bekannten Möglichkeiten der Modifikation des Bytecodes sowie der Ad-hoc-Erzeugung von Klassen zur Laufzeit und gliedert sich damit inhaltlich etwa zwischen beiden vorherigen Produkten ein. Im Gegensatz zu BCEL zeigt sich Javassist allerdings etwas benutzerfreundlicher, da es sowohl auf Byte- als auch auf Sourcecode-Ebene mit der JavaSyntax umgehen kann. Die Manipulationsmöglichkeiten von Javassist sind gleichzeitig Grundlage für die JBoss AOP-Lösung, die wir in Kapitel 2 schon einmal betrachtet haben. 1. http://www.csg.is.titech.ac.jp/~chiba/javassist/
AOP
177
3 – Architekturen mit AOP
Javassist bietet ähnlich der Java Reflection API (java.lang.reflect) auch eine Inspektion von Klassen zur Laufzeit. Im Gegensatz zur Standard-Reflection-API können hier aber auch Klassen einer Prüfung und Analyse unterzogen werden, die noch nicht in die VM geladen wurden. Daraus ergibt sich die Einschränkung, keine neuen Instanzen nach normalem Verhaltensmuster über newInstance() erzeugen zu können, dafür den Bytecode allerdings bereits vor dem Standardzugriff durch einen Classloader manipulieren zu können. Im nachfolgenden Beispiel wird eine Klasse einer Inspektion unterzogen und der Name ihres Package sowie der Superklasse ausgegeben. Danach wird die ursprüngliche Vererbungshierarchie geändert, indem die Beispielklasse test.MyClassA nun nicht mehr von java.lang.Object, sondern von test.MyClassB erbt. import javassist.ClassPool; import javassist.CtClass; import javassist.NotFoundException; public class Javassisttest { public static void main(String[] args) { try { ClassPool pool = ClassPool.getDefault(); CtClass pt = pool.get("test.MyClassA"); CtClass pt2 = pool.get("test.MyClassB"); System.out.println(pt.getPackageName()); System.out.println( pt.getSuperclass().getName()); // Manipulation der Vererbungshierarchie pt.setSuperclass(pt2); pt.writeFile(); Class clazz = Class.forName("test.MyClassA"); // oder Class clazz = pt.toClass(); System.out.println( clazz.getSuperclass().getName()); } catch (NotFoundException e) { e.printStackTrace();
178
Technische Grundlagen AOP } catch (CannotCompileException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
Der Erstzugriff erfolgt über das ClassPool-Singleton, das CtClass-Instanzen für die jeweiligen Klassenrepräsentationen aus dem Klassenpfad heraus erzeugt. In ähnlicher Art und Weise ist das Hinzufügen weiterer Felder oder Methoden möglich, solange die Klasse noch nicht geladen worden ist. CtMethod m = CtNewMethod.make( "public int increase(int i) { return i++; }", pt); pt.addMethod(m);
Hier zeigt sich die Mischung aus dem vorgestellten Janino-Produkt und BCEL, es ist weder notwendig, sich auf die Tiefen des Bytecodes herabzubegeben noch muss man auf den gewohnten Sourceansatz verzichten. Gleichzeitig besteht aber die Möglichkeit, den im Bytecodeformat wesentlich feingranulareren Manipulationsansatz zu verwenden. Mittels des im Beispiel gezeigten writeFile()-Befehls werden die Änderungen persistent gemacht, danach ist eine Manipulation der CtClass-Instanz standardmäßig nicht mehr erlaubt. Eine Möglichkeit, das Schreiben der Klasse zu umgehen, besteht darin, einen eigenen Classloader zu verwenden. In diesem Fall liefert Javassist bereits ein fertiges Exemplar mit. Der javassist.Loader ist ein solcher Classloader, der zudem interessierten Parteien ein Listener-Interface anbietet, um in den Lade-Event der Klasse selbst eingreifen zu können. In Abbildung 3.8 finden wir dies als eine typische AOP-Bytecode-Manipulationsvariante unter dem Begriff „angepasster Classloader“.
AOP
179
3 – Architekturen mit AOP
Abb. 3.8: Laden von Klassen in die VM ClassPool pool = ClassPool.getDefault(); Loader cl = new Loader(pool); Class c = cl.loadClass("test.MyClass"); Object instance = c.newInstance();
So können Listener beim Laden der Klasse den Bytecode manipulieren, bevor die Klasse allgemein zur Verfügung gestellt wird. Als zusätzliches nettes Feature erlaubt der Classloader von Javassist, die Baumhierarchie der Classloader zu ignorieren. So wendet er sich erst dann an seinen zugewiesenen parent, wenn er nicht in der Lage war, die Klasse selbst zu laden. Dies entspricht – wie wir bereits beim Thema Classloader behandelt haben – nicht dem erwarteten Standardverhalten, erlaubt es aber hier, ganz gezielt bestimmte Klasseninstanzen zu manipulieren. Die Änderung des Klassenpfads des ClassPool ermöglicht es dem Javassist-Classloader dabei, auf beliebige dateibasierte Klassenpfadelemente zuzugreifen, unabhängig davon, ob diese dem ursprünglichen Klassenpfad der Anwendung entsprechen oder nicht. Javassist und AOP Wie die Beispiele bereits zeigten, ist es grundsätzlich möglich, die Klassen vor dem eigentlichen Einsatz zu manipulieren und ihre Struktur mittels Javassist recht einfach zu modifizieren. Wie passt dies nun aber zu den typi-
180
Technische Grundlagen AOP
schen Einsatzgebieten wie dem Einweben von Aspekten in beliebige Methodenaufrufe? Javassist bietet für die Modifikationen entsprechende, spezielle Variablen und Methoden an. Darunter finden sich u.a. folgende besondere Symbole: $0, $1, $2, ... – Parameter der Methode $args – Array der Parameter $$ – alle Parameter der Methode $r – Rückgabeparameter (z.B. zum Typecasting) $_ – der Ergebniswert eines Aufrufs Dadurch sind die typischen Aufrufe und Einbindungen der auszuführenden Basismethode durch spezielle Variablenaufrufe möglich: System.out.println("vor der Ausführung"); // Ausführung der bestehenden Methode $_ = $proceed($$); System.out.println("nach der Ausführung");
Unterzieht man nun also über die CtClass-Instanzen bestimmte Methoden einer Modifikation, so ist das typische Verhalten eines Aspekt-Weavingtools vorstellbar: Einlesen von Originalklassen, Mapping zwischen Originalsource und Aspekten, Inhalt von Aspekten. ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("test.MyClassA"); CtMethod cm = cc.getDeclaredMethod("execute", cc); cm.insertBefore( "{ System.out.println(\"Ausführung vorher\"); }"); cc.writeFile();
Dies ist natürlich nur einer von vielen, bislang sehr einfachen Anwendungsfällen. Neben dem typischen Einbetten in die Ausführung fremder Methoden können weitere neue eingefügt, Instanzvariablen verändert oder ersetzt werden.
AOP
181
3 – Architekturen mit AOP
Weitere Bytecodemanipulationstools Am Markt finden sich neben den vorgestellten Produkten noch eine ganze Reihe weiterer typischer Produkte. Schon der reine Open-Source-Bereich bietet eine Fülle davon, die sich je nach Funktionsumfang, Größe und Geschwindigkeit jeweils leicht voneinander unterscheiden. Wer sich eingehender mit der Materie beschäftigen will, sollte auch einen Blick auf eines der folgenden Produkte werfen: cglib1 Mächtige und sehr performante Bibliothek unter Apache Software License cojen2 Bytecodegenerierungs- und Disassembly-Tool unter Apache Software License JBET3 Java Binary Enhancement Tool zur Manipulation von Javaklassen JClassLib4 Viewer und inkludierte Manipulationsbibliothek von ej-Technologies und GP License Jiapi5 Java Instrumentation API, Bytecode Weaving vor dem Laden der Klasse ObjectWeb ASM6 kompaktes und sehr performantes Bytecode Manipulations Framework unter BSD License
1. 2. 3. 4. 5. 6.
http://cglib.sourceforge.net/ http://cojen.sourceforge.net/ http://opensource.nailabs.com/jbet/ http://www.ej-technologies.com/products/jclasslib/overview.html http://jiapi.sourceforge.net/ http://asm.objectweb.org/
182
Technische Grundlagen AOP
SERP1 Highlevel-API für Bytecodemanipulation – BSD License Trove Class File API2 Highlevel Bytecode API Bruch der Sourcecodesicherheit? Java-Sourcen werden ja zum Compilezeitpunkt in Bytecode überführt, der dann von einer virtuellen Maschine wiederum ausgeführt und je nach Optimierungsmöglichkeiten auch in einem Hotspotverfahren in noch maschinen-/betriebssystemnäheren Code umgewandelt wird. Bei Microsofts .NET-Plattform findet sich für die verschiedenen Sprachen wie Visual Basic, C++ und allen voran inzwischen C# auch eine gemeinsame Bytecodeebene, hier als IL (intermediate language) bezeichnet. Eigentlich ist dieser Begriff noch etwas exakter als der Name „byte code“, wenn man einmal den semantischen Charakter der erzeugten Zeichen betrachtet. Auch Java bietet eine solche intermediate language mit dem Bytecode an, gibt es doch verschiedene Sprachen, deren Compiler inzwischen Bytecode erzeugen können. Mit der mehr skriptähnlichen Sprache Groovy bewegt man sich auf einem ähnlichen Weg und in einem Sun-Interview wurde vor einiger Zeit nochmals auf Java als Plattform hingewiesen, das weit mehr biete als Java als eine Sprache unter vielen. Andersherum bedeutet das, dass man nicht mehr mit Sicherheit sagen kann, woher ein Bytecode eigentlich stammt. Ist er nun mit Java geschrieben worden? Entstand er aus einem übersetzten Basic-Programm? Hat jemand eine Bytecodemanipulation nachträglich vorgenommen? Sind Aspekte hineingewoben worden, von denen der ursprüngliche Entwickler keinerlei Notiz nehmen konnte? Da Bytecode eine Intermediate Language ist und als solches noch ein sehr hohes Abstraktionsniveau oberhalb einer maschinennahen Sprache wie Assembler bietet, eignet es sich recht gut zur Übertragung in ein noch höheres 1. http://serp.sourceforge.net/ 2. http://teatrove.sourceforge.net/trove.html
AOP
183
3 – Architekturen mit AOP
Sprachniveau, sprich zurück nach Java, was auch gerne als „Reverse Engineering“ oder „Dekompilieren“ bezeichnet wird. In etlichen Lizenzvereinbarungen wird dieses Verfahren als illegal betrachtet, Fakt ist aber zunächst einmal, dass es technisch einfach machbar ist. Dem Angreifer auf den eigenen Sourcecode ist mit ein paar Tricks normalerweise beizukommen. Indem man einige Klassen weglässt, die zur Laufzeit nicht verwendet werden, verhindert man oftmals schon sehr schnell den erneuten Compile der eigenen fremd-dekompilierten Sourcen. Zudem hilft der Compile mit dem neuesten JDK und der allerneuesten Classfile-Version oftmals schon, einer Reihe von alten Decompilern die Chance auf Dekompilierung zu nehmen. Eine weitere sehr erfolgreiche Variante ist der Einsatz eines Obfuscator. Ein Obfuscator beginnt Klassennamen umzubenennen und mit sinnlosen Namen zu versehen. Er zerlegt teilweise Klassen und Methoden in mehrere Teile und benennt Referenzen und Signaturen um. Dies trägt vermehrt zur Verwirrung bei, da der Angreifer den Zweck der Klasse und des dekompilierten Source oft nicht mehr verstehen kann. Die Erhöhung des Sprachniveaus durch Dekompilierung trägt in diesem Fall nicht der Gewinnung weiterer Einblicke bei. Fremder Sourcecode der durch einen Obfuscator nicht mehr lesbar ist, macht es einem potenziellen Angreifer fast unmöglich, mittels Aspekten1 den Bytecode nachträglich so zu manipulieren, dass er sich definierbar anders verhält. Dennoch werden bei weitem nicht alle Applikationen einem Obfuscator unterworfen. Abgesehen davon, ob man dem Produkt über den Weg traut, dass die Software auch nachher noch vernünftig funktioniert, stellt die Verteilung von Klassen auf mehrere Jar-Files für Deploymentzwecke schnell ein Problem dar. Wenn Schnittstellen durch Obfuscator-Aktionen jeweils andere Namen bekommen könnten, muss man in den Prozess der Umbenennung eingreifen oder beginnen, Klassen auszuklammern.
1. bzw. Bytecodemanipulation
184
Technische Grundlagen AOP
Abb. 3.9: Beispielliste typischer obfuscated Klassen
Es gibt zahlreiche Applikationen, die mit signierten Jar-Files hantieren. Signierte Jar-Files tragen eine Art internen Code, der verhindert, dass sie noch gelesen werden, wenn Klassen im Jar verändert wurden. Sorge trägt dafür der Classloader, der die Signaturen primär prüft. Allerdings zeigt dies auch schon einen möglichen Angriffspunkt. Ein anderer Classloader könnte die Signaturen ignorieren. Selbst bei korrekter Signatur könnte die Bytecodemanipulation immer noch nach dem bzw. beim Laden in die VM durchgeführt werden. Um wirklich auf Nummer sicher zu gehen, benötigt man also verschlüsselte Jar-Files, deren Inhalt kein Bytecode mehr ist und die von einem speziellen Classloader geladen werden, der nicht manipulierbar oder ersetzbar ist. Ein extremes Sandboxprinzip, wo dem Code faktisch kaum oder kein Vertrauen geschenkt wird, findet sich in Java Applets, also in Webseiten eingebettete Applikationen, und in etwas abgemildeter Form in Java Webstart-Applika-
AOP
185
3 – Architekturen mit AOP
tionen, bei denen signierte Anwendungen den Endbenutzer vor dem Bildschirm um die Erteilung besonderer Rechte zur Ausführung bitten dürfen – z.B. für den Zugriff auf die lokale Festplatte. Dennoch sind solche Umgebungen eher die Seltenheit. Die Ersetzbarkeit der virtuellen Maschine ist heute ein wichtiges Kriterium für zahlreiche Anwendungen. Es gibt nur wenige Produkte, die eine feste und nicht ersetzbare VM mitliefern, was ein erster Schritt für die Verhinderung von Manipulationen nach dem Laden der Anwendung wäre. Code-Signierung allein, der man ein hohes Maß an Zuverlässigkeit bescheinigen mag, beschränkt sich primär in den meisten Anwendungsfällen darauf, einem Benutzer die Möglichkeit zu geben, potenziell durch Zertifizierungsstellen (CAs, certified authorities) vertrauenswürdig eingestufte Applikationen zu installieren. Mit der Sicherheit des Codes vor Angriffen hat das aber je nach Einsatzgebiet nur sehr bedingt zu tun. Nichtsdestotrotz findet schon das Gros dieser Sicherungsmechanismen heute wenig oder keine Anwendung, was wiederum gefundenes Fressen für AOP-Tools bzw. Bytecodemanipulation bedeutet. In Produkten wie AspectJ kann man schließlich nicht nur eigene Java-Files, sondern auch fremde Jar-Files mit Aspekten versehen lassen. Es gibt auch Anwendungsgebiete, wo solche Möglichkeiten durchaus von Vorteil sind. Denken Sie z.B. an einen Datenbanktreiber, der keine Log-Ausgaben über gesendete und empfangene Daten anbietet. Hier lassen sich leicht in die Statement- oder PreparedStatement-Klassen Logausgaben einkompilieren, die schnell Klarheit liefern können. Nehmen wir z.B. an, Sie verwenden ein fremdes O/R-Mapping-Tool, dem kein Laut zu entlocken ist, welches SQL es an den Datenbanktreiber gesendet hat, der sich ebenfalls über die angeforderten Aktionen ausschweigt. Ein anderer Anwendungsfall könnten Schnittstellen externer Libraries sein, in denen bestimmte Elemente (Methoden oder Felder) nicht zugänglich sind (private). Hier steht oftmals entweder die Möglichkeit an, auf diese Elemente per Reflection zuzugreifen oder sie bewusst vor der Einbindung ins Programm per AOP so zu manipulieren, dass die entsprechenden Java-Elemente öffentlich zugreifbar werden und somit vom eigenen Source aus angesprochen werden können – ein typisches Patchen fremder Klassen, wie es
186
Technische Grundlagen AOP
selbst bei Open-Source-Produkten manchmal hilfreich sein kann, wenn man kurzfristige Lösungen benötigt und keine Feature-Requests an den Hersteller stellen kann. Natürlich lässt sich mit AOP in diesen Bereichen auch ohne weiteres Schindluder treiben. Viele Registrierungsprodukte und Lizenzmodule in Applikationen weisen auch heute noch riesige Bibliotheken zur Prüfung von Lizenzfiles und Transformationen von riesigen Bitmustern auf, besitzen aber als Frontend häufig nur einfache Schnittstellen, die auf Methoden wie isLicenseValid() einfach nur true zurückzuliefern haben. Es ist leicht vorstellbar, wie man mit einem around()-Advice die Ausführung einer solchen Prüfung komplett isolieren und durch Bytecodeenhancement umgehen kann. An dieser Stelle sei nochmals deutlich darauf hingewiesen, dass derartige Maßnahmen abhängig von der Lizenz des Produkts illegal sein könnten. Aber dieses Beispiel weist ja zwei Seiten auf: Es geht nicht nur darum, welche Möglichkeiten sich mit AOP für Sie als Benutzer auftun, sondern auch welchen Schaden andere mit Ihrer Software anstellen können, indem sie daran Manipulationen vornehmen. Das Prinzip der Bytecodeverifikation durch den Classloader und des Signierens von Files ist im Vergleich zu früherer Software schon ein großer Schritt nach vorne. Aber auch diese beiden Ansätze reichen nicht aus, Java-Software auf Knopfdruck blindlings zu vertrauen, selbst wenn Java-Viren heute keine so große Massenmarkttauglichkeit bescheinigt werden kann wie Viren in Assembler oder C. Da auch bereits eingewobene Aspekte in einen Bytestrom erneut von anderen Aspekten überschrieben und damit wieder negiert werden können (so genannte Anti-Aspekte), ist es zunehmend schwieriger zu definieren, in welchem Zustand und nach welcher Weaving-Iteration sich der Bytecode so darstellt, wie er da vor einem liegt, und ob überhaupt manipuliert wurde. Schlimmer noch, stellen Sie sich vor, Sie fügen eigenen Source und Aspekte sowie benötigte Third-Party-Bibliotheken zu einem Weaving-Compile in einen Weaver ein. Im worst case könnten die Third-Party-Bibliotheken ihrerseits Aspekte enthalten, die Ihren eigenen Sourcecode bereits beim Compile berühren und unerwartet modifizieren. Unrealistisches Szenario?
AOP
187
3 – Architekturen mit AOP
Die Frage, die sich also anschließt, lautet: Was erkaufen wir uns für die Nutzung von AOP im Gegenzug? Programme, deren Verhalten nicht vorhersagbar ist? Wie können wir verhindern, dass fremde Personen unseren Bytecode so manipulieren, dass er zu unvorhersehbaren Effekten führt? Faktisch gibt es hierfür derzeit keine perfekte Lösung. Die Idee von signierten Applikationen beruht auf dem reinen Glauben, damit den Ursprung einer Anwendung angeben zu können. Sie sichert nicht zu, dass diese Anwendung nicht böse Absichten verfolgen kann, und schon gar nicht, dass sich das Programm für immer und ewig nicht modifizieren ließe.
3.3.4
Java Standard Edition mit AOP versus Java Enterprise Edition
Vor einiger Zeit beschäftigten wir uns im Kollegenkreis sehr intensiv mit der Frage, inwiefern die Java Enterprise Edition (vor Java 5 bekannt unter dem Namen J2EE, der hier synonym zu verstehen ist) für große Unternehmensanwendungen Lösungsansätze bietet. Das schließt zunächst für Neulinge die Frage mit ein, worum es sich bei der Java EE eigentlich handelt. Zunächst einmal ist fast alles, was sich heute mit dem Namen Java schmückt, zumeist aus einer Spezifikation hervorgegangen. Für Java selbst gibt es beispielsweise die Java-Sprachspezifikation1, die allerdings nur Auskunft über die Hauptschlüsselwörter und die Syntax liefert und wenig über Verwendungsart oder Design einer Anwendung. Mit Einführung des inzwischen etablierten Java Community Process2 wandelt sich allerdings die Java-Denkweise ein wenig. In den JCP kann faktisch jeder neue APIs, also Programmierschnittstellen, einbringen. Darin finden sich heute neben eher häufig verwendeten, wie Vorgaben über die Entwicklung von Java Server Pages, also Java-angepassten Webseiten, auch eher Exoten wie die Anbindung von Content-Management-Systemen oder regelbasierten Expert-Shell-Produkten. Die daraus resultierenden Spezifikationen beschreiben lediglich die Schnittstellen, die von einem Produkt, das einer solchen Spezifikation entsprechen 1. http://java.sun.com/docs/books/jls/ 2. http://www.jcp.org/
188
Technische Grundlagen AOP
will, einzuhalten sind. Mittels entsprechender Referenzimplementierungen wird die Realisierbarkeit einer solchen Spezifikation technisch unter Beweis gestellt1. Kompatibilitätskits als Testsoftware stellen mit ihren unabhängigen Testdurchläufen die Verknüpfbarkeit unterschiedlicher Produkte anhand der Schnittstelleneinhaltung sicher. So bleibt schließlich zum Thema Enterprise Edition nur die Aussage zu machen, es handele sich um eine Menge von Spezifikationen, die primär im JCP erarbeitet wurden. Oder anders formuliert: Bei der Enterprise Edition handelt es sich nicht um ein Produkt, sondern nur um eine abstrakte Beschreibung, wie ein Produkt zu funktionieren hat, das sich Java-EE-kompatibel nennen möchte. Die Enterprise Edition enthält jene Spezifikationen, die besonders für den Bau großer und umfangreicher Anwendungen gedacht sind. Dabei geht es nicht ausschließlich um die Frage des Einsatzes von Produkten, sondern auch um rudimentäre Vorgaben bezüglich Verantwortungsbereichen von Teammitgliedern oder Vorgehensmodelle in Fragen von Analyse, Design, Programmierung und Deployment von Anwendungen. The Java 2 Platform, Enterprise Edition (J2EE) is a set of coordinated specifications and practices that together enable solutions for developing, deploying, and managing multi-tier server-centric applications. - J2EE FAQ http://java.sun.com/j2ee/faq.html In der Enterprise Edition finden sich primär typische Spezifikationen, die sich mit einem Multi-tier-Umfeld beschäftigen, also mit der Frage, wie man Software so entwickeln kann, dass Teile der Logik auf unterschiedlichsten Maschinen ausgeführt dennoch zusammen ein Gesamtbild schaffen können. Dabei stehen also Fragen der Entwicklung von wiederverwendbaren Komponenten in Komponentenmodellen wie EJB (Enterprise Java Beans) genauso im Vordergrund wie Fragen der Security, des verteilten Transaktionsmanagements oder des Auffindens von Objekten oder Austauschens von Nachrichten (z.B. für RPC (Remote Procedure Calls)). Durch die recht enge Verzahnung der unterschiedlichen Spezifikationen ist die Auswahl nur schwer zu treffen. Zum Beispiel enthält die Spezifikation 1. typische Referenzimplementierungen sind z.B. Jakartas Tomcat, Pluto oder Jess
AOP
189
3 – Architekturen mit AOP
über die Integration und den Bau von Komponenten auch gleichzeitig Aussagen über die Deklaration von Fragen der Persistenz, des Transaktionsmanagements und der Frage von Security. Wer beispielsweise eine Webapplikation schreibt, bekommt Rollenkonzepte oder Remote-Aufrufmöglichkeiten von Enterprise Java Beans gleichzeitig mit, wer Datenbankaufrufe in einem Container samt Pooling verwenden möchte, gerät schnell an Such- und Verbindungsmechanismen1, um Datasources zu finden. Manchmal sind diese Technologien voneinander separierbar (für Webapplikationen braucht man nicht unbedingt immer die ganze Enterprise Edition) und manchmal nicht. Für Kritiker war die Java Enterprise Edition2 ein gefundenes Fressen, um an „aufgeblähten“ und „weltfremden“ Spezifikationen herumzumeckern. Frei nach dem Motto: „Du musst schon ganz schön Schmerzen im Bein haben, um sie zu vergessen, wenn du dir mit dem J2EE-Hammer auf den Daumen haust.“ Teilweise war diese Kritik nicht unberechtigt, denn der Entwickler fand sich in einem Wirrwarr von Spezifikationen, zu implementierenden Interfaces und Superklassen wieder, musste, um seine arme Komponente zum Laufen zu bekommen, wildeste Klassenpfadeinträge in Manifest-Dateien und holprige Ritte über XML-Deploymentdeskriptoren, die überall auf Deklaration lauerten, erdulden. Für den richtigen Einsatz von J2EE bedurfte es eines großen Know-hows oder eines sehr intelligenten Tools, das die schlimmsten Klippen umschiffte. Aus heutiger Sicht kämpften die Männer und Frauen der ersten Spezifikationen genau an den Stellen, an denen AOP heute mehr denn je eine Antwort liefert. Es geht um die altbekannte AOP-Frage: Wie schaffe ich es, Businesslogik und vorwiegend technisch Aspekte voneinander zu trennen? Werfen wir dazu mal einen Blick auf einen typischen Deploymentdeskriptor. Ein solcher Deskriptor sagt dem Application Server3, in den eine Kom-
1. JNDI – Java Naming and Directory Interface 2. zumindest bis zur Evolution von Java 5 und EJB 3.0 3. einem Produkt, das die Java Enterprise Edition-Spezifikation erfüllt
190
Technische Grundlagen AOP
ponente (vorstellbar wie eine Java-Package oder eine Kleinstapplikation) eingebettet wird, wie er mit dieser Komponente, diesem EJB, umzugehen hat. Wo ist die Einstiegsklasse, welche Methoden sind für wen zugreifbar, beim Aufruf welcher Methoden soll eine Transaktion gestartet werden etc.? Beschreibung des Beans Beschreibung 1 anzuzeig. Name ExampleA test.Ex1Home test.Ex1 test.Ex1Bean Stateful Container key1 java.lang.String value1 ejb/xyz1 session test.XYZHome test.XYZ jdbc/mydb javax.sql.DataSource Application
AOP
191
3 – Architekturen mit AOP ExampleA * Required ExampleA Home * Supports ExampleA test1 Mandatory ExampleA test2 NotSupported
Die Editierung einer solchen Datei ist auch mit entsprechenden Tools, die sich oft in Entwicklungsumgebungen verbergen, zumeist mit hohem Auf-
192
Technische Grundlagen AOP
wand verbunden gewesen, obwohl es sich bei diesem Beispiel noch um eine sehr einfache Konfiguration handelt. Für jedes Enterprise Bean ist ein solcher Deskriptor zu erstellen, dann noch einmal für das Enterprise Archive, also die Gesamtapplikation, für jede Webanwendung darin ein eigenes web.xml-File plus im Standardfall ein Deskriptor für jedes dieser Elemente, der noch Application Server-spezifisch dazukommt. Bei EJBs kann man noch zwischen zwei Modi wählen, dem „Rundum-sorglos-Ansatz“, bei dem der Container über Persistenz und Transaktionsmanagement selbst die Aufsicht hat (CMT, CMP), und der Variante, in der dann die Geschäftslogik selbst doch wieder in SELECT-, INSERT-, UPDATE-, DELETE-Statements sowie Rollbackverhalten eingreift (BMT, BMP). Nüchtern betrachtet haben wir hier bei EJBs, die natürlich nur einen Teilbereich der Enterprise Edition darstellen, den Versuch, Geschäftslogik und Aspekte zu trennen. Im Gegensatz zum AOP-Ansatz, in dem jeder Aspekt separat betrachtet wird, haben wir es allerdings in diesem ebenfalls deklarativen EE-Ansatz in Version 2.1 noch mit einem riesigen Deploymentdeskriptor zu tun, der sich auf eine feste Menge von Aspekten stützt und die Implementierung dieser Aspekte nicht offen legt. Wollten Sie also beispielsweise das Security-Verhalten von J2EE beeinflussen, so gab es nur die Möglichkeit, Rollen zu definieren und im Deploymentdeskriptor Schalter an- und auszuschalten oder im Sourcecode (isUserInRole()). Wenn Sie sich aber ein ganz anderes Rollenkonzept oder erweitertes Prinzip gewünscht haben (z.B. Kombination einer Berechtigung mit einer Uhrzeit oder einer bestimmten Serverinstanz), stoßen Sie mit dem J2EE-Konzept sehr schnell an Grenzen. Fairerweise muss man zugeben, dass das Ende der Ära der Deploymentdeskriptoren und dieser Art der deklarativen Aspektkonfiguration gekommen ist. Wenn Sie das Produkt XDoclet1 kennen sollten, haben Sie den Vorläufer in Sachen attributorientierte Softwareentwicklung bereits kennen gelernt. In Java 5 heißt dies jetzt Metatags oder Annotations.
1. http://xdoclet.sourceforge.net
AOP
193
3 – Architekturen mit AOP
Wie auch immer man das Kind benennen will, es handelt sich jetzt um eine Metaprogrammierung, die wir uns im nächsten Kapitelabschnitt noch genauer ansehen wollen. Im Folgenden ist eine Mischung aus Aspektattributen in Form von Annotations und Sourcecode zu sehen, wie sie in Java EE 5 anzutreffen ist: import javax.ejb.*; @Stateful public class AccountBean implements Account { @Tx(TxType.REQUIRED) @MethodPermission ({"administrator"}) public void createAccount( User user, String password) { ... } }
Es verbleibt leider das Problem, dass die Aspekte zunächst funktional auf eine Java EE gemünzt sind. Ein weiterer, fast noch wesentlicherer Fortschritt, den die EJB-Spezifikation von 2.1 (Deploymentdeskriptor oben) zu 3.0 (Annotations im Beispiel) gemacht hat, ist aber die Abwendung von der fixen Designeinbettung von Beans zu POJOs. Klassen, die früher bestimmte Interfaces implementierten und andere J2EEKlassen erweitern mussten, machten sich damit auch von einem architektonischen Paradigma abhängig. Wer eine Komponente als EJB programmiert hatte, konnte zwar theoretisch seine Anwendung mit entsprechendem Anpassungsaufwand von einem Application-Server-Hersteller zum nächsten migrieren, aber es gab nur die Variante entweder Application Server oder gar nicht. Der Ansatz eines POJO (Plain Old Java Object) ist jetzt kaum noch von Abhängigkeiten von Produkten gekennzeichnet. Was benötigt wird, wird entweder per Dependency Injection (also über den Aufruf einer Set-Methode in
194
Technische Grundlagen AOP
der Businesslogik durch den Application Server) hineingereicht oder eben per Annotation deklariert. Wie macht man nun aber ein POJO zu einem Enterprise Bean? Wie gelingt es, der Annotation @Tx(TxType.REQUIRED)(wenn diese Methode betreten wird, muss eine Transaktion gestartet sein) Leben einzuhauchen? Einige Hersteller wie z.B. JBoss beantworten dies mit „AOP“. Versteht man Annotations als die Beschreibung der Joinpoints, an denen ein Weaver in Aktion treten soll, so kann man sich gegebenenfalls mit dem Einweben der Aspekte sogar bis zum Laden der Klasse Zeit lassen. In früheren EJB-Versionen wurden beim Deployment der Komponenten oftmals bereits neue Klassen erzeugt, die erweiterte Methoden und Eigenschaften aufwiesen. Eigentlich hatte die Enterprise Edition von Java schon lange den Touch eines aspektorientierten Vorgehens, was AOP als Leitgedanken zusätzlich salonfähig für Großanwendungen macht. Bis EJB 2.1/J2EE 1.4 stellte sich die Frage: Wollen wir Aspekte auf die J2EE-Methode vom Kern-Source trennen oder wollen wir ganz auf J2EE verzichten und andere Container verwenden? Für Verfechter der Anti-J2EEFraktion gibt es z.B. Produkte wie das Springframework1. Zu versuchen, in wenigen Sätzen Vor- und Nachteile zu beleuchten, würde dem Produkt sicherlich nicht gerecht werden. Aber es handelt sich um eine echte Alternative zu einem J2EE-Server, die mit Dependency Injection und Interceptor-Technologien POJOs und Aspekte vereint. Die nunmehr aufkommenden EJB 3.0-/Java EE 5.0-Technologien nehmen jetzt Abstand von früheren Verfahrensweisen, ein weiter Weg muss dabei für Tools, Application-Server-Entwickler und spätere Migrationen von Altanwendungen zurückgelegt werden. Ein bisschen mag sich da der Eindruck einschleichen, man habe den J2EE/ EJB-Laster mit hoher Geschwindigkeit die Autobahn entlang brausen lassen, bis er von der Fahrbahn abkam und nicht mehr manövrierfähig stecken blieb. Jetzt wird er wieder zurückgeschoben und man versucht es mit einem
1. http://www.springframework.org
AOP
195
3 – Architekturen mit AOP
etwas einfacheren Modell, ähnlichen Prinzipien und etwas anderem Namen als Java Enterprise Edition noch einmal. Für viele Applikationen, die dem J2EE-Paradigma verhaftet waren, steht nun der steinige Weg an, den die AOP-Gemeinde mit ihrem Ansatz, Aspekte zu transformieren und evolutionär weiterzuentwickeln, als bewältigt erachtet. Zwar ist das Java Enterprise Edition-Konzept von jeher aspektorientiert angelegt worden. Statt wie früher in typischen XML-Files finden sich nun die Bindings wieder im Sourcecode als Annotations. Aber wie einfach ist es, diese Konzepte durch eigene zu ersetzen. Wenn Ihnen das Securitymanagement von Java EE nicht gefällt, wie leicht lässt es sich dann durch ein anderes ersetzen? Wenn Sie Ihr eigenes Logging definiert haben, konnten Sie es bisher in einen J2EE-XML-Deskriptor einbauen? J2EE-Lösungen waren und blieben J2EE-Lösungen. Die Implementierung war faktisch nicht austausch- oder ersetzbar, außer man löste sich komplett von der J2EE-Lösung und wählte z.B. statt der J2EE-Persistenz die eigene JDBC-Lösung. Auch Java EE 5.0 verspricht, weite Teile der alten EJB-Strukturen weiterhin zu unterstützen. In der Java-Welt etabliert sich allerdings in den letzten Jahren eher der Rückwärtstrend: weg von immer mehr Komplexität, zurück zu den guten alten POJO- und Java Beans-Zeiten, in denen Software nicht nur schreib-, sondern auch beherrsch- und konfigurierbar erschien. Wer eine sinnvolle Integration von J2EE Version 2.1 oder früher und AOP sucht, dem könnte – ob der zahlreichen Designrichtlinien, die es einzuhalten gilt1 – der AspectJ Compiler ein nützlicher Helfer sein, der die Einhaltung entsprechender Designrichtlinien mittels Aspekten überwachen hilft.
1. z.B. in Sachen Vererbung, Sichtbarkeit von Methoden, Verbot des Startens von Threads, Verbot der Einführung von synchronized-Elementen etc.
196
Technische Grundlagen AOP
Java EE/J2EE – das etwas andere AOP? J2EE benutzt Aspekte in Form von Deploymentdeskriptoren, aber auch in Form von Klassenbeziehungen. J2EE ist dafür allerdings schwer erweiterbar. Java EE wird im Bereich der EJBs nunmehr auf Annotations umschwenken, was sicherlich ein gutes Zeichen für mehr Flexibilisierung und die Konzentration von Sourcecodeverhalten an einer Stelle ist. Da die Enterprise Edition sich aber bezüglich der Implementierung auch nur bedingt frei bewegen kann, liegt es nahe, Aspekte wie Security und Transaktionsmanagement selbst mit Aspekten zu implementieren und die vollkommene Freiheit über das Verhalten zu erlangen. Besonders das recht starre User-zentrierte Autorisationsverhalten von Java EE lässt sich mittels eigener Implementierung deutlich verbessern. Der Hype um J2EE-Alternativen wie das Springframework und das langsame Umschwenken der bisherigen J2EE zeigt den Trend, dass sich „reines“ AOP als einzige tragbare Zielsetzung bei der Umsetzung dieser technischen Aufgaben herausstellt. Die Enterprise Edition von Java wird in Sachen Webservices und EJBs deutliche Zeichen setzen, dass AOP inhärenter Bestandteil von Java ist und sein wird.
3.3.5
Meta-Tags für die Realisierung von AOP
Wer schon einmal ein Enterprise Java Bean nach der inzwischen schon älteren Spezifikation 2.1 implementiert hat oder einen Webservice mit einem Produkt wie Apache Axis zum Laufen bringen musste, weiß, wie viel Aufwand nachträglich noch bei der Überarbeitung oder Erstellung von Descriptor-Files oder generierten Klassen anfällt1. Fast spielerisch wirkten da erste Implementierungsbeispiele in C#, in denen ein Webservice und eine öffentliche Webservicemethode mit Hilfe von Vererbung und Nutzung eines Metatags (hier [WebMethod]) beschrieben und zum Laufen gebracht werden können. 1. Zwar bietet Axis auch Schnellstartlösungen wie JWS, allerdings ist dies für komplexe Objektmodelle nur bedingt tragbar, wenn überhaupt. Der Grund liegt u.a. an den bis Java 5 fehlenden Generics, die es Axis unmöglich machen, zu wissen, welche Klassen bei Listen im Webservice anzubieten sind.
AOP
197
3 – Architekturen mit AOP using using using using using using using
System; System.Collections; System.ComponentModel; System.Data; System.Diagnostics; System.Web; System.Web.Services;
namespace WebService1 { public class Service1 : System.Web.Services.WebService { public Service1() { InitializeComponent(); } [WebMethod] public string HelloWorld() { return "Hello World"; } } }
Erst Schritt für Schritt zog man auch auf der Javaseite nach und bietet nunmehr mit JSR 181 (Web Service Metadata) eine ähnliche Lösung an, deren Final Release erst im Juni 2005 erschien. Dabei hat man sich eine BEA-Lösung abgeguckt, die bereits früh in ihrem BEA Weblogic Workshop-Produkt mit Metadaten Kontakt aufnahm. So entstanden auf Java-Seite kürzlich folgende Annotations samt einer BEA-Referenzimplementierung: javax.jws.WebService javax.jws.WebMethod javax.jws.OneWay javax.jws.WebParam javax.jws.WebResult javax.jws.HandlerChain javax.jws.soap.SOAPBinding javax.jws.soap.SOAPMessageHandlers
198
Technische Grundlagen AOP
Was zu folgender Implementierung auf Java-Seite führen kann: package webservice1; import javax.jws.WebService; @WebService public class Service1 { public String helloWorld() { return "Hello World"; } }
Als Leserkommentar zum Erscheinen der Java WebService Metadata-Spezifikation fand sich z.B. folgender typischer Auszug auf einer theServerSide-Webseite: My oh my. One the one hand we boast about trying to make things as simple as possible by using POJOs whereever we can, on the other hand we engage into polluting those with "annotations" (essentially nothing but deployment descriptors that are in-line) in order to introduce dependencies again. - Karl Banke Eine höchst menschliche und nachvollziehbare Reaktion auf AOP. Allerdings auch eine, die die Chancen hinter den Problemen mehr zurückstellt als aus meiner Sicht angemessen ist. Nach diesem kleinen Webservice-Ausflug zurück zum Thema Metadaten. Die Namen für solche zusätzlichen Sourcebeschreibungselemente, die gleichzeitig auf die Erzeugung des Programms Einfluss nehmen, sind vielfältig und reichen aus der Java XDoclet-Welt über den Namen „(Meta)tag“ und „attributorientierte Programmierung“ über „Sourceproperties“ und „Metadaten“ bis hin zum Begriff der Java 5 „Annotations“. Auch Java 5 bringt wie gesehen inzwischen die Fähigkeit mit, solche Metatags in den Sourcecode zu integrieren. Eine solche Annotation ähnelt in ihrer Struktur der Deklaration eines Interface und kann mit den typischen Klassenelementen wie dem Klassenkörper oder auch den Methoden assoziiert werden.
AOP
199
3 – Architekturen mit AOP
Wie sieht ein Implementierungsbeispiel mit Annotations nun bei Java genau aus? Im folgenden Sourcecodesnippet ist eine Annotation gezeigt, mit deren Hilfe Objektfelder auf Datenbankspalten gemappt werden sollen, um diese z.B. in einem objektrelationalen Mappingtool für Persistenzzwecke zu verwenden: package test; public @interface Persistent { public String tableName(); public String columnName(); public boolean nullable() default false; }
Vorgegeben ist im Beispiel, dass die Elemente tableName und columnName bei Verwendung des Persistent-Metatags zu definieren sind. Das nullable()-Element ist optional mit dem Default-Wert false ausgestattet. Ein möglicher Einsatz dieser Annotation zeigt sich in folgender Klasse: package test; public class ExampleClass { @Persistent(tableName="TEST",columnName="ID") private int id = 0; @Persistent(tableName="TEST",columnName="NAME", nullable=true) private String name = ""; public int getId() { return id; }
200
Technische Grundlagen AOP public String getName() { return name; } }
Bei Annotations kann zwischen drei verschiedenen Generierungstechniken gewählt werden, die bestimmen, in welchem Kontext auf die in den Annotations hinterlegten Informationen zugegriffen werden kann bzw. soll (so genannte retention policies). Source-file retention Diese Art wird vom Compiler bei der Erzeugung des Bytecodes berücksichtigt, findet sich allerdings nicht in den kompilierten Class-Files wieder. Class-file retention Bei der Class-file retention werden die Annotations in das Class-File übernommen (Standardverhalten). Runtime retention Diese Form der Annotations kann zur Laufzeit (primär über ReflectionMechanismen) sichtbar gemacht werden und befindet sich dafür ebenfalls im Bytecode. Die unterschiedlichen Policies gepaart mit den zulässigen Einsatzpositionen einer Annotation lassen sich im Java-Sourcecode bei Definition der Annotation ebenfalls festlegen. import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface Test { String value(); }
Die Metadaten-Erweiterung von Java 5 ist sicherlich zusammen mit Generics eine der herausragendsten Neuerungen der Java-Sprache in den letzten Jahren.
AOP
201
3 – Architekturen mit AOP
Zunächst klingt die Annotationsfähigkeit weniger spektakulär und mutet wie eine Art Konfigurationsmechanismus mit Reflectionfähigkeit an, eine Funktionalität, die man mit dem „guten alten“ Java 1.4 auch umsetzen könnte. Tatsächlich entfalten diese Metainformationen aber großes Potenzial, wenn sie von Generatoren ausgelesen oder für die Entwicklung von Aspekt-Joinpoints verwendet werden. Die Möglichkeiten, die sich mit Annotations bieten, erstrecken sich dabei von der simplen, strukturellen Auslagerung von Entscheidungselementen, über Konfigurationsmanagement/Deploymentdaten bis hin zur Festlegung von n-dimensionalen Konfigurationsdaten. Letzteres ist besonders spannend für den Einsatz von AOP. Jede einzelne Annotation, die z.B. einer Methode zugefügt wird, bringt nicht nur einen eigenen Namen mit sich, an dem sich ein Pointcut ausrichten kann und worauf sich Joinpoints unabhängig vom tatsächlichen Java-Element aufbauen lassen. Es besteht auch die Möglichkeit, in jeder Annotation weitere Attribute mitzugeben, die dann gleichzeitig das Verhalten dieser Aspektdimension definieren. Beispielsweise könnte man in einen Sourcecode ein Security-Tag einbauen. Es kann gleichzeitig Benutzerrolle oder User und Passwort definieren, die durch Authentifizierungsmaßnahmen geprüft sein müssen, damit ein Benutzer diese Java-Methode ausführt. public class ExampleClass { @Security(isUserInRole=“administrator“) public void deleteDataElement(DataElement d) { [...] } }
Methodensignatur-basierte Joinpoints erschweren die Erzeugung von cross cuttings concerns und komplizieren gleichzeitig immens die Ausführung von Refactoringaktivitäten. Da Aspektjoinpoints nicht auf Vererbungsbeziehungen und Implementierung von bestimmten Interfaces angewiesen sind wie die früher klassischen
202
Technische Grundlagen AOP
Realisierungen, entsteht hier mit den Metadaten von Java 5 eine neue Art einer Schnittstellendefinition. Bei Nichtverwendung von Annotations bilden sich bei vielen cross cutting concerns schnell entsprechend komplexe Pointcutdefinitionen wie die folgende: pointcut transactionPointCut() : execution(public void A.test1(..)) || execution(public void A.executeA(..)) || execution(public void C.getIt(..)) || execution(public void C.setIt(..)) || execution(public void C.removeData(..)); || execution(public void E.saveData(..));
Dies riecht förmlich nach der Einführung einer zugehörigen Annotation wie @Transactional. @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface Transactional { int mode(); }
Hier lässt sich der Pointcut wesentlich einfacher und abstrakter für alle Methoden definieren, die die Annotation @Transactional verwenden: pointcut execution(@Transactional * *.*(..));
Wir werden in diesem Kapitel noch an einigen Stellen Annotationbeispiele sehen und auch die in AspectJ vorgenommenen Annotationerweiterungen kurz beleuchten. Da sicherlich viele Leser aber noch Altsource verwenden, der den Einsatz von Annotations verbietet bzw. die Migration erschwert, zeigen zahlreiche Sourcecodesnippets die klassische nicht-Annotation-basierte Variante auf. Interessierte Leser, die Aspekte in Kombination mit Java 5 oder der zurzeit entstehenden Version Java 6 verwenden möchten, sei der Einsatz dieses Me-
AOP
203
3 – Architekturen mit AOP
tatagging eher empfohlen, um die Kombination zwischen proprietärem Java-Kerncode und Aspekten zu realisieren und Pointcuts zu bestimmen. Fairerweise muss man aber auch anmerken, dass in der Annotationvariante Konfiguration und Sourcecode wesentlich enger miteinander verschmolzen werden als in der klassischen Bindingvariante außerhalb des Source. Bei der Realisierung müssen Sie sich daher sehr genau überlegen, welche Werte und Konfigurationseinstellungen tatsächlich in den Sourcecode gehören und welche besser außerhalb aufgehoben sind.
3.4 Auswirkungen von Refactorings auf AOP Aspektorientierte Programmierung beschäftigt sich, wie dieses Buch bereits an etlichen Stellen näher zu bringen versuchte, mit der Formulierung von orthogonalen Softwareaspekten. Trotz der Formulierung, es handele sich dabei um separate Dimensionen, die auf der objektorientierten Basisimplementierung aufsetzen, lässt sich nicht gänzlich leugnen, dass AOP in seiner heute typischen Ausprägung sehr eng mit der Hauptdimension, dem OOKern-Source interagiert. Primärer Grund sind die aspektorientierte Art und Weise, Pointcuts und Joinpoint-Deklarationen zu erstellen. Sie bilden ein Basisgerüst, ähnlich wie die auf einem PC-Mainboard einsteckbaren Slots für PCI- oder AGP-Karten. Schnittstellen werden zwar durch Pointcuts definiert, ihre Existenz wird aber durch den Sourcecode an sich sichergestellt. Stellen Sie sich dafür eine simple Klasse vor, die in einem Sourcecode vorkommen könnte. public class Example { private void doIt() { System.out.println(“done“); } public void action1() { doIt(); System.out.println(“have done it“); } }
204
Auswirkungen von Refactorings auf AOP
Typische Joinpoints könnten nun auf der doIt()- oder action1()-Methode aufsetzen und diese mit Advices unterbrechen bzw. erweitern. Je nach IDE, mit der Sie die Programmierung durchführen, wird es schnell schwierig zu garantieren, dass die Methoden dabei ihre Form, sprich ihren Namen nicht ändern. Da Entwicklungsumgebungen wie Eclipse oder IDEA ein Refactoring von Methodennamen sowie die Extraktion oder das Inlining von Sourcecode sehr leicht unterstützen, kann es schnell vorkommen, dass die ursprüngliche Klasse nach einem Refactoring wie folgt aussieht: public class Sample { public void execute() { System.out.println(“done“); System.out.println(“have done it“); } }
Es stellt sich durchaus die Frage, wie Pointcutdefinitionen hier so „überlebensfähig“ gestaltet werden können, dass sogar die Umbenennung von Klassen und Methoden keinen allzu großen Schaden anrichtet. Refactoring beschreibt per Definition eigentlich eine Handlungsweise, in der die Programmstruktur ohne Auswirkungen auf die Funktionsweise geändert wird. Oftmals passiert dies unter der Maßgabe, gleichzeitig Redundanzen aus dem Sourcecode zu entfernen und zu einem verbesserten Design zu gelangen. Ziele, die sich mit aspektorientierter Programmierung decken, weshalb beide Ansätze vom Grundsatz her eigentlich mehr harmonieren als sich ausschließen sollten. Es gibt also zwei Seiten beim Thema Refactoring zu beachten: 1.) Wie kann ich mittels Aspekten meinen Sourcecode besser strukturieren (womit sich dieses Buch in Teilen beschäftigt)? und 2.) Wie kann ich Refactoring an der Kerndimension des Sourcecodes vornehmen (den „Nicht-Aspekten“), ohne die Verhaltensweise der Aspekte dadurch negativ zu beeinflussen. Wenn die Funktionsweise des Programms beim bzw. nach dem Refactoring unverändert gegeben sein soll, werden Refactoring-Techniken bzw. -Tools
AOP
205
3 – Architekturen mit AOP
benötigt, die nicht nur auf die reine Kern-Sourcedefinition einwirken, sondern auch auf die Aspekte (aspect-aware refactorings). Hierfür gibt es zu den unterschiedlichen Produkten auch erste Zusatztools, die ein Auge auf die durch Refactorings entstehenden Konstellationen haben und in der Lage sind, kritische Refactorings zu bemerken. Im Beispiel bedeutet besonders das Inlining der doIt()-Methode ein größeres Problem. Nach dem Refactoring entsteht kein ausreichend äquivalentes Substitut. Da der Sourcecode der doIt()-Methode nicht vollkommen von der execute()-Methode übernommen wird, sondern diese noch weitere Statements enthält, entwickelt sich hier bereits ein inkompatibles Abbild gleichen Verhaltens, aber anderer Struktur. Refactorings, die in einem mit Aspekten durchsetzten System stattfinden, müssen also darauf achten, die durch die Joinpoints definierte Menge von Events bzw. Sourcecode nicht zu verändern. In einem Papier der Universität Duisburg-Essen findet sich hier der Begriff der „äquivalenten Position innerhalb des Programm-Kontrollflusses“. Äquivalente Position heißt in diesem Fall, dass nach dem Refactoring eine Umbenennung oder Verschiebung eines Joinpoint auf einer Methode oder einer Variablen beispielsweise wenig Auswirkungen haben dürfte, solange die Pointcutdefinition sich entsprechend anpasst. Problematisch hingegen wirken sich der Wegfall von Methoden innerhalb des Ausführungsstacks oder die Entfernung ganzer Felder aus. In diesem Fall ist es fraglich, ob das resultierende Programmkonstrukt die gleiche Einwebmöglichkeit eines Aspekts liefert, den es vorher besessen hat. Eine vertiefte Diskussion über die mit AO-Refactoring einhergehenden Bedingungen und Fähigkeiten finden Sie beispielsweise im Internet1. Vom Grundsatz her gilt die Regel, bei der Verwendung von AOP die direkte Kopplung von Aspekten und Klassen auf ein Minimum zu reduzieren oder nur auf öffentliche Schnittstellen anzusetzen, die auch von anderen mitverwendet werden.
1. http://dawis.informatik.uni-essen.de/site/publications/papers/aop/ 2003_HOU_ApplyingRefactoringInAnAspectOrientedWorld.pdf
206
Auswirkungen von Refactorings auf AOP
Versucht man die Menge der Joinpoints zu reduzieren, ist das vergleichbar mit einem Ozeandampfer, der im Fahrwasser nur minimal mit dem Bootsrumpf eintaucht und dennoch über Wasser enorme weitere Massen zu tragen in der Lage ist. Diese Reduktion kann durch zwei Arten geschehen: 쐌 Minimierung der Menge von möglichen Joinpoints, die notwendig sind, um einen Aspekt mit dem Sourcecode zu verbinden 쐌 Erhöhung der Abstraktion der im Pointcut definierten Bedingungen, unter denen ein Joinpoint eintritt Beide Varianten widersprechen in einigen Teilen einander. Je weniger Aspekte in einem Sourcecode vorkommen, umso geringer wird das Risiko sein, dass während eines Refactoring für die Funktionsweise auftritt. Wichtig ist dabei natürlich auch, an welchen Stellen der Pointcut die Joinpoints definiert und ob all diese Stellen auch nach dem Refactoring noch in gleicher Art und Weise existieren. Gleichzeitig bedeutet die enge Bindung eines Aspekts an eine exakte Stelle (hier im Beispiel an die Methode setX() der Klasse Point) eine hohe Abhängigkeit vom Klassen- und Methodennamen. pointcut setter(Point p): call(void Point.setX(*)) && target(p);
Formuliert man – um diese Abhängigkeit zu vermeiden – den Pointcut wesentlich generalisierter, kommen nun mehr Joinpoints in Frage als zuvor. pointcut setter(): call(void *.*(*));
Da sich die Menge der Joinpoints erhöht, erhöht sich gleichzeitig das Risiko eines Refactoring und der Entwicklung überhaupt, da nun auch neue Klassen, die über die definierten Pointcuts nicht informiert sind, zum Aspekt in Beziehung gesetzt werden könnten. Aus den genannten Gründen scheint es oftmals unverzichtbar, mit der Einführung von AOP in der Softwareentwicklung nicht nur den Buildprozess oder das Laufzeitverhalten zu beeinflussen, sondern die entstehenden Ände-
AOP
207
3 – Architekturen mit AOP
rungen gegenüber dem Entwickler mit entsprechender Toolunterstützung transparent zu machen. Ein anderer wichtiger Faktor für den Erfolg von Aspekten bildet die Definierbarkeit der Pointcuts. Da sie das Hauptbindeglied zwischen Aspekten und Kernsource bilden, sollte ihre Beschreibung so klar und einfach wie möglich machbar sein. Voraussetzung dafür sind Designrichtlinien und somit statische Architekturgrundpfeiler. Dahinein fallen z.B. notwendige Namenskonventionen, was die Benennung von Klassen, Methoden oder Packages angeht. Ist beispielsweise definiert, dass sich Klassen immer in einem Model-Package befinden, Java Beans immer mit dem Namen Bean enden oder Requestund Response-Objekte immer eine getKey()-Methode besitzen, so sind dies fixe Einsprungspunkte in den Sourcecode, die gut für Aspekte genutzt werden können. Im Falle eines Refactoring könnte es hier schlimmstenfalls zu Designverletzungen kommen, die durch entsprechende qualitätssichernde Maßnahmen (z.B. Checkstyleprüfungen oder die in diesem Kapitel behandelten Prüfungen von Architekturvorgaben) aufzudecken und entsprechend zu korrigieren sind. Klare Schnittstellen, Design- und Architekturvorgaben unter Reduktion von Kopplung von Anwendungsteilen untereinander sind die Grundvoraussetzung für einen möglichst problemlosen Einsatz von Aspekten in vorhandenem Source. Die im Annotationsabschnitt dieses Kapitels angesprochene Lösung, Pointcuts statt über Klassen und Methoden über Metadaten an den Sourcecode zu binden, verringern die Risiken von Umbenennungen von Javaelementen. Die durch Extraktion und Inlining von Sourceteilen entstehenden nichtäquivalenten Programmkonstrukte lassen sich mit Annotations aber nicht lösen.
3.5 Performancekiller AOP? Sicherlich sollte auch ein Wort zum Thema Performance fallen. Es gibt entsprechende Performancestatistiken, die auch im Internet frei verfügbar sind, die mal das eine, mal das andere Produkt vorne sehen, was nicht weiter verwundert, angesichts dessen, dass die Ausführungsgeschwindigkeit auch nur
208
Performancekiller AOP?
eines unter vielen Kriterien ist, wenn man bedenkt, auf wie vielfältige Art und Weise ein Aspekt in ein Programm gelangen kann. Auf einer AspectWerkz-Webseite1 findet man z.B. erste Anhaltspunkte für die Performance der unterschiedlichen Produkte. Dabei stellen sich bereits deutliche Unterschiede heraus, so kosten die typischen before()- und around()-Advices bei AspectJ 10 bzw. 80 ns pro Aufruf, während wir bei JBoss bereits einen Performancenachteil entdecken können (220 bzw. 290 ns). Dies ist allerdings fast nichts im Gegensatz zu den bereits bei Dynamic Proxies angesprochenen Verzögerungen, die den Standardaufruf um 150% bzw. 4000% langsamer erscheinen lassen, abhängig vom eingesetzten JDK. Wie jede Statistik über Performance, die man in diesem Buch aber hätte hinterlassen können, hätten ein paar Balken die Grafik gebildet, die zum Erscheinungszeitpunkt dieser Seiten nicht mehr aktuell, für Ihren eigenen PC oder Apple und für Ihr Anwendungsfeld nicht angepasst wären. Insofern ist echte Performancemessung oftmals nur dann sinnvoll, wenn man von einer echten Deltaanalyse sprechen kann, sprich: Wie schnell war meine Anwendung vorher, wie schnell war sie nachher, und nicht, welches Waschpulver wäscht weißer. Adrian Colyer, Projektleiter von AspectJ, sprach auf der JAX 2005 von einem Testfall, in dem Tracingausgaben in ein Programm hineingewoben einen Overhead von ca. 1,5% gegenüber einer perfekt kodierten händischen Variante erzeugen. Selbstverständlich kostet das Einweben der Aspekte in einen Sourcecode Zeit und insofern ist schon wichtig zu überlegen, wo und wann der Aspekt ins Programm kommt. Genauso richtig ist sicherlich auch, dass die Performance in den Keller gehen wird, wenn Sie einen Logging-Aspekt mit „Hallo, hier fängt meine Methode an“ und „Ups, hier ist sie auch schon zu Ende, wie lange hat sie denn gedauert?“ an jeden greifbaren Joinpoint heften. Oft unterschätzen Ent-
1. http://docs.codehaus.org/display/AW/AOP+Benchmark
AOP
209
3 – Architekturen mit AOP
wickler doch noch immer, wie viele Millisekunden sich mit dem sinnlosen Schreiben von Logstatements in eine Datei aufsummieren können. Aber diesem Performance-Problem steht auf der anderen Seite der Medaille ja auch eine Performance-Chance gegenüber. Nie konnten Sie so leicht Geschwindigkeit im Programm ad hoc messbar machen, nie so schnell unnötige Aspekte auch wieder entfernen! Erinnern will ich an dieser Stelle an die kleine Feinheit zwischen guter und angeblicher weniger guter Programmierung. LOG.info(„Hier fängt meine Methode an“); berechneIrgendwas(); LOG.info(„Hier hört die Methode auf“);
und if (LOG.isInfoEnabled()) { LOG.info(“Hier fängt meine Methode an“); } berechneIrgendwas(); if (LOG.isInfoEnabled()) { LOG.info(“Hier hört die Methode auf“); }
Obwohl die zweite Variante mehr als das Doppelte an Sourcecodezeilen aufweist, wird sie mit Sicherheit in den meisten Fällen die performantere sein. Stellt man sich vor, das erste Beispiel zeige eine statische, hart-kodierte Variante und das zweite eine AOP-Modifikation, wird die zweite nochmals sinnvoller. Selbst wenn die zweite mit AOP-Mitteln eingewoben würde, wäre der entstandene Bytecode einem nativen Original sicherlich ebenbürtig. Richtig ist auch, dass eine starke Trennung der Aspekte rein technisch einem Compiler je nach Variante, wie er die Aspekte zusammenführt, einen geringeren Optimierungsspielraum lassen könnte. Wählt man sogar die hochdynamische AOP-Variante, bei der die Aspekte erst zur Laufzeit und gegebenenfalls immer wieder in Instanzen hineingesetzt und Bytecode neu geladen werden muss, stellt sich natürlich irgendwann zu Recht die Frage, wie viel Zeit eigentlich mit Veränderung des Lauf210
Performancekiller AOP?
zeitverhaltens zum Tragen kommt. Dafür gewinnt man architektonische Freiheiten, die ungeahnte Spielräume offerieren. Die Frage bleibt, in welcher Liga man seinen Ball bewegen möchte. Man unterscheidet Prototypen und Produktionsumgebungen. Es handelt sich um unterschiedliche Spielwiesen. Gebote der Architektur – Regel 7 Sicherlich ist die Antwort, AOP sei so schnell, wie Sie es sich machen wollen, sehr wahrscheinlich unbefriedigend und doch trifft es auch ein wenig den Kern. In Zeiten, in denen ein Hochleistungsrechner den anderen ablöst, auf CPUs gleich mehrere Threads echt-parallel ausgeführt werden und jedes Buch über Architektur von Layer- und Tier-Trennung und sauberer Designmodellierung schwärmt, wird das Thema Performance immer irrelevanter. Hier stellt sich ernsthaft die Frage, ob bei der Entwicklung von Software die Geschwindigkeitsunterschiede, die sich mit oder ohne AOP im Millisekundenbereich bewegen könnten, wirklich so schwer wiegen. Besonders wenn sie verglichen werden mit der Zeit, die Entwickler durch unsinnige Softwarestrukturen und mangelnde Aspekttrennung bei der Entwicklung bereits vergeuden. Man spricht heute über modellgetriebene und service-orientierte Architekturen und über die Mächtigkeit skalierbarer Client-Server-Systeme. Vergleichen wir allein die Geschwindigkeitsverluste, die durch Transformation von Objekten in XML-Repräsentationen und zurück (Stichwort: SOAP und Webservices) entstehen, muss AOP neu betrachtet werden. AOP ist eben ein Ansatz, um Softwareentwicklung, -strategien und -strukturen zu verbessern. Er ist nicht dafür gedacht, Geschwindigkeitsvorteile gegenüber Standardcode zu erreichen. Dort, wo es um Fragen der Millisekunden, der Bytes im RAM-Speicher und der CPU-Takte geht (z.B. im Realtime-Bereich oder auf der Java Micro Edition, also bei der Entwicklung für Handheld und Handy-Systeme), wird, solange es nicht auch sinnvoll integrierbar ist und ins Gesamtsystem passt, niemand nach AOP-Utilities rufen, aber auch nicht nach laufenden Webservices oder service-orientierter Architektur, um das Handy zum HomeStandby-Server zu deformieren.
AOP
211
3 – Architekturen mit AOP
Andererseits gibt es aber auch im Java Micro Edition positive Effekte durch AOP, z.B. dort, wo die Anpassung von Software an die jeweiligen individuellen Gegebenheiten des jeweiligen Geräts (device adoption) notwendig wird. Hier können Aspekte helfen, geräteunabhängige Software mit geräteabhängiger Implementierung zu verbinden. Eberhard Wolff, ein sehr kompetenter Autor, der auch zahlreiche Artikel und Bücher im Java-Bereich veröffentlichte, soll an dieser Stelle nicht unerwähnt bleiben. Er schrieb mir in der ersten Version dieses Textes die Anmerkung: „Wenn ich Transaktionen oder Security mit Aspekten mache, dann ist der Overhead der Aspekte vernachlässigbar gegenüber dem eigentlichen Transaktions- und Sicherheitshandling.“ Dem ist eigentlich nicht viel hinzuzufügen. Über wie viel PerformanceOverhead reden wir bei AOP ins Verhältnis gesetzt zu den Nachteilen der eingesetzten Technologien und vor allen Dingen der I/O-Zugriffe, die sie ausführen? Oftmals dürfte die Ausführung der Implementierung des Aspekts ein Vielfaches von der Anwendung des Aspekts verschlingen. Aber gut, wenn Sie die AOP-Variante gewählt haben, denn dann haben Sie auch nur einen SinglePoint-of-Failure bzw. ein zentrales Bottleneck, anstatt zig Klassen nach langsamen Zugriffen durchsuchen und ändern zu müssen.
3.5.1
Performance anders verstanden
Die Frage nach der Performance von AOP ist nicht unberechtigt, bedenkt man, was der Weaver im Hintergrund treibt. Und wenn Sie erst einmal den Griff zu einem IDE-integrierten Tool wie AspectJ getan haben, wird sehr schnell die Frage aufkommen, wann kompiliert werden muss, wie lange das dauert und wie oft man das machen muss, denn das kostet den Entwickler Zeit, nicht die Sekunden, die der Server zur Laufzeit im Rechenzentrum nachts um drei Uhr zum Reboot benötigt, während sich der Entwickler im nächtlichen Sourcecode-Alptraum zu Hause im Bett die Decke über den Kopf zieht. Spaß beiseite. Die Frage, die es als Erstes zu beantworten gilt, ist: Wie kann ich AOP für meine Zwecke am besten nutzen, am verständlichsten für ande-
212
Anwendungsgebiete
re, die mit mir arbeiten, in ein gemeinsames Konzept und in meine Entwicklungsabläufe einbetten? Welches ist der logischste, einfachste und gleichzeitig effizienteste Prozess, um Software zu entwickeln? Der, der nach Forschen, Ausprobieren, Überarbeiten und Wegwerfen übrig geblieben ist und am besten funktioniert hat. Er wird so lange überleben, bis sich ein besserer gefunden hat, und der wird unweigerlich irgendwann wohl kommen. Dies ist kein Buch, das Ihnen ein fertiges Kochrezept geben kann oder geben will. Es liegt kein Überweisungsformular und kein Vertrag für ein 24-Wochen-Seminar bei, aber ein Satz, der lautet: Hören Sie nie auf zu überlegen, wie Sie die Dinge einfacher, präziser, durchschaubarer und schneller realisieren können, und überlegen Sie, ob AOP ein Teil der Lösung sein kann. In der Frage der Entwicklungsgeschwindigkeit ist Performance heutzutage am Markt manchmal überlebenswichtig, weniger in der Frage der Geschwindigkeit des schließlich entstehenden Programms. Genau für diese Frage versucht AOP eine Antwort zu finden und weniger für die des Zugriffs.
3.6 Anwendungsgebiete 3.6.1
Design by Contract
Zur Laufzeit Vertragsverletzungen aufdecken Wer eine öffentliche Schnittstelle zu einem Programm oder einer internen Programmkomponente entwickelt, tut gut daran, dem Client, der sie aufruft, erst einmal zu misstrauen. Illegale, inkonsistente Datenzustände, die als Inputinformationen an das Programm herangetragen werden, gibt es weit häufiger als angenommen. Wer eine fremde Komponente aufruft und nicht über ausreichend Knowhow oder eine ausführliche Anleitung verfügt, kann schnell in Schwierigkeiten geraten, auch unbeabsichtigt einen erfolgsversprechenden Request stellen zu können.
AOP
213
3 – Architekturen mit AOP
Abb. 3.10: Komponentenschichten
Als Implementierer einer weiter hinten gelagerten Schicht eines Programms tut man sich oft schwer damit, zu beurteilen, ob die Daten, die einem da auf dem Tablett von einem Aufrufer serviert werden, bereits geprüft wurden oder nicht. Ein Beispiel soll diesen Sachverhalt kurz verdeutlichen. Gegeben sei eine Klasse mit einer public-Methode, die zu einem gegebenen Datum eine Anzahl von Tagen hinzuaddiert. Während die öffentliche Schnittstelle (die public-Methode) definiert, dass das Datum nicht leer sein darf, definiert die interne Schnittstelle, die Anzahl der Tage müsse >= 0 sein. import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; public class DBCExample { public static void main(String[] args) { DBCExample dbc = new DBCExample(); Date newDate = dbc.calculateDate(new Date(), 10); System.out.println(newDate); }
214
Anwendungsgebiete /** * @param input must not be null * @param numberOfDaysToIncrease number of days * @return new calculated date */ public Date calculateDate( Date input, int numberOfDaysToIncrease) { if (input == null) { throw new IllegalArgumentException("Argument " + input + " is null"); } Date newDate; try { newDate = calculateInternal(input, numberOfDaysToIncrease); return newDate; } catch (RuntimeException e) { e.printStackTrace(); return null; } } /** * @param input must not be null * @param numberOfDaysToIncrease must be * greater than 0 * @return new date */ private Date calculateInternal( Date input, int numberOfDaysToIncrease) { if ((input == null) || (numberOfDaysToIncrease 0) { throw exc; } }
Jede Testmethode hat als Ergebnis alle Vertragsverletzungen zurückzuliefern. Finden sich abschließend solche Verletzungen, wird eine RuntimeException erzeugt (hier eine Unterklasse BusinessContractViolatedException, die die Fehler schließlich zurückwirft). public class BusinessContractViolatedException extends RuntimeException { private List violatedContracts = new ArrayList(); public BusinessContractViolatedException() { super(); } public BusinessContractViolatedException(String s) { super(s); } public List getViolatedContracts() { return violatedContracts; } public void addViolatedContract(String contract) { violatedContracts.add(contract); } public String toString() { String returnElement = ""; for (Iterator iter = violatedContracts.iterator(); iter.hasNext();) { returnElement += iter.next();
AOP
221
3 – Architekturen mit AOP } return returnElement; } }
Anstatt – wie im normalen Quellcode üblich – für jeden Nullcheck gegebenenfalls einen eigenen Text und ein eigenes Exceptionhandling zu machen, prüft die Testmethode auf Nullelemente ganz allgemeiner Natur: private List testForNulls( Method method, List arguments) { List problems = new ArrayList(); for (int i = 0; i < arguments.size(); i++) { if (arguments.get(i) == null) { problems.add("argument " + i + " of type " + method.getParameterTypes()[i] + " of method " + method.getName() + " is null"); } } return problems; }
Wird der Interceptor nun auf die calculateDate()-Methode angewandt und das Datum bleibt bei der Übergabe leer, kommt es zu folgendem Fehler: argument 0 of type class java.util.Date of method calculateDate is null at test.DBCInterceptor.checkContracts (DBCInterceptor.java:53) at test.DBCInterceptor.invoke (DBCInterceptor.java:26) [...]
Einerseits scheint die Geschäftlogik an Aussagekraft zu verlieren, da sie nunmehr keinen direkten Bezug zu den Vorbedingungen mehr aufweist, an-
222
Anwendungsgebiete
dererseits sind solche Schnittstellenverträge kein wirklicher Geschäftslogikinhalt, sondern sie weisen nur auf einen Aspekt hin. Der Aspekt ähnelt einer Sicherheitsbeschränkung eines Users und kann z.B. auch zur Validierung von Autorisationselementen in den Aufrufparametern verwendet werden. Er verhindert nicht nur Redundanz im Sourcecode, er erlaubt es zusätzlich, Regeln ad hoc zu aktivieren und zu deaktivieren. Die Deaktivierung dieser „Laufzeitverträge“ über einen Interceptor/AspektMechanismus erlaubt zudem in Testcases (wie z.B. JUnit-Tests), die Vor-/ Nachbedingungen separat zu testen sowie sie für Tests der Hauptbusinesslogik kurzfristig abzuschalten. Denken Sie beispielsweise an Datenbanksecurityprüfungen im Source, die dafür sorgen sollen, dass nur autorisierte Personen Abfragen machen. Schön ist es, diese kurzfristig für Testzwecke deaktivieren zu können. Zum Abschluss sei die Idee noch einen Schritt weitergesponnen. In vielen Applikationen werden Listen (java.util.List) verwendet. Der Unterschied zwischen einer leeren Liste und einer nicht vorhandenen Liste ist oftmals so marginal, dass er in den meisten Schnittstellendefinitionen unter den Tisch fällt. Ergebnis sind Programmierabschnitte wie folgender: public List filterList( List neueDaten, String likeString) { List resultList = new ArrayList(); if (neueDaten != null) { for (Iterator iter = neueDaten.iterator(); iter.hasNext();) { Object element = (Object) iter.next(); if (!(element instanceof String)) { throw new RuntimeException( "Illegal element in list"); } String s = (String) element; if (s.indexOf("likeString") != -1) { resultList.add(s); }
AOP
223
3 – Architekturen mit AOP } } return resultList; }
Von den zwanzig Zeilen Sourcecode im Beispiel beschäftigen sich sieben (also rund 35%) allein mit der Prüfung gültiger Eingabedaten. Die mit Java 5 eingeführten Generics vereinfachen das Problem bereits etwas und machen die Prüfung mittels instanceof überflüssig. public List filterList( List neueDaten, String likeString) { List resultList = new ArrayList(); if (neueDaten != null) { for (String s: neueDaten) { if (s.indexOf("likeString") != -1) { resultList.add(s); } } } return resultList; }
Hier bleiben bei exakt gleicher Logik noch zwölf Zeilen Logik mit zwei Zeilen Prüfung übrig (16,7%). Über eine Annotation ließen sich jetzt weitere Vertragsprüfungen außerhalb des Klassensource definieren. In einiger Art müsste dafür eine Annotation definiert und mittels Aspekten geprüft werden. public @interface DBC { int index() default -1; String paramType() default ""; String comparator() default ""; String value() default ""; }
224
Anwendungsgebiete @DBC (paramType="java.util.List", comparator="NOT NULL") public List filterList( List neueDaten, String likeString) { List resultList = new ArrayList(); for (String s: neueDaten) { if (s.indexOf("likeString") != -1) { resultList.add(s); } } return resultList; }
Mit wenigen Handgriffen lassen sich so Hauptsource und Vertragselemente voneinander trennen. Erkennt der Aspekt nun eine Liste, deren Inhalt „null“ ist, so kann er gegebenenfalls eine neue „leere“ Liste erzeugen oder einen Fehler an den Aufrufer zurückgeben. Der Methodenaufruf bekommt von dieser Prüfung nichts mit. Architekturvorgaben prüfen Die bisher vorgestellten Beispiele decken den Bereich der Laufzeitprüfungen ab. Kein Compiler ist in der Lage, den Inhalt einer Liste oder die Füllung eines Felds bzw. einer Variablen jenseits einer statischen Typprüfung durchzuführen. Um bestimmte syntaktisch-architektonische Prüfungen durchführen zu können, ist eine Erweiterung der Compilerfähigkeiten von Nöten. Typische Beispiele könnten sein: 쐌 쐌 쐌 쐌
Analyse von Syntaxrichtlinien/Kodierungskonventionen Analyse von Vererbungshierarchien Prüfung von Aufrufbeziehungen Benennung von Klassen und Packages
Es gibt verschiedene Varianten, der Forderung nach Prüfung von Design/Architekturrichtlinien zu entsprechen.
AOP
225
3 – Architekturen mit AOP
Im Bereich der Syntaxrichtlinien unterstützen bereits Entwicklungsumgebungen eine Reihe von Prüfungen (darunter die sehr bekannten Sun Code Conventions). Darauf aufbauend, können Tools wie Checkstyle und PMDReports1 weitere Hilfestellung leisten, um in separaten Buildprozessen typische Prüfungen wie Sourceeinrückungen, Verschachtelungstiefen von IfKonstrukten oder zyklomatische Sourcecodekomplexität zu messen. PMD erlaubt zusätzlich auch bestimmte Missachtungen, wie leere Catch-Blöcke oder operative Stringvergleiche anzumeckern. Allgemein sind diese Tools aber primär dazu gedacht, die Einhaltung der Regeln (so genannter „policies“, also Richtlinien) zu dokumentieren. Sie haben keine Auswirkung auf die Durchführung des Compile, vielleicht aber auf die Qualitätsbewertung von Source und Build. Das Tool AspectJ bringt zusätzlich in der AspectJ-Sprache auch die Ausdrucksfähigkeit mit, dem zugehörigen Compiler weitere Compilerichtlinien („compile-time enforcements“) mitzugeben. Sie werden auch als „policy enforcement concerns“ (Richtlinien durchzusetzender Aspekte) bezeichnet. Zusätzlich ist AspectJ natürlich auch in der Lage, solcherlei Richtlinien auch erst zur Laufzeit festzustellen; dennoch erweckt die Prüfung der Einhaltung architektonischer Rahmenbedingungen zum Kompilierungszeitpunkt oftmals einen besseren Eindruck. Als Adrian Colyer in seinem Einführungsvortrag über AspectJ auf JAX damit begann, die Möglichkeiten von AspectJ in diesem Bereich vorzustellen, löste das durchaus etwas Stirnrunzeln bei den Anwesenden aus. Designprüfungen sind ganz und gar nicht das, womit man aspektorientierte Programmierung verbindet. Dort hat man das Bild von zerstreuten, redundanten Codestellen im Auge, die es bei dieser Art von Prüfung gar nicht gibt. Genau genommen sind aber Design-by-Contract und Contracts-for-Design, wenn Sie wollen, nicht so weit auseinander. Das eine kümmert sich primär um den Zustand der übergebenen Daten zur Laufzeit, das andere um die Struktur des Programms selbst zur Compiletime. Beides sind Aspekte, die sich allerdings sehr unterschiedlich manifestieren.
1. http://pmd.sourceforge.net/
226
Anwendungsgebiete
Insofern ist die Einbettung solcher Policies in die verschiedenen AOP-Tools nicht ungewöhnlich, wenn sie auch zu unterschiedlichen Ergebnissen führen. Typische Richtlinien, die in den Source aufgenommen werden, könnten z.B. betreffen: 쐌 derivate Aufrufvarianten (statt mit Jakartas Common Logging Framework wird z.B. mittels System.out.println() geloggt) 쐌 verbotene Instanziierung oder Aufruf bestimmter erreichbarer Klassen (z.B. unerlaubtes Starten von neuen Threads innerhalb von EJBs) 쐌 architektonisch unerwünschte Referenzbildung (z.B. Überschreitung von virtuellen Layer- oder Tiergrenzen in der Software/Systemarchitektur) Wir haben bisher über zwei Arten von Policies gesprochen: 1. Policies, die für die Struktur des Programms zum Design- bzw. Entwicklungszeitpunkt relevant sind 2. Policies, die den Zustand und den Inhalt von Daten im Kontext eines Anwendungsfalls prüfen Der Begriff der Policy begegnet uns des Weiteren in Java im Bereich der Rechte, die eine Software überhaupt innerhalb einer VM genießt. Diese Policies behandeln die Frage: Welcher Sourcecodeteil darf was? Die Installation eines Securitymanagers innerhalb einer laufenden VM-Instanz verhindert z.B. die Ausführung einer Reihe von Aktionen in den Java Runtime Libraries, falls ihr Aufruf nicht durch entsprechende Rechtevergabe (in einer Browser-Sandbox-VM z.B. durch die Java Policy-Datei) zulässig ist. Diese Form der Einschränkung ist eine so genannte code-centric policy, die Ausführung bestimmter Aktionen orientiert sich an der Herkunft der Applikation und dem Vertrauen gegenüber dem Hersteller. Eine andere Form von Abgrenzung führen die Javasprachkonstrukte von Packages, Interfaces und Modifizierern ein. Über die bekannten Modifier public, private, package-visible und protected lassen sich grundlegende Richtlinien im Design steuern.
AOP
227
3 – Architekturen mit AOP
Allerdings ist es Interfaces schon nicht mehr möglich, dynamisch ihr Aussehen zu verändern. Sollten also zwei Personen mit unterschiedlichen Rollen/Berechtigungen den Blick auf eine Schnittstelle (Interface) werfen, werden sie beide jedes Mal das Gleiche sehen. Erst beim Aufruf könnte klar werden, dass die Rechte sich unterscheiden. Die dritte Form von Policies (eben die policy enforcement concerns) können sowohl statisch (beim Compile und damit rechteunabhängig) oder dynamisch (zur Laufzeit) über Aspekte modelliert werden. Zurück zum praktischen Beispiel. Gegeben sei eine Klasse AutoImpl mit einem zugehörigen Auto-Interface, eine Fabrik-Klasse, die Autos (also AutoImpls) erzeugen kann und eine Reihe von Designverletzungen. Eine Mainmethode in AutoImpl erzeugt verbotenerweise Instanzen von AutoImpl direkt und nicht über die Fabrik. Die Fabrik selbst schreibt illegalerweise über System.out auf die Konsole und startet zusätzlich noch Threads. package factory; public interface Auto { public int getAnzahlRaeder(); } package factory; public class AutoImpl implements Auto { private int anzahlRaeder = 0; public static void main (String[] args) { AutoImpl impl = new AutoImpl(); impl.setAnzahlRaeder(5); } public int getAnzahlRaeder() { return anzahlRaeder; }
228
Anwendungsgebiete public void setAnzahlRaeder(int anzahlRaeder) { this.anzahlRaeder = anzahlRaeder; } } package factory; public class AllgemeineFabrik { public static Auto erzeugeAuto(int anzahlRaeder) { AutoImpl auto = new AutoImpl(); auto.setAnzahlRaeder(anzahlRaeder); System.out.println("Auto erzeugt!"); Thread t = new Thread(); t.run(); return auto; } }
Sourcecodereviews können solche Probleme immer sehr leicht aufdecken, allerdings ist es dann meist – besonders in schwierig zu therapierenden Fällen – nur noch mit großer Mühe möglich, die Fehler wieder zu beseitigen. Ich persönlich bin daher zwar ein Freund von entsprechenden Rahmenrichtlinien in der Entwicklung, halte aber wenig davon, dem Entwickler die Verfehlungen erst nachträglich zu präsentieren. Fehler werden immer wieder gemacht, egal ob groß oder klein, der Entwickler muss sie frühzeitig und das heißt am besten beim Compile erfahren. Für die Prüfung der obigen drei Verstöße gönnen wir uns an dieser Stelle drei separate Aspekte, in diesem Fall als Beispiele mit AspectJ umgesetzt: public aspect ThreadAspect { declare error : call (java.lang.Thread.new(..)) : "Erzeugung von Threads verboten"; }
Beginnen wir zunächst mit dem harmlosesten, der Prüfung, ob ein neuer Thread gestartet wurde. AspectJ kennt für solche Compile-Time-Prüfungen
AOP
229
3 – Architekturen mit AOP
das Schlüsselwort declare zusammen mit einem Schweregrad des Problems (severity), hier mit error bezeichnet. Anschließend folgt die Bedingung, unter der hier ein Fehler eintritt, der Aufruf des Konstruktors (hier als new bezeichnet) auf java.lang.Thread. Das Ausschreiben des vollqualifizierten Klassennamens ist für einige Tools oftmals sinnvoll, zuweilen sogar notwendig, weshalb man sich dieses Vorgehen aneignen sollte. Nach einem weiteren Doppelpunkt folgt der Fehlertext, der erscheinen soll. Für den Fall des Aufrufs von System.out.println() prüfen wir den Zugriff auf das Feld out: public aspect SystemOutPolicyAspect { declare error : get (* System.out) : "direkter Zugriff auf System verboten"; }
Da Sun sich hier im Gegensatz zu einer Methode eine Klassenvariable gegönnt hat, wird als Schlüsselwort get benötigt. Der Fehlertext erfolgt erneut im anschließenden String. Der abschließende Fabrikaspekt ist nur unwesentlich komplexer und macht sich zunutze, dass mehrere Bedingungen mit den booleschen Operatoren (UND und ODER) verknüpfbar sind, um zu einem Compile-Problem zu führen. Der Aufruf des Konstruktors von AutoImpl verursacht einen Fehler, allerdings auch nur dann, wenn dieser Aufruf nicht aus der erzeugeAuto()-Methode der Fabrik heraus geschieht (denn dies ist ja zulässig). public aspect FabrikPolicyAspect { declare error : call (factory.AutoImpl.new(..)) && (!withincode(* factory.AllgemeineFabrik.erzeugeAuto(..))) : "Erzeugung von Instanzen nur über die Fabrik"; }
230
Anwendungsgebiete
Das Beispiel macht noch einmal ganz eindringlich deutlich, dass es wichtig ist, sich nicht nur Gedanken darüber zu machen, unter welchen Umständen die Beschreibung des Eintritts eines solchen Verstoßes vorliegt, sondern wann auch nicht. Schlimmstenfalls kann man Aspekte formulieren, deren Inhalt gegen sich selbst verstößt oder zu Endlosschleifen führt. Achten Sie also bewusst und aufmerksam auf die Eintrittsbedingungen bzw. Pointcutelemente, die zu einem Join mit dem Sourcecode führen.
3.6.2
Singletons mit AOP realisieren
An dieser Stelle könnte man sicherlich etliche praktische Designpatterns vorführen, was den Rahmen des Buchs aber sprengen würde. Ein recht typisches Designpattern, dass schon hunderte Male implementiert vermutlich in fast jedem Source vorkommt, ist das Singleton-Pattern. Die Idee dabei ist, von einer Klasse nur genau eine Instanz zu erzeugen. Es gibt verschiedene, in der Literatur aufgezählte Varianten, wie ein Singleton zu implementieren ist. Eine recht typische Standardvariante ist die folgende: package test; public class StandardSingleton { private static StandardSingleton instance = null; private StandardSingleton() { } public static synchronized StandardSingleton getInstance() { if (instance == null) { instance = new StandardSingleton(); } return instance; } }
AOP
231
3 – Architekturen mit AOP
Wichtige Merkmale dieser Variante sind eine statische Klassenvariable mit dem Typ der Instanz, ein privater Konstruktor und eine synchronisierte Klassenmethode, die erst beim erstmaligen Aufruf auch eine Instanz erzeugt. Insgesamt sind also zehn Zeilen Sourcecode hier nötig, um ein Singleton zu beschreiben. Da ein solches Singleton aber nicht nur in einer Klasse vorkommt, werden es mit Sicherheit noch mehr sein. Hinzu kommt das Risiko, das Pattern falsch anzuwenden (z.B. kein privater Konstruktor) oder in die Diskussion zu verfallen, ob die Klasse jetzt bereits beim Laden eine Instanz erzeugen sollte, ob das Double-Checked-Idiom notwendig ist usw. Lösen wir den Aspekt aus der Klasse heraus, entsteht folgendes Konstrukt unter Zuhilfenahme der Java 5-Annotations: package test; @Singleton() public class ExampleClass { private long invocationTime = System.currentTimeMillis(); public long getInvocationTime() { return invocationTime; } }
Die Variable invocationTime dient allerdings nicht zur Realisierung des Pattern, ebenso wenig wie die Methode getInvocationTime(). Beide sollen zum Schluss nur der Verdeutlichung des funktionierenden Aspekts dienen. Im Beispiel sind die Klassenvariable, die getInstance()-Methode und der private Konstruktor verschwunden. Warum? Die Klassenvariable lässt sich zur Laufzeit in die Klasse setzen, so denn der Aspekt pro Klasse gilt. Aspekte selbst sind normalerweise auch Singletons, man kann allerdings pro Klasse einen eigenen Aspekt erzeugen lassen, was das Beispiel auch später zeigen wird. 232
Anwendungsgebiete
Die getInstance()-Methode ist überflüssig, da ab jetzt jeder Konstruktoraufruf immer exakt die gleiche, einmal erzeugte Instanz zurückliefern wird. Somit kann auch der private Konstruktor unterbleiben. Sehen wir uns zunächst die Definition der Annotation an. package test; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) public @interface Singleton { }
Die Annotation ist faktisch leer, auf Wunsch lässt sich das Beispiel mit den verschiedenen Varianten von sofortigem Erzeugen bei Klassenerzeugung und späterem Erzeugen (eager und lazy creation) ausbauen, indem man weitere Parameter mit in die Deklaration aufnimmt. Wichtig ist auch hier im Java 5-Fall zu überlegen, ob die Annotation zur Laufzeit des Programms über Reflection noch erreichbar sein soll. AspectJ brauchte im Milestone 2 der Version 5 nicht nur die Einkompilierung, sondern auch die Runtime-Verfügbarkeit der Annotation. Die nächste Klasse zeigt den Test, der ausgeführt werden soll, um die Sicherheit der Singletonanwendung zu prüfen. Sollte tatsächlich nur eine Instanz erzeugt werden, so müssten beide Objekte bei der Abfrage der getInvocationTime()-Methode den gleichen Wert in Millisekunden zurückliefern, da es sich um die gleiche Instanz handelt. package test; public class SingletonTest { public static void main(String[] args) { ExampleClass instance = new ExampleClass(); System.out.println( instance.getInvocationTime()); ExampleClass instance2 = new ExampleClass();
AOP
233
3 – Architekturen mit AOP System.out.println( instance2.getInvocationTime()); System.out.println( instance.getInvocationTime() == instance2.getInvocationTime()); } }
Fehlt schließlich noch der Aspekt selbst, hier implementiert mit AspectJ 1.5.0 Milestone 2: package test; public aspect SingletonAspect pertypewithin( @Singleton *) { Object _instance = null; static aspect Helper { pointcut singletonCreation() : call((@Singleton *).new(..)); Object around() : singletonCreation() { Class clazz = thisJoinPoint.getSignature() .getDeclaringType(); SingletonAspect mgrInstance = SingletonAspect.aspectOf(clazz); if (mgrInstance._instance == null) { mgrInstance._instance = proceed(); } return mgrInstance._instance; } } }
Der Aspekt wird – weil er selbst die Klassenvariable bilden soll – pro Klasse, die sich mit @Singleton markiert, gebildet:
234
Anwendungsgebiete pertypewithin(@Singleton *)
Für das Einweben wiederum wird aber eine statische Variante benötigt, die hier über einen statischen Hilfsaspekt implementiert wird. Er beschreibt, dass bei allen Klassen, die eine neue Instanz erzeugen und @Singleton-markiert sind, ein singletonCreation()-Joinpoint(-Event) vorliegt. pointcut singletonCreation() : call((@Singleton *).new(..));
Im Falle des Aufrufs des Konstruktors (around()-Advice) wird nun zunächst die Klasse festgestellt, zu der das „zukünftige“ Instanzobjekt gehören soll. Hierfür bietet AspectJ das Schlüsselwort thisJoinPoint, das auf den Joinpoint selbst verweist und es erlaubt, den Typ der deklarierenden Klasse festzustellen. Über diesen Klassennamen (clazz-Objekt) lässt sich nun wiederum auch die zugehörige Aspektinstanz, die ja pro Singletonklasse existiert, aufspüren (aspectOf(clazz)). Class clazz = thisJoinPoint.getSignature() .getDeclaringType(); SingletonAspect mgrInstance = SingletonAspect.aspectOf(clazz);
Der Rest des Sourcecodes ist nicht mehr so interessant. Die jeweilige Aspektinstanz hat eine Variable, in der die gegebenenfalls neu erzeugte Singletoninstanz gespeichert und zurückgegeben wird. Führt man das genannte Beispiel mit AspectJ aus, ergeben sich in der Konsole die erwarteten Ergebnisse. Beide Instanzen – erzeugt in der SingletonTestklasse – weisen den gleichen Timestamp auf, ein Vergleich der hashCodes beider Objekte zeigt das Gleiche. 1123943716136 1123943716136 true
AOP
235
3 – Architekturen mit AOP
Statt der zehn Zeilen Sourcecode wird pro Standard-Singleton aus dem Beispiel in der aspektorientierten Variante so nur noch eine Zeile (die Annotation) als Sourcecode benötigt. Ebenso wichtig: Die Verwendung der Klasse unterscheidet sich faktisch nicht, egal, ob es sich dabei um ein Singleton handelt oder nicht, was die Implementierung sehr einfach macht.
3.6.3
Lazy creation/initialization
Sicherlich finden sich in einer ganzen Reihe von Applikationen komplexe Objektstrukturen, die mit zunehmender Entwicklungsdauer der Anwendung nicht nur immer größer und schwieriger zu handhaben sind, sondern oftmals auch wesentlich langsamer werden. Um die Performance zu steigern, werden Teile des Objektnetzes gerne erst auf Anfrage (als „on demand“ oder auch „lazy“ bezeichnet) nachgeladen, wenn sie benötigt werden. Dies kann auch bei Datenübertragungen zwischen Client und Server genutzt werden, um die übertragene Datenmenge zu reduzieren und erst auf der jeweils anderen Seite um statische Datenelemente zu ergänzen (siehe Abbildung 3.11).
Abb. 3.11: Datenübertragung in Anlehnung an Flyweight Pattern
236
Anwendungsgebiete
Besonders gut eignen sich dafür so genannte „immutable object leaves“, das sind Enden bzw. Blattstrukturen der Objektbaumstruktur, die sich faktisch selten oder nie ändern, während die dynamischen Teile des Baumgeflechts darauf Bezug nehmen. In einer Adressliste könnten beispielsweise Namen und Telefonnummern als dynamisch, Straßennamen, Postleitzahlen und Orte allerdings als relativ statisch betrachtet werden. Die statischen, „unsterblichen“ Anteile des Modells werden oftmals nach dem Auslesen aus einer Ressource wie einer Datenbank im Hintergrund in Objektfabriken zwischengespeichert (gecacht), um im Bedarfsfall schnell verfügbar zu sein (Flyweight Pattern). Dazu ein Beispiel: Eine Adressklasse hat neben dem Namen des Bewohners und dem Straßennamen eine Referenz auf eine City. Die City wird mittels eines eindeutigen Primärschlüssels (hier einer ID) in einer Factory verwaltet. So reicht der Adresse die Speicherung der ID und die Verbindung zur Fabrik. package address; public class Address { private int cityId = -1; private String streetName = ""; private String name = ""; public City getCity() { return CityFactory.getInstance() .getCity(cityId); } public void setCity(City city) { if (city != null) { this.cityId = city.getId(); } else { this.cityId = -1; } } public String getName() { return name; }
AOP
237
3 – Architekturen mit AOP public void setName(String name) { this.name = name; } public String getStreetName() { return streetName; } public void setStreetName(String streetName) { this.streetName = streetName; } }
Werfen wir nun noch einen kurzen Blick auf die Implementierung der CityKlasse und der Factory. Die Unschärfe, dass ein Ort normalerweise weit mehr als eine Postleitzahl aufweisen kann, wollen wir an dieser Stelle mal unberücksichtigt lassen. package address; public class City { private String name; private String postleitzahl; private int id = -1; public City( int id, String postleitzahl, String name) { this.id = id; this.postleitzahl = postleitzahl; this.name = name; } public String getName() { return name; } public String getPostleitzahl() { return postleitzahl; } public int getId() { return id; } }
238
Anwendungsgebiete package address; import java.util.ArrayList; import java.util.List; public class CityFactory { private static CityFactory instance = new CityFactory(); private List cities = new ArrayList(); private CityFactory() { cities.add(new City(1, "30159", "Hannover")); cities.add(new City(2, "22045", "Hamburg")); cities.add(new City(3, "80097", "München")); } public static CityFactory getInstance() { return instance; } public City getCity(int id) { for (City city : cities) if (city.getId() == id) return city; return null; } public City getCityByPLZ(String plz) { for (City city : cities) if (city.getPostleitzahl().equals(plz)) return city; return null; } }
AOP
239
3 – Architekturen mit AOP
Die Implementierung des Singleton der Fabrik ließe sich durch den Singletonaspekt noch weiter verschlanken. Sehen wir uns schließlich noch ein Nutzungsbeispiel der Klassen an: package address; public class AddressExample { public static void main(String[] args) { Address newAddress = new Address(); newAddress.setName("Müller"); newAddress.setStreetName("Apfelallee"); newAddress.setCity( CityFactory.getInstance() .getCityByPLZ("22045")); System.out.println("Adresse:" + newAddress.getStreetName() + "/" + newAddress.getCity() .getPostleitzahl() + "/" + newAddress.getCity() .getName()); } }
Je nachdem, wie lange das Laden der City über deren ID benötigt, macht es gegebenenfalls auch Sinn nicht nur ausschließlich die ID zu speichern, sondern auch ein gecachtes Objekt in der Adresse zu halten. Das wiederum setzt aber auch voraus, dass es sich bei den gecachten Objekten um wirklich unsterbliche Elemente handelt, da ansonsten jede Adressinstanz trotz gleicher City-ID unterschiedliche Städtenamen gecacht haben könnte. Das hier gezeigte Modell kann sich aus architektonischer Sicht sehr schnell zum Reinfall entwickeln. Einerseits kennt das Modell (die Adresse) die Fabrik, die wiederum selbst Modellobjekte erzeugt, was schnell zu zirkulären Beziehungen zwischen Klassen oder Packages führen kann. Zum anderen werden gerade diese zirkulären Beziehungen die Komponentengrenzen zu „betonieren“ beginnen.
240
Anwendungsgebiete
Das heißt, dadurch dass das Modell die Fabriken kennt, die wiederum die Ressourcen kennen1, bindet sich das Modell an die Storage-Systeme2. Es wird damit nicht mehr richtig portabel. Man kann nun hier versuchen, mit Interfaces und Implementierung das Problem leicht abzumildern, doch es bleibt die Tatsache, dass etwas Kontextfremdes (die Fabrikreferenz) im Modell auftaucht. „Nun ja“, könnte der Einwand lauten, „selbst wenn es Beziehungen aufweist, so sind die Daten immer noch gut serialisierbar.“ Die Adresse lässt sich perfekt mittels der ID speichern, alle restlichen City-Daten und die Referenz zur City-Fabrik sind irrelevant für die Speicherung der Instanz! Das ist richtig. Nur, überträgt man das Modell über eine Tier-Grenze (also z.B. von einem Server auf einen Client), werden bestimmte Fabriken auf der anderen Seite entweder gar nicht existieren, anders angesprochen oder andere Namen tragen (siehe Abbildung 3.11). Am einfachsten zu verstehen (und oftmals zu handhaben) ist ein Modell, das rein Java-Bean-konform daherkommt und außer Getter/Setter-Methoden (also „accessor methods“) keine weitere Logik enthält. Damit dies funktioniert, müssen wir praktisch den Zugriff auf die Variable mittels eines Aspekts patchen. Er ist dann dafür zuständig, die korrekte Instanz zu laden. Durch unterschiedliche Aspektimplementierungen auf unterschiedlichen Tiers kann die Realisierung dieses Lazy-Loading-Ansatzes sehr variabel verlaufen, ohne dass das Objektmodell davon in Kenntnis gesetzt werden müsste. Eine denkbare Aspektimplementierung könnte wie folgt aussehen: package address; import java.lang.reflect.Field;
1. z.B. per Datenbankzugriff Daten zu besorgen 2. XML-Dateien, Datenbanken, Properties-Dateien, ...
AOP
241
3 – Architekturen mit AOP public aspect LazyLoadingAspect { Object around(): get (private City Address.city) { try { Class clazz = thisJoinPoint.getSignature() .getDeclaringType(); Field field = clazz.getDeclaredField (thisJoinPoint.getSignature() .getName()); Object value = field.get(thisJoinPoint.getThis()); if (value == null) { // individuelle Reaktion } else { return value; } } catch (Exception e) { e.printStackTrace(); return null; } } }
An dieser Stelle wird ein around-Advice auf den Zugriff der city-Variablen in der Klasse Address vom Typ City gesetzt. Wird auf die Variable zugegriffen, wird zunächst ihr Wert ermittelt. Dazu prüft thisJoinPoint.getSignature().getDeclaringType()
den Typ der Klasse (in diesem Fall Address) und ermittelt über den Namen des Felds thisJoinPoint.getSignature().getName()
242
Anwendungsgebiete
den Wert des city-Instanzfelds. Abhängig von dessen Inhalt und der Variante, in der das Lazy-Loading entwickelt ist, muss jetzt entschieden werden, was zu tun ist1.
Abb. 3.12: Mögliche Implementierung hinter einem City-Interface
Einen ähnlichen Lazy-Fetch-Ansatz können Sie statt mittels AspectJ-Aspekt auch verfolgen, wenn Sie die City-Instanz im Design in Interface und Implementierung zerlegen und über Dynamic Proxies der Implementierung unterschiedliche Verhaltensmuster unterschieben (siehe Abbildung 3.12). Beim Aufbau des Objektmodells muss dann entschieden werden, ob der Zugriff auf eine City-Implementierung direkt oder über eine Fabrik geschieht.
3.6.4
Instanzen und Ergebnisse cachen
Neben der eben beschriebenen Lösung wollen wir uns jetzt einmal anschauen, wie man Objekte bzw. Ergebnisse von Methodenaufrufen zwischenspeichern kann. Unsere Zielsetzung ist dabei, die Ergebnisse jedes beliebigen Aufrufs anhand der übergebenen Parameter zu cachen.
1. z.B. das echte Objekt aus der Datenbank mittels einer zusätzlichen ID-Information lesen
AOP
243
3 – Architekturen mit AOP
Das Beispiel sei dazu unterteilt in zwei Hauptklassen: einen BusinessController, der Geschäftslogik ausführt, um Ergebnisse zu errechnen bzw. zu erzeugen, und eine Testklasse, die den BusinessController aufruft: package cache2; public class TestClass { /** * @param args */ public static void main(String[] args) { BusinessController ctrl = new BusinessController(); String s = ctrl.getResult(5, 7); System.out.println(s); s = ctrl.getResult(5, 9); System.out.println(s); s = ctrl.getResult(5, 7); System.out.println(s); System.out.println("-----"); s = ctrl.getDifference(5, 7); System.out.println(s); s = ctrl.getDifference(5, 9); System.out.println(s); s = ctrl.getDifference(5, 7); System.out.println(s); } }
Auf dem BusinessController seien zwei Methoden definiert. Beide führen relativ belanglose Aktionen aus. Nur eine der beiden Methoden, die getResult()-Methode, unterstützt Caching. Die andere nicht. Wir haben je zweimal die Argumente 5 und 7. Um zu zeigen, dass bei der getResult()-Methode tatsächlich ein Cacheergebnis zurückkommt, hilft uns eine Ausgabe auf System.out.
244
Anwendungsgebiete package cache2; public class BusinessController { @CacheResults() public String getResult(int a, int b) { System.out.println("getResult() created : " + System.currentTimeMillis()); return a * b + "/" + a + b; } public String getDifference(int a, int b) { System.out.println( "getDifference() created :" + System.currentTimeMillis()); return "" + (a – b); } }
Die getResult()-Methode ist mit einer Annotation versehen. Sie definiert, dass die Ergebnisse dieser Methode gespeichert und bei Bedarf aus dem Cache geholt werden sollen. Die Annotation ist dafür relativ schlicht gehalten: package cache2; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) public @interface CacheResults { }
Fehlt schließlich noch die Implementierung des Aspekts. Er muss auf dem Vorhandensein der Annotation in einer Methode aufsetzen und die Ergebnisse adäquat zwischenspeichern. Dazu verwenden wir eine zusätzliche Unterklasse namens CachedElement, die hilft, die Argumente in der Liste der gecachten Elemente bei einem späteren Zugriff wiederzufinden. AOP
245
3 – Architekturen mit AOP package cache2; import java.util.ArrayList; import java.util.List; public aspect CacheAspect pertarget(cacheResults()) { private List cachedElements = new ArrayList(); pointcut cacheResults() : execution(@CacheResults * *(..)); Object around() : cacheResults() { Object[] arguments = thisJoinPoint.getArgs(); CachedElement arg1 = new CachedElement(arguments); int index = cachedElements.indexOf(arg1); if (index != -1) { System.out.println( "returning cached element"); return cachedElements.get(index).result; } Object result = proceed(); arg1.result = result; cachedElements.add(arg1); return result; } private class CachedElement { Object[] arguments = null; public Object result = null; public CachedElement(Object[] arguments) { this.arguments = arguments; } public boolean equals(Object o) { CachedElement o1 = (CachedElement) o; if ((o1.arguments == null)
246
Anwendungsgebiete && (this.arguments == null)) { return true; } for (int i = 0; i < this.arguments.length; i++) { if (!this.arguments[i] .equals(o1.arguments[i])) { return false; } } return true; } } }
Die eigentliche Implementierung erweist sich als relativ unspektakulär. Die Argumente des Methodenaufrufs werden ausgelesen, in der Liste wird ein entsprechendes gecachtes Objekt gesucht und zurückgeliefert, falls sich eines finden lässt. Ansonsten wird der echte Methodenaufruf ausgeführt und das Ergebnis neu in die Liste gestellt. Der entstehende Konsolenoutput zeigt, dass in einem Fall – nämlich dort, wo eine Abfrage auf der getResult()-Methode ausgeführt wird, die bereits gespeichert war – tatsächlich eine Cacheantwort zurückgeliefert wird. getResult() created : 1127597808594 35/57 getResult() created : 1127597808604 45/59 returning cached element 35/57 ----getDifference() created : 1127597808604 -2 getDifference() created : 1127597808604 -4 getDifference() created : 1127597808604 -2
AOP
247
3 – Architekturen mit AOP
Eine solche Cachingvariante lässt sich nun vielseitig anwenden. Sie können Konstruktoraufrufe cachen, um Objekte mit bestimmten Ausgangsparametern nur einmal zu erzeugen. Sie können Factories zum Cachen bringen, selbst wenn diese das vorher nicht gewohnt waren, oder Sie können, wie im Beispiel, langläufige Transaktionen beschleunigen, indem Sie Ergebnisse von Methodenaufrufen zwischenspeichern.
3.6.5
Exception-Softening
Java kennt das Sprachmittel der RuntimeExceptions, die es sehr leicht machen, Ausnahmesituationen zu übermitteln, ohne diese Ausnahmen in der Signatur der Methode zu benennen. Dies verschlankt den Sourcecode des Aufrufers sehr, da keine unnötigen Catch-Blöcke erzeugt werden müssen. Sinnvoll ist die Anwendung solcher RuntimeExceptions dann, wenn Fehler potentiell fast nie auftreten oder ein konkretes bzw. fallspezifisches Fehlerhandling schwierig ist. Es gibt allerdings auch das genaue Gegenteil, und zwar jene Exceptions, die zwar deklariert sind, allerdings vermutlich nie fliegen werden. Grund sind z.B. Vorgaben in zu implementierenden Interfaces oder Designvorgaben, die im Kontext von Remotezugriffen erfolgen. package service; import java.rmi.RemoteException; public class Callee { public String execute(String s) throws RemoteException { return "Hallo " + s; } }
Eine solche typische Exception ist die java.rmi.RemoteException, die selbst dann in RMI-fähigen Controllerklassen deklariert werden muss, wenn sie zumindest bei Aufrufen in der gleichen VM nie fliegen würde.
248
Anwendungsgebiete
Der Aufruf in einer fremden Klasse führt unweigerlich zum Compile-Fehler, da im Gegensatz zu den RuntimeExceptions die RemoteException gecacht werden muss. public class Caller { public static void main(String[] args) { Callee instance = new Callee(); String t = null; t = instance.execute("Test"); //Compilefehler! System.out.println(t); } }
AspectJ bietet das Schlüsselwort declare soft für zu catchende Exceptions an. In unserem Beispiel würde die Exception wie folgt verarbeitet werden: package service; import java.rmi.RemoteException; public aspect RemoteExceptionSoftenAspect { declare soft : RemoteException : call (public void service.Callee.execute(**)); }
Intern wird der Aufruf auf Callee.execute() gewrappt und die RemoteException durch eine so genannte SoftException (eine RuntimeException, die weitergeworfen wird) ersetzt. Allerdings ist dieses Verfahren vermutlich nur dort wirklich gut aufgehoben, wo man sicher sein kann, dass nie eine Exception auftritt und die Signatur somit zum Catchen nicht existenter Fehlerzustände zwingt. Es gibt auch weitergehende Empfehlungen, die beispielsweise SQLExceptions nicht catchen, sondern als technische Probleme betrachten und sie somit nicht mit dem sonst fachlichen Code zu verheiraten. Damit die reine Businesslogik sich nicht mit technischen Details beschäftigen muss, wird die technisch zu catchende Exception zur RuntimeException mutiert und
AOP
249
3 – Architekturen mit AOP
kann so recht mühelos durch zahlreiche Layer geführt werden, weil der reine Geschäftscode diese oft nur unvollständig behandeln kann. In Java EE kommt einer geworfenen RuntimeException z.B. die Bedeutung eines Abbruchs der Transaktion bei, wenn sie aus der Komponente „entkommen“ sollte. Ob dieses Verhalten allerdings sinnvoll ist, hängt von Ihrer aktuellen Architektur ab und der Frage, wer zum Schluss die RuntimeException entgegennimmt. Irgendeiner wird es hoffentlich tun, damit die Anwendung nicht wegstirbt. Ansonsten ähnelt dieses Verhalten sehr dem Programmiervorgehen in C#, bei dem nie irgendeine Exception gefangen werden muss, sondern das gesamte Handling von Fehlerzuständen mehr oder minder als optional betrachtet wird, was sicherlich auch nicht immer die beste Designlösung darstellt.
3.6.6
Coverage-Tests
In die Themenkomplexe „Logging“ und „Tracing“ fällt auch das Thema „Codecoverage“, also die Messung der durchlaufenen Statements. Da AOP es ermöglicht, auf jedem beliebigen Methodenaufruf ein Tracing auszuführen, müsste ein einfacher Aspektaufruf wie: aspect CoverageAspect{ call (* model..*.*(..)) && ! within( model..*); }
eine erste Näherung bringen. Eine Pointcutdefinition, die jene Aufrufe von Methoden im Package model als Joinpoints betrachtet, die nicht aus dem model-Package selbst stammen. Damit ist die Lösung allerdings nur zur Hälfte erreicht. Wir müssten nun noch zusätzlich die Zeilennummern und gegebenenfalls den Methodenaufruf selbst speichern – was nicht allzu schwer fallen dürfte.
250
Anwendungsgebiete
Abgesehen vom Performance-Nachteil gibt es allerdings bei dieser Art von Coverage-Prüfung ein weit schwierigeres Problem zu lösen. Neben dem reinen Aufruf von Statements finden sich im Source auch syntaktische Elemente von Bedingungen und Schleifen, also if-, while- oder switch/case-Statements. Sie selbst führen weder zum Zugriff auf eine Objektinstanz noch zum Aufruf einer Methode. Zurzeit halten JBoss AOP und AspectJ als bekannte Vertreter der Java-AOPToolgemeinde allerdings noch keine Sprachelemente bereit, um diese Sourceartefakte als Teil des Joinpoint einzubeziehen. AOP eignet sich im Gegensatz zu klassischen Coverage-Analysetools, die z.B. über die Java-Debug-Schnittstellen in die VM eingreifen und Informationen von dort beziehen, derzeit also nur bedingt zur Coverage-Analyse. Allerdings lässt sich eine gezielte Prüfung, ob bestimmte Teile des Sourcecodes durchlaufen werden und gegebenenfalls wie oft – was sehr schnell eine Aussage über IST- und SOLL-Verhalten der Software erlaubt –, durchaus auch mit AOP-Mitteln bewerkstelligen. Eine solche Aussage ist sowohl im Test- als auch im produktiven Umfeld gegebenenfalls durchaus interessant.
3.6.7
Asynchrone Aufrufe realisieren
Als asynchronen Aufruf bezeichnet man die Aufsplittung des aktuellen Programmverlaufs auf mindestens zwei parallel oder quasiparallel laufende Programmstränge, wobei die Ausführung des ersten nach der Abspaltung nicht auf die Beendigung des zweiten wartet. Versuchen wir, dies zunächst an einem Beispiel zu verdeutlichen. Die nachfolgenden zwei Klassen regeln die Speicherung eines komplexen Objekts (hier eines javax.swing.JFrame()) als XML. Das Beispiel wurde deshalb gewählt, da diese Transformation mehrere Millisekunden braucht (abhängig vom Rechner) und der normale Programmablauf davon aber nicht behindert werden soll.
AOP
251
3 – Architekturen mit AOP package model; import java.io.File; import javax.swing.JFrame; public class MainProgram { public static void main(String...args) { System.out.println("Start: " + System.currentTimeMillis()); Transformation ts = new Transformation(); JFrame frm = new Jframe(); ts.transformAndSaveState(frm, new File("C:/frame.xml")); System.out.println("Done it! : " + System.currentTimeMillis()); } }
Die markierte Methode führt die Transformation in eine XML-Datei durch. Zur Prüfung des Ausführungsverlaufs wird der aktuelle Zeitpunkt in Millisekunden geloggt. Der Inhalt der Transformation-Klasse sieht wie folgt aus: package model; import java.io.File; import java.io.FileWriter; import java.io.IOException; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.io.xml.DomDriver; public class Transformation { public void transformAndSaveState( Object o, File targetFile) {
252
Anwendungsgebiete try { XStream xstream = new XStream(new DomDriver()); xstream.toXML( o, new FileWriter(targetFile)); System.out.println("File saved: " + System.currentTimeMillis()); } catch (IOException e) { e.printStackTrace(); } } }
Ein Blick auf die Konsole verrät in diesem Fall wenig Überraschendes. Die Transformation und das Speichern dauern einige Sekunden, danach wird die Nachricht Done it! ausgegeben. Start: 1124641227250 File saved: 1124641228140 Done it! : 1124641228140
Um nun zu verhindern, dass der Hauptthread stehen bleibt, während die Transformation durchgeführt wird, bedarf es nur eines einfachen Aspektmechanismus. Auch hier können Sie gegebenenfalls wählen zwischen der impliziten Deklaration des Aspektaufrufs über eine Sourcecode-statische Benennung im Aspekt und dem expliziten Aufruf im Code mittels einer Annotation. package model; public aspect WorkerAspect { pointcut asynCall() : call(* model.Transformation .transformAndSaveState(..)); void around(): asynCall() { Runnable worker = new Runnable() {
AOP
253
3 – Architekturen mit AOP public void run() { proceed(); } }; Thread th = new Thread(worker); th.start(); } }
Im Beispiel wird ein Pointcut mit Namen asynCall() definiert, der auf allen Aufrufen auf die transformAndSaveState()-Methode von model.Transformation anschlägt. Ein eigener Worker-Thread wird definiert, der lediglich die weitere Durchführung der Geschäftslogik mit proceed() initiiert. Dies führt zu folgendem Konsolenoutput: Start: 1124642494984 Done it! : 1124642495187 File saved: 1124642495593
Erwartungsgemäß wird das Ende der Transformation asynchron vermeldet, so dass die Done it!-Ausgabe vor dem File saved erfolgt. Möchte man diese asynchrone Verarbeitung im Kontext von Swing-Anwendungen verwenden, wird statt des Aufrufs von Thread.start() das Statement java.awt.EventQueue.invokeLater(worker) verwendet. Einfache Callbacks implementieren Die soeben vorgestellte Worker-Thread-Implementierung lässt sich leicht auch für die Implementierung eines Callback verwenden. Callbacks melden sich im Gegensatz zum Standard-Fire-and-Forget-Ansatz nach Beendigung ihrer Aufgabe wieder. Wir haben somit zwei nebenläufige Threads, wobei der initiierende von der Wiederkehr des zweiten erfährt. Der nachfolgende Sourcecode zeigt einen solchen Callback-Beispielaufruf. Die Methode asyncCall() wird asynchron aufgerufen. Das bedeutet in diesem Fall, dass in der Methode bis zehn gezählt wird und der letzte Wert als Integer zurückkommt.
254
Anwendungsgebiete
Während der erste Aufruf von asyncCall() in der main-Methode lediglich null zurückgibt, da der Thread noch nicht beendet ist, wird das Ergebnis der asyncCall()-Methode an die Methode callBack(Object) weitergeroutet, die dann das Ergebnis ausgibt. Die im Beispiel hinterlegten Thread.sleep()-Aufrufe dienen lediglich dem besseren Verständnis des ausgeführten Programmablaufs und sind grundsätzlich nicht notwendig. package test; public class TestClass { public static void main(String[] args) { TestClass tc = new TestClass(); // hier kommt zunächst kein Ergebnis zurück: tc.asyncCall(); for (int a=0;a