Westfälische Wilhelms-Universität Münster Ausarbeitung Varianten zur Laufzeit: Java Reflection Im Rahmen des Hauptseminars „Variantenmanagement“ Christian Müller Themensteller: Prof. Dr. Herbert Kuchen Betreuer: Dipl.-Wirt. Inform. Christoph Lembeck Institut für Wirtschaftsinformatik Praktische Informatik in der Wirtschaft Inhaltsverzeichnis 1 Variantenmanagement zur Laufzeit........................................................................... 3 2 Java Reflection - Grundlagen .................................................................................... 4 3 4 2.1 Grundbegriffe bei Java Reflection.................................................................... 4 2.2 Möglichkeiten von Java Reflection .................................................................. 5 2.3 Sicherheitsaspekte bei Java Reflection ........................................................... 14 Beispielhafte Ausführungen zum Thema Reflection............................................... 17 3.1 Reflection in der Praxis – JUnit...................................................................... 17 3.2 Reflection in der Anwendung – Ein EventDispatcher.................................... 18 Java Reflection – Flexibilität für die Applikation ................................................... 23 A. Zeitliche Charakterisierung des Reflection......................................................... 24 B. Beispielklasse C .................................................................................................. 25 C. JUnit-Methode isTestMethod(Method m) .......................................................... 26 D. Ereignisbehandlung mit und ohne EventDispatcher........................................... 27 Literaturverzeichnis ........................................................................................................ 28 II Kapitel 1: Variantenmanagement zur Laufzeit 1 Variantenmanagement zur Laufzeit Die zunehmende Komplexität heutiger Applikationen erfordert eine Flexibilität, die es gestattet, Anwendungen im Rahmen ihres Lebenszyklus nicht als statisch zu begreifen. Vielmehr ist es heutzutage notwendig, Voraussetzungen zu schaffen, die es ermöglichen, eine Applikation an die sich ändernden Anforderungen zu erweitern und anzupassen. Für diese Zielsetzung werden Metaarchitekturen benötigt, durch die Informationen über die Struktur und Ausführungszustand der Applikation zur Laufzeit bereitgestellt werden. Mit Java Reflection wurde eine Software-Bibliothek geschaffen, welche dem Softwareentwickler eine Metaarchitektur zur Verfügung stellt, die eine Variantenbildung zur Laufzeit ermöglicht. Varianten einer Applikation werden begründet durch Hinzufügen neuer oder Ändern bereits bestehender Funktionalität, können aber auch aus der Modifikation weniger relevanter Implementierungsdetails resultieren. [Ma94, S. 74 f] Aus diesem Grund soll in der vorliegenden Arbeit ein thematischer Überblick zum Reflection erfolgen, der sowohl Grundlagen und Anwendungsfelder reflexiver Programmierung als auch Sicherheitsaspekte beleuchtet. Im Kapitel 2 der Arbeit befinden sich grundlegende Ausführungen zur Thematik des Reflection, in denen eine definitorische Abgrenzung von Begriffen vorgenommen wird, die im Rahmen der Themenstellung relevant sind. Daran anschließend erfolgt eine Vorstellung der sich aus der Nutzung von Reflection ergebenden Möglichkeiten bspw. für die Softwareentwicklung. Die durch Reflection garantierte Flexibilität bietet jedoch auch geeignete Angriffspunkte für die schadhafte Auslegung und Nutzung von Applikationen, so dass hierzu kurz Sicherheitsaspekte diskutiert werden. Den theoretischen Überlegungen folgend, werden in Kapitel 3 zwei exemplarische Einsatzfelder des Reflection skizziert. Zum einen erfolgt eine kurze Darstellung des Test-Frameworks JUnit und die dort erfolgreich verwendeten reflexiven Bestandteile. Zum anderen wird ein EventDispatcher vorgestellt, der es durch Einsatz reflexiver Programmierung ermöglicht, die Registrierungsarbeiten im Rahmen der Ereignisbehandlung in Java zu automatisieren bzw. die konkrete Ereignisbehandlung austauschbar gestaltet. 3 Kapitel 2: Java Reflection - Grundlagen 2 Java Reflection - Grundlagen 2.1 Grundbegriffe bei Java Reflection Der Einsatz reflexiver Programmierung, bereitgestellt durch java.lang.reflect.*, ermöglicht es, Zugriff auf Strukturinformationen von Klassen zu erlangen und Statusinformationen von Objekten zur Laufzeit in einem begrenzten Rahmen zu modifizieren. Darüber hinaus erlaubt die Nutzung der Bibliothek eine Ausführung von Methoden, deren Name und Signatur erst zur Laufzeit bekannt sind. Im Rahmen der Diskussion des Reflections finden sich unterschiedliche Begriffe, die zunächst definitorisch abgegrenzt werden sollen. Reflection umfasst laut Definition die Fähigkeit sowohl Objekte zu beobachten als auch Zustandsänderungen von Objekten herbeizuführen. Um die auch introspection genannte Untersuchung von Objekten und deren Zustand zu garantieren, werden entsprechende Daten über die konkrete Datenstruktur, ergo Metadaten zur Verfügung gestellt. [FoFo04, S. 15] Wird von intercession gesprochen, ist die Modifikation von Objekten und Ausführung von Programmcode gemeint. [FoFo04, S. 74] Vorgehalten werden diese Metadaten durch Meta-Level-Objekte, die Funktionalität zur Verfügung stellen, um Meta-Level-Operationen auf Basisobjekte zu erlauben. Eine Untermenge der MetaLevel-Objekte sind die Metaobjekte, welche strukturelle Informationen über Attribute, Konstruktoren und Methoden beinhalten und die Struktur sog. Basisobjekte darstellen. Bspw. fungieren Instanzen der Klasse java.lang.Class als Metaobjekte. Die Zielsetzung der Metaprogrammierung ist eine Separation von funktionalen und nicht-funktionalen Aspekten der Applikation. Basisobjekte werden als funktionale Aspekte verstanden, die mit dem Zweck der Applikation einhergehen, Meta-Level-Objekte hingegen dienen zur Administration der Objekte des Basislevels. [MiGo97, S. 6] Abbildung 1: Beziehungen zwischen Meta- und Basissystem 4 Kapitel 2: Java Reflection - Grundlagen Für Anwendungen, wie User-Interface-Builder (UI-Builder) oder Debugger sind MetaLevel-Objekte und die dadurch gekapselten Metadaten von großer Bedeutung. Erst durch die Meta-Level-Objekte ist es bspw. für Debugger möglich Objekte zur Laufzeit zu inspizieren und für den Anwender auf bequeme Weise zu präsentieren. Ebenso verwenden UI-Builder notwendigerweise diese Daten. Weiterhin ist es möglich, Reflection-Arten hinsichtlich zeitlicher Kriterien zu charakterisieren. An dieser Stelle wird zwischen Reflection zur Kompilierzeit (compiletime reflection), zur Programmladezeit (load-time reflection) und Reflection zur Laufzeit (run-time reflection) differenziert. [MiGo97, S. 9 f] Im Mittelpunkt der Charakterisierung steht der Zeitpunkt, bei dem das Basissystem an das Metasystem gebunden wird. An dieser Stelle erfolgt keine weitere Diskussion der Arten. (Vgl. Anhang A für eine schematische Darstellung) 2.2 Möglichkeiten von Java Reflection Um einführend die Nutzungsmöglichkeiten von Java Reflection aufzuzeigen, werden die Kernelemente reflexiver Programmierung vorgestellt und anhand geeigneter Beispiele erläutert. Dazu wird zunächst das Metaobjekt java.lang.Class thematisiert, mit dessen Nutzung zur Laufzeit Statusinformationen von Objekten verfügbar werden. Im Anschluss daran werden die Ausführung von Methoden und die Instanziierung von Objekten zur Laufzeit angesprochen. Metaobjekt Class Das Java Reflection API bietet verschiedene Möglichkeiten, um an Informationen über Objekte zur Laufzeit zu gelangen. Zentraler Bestandteil von Java Reflection ist die Nutzung des Class-Objekts. Für jede geladenen Klasse werden von der Java Virtual Machine (JVM) Class-Objekte [JADCl05] erzeugt, mit denen der Zugriff auf weitere Metaobjekte wie java.lang.reflect.Method oder java.lang.reflect.Field möglich ist, die jeweils die Methoden und Felder einer Klasse repräsentieren. Informationen zur Laufzeit Häufig ergeben sich Situationen, in denen bestimmte Informationen erst zur Laufzeit bekannt sind bzw. dynamisch geladen werden sollen. Ausgangspunkt reflektiver Programmierung bildet meist das Bemühen, das Class-Objekt der jeweiligen Klasse oder des Objektes zu erhalten. Das folgende Beispiel illustriert unterschiedliche 5 Kapitel 2: Java Reflection - Grundlagen Möglichkeiten, um Zugriff auf das Class-Objekt zu erlangen. Im Beispiel wird differenziert zwischen folgenden Fällen: a) Es existiert bereits vor dem Kompilierzeitpunkt eine Referenz auf das zu untersuchende Objekt. Die Klasse Object definiert die Methode getClass(), mittels derer die objekteigene Class-Referenz ausgelesen werden kann. In diesem Fall liegt die Klasse schon in instanziierter Form als Objekt vor d. h. der Classloader hat die Klasse schon geladen. b) Der Name der Klasse ist vor dem Kompilierzeitpunkt verfügbar. Das ClassObjekt ist über eine konstante Referenz ansprechbar, wobei hierfür der voll qualifizierte Name (Identifier) der Klasse benötigt wird. c) Der voll qualifizierte Name der Klasse wird erst zur Laufzeit spezifiziert. Die mithin flexibelste Art, um an eine Referenz des Class-Objektes zu gelangen, erfolgt durch die Nutzung der statischen Class-Methode forName(String s). In allen drei Fällen resultiert ein Class-Objekt, dem Statusinformationen des Objekts und Strukturinformationen der zugehörigen Klasse entnommen werden können. public class GetClassBsp { public static void main(String[] args) { Class classA, classB, classC; javax.swing.JFrame frame = new javax.swing.JFrame(); try { /** Fall a */ classA = frame.getClass(); /** Fall b */ classB = javax.swing.JFrame.class; /** Fall c */ classC = Class.forName(“javax.swing.JFrame”); System.out.println("Fall System.out.println("Fall System.out.println("Fall } catch (Exception e) {/** a: " + classA.toString()); b: " + classB.toString()); c: " + classC.toString()); Exception Handling */} } } Eine Ausführung des Programms führt zu folgender Ausgabe: Fall a: class javax.swing.JFrame Fall b: class javax.swing.JFrame Fall c: class javax.swing.JFrame Nun ist es möglich, mit Hilfe des Class-Objekts weitere Informationen über das Objekt und dessen Zustand zu bekommen. Für diesem Zweck verfügt das Class-Objekt über die 6 Kapitel 2: Java Reflection - Grundlagen Methoden getField(String Feldname) und getFields(), mit denen sich gezielt einzelne aber auch alle Felder einer Klasse ansprechen lassen, um bspw. den Inhalt oder die Modifier des Feldes zu ermitteln. Die Methode getFields() gibt ein Array mit Metaobjekten vom Typ java.lang.reflect.Field zurück, die den deklarierten oder geerbten Feldern der Klasse mit dem Modifier public entsprechen. Im Unterschied dazu erfolgt durch die Methode getDeclaredFields() eine Rückgabe aller in der Klasse deklarierten Felder, unabhängig vom zugehörigen Modifier des Feldes. Bei der Betrachtung von Sicherheitsaspekten wird in Kapitel 2.3 nochmals auf die Methode getDeclaredFields() eingegangen. Die Klasse C verfügt über zwei Felder – intValue vom primitiven Typ int und stringValue vom Typ String. Beide Felder sind mit dem Modifier public versehen. (Vgl. Anhang B für Klasse C) /** relevanter Teil der Klasse C.java */ public int intValue; public String stringValue; /** relevanter Teil der Applikation.java */ public static void readC(Object c) { Class clazz; try { clazz = c.getClass(); Field[] fields = clazzA.getFields(); /** Auflisten aller Felder der Klasse */ for (Field field: fields) { System.out.println("Name des Feldes: " + field.getName()); System.out.println("Typ: " + field.getType().getName()); /** Liefert einen int zurück... */ int modifier = field.getModifiers(); /** ...der durch die Nutzung dieser statischen * Methode umgewandelt wird */ System.out.println("Modifizierer: " + Modifier.toString(modifier)); /** Auslesen und Verändern der Werte */ if (field.getType().equals(int.class)) { System.out.println("Inhalt von: " + field.getInt(c)); /** Modifizierung des Datenfeldes */ System.out.println("--modifiziert--"); field.setInt(c, 112); System.out.println("Inhalt von: " + field.getInt(c)); } else if (field.getType().equals(String.class)) { System.out.println("Inhalt von: " + field.get(c)); System.out.println("--modifiziert--"); field.set(c, new String("Es schneit!")); System.out.println("Inhalt von: " + field.get(c)); }}} catch (Exception e) {/** Exception Handling */} } 7 Kapitel 2: Java Reflection - Grundlagen Die Ausgabe des Programms liefert: Name des Feldes: intValue Typ: int int-Modifizierer: 1 Modifizierer: public Inhalt von: 4711 --modifiziert-Inhalt von: 112 Name des Feldes: stringValue Typ: java.lang.String int-Modifizierer: 1 Modifizierer: public Inhalt von: Hallo --modifiziert-Inhalt von: Es schneit! Unter Verwendung der Methoden getInt(Object obj), get(Object obj), sowie setInt(Object obj, int i) und set(Object obj, Object value), die durch die Klasse Field bereitgestellt werden, ist es möglich Datenfelder auszulesen und deren Inhalte entsprechend zu manipulieren. Darüber hinaus stehen get-/set-Methoden für jeden anderen primitiven Datentyp, bspw. getBoolean(Object obj) und setBoolean(Object obj, boolean z), zur Verfügung. Methoden zur Laufzeit finden und ausführen Nachdem Felder und Strukturinformationen über die Klasse C ausgelesen werden können, ist ein weiterer Schritt bspw. deklarierte und geerbte Methoden von C zu ermitteln. Ausgangspunkt hierzu bildet wieder das Vorliegen eines Class-Objekts der zu analysierenden Klasse. Durch die Verwendung von getMethod(String name, Class[] parameterTypes) oder getMethods(), bereit gestellt durch Class, werden die einzelnen Methoden der spezifizierten Klasse zurück gegeben. Eine Methode der zu untersuchenden Klasse wird jeweils repräsentiert von einem Metaobjekt des Typs java.lang.reflect.Method. Auf die Verwendung der Methode getDeclaredMethods() wird in Kapitel 2.3 detaillierter eingegangen. Es sei hier kurz erwähnt, dass sich getDeclaredMethods() in ähnlicher Weise verhält wie die bereits aufgeführte Methode getDeclaredFields(). Wird bspw. ein Array gefüllt mit Method-Objekten der einzelnen Klassenmethoden zurückgegeben, kann dieser Array durchlaufen werden, um Informationen über die Methoden zu erhalten. Hierbei ermöglicht z. B. die durch Method bereit gestellte Methode getParameterTypes() die Signatur der zu untersuchenden Methode zu ermitteln. Darüber hinaus können die durch die Methode 8 Kapitel 2: Java Reflection - Grundlagen ausgelösten Exceptions (getExceptionTypes()), der Typ der zurückgegebenen Werte (getReturnType()) oder die Modifizierer der Methode (getModifiers()) erfragt werden. Folgende Methode showMethods(Object c) illustriert einen Teil der dargestellten Möglichkeiten, um an Informationen über die Methoden der Beispielklasse C zu gelangen. static void showMethods(Object c) { Class clazz = c.getClass(); Method[] methods = clazz.getMethods(); for (Method method: methods) { String methodName = method.getName(); System.out.println("Name: " + methodName); String returnType = method.getReturnType().getName(); System.out.println("Return Type: " + returnType); Class[] parameterTypes = method.getParameterTypes(); System.out.print("Parameter Types:"); for (Class parameterType: parameterTypes) { String parameterString = parameterType.getName(); System.out.print(" " + parameterString); } System.out.println(); } } Die Ausführung der Methode resultiert in folgender gekürzter Ausgabe: Name: getIntValue Return Type: int Parameter Types: int java.lang.String Name: getIntValue Return Type: int Parameter Types: Name: printString Return Type: void Parameter Types: […] /** weiter Auflistung */ Reflection bietet die Möglichkeit, die auszuführenden Methoden eines Objektes erst zur Laufzeit zu spezifizieren. Diese Ausführung wird ermöglicht durch die Nutzung der Funktionalität des bereits erwähnten Metaobjekts Method. Durch Überladen sind die Methoden einer Klasse, im Gegensatz zu deren Feldern, nicht eineindeutig mithilfe der Methodennamen ermittelbar. Um eine Methode eindeutig zu bestimmen bzw. auszuführen, wird daher die Kenntnis der Methodensignatur vorausgesetzt. 9 Kapitel 2: Java Reflection - Grundlagen Es sei bemerkt, dass primitive Datentypen, wie int oder boolean ebenso über ein ClassObjekt verfügen, welches bspw. durch int.class oder boolean.class abgebildet wird. [FoFo04, S. 13] Diese Class-Objekte werden meist benötigt, um Methoden zu identifizieren, deren Ausführung eine Übergabe primitiver Datentypen erfordern. Die auszuführende Methode wird spezifiziert durch die Übergabe eines Objekt-Arrays, der Class-Objekte enthält. Die übergebenen Class-Objekte wiederum repräsentieren in ihrer Art und Reihenfolge die Signatur der zu findenden Methode. Unter der Verwendung der Metaobjekt-Methode invoke(Object obj, Object… args) ist es letztendlich möglich, die gewünschte Methode auszuführen. Hierbei finden jedoch die Class-Objekte der primitiven Datentypen keine Verwendung. Vielmehr kommen die korrespondierenden Wrapper-Objekte stellvertretend für die primitiven Datentypen zum Einsatz. (z. B. int Æ java.lang.Integer) Die Methode invoke() konvertiert vor der eigentlichen Ausführung die übergebenen Wrapper-Objekte in primitive Datentypen. Grundsätzlich resultiert auch das Ergebnis einer in dieser Art ausgeführten Methode mit Rückgabewert in Form eines Objektes. [FoFo04, S. 16 f] Im folgenden Beispielcode wird deutlich, dass die Methode getIntValue(int i, String s) unter Verwendung von int.class identifiziert wird, aber mit einem zu übergebenen Integer-Objekt aufgerufen wird. /** Relevanter Teil der Klasse C */ public int getIntValue(int i, String s) {/** Quellcode */} /** Relevanter Teil der Applikation */ static void invokeMethods(Object c) { try { Class clazz = c.getClass(); Method method = clazz.getMethod("getIntValue", new Class[] { int.class, String.class }); Integer myInt = new Integer(5); String myString = "fuenf"; Object returnedObject = method.invoke(c, new Object[] { myInt, myString }); System.out.println("Ergebnis: " + returnedObject + "\n Typ:" + returnedObject.getClass().getName()); } catch (Exception e) {/** Exception Handling */} } Es resultiert folgende Ausgabe: getIntValue erfolgreich: 5 fuenf Ergebnis: 5 Typ:java.lang.Integer 10 Kapitel 2: Java Reflection - Grundlagen Um den Ablauf bzw. die beteiligten Objekte bei der Nutzung der invoke-Methode zu skizzieren, sei auf Abbildung 2 verwiesen. Abbildung 2: Sequenzdiagramm: Nutzung der invoke-Methode Objekte zur Laufzeit instanziieren Die Anforderungen denen Applikationen gerecht werden müssen, ändern sich im Laufe ihrer Verwendung. Dies erfordert die Implementierung von Mechanismen, welche neuen Programmcode in einer bereits kompilierten Applikation berücksichtigen. Um den geforderten Grad an Flexibilität zu bieten, erlaubt eine reflexive Programmierweise, neue Klassen zur Laufzeit einzubinden und deren bereitgestellte Funktionalität zu nutzen. [FoFo04, S. 50] Speziell die Forderung eine Applikation in Verbindung mit unterschiedlichen Komponenten zu betreiben, setzt ein hohes Maß an Flexibilität voraus. In diesem Fall sind zum Zeitpunkt des Kompilierens keine Informationen über Ablageort und Funktionalität der Erweiterungen bekannt. Um den Sachverhalt der Instanziierung von Objekten zur Laufzeit exemplarisch zu beschreiben, werden im Rahmen der Ausführungen explizit Entwurfsmuster, deren Einsatz für die Problemstellung zweckmäßig erscheint, genannt und kurz erläutert. Ist es bspw. notwendig, der Applikation den Zugriff auf unterschiedliche Datenbanken zu ermöglichen, muss eine Schnittstelle für jede Datenbank geschaffen werden, die sowohl heutigen als auch zukünftigen Anforderungen genügt. Natürlich wäre diesbezüglich ein Einsatz der JDBC-Technologie [JDBC05] wesentlich geeigneter. Um jedoch die Problemstellung in 11 Kapitel 2: Java Reflection - Grundlagen geeigneter Weise zu skizzieren, wurde dieser Sachverhalt gewählt. In diesem Kontext eignet sich der Einsatz des Strukturmusters Fassade [GHJ+97, S. 189] kombiniert mit dem Erzeugungsmuster Fabrik [GHJ+97, S. 115]. Hierzu wird das Entwurfsmuster Fassade verwendet, um die Funktionalität des Zugriffs auf die Datenbank zu kapseln. Abbildung 3: Entwurfsmuster Fabrik und Fassade Der Applikation wird auf diese Weise ein standardisierter Zugriff ermöglicht, wobei die Fabrik kontextabhängig eine Fassade generiert, die den Anforderungen der Datenbank entspricht. Dazu wird eine Instanz der Fassade beim ersten Zugriff dynamisch geladen, d. h. die Fassaden-Klasse liegt in kompilierter Form an entsprechender Stelle im Paket und wird von der JVM zur Laufzeit geladen. [FoFo04, S. 53] Ist bspw. der voll qualifizierte Name der Klasse in einer Konfigurationsdatei hinterlegt, wird die Datei beim Programmstart ausgelesen und geladen. Der Classloader gibt unter Verwendung der Methode forName(String className) mithilfe des voll qualifizierten Namen das Class-Objekt der gewünschten Fassade zurück. [FoFo04, S. 55] In diesem Fall wird zu Beginn kein Class-Objekt der Fassade existieren, sodass der Classloader entsprechend dem Verzeichnis- und Paketpfad eine .class Datei sucht und diese nach erfolgreicher Suche lädt. Besitzt die Fassadenklasse einen einfachen Konstruktor der Form public DBFassade1(), ist es durch den Aufruf von fassadeCls.getInstance() möglich, eine Instanz der Klasse zu erzeugen. Weist der Konstruktor jedoch eine höhere Komplexität auf, da bspw. ein String bei der Instanziierung erforderlich ist, findet das Metaobjekt java.lang.Constructor Verwendung. [FoFo04, S. 57 f] Das Constructor-Objekt ist dem bereits erwähnten Method-Objekt ähnlich, bietet aber zusätzlich Informationen zu den jeweiligen Konstruktoren einer Klasse an. Bevor jedoch eine neue Instanz der Fassade erzeugt werden kann, muss der passende Konstruktor vorliegen. Dies erfordert ein Class-Objekt vom im Konstruktor übergebenen Objekt. Der passende Konstruktor der 12 Kapitel 2: Java Reflection - Grundlagen die gewünschte Signatur zur Instanziierung besitzt, wird durch den Aufruf getConstructor(new Class[] {Class c}) ermittelt. Die Erzeugung einer Instanz der Klasse erfolgt durch den Methodenaufruf newInstance(new Object[] {initargs}). Der voll qualifizierte Name der Fassade (nameFassade), die Art des zu übergebenden Objekts (paramter) und der Übergabeparameter für den Konstruktor (contentConstructor) können bspw. durch Verwendung von java.util.Properties aus einer Konfigurationsdatei gelesen. public class Fabrik { /** Quellcode */ private AbstractFassade createDB(String nameFassade) { if (fassade == null) { try { Class fassadeCls = Class.forName(nameFassade); Class parameterCls = Class.forName(parameter); Constructor fassadeCons = fassadeCls.getConstructor( new Class[] {parameterCls}); fassade = (AbstractFassade) fassadeCons.newInstance( new Object[] {content}); } catch (Exception e) {/** Exception Handling */} } return fassade; } } /** beispielhafter Auszug aus der Konfigurationsdatei */ Fassade.Name=wwu.de.inf.SemArbBsp.DynLaden.DBFassade1 Konstruktor.Parameter=java.lang.String Konstruktor.Inhalt=Dies ist ein Test! Durch diese Vorgehensweise ist es möglich, Fassadenobjekte zur Laufzeit oder beim Programmstart einzubinden, die zum Erstellungszeitpunkt der Applikation noch nicht existierten. Es ist lediglich notwendig, die entsprechende .java Datei der Fassade zu kompilieren und als .class Datei an die spezifizierte Stelle des Verzeichnispfads zu hinterlegen und die erforderlichen Einstellungen in der Konfigurationsdatei zu pflegen. Im Rahmen der vorangegangen Ausführungen wurde ein Überblick zu den vielseitigen Einsatzmöglichkeiten von Java Reflection gegeben. Diese Flexibilität zur Laufzeit bietet jedoch auch Angriffspunkte für Intentionen, die weniger schadfrei sind. Dementsprechend folgt an dieser Stelle eine Beleuchtung der Sicherheitsaspekte im Zusammenhang mit der Nutzung von Reflection. 13 Kapitel 2: Java Reflection - Grundlagen 2.3 Sicherheitsaspekte bei Java Reflection Wie in den vorangegangen Kapiteln bereits deutlich geworden ist, werden mit Java Reflection Methoden zur Verfügung gestellt, mit denen sich Informationen und Inhalte von Feldern anzeigen oder Methoden ausführen lassen, die über die Modifier protected oder private verfügen. Um zu verhindern, dass bewährte Sicherheitskonzepte, wie die Sichtbarkeit [So98, S. 18 f] an Bedeutung verlieren, müssen Schutzmechanismen geschaffen werden, die den Einsatz von Reflection überwachen und reglementieren. Die erste Restriktion im Umgang mit Reflection bildet die Einschränkung der Objekte, die berechtigt sind ein Metaobjekt instanziieren. Um Veränderungen an Feldinhalten, Instanziierungen von Objekten oder Methodenausführungen vorzunehmen, bedarf es der Metaobjekte Field, Constructor oder Method, die jedoch nur von Class-Objekten erzeugt werden können. [JCR05] Dementsprechend stellen Basisobjekte keine Funktionalität bereit, die eine direkte Erzeugung von Metaobjekten, abgesehen von Class-Objekten, gestattet. Ein weiteres Element zum Schutz der Applikation und der Laufzeitumgebung vor unerwünschten Modifikationen, ist die Verwendung des Java-eigenen SecurityManagers (java.lang.SecurityManager), der beim Start der Applikation explizit geladen werden muss. Um die einzelnen Stufen der Sicherheit zu erläutern dient als Beispielcode wiederum ein Auszug der Klasse C sowie der dazu relevante Bestandteil einer Beispielapplikation. Dies ermöglicht die entsprechend vorgenommene Konfiguration des Security-Manager und die damit verbundenen Konsequenzen zu erläutern. /** Auszug aus der Klasse C.java */ private String stringValue; private int getInt(int i, String s) {/** Quellcode */} /** Auszug aus der Applikation.java */ public static final boolean ACCESS = true; classA = Class.forName("C"); /** Ein Field-Objekt des Feldes "stringValue" bekommen */ Field field = classA.getField("stringValue"); Field declaredField = classA.getDeclaredField("stringValue"); /** Ein Method-Objekt der Methode "getInt" bekommen */ Method method = classA.getMethod("getInt", new Class[] {int.class, String.class}); Method declaredMethod = classA.getDeclaredMethod("getInt", new Class[] {int.class, String.class}); 14 Kapitel 2: Java Reflection - Grundlagen if (!Modifier.isPublic(declaredField.getModifiers()) || (!Modifier.isPublic(declaredMethod.getModifiers()))) { declaredField.setAccessible(ACCESS); declaredMethod.setAccessible(ACCESS); } Object obj = clazzA.newInstance(); System.out.println(declaredField.getInt(obj)); declaredMethod.invoke(obj, new Object[] {new Integer(5), new String("Test")}); Die Ausführung des Programmteils führt bei getField() und bei getMethod() jeweils zu einer java.lang.NoSuchFieldException oder java.lang.NoSuchMethodException, da das gesuchte Feld und die gesuchte Methode jeweils mit der Sichtbarkeitsstufe private deklariert wurden. Aus dem Aufruf getDeclaredField() sowie getDeclaredMethod() resultieren jedoch unabhängig vom Modifier ein Field- oder ein Method-Objekt [FoFo04, S. 11 und S. 32], welche in bereits vorgestellter Weise verwendet werden können. Die Standardkonfiguration des Java-eigenen Security-Manager verbietet grundsätzlich alle weiteren Reflectionaktivitäten, die bspw. über ein Anzeigen der deklarierten Felderund Methodennamen (declaredField.getInt(obj)) hinaus gehen. auszulesen Sowohl als auch der die Versuch Methode das zu Feld starten (declaredmethod.invoke([…]) resultiert in einer java.lang.IllegalAccessException. Verhindert wird diese Nutzung durch das auf false gesetzte Accessible-Flag, da sowohl Method als auch Field die übergeordnete Klasse java.lang.reflect.AccessibleObject besitzen. AccessibleObject implementiert das Accessible-Flag, welches im Rahmen der Zugriffskontrolle vom Security-Manager verwendet wird. Ein Aktivieren (=true) des Accessible-Flag durch setAccessible(boolean b) ermöglicht einen unbeschränkten Zugriff auf das jeweilige Basisobjekt. [FoFo04, S. 38 f] Dieses Setzen des Flags wird jedoch vom Security-Manager verhindert, sodass der Versuch mit einer java.security.AccessControlException endet. Wird die Konfigurationsdatei (hier java.property) des Security-Managers um den folgenden Eintrag ergänzt, kann das Accessible-Flag aktiviert werden. /** Auszug aus der java.property */ grant { permission java.lang.reflect.ReflectPermission "suppressAccessChecks"; }; 15 Kapitel 2: Java Reflection - Grundlagen Die Änderung der Konfiguration erfordert einen Neustart der Applikation oder zumindest des Security-Managers, wobei letzteres im Rahmen der Standardsicherheitskonfiguration nicht möglich sein sollte. Der entsprechend dem oben aufgeführten Auszug der java.property konfigurierte Security-Manager erlaubt das Setzen des Accessible-Flags, welches wiederum die Grundlage für ein Ausführen der Methode und Auslesen des Feldes darstellt. Darüber hinaus ist eine detaillierte Konfiguration der ReflectPermission für einzelne Pakete oder Klassen möglich. Zusammenfassend lässt sich sagen, dass mehrere Sicherheitsebenen existieren, die eine schadhafte Verwendung von Reflection verhindern. Durch die Definition von Sichtbarkeiten (public, protected, private) der Methoden und Felder, werden diese vor unkontrollierten Zugriff geschützt. Auf dieser Ebene ist es jedoch möglich, Namen deklarierter Felder und Methoden zu erfahren. Eine weiterer Schutz vor ungewollter Nutzung und Ausführung der Programmteile wird durch das AccessibleObject mit dem Accessible-Flag geboten. Dieses Flag wird bei reflexiven Zugriffsversuchen durch den Sicherheitsmanager geprüft und verhindert ggf. den Zugriff. Darüber hinaus kann der Security-Manager den eigenen Bedürfnissen entsprechend konfiguriert werden, sodass dennoch eine reflexive Programmierweise zur Steigerung der Applikationsflexibilität ermöglicht wird. [JCR05] 16 Kapitel 3: Beispielhafte Ausführungen zum Thema Reflection 3 Beispielhafte Ausführungen zum Thema Reflection 3.1 Reflection in der Praxis – JUnit Das Testen von Applikationen stellt einen integralen Bestandteil heutiger Programmierarbeit dar. Aus diesem Grund wurde mit JUnit [JU05] ein Test-Framework geschaffen, um Testfälle zu formulieren. Diese vergleichen die Ergebnisse der bereitgestellten Funktionalität einer Applikation in automatisierter Weise mit den erwarteten Ergebnissen und identifizieren Methoden, deren Resultate von den Erwartungen abweichen. Für Vertreter der agilen Softwareentwicklung ist die Bedeutung von JUnit unbestritten, da die gestellten Anforderungen an die Software durch Testfälle prüfbar sind und auch nach Änderungen bereits implementierte Funktionalität garantieren. Für die Erstellung und Ausführung von erstellten Testfällen, ist ein gewisser Grad an Flexibilität notwendig, der durch den Einsatz reflexiver Programmierung bereit gestellt werden kann. Aus diesem Grund werden exemplarisch zwei Stellen im JUnitFramework aufgezeigt, an denen der Einsatz von Reflection deutlich wird. Testfälle dienen im Zusammenhang mit einigen agilen Vertretern zur Softwareentwicklung (bspw. eXtreme Programming) als Rahmen zur Implementierung der Funktionalität. Dementsprechend ist es notwendig, die Definition und Ausführung von Tests auf möglichst bequeme Art und Weise zur Verfügung zu stellen. Um die spezifizierten Testfälle automatisch auszuführen, sind bei der Spezifikation der Testfälle bestimmte Implementierungsbedingungen zu berücksichtigen. So muss bspw. die Klasse, welche die Testmethoden enthält, von junit.framework.TestCase erben. Darüber hinaus erfordern die Testmethoden den Präfix „test“, um identifiziert werden zu können. Zu Beginn der Testausführung werden alle Methoden, die den Präfix „test“ besitzen auf ihre Eignung geprüft und in einer Testsuite zusammengetragen. /** Auszug aus TestSuite.java */ while (Test.class.isAssignableFrom(superClass)) { Method[] methods= superClass.getDeclaredMethods(); for (int i= 0; i < methods.length; i++) { addTestMethod(methods[i], names, theClass); } superClass= superClass.getSuperclass(); } 17 Kapitel 3: Beispielhafte Ausführungen zum Thema Reflection Nachdem alle Testmethoden in einer Testsuite gesammelt wurden, ist ein weiterer Bestandteil des Frameworks die Möglichkeit, formulierte Testfälle automatisch zu laden und auszuführen. /** Auszug aus TestCase.java */ protected void runTest() throws Throwable { assertNotNull(fName); Method runMethod= null; try { runMethod= getClass().getMethod(fName, null); } catch (NoSuchMethodException e) { fail("Method \""+fName+"\" not found"); } if (!Modifier.isPublic(runMethod.getModifiers())) { fail("Method \""+fName+"\" should be public"); } try { runMethod.invoke(this, new Class[0]); } catch (Exception e) {/** Exception Handling */} } Es wird ersichtlich, dass JUnit die Methode getDeclaredMethods() nutzt, um alle Methoden des spezifizierten Testfalls zu laden. Im Anschluss daran werden die Methoden auf das Vorhandensein des Präfixes „test“ und hinsichtlich der notwendigen öffentlichen Sichtbarkeit (public) geprüft. Ziel ist es alle deklarierten Testfälle und Testmethoden in einer TestSuite zusammenzufassen und auszuführen. (Vgl. hierzu Anhang C) Durch invoke() werden schließlich alle Testmethoden aufgerufen und das Ergebnis des Tests präsentiert. 3.2 Reflection in der Anwendung – Ein EventDispatcher Die grafische Ereignisprogrammierung in Java unter Verwendung der durch java.awt.event.* bereitgestellten Klassen gestaltet sich relativ aufwendig. (Vgl. Anh. D) Die Listener, die für das Event-Handling installiert werden, haben i. d. R. nur eine Adapter-Funktionalität – sie delegieren die eigentliche Arbeit an ein anderes Objekt, welches die eigentliche Methode für das Event-Handling beinhaltet. Bevor diese Delegation stattfinden kann, müssen jedoch alle Ereignisquellen bei den entsprechenden Ereignissenken registriert werden. Da beim Abschluss der Programmierarbeiten alle Ereignisbehandlungen bekannt sind, ist es denkbar, eine zentrale Instanz einzurichten, welche die Registrierung zum Programmstart automatisiert vornimmt. Diese zentrale Instanz wird im Weiteren als EventDispatcherCreator bezeichnet. 18 Kapitel 3: Beispielhafte Ausführungen zum Thema Reflection Weiterhin ist es denkbar, die eigentliche Ereignisbehandlung austauschbar zu gestalten, sodass ein Austausch von class-Dateien eine andere Reaktion der Applikation hervorruft. Abbildung 4: Klassendiagramm des EventDispatcher Im folgenden soll unter Verwendung von Reflection eine Lösung skizziert werden, die eine Registrierung von Adapterinstanzen bei den Eventquellen automatisiert vornimmt. Um diesen Automatismus zu garantieren, sind zunächst formale Kriterien zur Benennung der Methoden für die Ereignisbehandlung zu treffen. Aus dem Methodennamen muss die auslösende Komponente und die Ereignisart ermittelt werden können. Wird bspw. das Drücken eines Knopfes auf der Oberfläche als Ereignis gewünscht, bezeichnet sich die Methode zur Ereignisbehandlung als eine Zusammensetzung aus dem Namen des Knopfes (bGreen) und dem Ereignistyp (actionPerformed). /** Auszug aus Application.java */ private Button bGreen = new Button("green"); /** Quellcode */ private void bGreen_actionPerformed(ActionEvent e) { this.setBackground(Color.green); } Beim Start des Programms werden durch die Implementierung des EventDispatcherCreator alle Felder und Methoden der Klasse ausgelesen und geprüft, inwieweit eine Ereignisregistrierung möglich und notwendig ist. Durch die Methode 19 Kapitel 3: Beispielhafte Ausführungen zum Thema Reflection Component.class.isAssignableFrom(Class cls) werden die zu berücksichtigenden Elemente eingeschränkt auf Objekte, die von java.awt.Component erben. [JADC05] /** Auszug aus EventDispatcherCreator.java */ public EventDispatcherCreator(Object target) { this.target = target; this.methods = target.getClass().getDeclaredMethods(); this.fields = target.getClass().getDeclaredFields(); for (Field field: fields) { if (!Component.class.isAssignableFrom(field.getType())) continue; for (Method method: methods) { try { tryToAddListener(field, method); /** Quellcode */ } } } } In der Methode tryToAddListener(Field f, Method m) wird die Registrierung des EventListener bei der entsprechenden Komponente vorgenommen, um Ereignisquelle und das bei Ausführung gewünschte Event-Handling zu verknüpfen. /** Auszug aus EventDispatcherCreator.java */ Method addMethod = obj.getClass().getMethod( md.getAddMethodName(), new Class[] { md.getIface() }); /** EventMetaData md – Objekt, welches Metadaten über die Actionklasse beinhaltet */ if (addMethod == null) return; Class cls = Class.forName("reflection5." + md.getEventName() + "EventDispatcher"); Constructor constr = cls.getConstructor( constructorParamTypes); Object dispatcher = constr.newInstance(new Object[] { this.target, field.getName() }); addMethod.invoke(obj, new Object[] { dispatcher }); Im Rahmen der Ereignisbehandlung werden die in Java auftretenden Ereignisse unterschiedlichen Ereignisklassen zugewiesen. So setzt bspw. die Verwendung des Interfaces java.awt.event.ActionListener die Implementierung von Methoden , die u. a. nach dem Drücken eines Knopfes ausgeführt werden. Damit letztendlich die Methode zur Ereignisbehandlung ausgeführt wird, ist in der Klasse EventDispatcher die Methode callHandler(String methodName, ActionEvent e) implementiert. 20 Kapitel 3: Beispielhafte Ausführungen zum Thema Reflection /** Auszug aus ActionEventDispatcher.java */ public void actionPerformed(ActionEvent e) { this.callHandler("actionPerformed", e); } Der Methode callHandler() wird hierfür der auszuführende Methodennamen zur Ereignisbehandlung und ein Objekt des Typs java.awt.event.ActionEvent übergeben. Die Ausführung der Methode geschieht durch die Verwendung der eingangs vorgestellten Methode invoke() des Metaobjekts Class. /** Auszug aus EventDispatcher.java */ protected void callHandler(String methodName, EventObject e) { try { methodName = this.name + "_" + methodName; Method method = this.target.getClass().getDeclaredMethod( methodName, new Class[] {this.eventClass }); method.setAccessible(true); method.invoke(this.target, new Object[] { e }); } catch (Exception e) {/** Exception Handling */} } myFrame bGreen actionEventDispatcher eventDispatcher actionPerformed(ActionEvent e) callHandler(String m, ActionEvent e) bGreen_actionPerformed(ActionEvent e) Abbildung 5: Sequenzdiagramm für den EventDispatcher Das vorliegende Sequenzdiagramm skizziert den Ablauf der Ereignisbehandlung. Der zweite Teil der Ausführungen zur Verwendung reflexiver Programmierung skizziert die Möglichkeit die Ereignisbehandlung austauschbar zu gestalten. Hierbei sind unterschiedliche Herangehensweisen denkbar. Wird davon ausgegangen, dass die hier EventlClass genannte Klasse zur Ereignisbehandlung immer den gleichen Aufbau und Namen besitzt, kann EventClass.class verwendet werden. Ist der Name der EventClass zum Zeitpunkt der Kompilierung jedoch noch nicht bekannt, hilft Class.forName(String s) und das vorhergehende Auslesen einer Konfigurationsdatei, um den Klassennamen und Pfad erst zur Laufzeit zu spezifizieren. Ebenso kann die 21 Kapitel 3: Beispielhafte Ausführungen zum Thema Reflection Festlegung der auszuführenden Methode (eventMethod) der EventClass auf diesem Weg erfolgen. Im folgenden Beispiel wird die Methode eventMethod der EventClass verwendet, welche eine Referenz auf ein Fenster vom Typ java.awt.Frame erfordert. Diese Referenz kann zu Änderungen des Erscheinungsbildes des Fensters verwendet werden und nach der Ereignisauslösung bspw. in einer Änderung der Hintergrundfarbe resultieren. private void bYellow_actionPerformed(ActionEvent e) { try { Class eventClass = null; Object eventObject = null; Method eventMethod = null; eventClass = Class.forName("EventClass"); //eventClass = EventClass.class; eventObject = eventClass.newInstance(); eventMethod = eventClass.getDeclaredMethod("eventMethod", new Class[] {Frame.class}); eventMethod.setAccessible(true); eventMethod.invoke(eventObject, new Object[] {this}); } catch (Exception e) {/** Exception Handling */} } Dieses Vorgehen erlaubt ein Austausch oder Hinzufügen von class-Dateien zur Laufzeit, die zu einer geänderten Ereignisbehandlung führen. 22 Kapitel 4: Java Reflection – Flexibilität für die Applikation 4 Java Reflection – Flexibilität für die Applikation Durch den Einsatz von Java Reflection ist es möglich, Struktur- und Statusinformationen von Applikationen zur Laufzeit zu erfahren, die dazu verwendet werden können in flexibler Weise funktionale Ergänzungen und Änderungen der Applikation vorzunehmen. In der vorliegenden Arbeit wurden die Grundlagen aufgezeigt, die eine Verwendung von Reflection auszeichnet. Bedingt durch die Flexibilität, die Reflection im Zusammenhang mit der Anwendung bietet, wurden Sicherheitsaspekte diskutiert, inwieweit eine schadhafte Ausführung und Nutzung der Applikation möglich ist. Es wurde festgestellt, dass die Regelungen zur Sichtbarkeit von Methoden und Feldern einzelner Klassen durch die Verwendung des Java-eigenen Security-Managers ergänzt werden, der jegliche Methodenaufrufe prüft und nicht autorisierte Aktionen verhindert. Im zweiten Teil der Ausarbeitung wurde zunächst ein Anwendungsbereich des Java Reflection skizziert, der sich speziell im Bereich der agilen Softwareentwicklung enormer Beliebtheit erfreut. JUnit, ein Test-Framework, ermöglicht die Formulierung und automatisierte Ausführung von Testfällen, so dass die Funktionalität der Applikation sowohl im täglichen Programmieralltag als auch nach umfangreichen Software-Restrukturierungsmaßnahmen garantiert werden kann. Der zweite skizzierte Anwendungsbereich bezog sich auf eine automatisierte Registrierung von Ereignisbehandlern bei entsprechenden (grafischen) Komponenten in Java. Hierzu wurde eine Klasse formuliert, die zum einen die entstehende Registrierungsarbeit der Ereignisbehandler übernimmt und die Ausführung der konkreten Ereignisbehandlung delegiert. Des Weiteren wurde skizziert, inwieweit Reflection verwendet werden kann, um den Austausch von Klassen der Ereignisbehandlung zur Laufzeit zu realisieren. Es ist deutlich geworden, dass der bewusste Einsatz von Java Reflection zu einer nachhaltigen Steigerung der Applikationsflexibilität führt, der heutzutage notwendig ist, um Anwendungen dynamisch an die sich ändernden Anforderungen anzupassen, ohne jedoch sicherheitskritische Angriffspunkte innerhalb der Applikation zu bieten. 23 Anhang B: Titel von Anhang 2 A. Zeitliche Charakterisierung des Reflection Abbildung 6: Erstellen der Meta-Architektur zur Kompilierzeit Abbildung 7: Erstellen der Meta-Architektur beim Laden des Programms Abbildung 8: Erstellen der Meta-Architektur zur Laufzeit 24 Anhang B: Titel von Anhang 2 B. Beispielklasse C public class C { public int intValue; public String stringValue; /** Konstruktor der Klasse C */ public C() { this.intValue = 4711; this.stringValue = "Hallo"; } public void printString() { System.out.println("printString erfolgreich: " + this.intValue + " "+ this.stringValue); } public int getIntValue(int i, String s) { System.out.println("getIntValue erfolgreich: " + i + " " + s); this.intValue = i; this.stringValue = s; return this.intValue; } public void setStringValue(String s) { this.stringValue = s; } public void setIntValue(int i) { this.intValue = i; } public String getStringValue() { return this.stringValue; } public int getIntValue() { return this.intValue; } } 25 Anhang B: Titel von Anhang 2 C. JUnit-Methode isTestMethod(Method m) /** Auszug aus junit.framework.TestSuite.java */ private boolean isTestMethod(Method m) { String name = m.getName(); Class[] parameters = m.getParameterTypes(); Class returnType = m.getReturnType(); return parameters.length == 0 && name.startsWith("test") && returnType.equals(Void.TYPE); } 26 Anhang B: Titel von Anhang 2 D. Ereignisbehandlung mit und ohne EventDispatcher Ereignisbehandlung ohne Verwendung des EventDispatcher class MyFrame extends Frame { private Button bSave = new Button ("Save"); /** Quellcode */ public MyFrame () { this.add (this.bSave); this.bSave.addActionListener (new ActionListener () { public void actionPerformed (ActionEvent e) { bSave_actionPerformed (e);} }); /** Quellcode */ } private void bSave_actionPerformed (ActionEvent e) { /** Event-Handling */ } } Ereignisbehandlung mit Verwendung des EventDispatcher class MyFrame extends Frame { private Button bSave = new Button ("Save"); /** Quellcode */ public MyFrame () { this.add (bSave); new EventDispatcherCreator (this); /** Quellcode */ } private void bSave_actionPerformed (ActionEvent e) { /** Event-Handling */ } } 27 Literaturverzeichnis [FoFo04] Ira R. Forman, Nate Forman: Java Reflection in Action, Manning Publications, 2004. [GHJ+97] Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides: Entwurfsmuster, Elemente wieder verwendbarer objektorientierter Software, Addison-Wesley-Longman, 1997. [JADC05] Java J2SE API Documentation - Component, http://java.sun.com/j2se/1.5.0/docs/api/index.html, Zugriffsdatum: 2005-11-13. [JADCl05] Java J2SE API Documentation – Class, http://java.sun.com/j2se/1.5.0/docs/api/java/lang/Class.html, Zugriffsdatum: 2005-11-13. [JCR05] Java Core Reflection - Security Model, http://java.sun.com/j2se/1.4.2/docs/guide/reflection/spec/javareflection.doc.html, Zugriffsdatum: 2005-11-27. [JD05] JDBC technology, http://java.sun.com/products/jdbc/, Zugriffsdatum: 2005-12-01. [JU05] JUnit.org, http://www.junit.org/index.htm, Zugriffsdatum: 2005-11-08. [Ma94] Axel Mahler: Variants: Keeping things together and telling them apart. In Configuration Management, W. F. Tichy, Ed., Vol. 2, Trends in Software, Wiley, 73–98, 1994. [MiGo97] Michael Golm: Design and Implementation of a Meta Architecture for Java, http://www4.informatik.uni-erlangen.de/Projects/PM/Java/DA.ps.gz, Zugriffsdatum: 2005-11-03. [SO98] Scott Oaks: Java Security, O’Reilly & Associates, Inc., 1998.