Sehen Sie sich die Java Reflection-API genauer an

In "Java In-Depth" vom letzten Monat habe ich über Introspektion und Möglichkeiten gesprochen, wie eine Java-Klasse mit Zugriff auf Rohklassendaten "in" eine Klasse schauen und herausfinden kann, wie die Klasse aufgebaut ist. Außerdem habe ich gezeigt, dass diese Klassen durch Hinzufügen eines Klassenladeprogramms in die laufende Umgebung geladen und ausgeführt werden können. Dieses Beispiel ist eine Form der statischen Selbstbeobachtung. Diesen Monat werde ich einen Blick auf die Java Reflection-API werfen, mit der Java-Klassen dynamische Introspektion durchführen können: die Möglichkeit, in bereits geladene Klassen zu schauen.

Der Nutzen der Selbstbeobachtung

Eine der Stärken von Java besteht darin, dass es mit der Annahme entworfen wurde, dass sich die Umgebung, in der es ausgeführt wurde, dynamisch ändern würde. Klassen werden dynamisch geladen, die Bindung wird dynamisch durchgeführt und Objektinstanzen werden dynamisch im laufenden Betrieb erstellt, wenn sie benötigt werden. Was historisch nicht sehr dynamisch war, ist die Fähigkeit, "anonyme" Klassen zu manipulieren. In diesem Zusammenhang ist eine anonyme Klasse eine Klasse, die zur Laufzeit geladen oder einer Java-Klasse präsentiert wird und deren Typ dem Java-Programm zuvor unbekannt war.

Anonyme Klassen

Die Unterstützung anonymer Klassen ist schwer zu erklären und in einem Programm noch schwieriger zu gestalten. Die Herausforderung, eine anonyme Klasse zu unterstützen, kann folgendermaßen angegeben werden: "Schreiben Sie ein Programm, das, wenn es ein Java-Objekt erhält, dieses Objekt in seinen weiteren Betrieb einbeziehen kann." Die allgemeine Lösung ist ziemlich schwierig, aber durch Einschränkung des Problems können einige spezielle Lösungen erstellt werden. In der Version 1.0 von Java gibt es zwei Beispiele für spezielle Lösungen für diese Problemklasse: Java-Applets und die Befehlszeilenversion des Java-Interpreters.

Java-Applets sind Java-Klassen, die von einer laufenden virtuellen Java-Maschine im Kontext eines Webbrowsers geladen und aufgerufen werden. Diese Java-Klassen sind anonym, da die Laufzeit die erforderlichen Informationen zum Aufrufen jeder einzelnen Klasse nicht im Voraus kennt. Das Problem des Aufrufs einer bestimmten Klasse wird jedoch mithilfe der Java-Klasse gelöst java.applet.Applet.

Gängige Superklassen wie Appletund Java-Schnittstellen wie AppletContextlösen das Problem anonymer Klassen, indem sie einen zuvor vereinbarten Vertrag erstellen. Insbesondere gibt ein Lieferant einer Laufzeitumgebung an, dass er jedes Objekt verwenden kann, das einer angegebenen Schnittstelle entspricht, und der Konsument der Laufzeitumgebung verwendet diese angegebene Schnittstelle in jedem Objekt, das er zur Laufzeit bereitstellen möchte. Bei Applets existiert eine gut spezifizierte Schnittstelle in Form einer gemeinsamen Oberklasse.

Der Nachteil einer gemeinsamen Superklassenlösung, insbesondere wenn keine Mehrfachvererbung vorliegt, besteht darin, dass die für die Ausführung in der Umgebung erstellten Objekte nicht auch in einem anderen System verwendet werden können, es sei denn, dieses System implementiert den gesamten Vertrag. Bei den AppletSchnittstellen muss die Hosting-Umgebung implementiert werden AppletContext. Für die Applet-Lösung bedeutet dies, dass die Lösung nur funktioniert, wenn Sie Applets laden. Wenn Sie eine Instanz eines HashtableObjekts auf Ihrer Webseite platzieren und Ihren Browser darauf verweisen, kann es nicht geladen werden, da das Applet-System nicht außerhalb seines begrenzten Bereichs arbeiten kann.

Zusätzlich zum Applet-Beispiel hilft Introspection bei der Lösung eines Problems, das ich im letzten Monat erwähnt habe: herauszufinden, wie die Ausführung in einer Klasse gestartet werden kann, die die Befehlszeilenversion der Java Virtual Machine gerade geladen hat. In diesem Beispiel muss die virtuelle Maschine eine statische Methode in der geladenen Klasse aufrufen. Konventionell wird diese Methode benannt mainund verwendet ein einzelnes Argument - ein Array von StringObjekten.

Die Motivation für eine dynamischere Lösung

Die Herausforderung bei der vorhandenen Java 1.0-Architektur besteht darin, dass es Probleme gibt, die durch eine dynamischere Introspection-Umgebung gelöst werden könnten, z. B. ladbare UI-Komponenten, ladbare Gerätetreiber in einem Java-basierten Betriebssystem und dynamisch konfigurierbare Bearbeitungsumgebungen. Die "Killer-App" oder das Problem, durch das die Java Reflection-API erstellt wurde, war die Entwicklung eines Objektkomponentenmodells für Java. Dieses Modell ist jetzt als JavaBeans bekannt.

Komponenten der Benutzeroberfläche sind ein idealer Entwurfspunkt für ein Introspektionssystem, da sie zwei sehr unterschiedliche Verbraucher haben. Einerseits werden die Komponentenobjekte miteinander verbunden, um als Teil einer Anwendung eine Benutzeroberfläche zu bilden. Alternativ muss eine Schnittstelle für Tools vorhanden sein, die Benutzerkomponenten bearbeiten, ohne die Komponenten kennen zu müssen oder, was noch wichtiger ist, ohne Zugriff auf den Quellcode der Komponenten.

Die Java Reflection-API ist aus den Anforderungen der JavaBeans-API für Benutzeroberflächenkomponenten hervorgegangen.

Was ist Reflexion?

Grundsätzlich besteht die Reflection-API aus zwei Komponenten: Objekten, die die verschiedenen Teile einer Klassendatei darstellen, und einem Mittel zum sicheren Extrahieren dieser Objekte. Letzteres ist sehr wichtig, da Java viele Sicherheitsvorkehrungen bietet und es nicht sinnvoll wäre, eine Reihe von Klassen bereitzustellen, die diese Sicherheitsvorkehrungen ungültig machen.

Die erste Komponente der Reflection-API ist der Mechanismus zum Abrufen von Informationen zu einer Klasse. Dieser Mechanismus ist in die genannte Klasse integriert Class. Die spezielle Klasse Classist der universelle Typ für die Metainformationen, die Objekte innerhalb des Java-Systems beschreiben. Klassenlader im Java-System geben Objekte vom Typ zurück Class. Bisher waren die drei interessantesten Methoden in dieser Klasse:

  • forName, die eine Klasse mit einem bestimmten Namen mit dem aktuellen Klassenladeprogramm laden würde

  • getNameDies würde den Namen der Klasse als StringObjekt zurückgeben, was nützlich war, um Objektreferenzen anhand ihres Klassennamens zu identifizieren

  • newInstance, der den Nullkonstruktor für die Klasse aufruft (falls vorhanden) und Ihnen eine Objektinstanz dieser Objektklasse zurückgibt

Zu diesen drei nützlichen Methoden fügt die Reflection-API der Klasse einige zusätzliche Methoden hinzu Class. Diese sind wie folgt:

  • getConstructor, getConstructors,getDeclaredConstructor
  • getMethod, getMethods,getDeclaredMethods
  • getField, getFields,getDeclaredFields
  • getSuperclass
  • getInterfaces
  • getDeclaredClasses

Zusätzlich zu diesen Methoden wurden viele neue Klassen hinzugefügt, um die Objekte darzustellen, die diese Methoden zurückgeben würden. Die neuen Klassen meist sind Teil des java.lang.reflectPakets, aber einige der neuen Basistyp - Klassen ( Void, Byte, usw.) sind im java.langPaket. Die Entscheidung wurde getroffen, die neuen Klassen dort zu platzieren, wo sie sind, indem Klassen, die Metadaten repräsentierten, in das Reflexionspaket und Klassen, die Typen repräsentierten, in das Sprachpaket eingefügt wurden.

Daher stellt die Reflection-API eine Reihe von Änderungen an der Klasse dar Class, mit denen Sie Fragen zu den Interna der Klasse stellen können, sowie eine Reihe von Klassen, die die Antworten darstellen, die diese neuen Methoden Ihnen geben.

Wie verwende ich die Reflection-API?

Die Frage "Wie verwende ich die API?" ist vielleicht die interessantere Frage als "Was ist Reflexion?"

Die Reflection-API ist symmetrisch . Wenn Sie also ein ClassObjekt halten, können Sie nach dessen Interna fragen. Wenn Sie eines der Interna haben, können Sie es fragen, welche Klasse es deklariert hat. Auf diese Weise können Sie von Klasse zu Methode zu Parameter zu Klasse zu Methode usw. wechseln. Eine interessante Anwendung dieser Technologie besteht darin, die meisten Abhängigkeiten zwischen einer bestimmten Klasse und dem Rest des Systems herauszufinden.

Ein funktionierendes Beispiel

Auf einer praktischeren Ebene können Sie jedoch die Reflection-API verwenden, um eine Klasse auszugeben, ähnlich wie meine dumpclassKlasse in der Spalte des letzten Monats.

To demonstrate the Reflection API, I wrote a class called ReflectClass that would take a class known to the Java run time (meaning it is in your class path somewhere) and, through the Reflection API, dump out its structure to the terminal window. To experiment with this class, you will need to have a 1.1 version of the JDK available.

Note: Do not try to use a 1.0 run time as it gets all confused, usually resulting in an incompatible class change exception.

The class ReflectClass begins as follows:

import java.lang.reflect.*; import java.util.*; public class ReflectClass { 

As you can see above, the first thing the code does is import the Reflection API classes. Next, it jumps right into the main method, which starts out as shown below.

 public static void main(String args[]) { Constructor cn[]; Class cc[]; Method mm[]; Field ff[]; Class c = null; Class supClass; String x, y, s1, s2, s3; Hashtable classRef = new Hashtable(); if (args.length == 0) { System.out.println("Please specify a class name on the command line."); System.exit(1); } try { c = Class.forName(args[0]); } catch (ClassNotFoundException ee) { System.out.println("Couldn't find class '"+args[0]+"'"); System.exit(1); } 

The method main declares arrays of constructors, fields, and methods. If you recall, these are three of the four fundamental parts of the class file. The fourth part is the attributes, which the Reflection API unfortunately does not give you access to. After the arrays, I've done some command-line processing. If the user has typed a class name, the code attempts to load it using the forName method of class Class. The forName method takes Java class names, not file names, so to look inside the java.math.BigInteger class, you simply type "java ReflectClass java.math.BigInteger," rather than point out where the class file actually is stored.

Identifying the class's package

Assuming the class file is found, the code proceeds into Step 0, which is shown below.

 /* * Step 0: If our name contains dots we're in a package so put * that out first. */ x = c.getName(); y = x.substring(0, x.lastIndexOf(".")); if (y.length() > 0) { System.out.println("package "+y+";\n\r"); } 

In this step, the name of the class is retrieved using the getName method in class Class. This method returns the fully qualified name, and if the name contains dots, we can presume that the class was defined as part of a package. So Step 0 is to separate the package name part from the class name part, and print out the package name part on a line that starts with "package...."

Collecting class references from declarations and parameters

With the package statement taken care of, we proceed to Step 1, which is to collect all of the other class names that are referenced by this class. This collection process is shown in the code below. Remember that the three most common places where class names are referenced are as types for fields (instance variables), return types for methods, and as the types of the parameters passed to methods and constructors.

 ff = c.getDeclaredFields(); for (int i = 0; i < ff.length; i++) { x = tName(ff[i].getType().getName(), classRef); } 

In the above code, the array ff is initialized to be an array of Field objects. The loop collects the type name from each field and process it through the tName method. The tName method is a simple helper that returns the shorthand name for a type. So java.lang.String becomes String. And it notes in a hashtable which objects have been seen. At this stage, the code is more interested in collecting class references than in printing.

The next source of class references are the parameters supplied to constructors. The next piece of code, shown below, processes each declared constructor and collects the references from the parameter lists.

 cn = c.getDeclaredConstructors(); for (int i = 0; i  0) { for (int j = 0; j < cx.length; j++) { x = tName(cx[j].getName(), classRef); } } } 

As you can see, I've used the getParameterTypes method in the Constructor class to feed me all of the parameters that a particular constructor takes. These are then processed through the tName method.

An interesting thing to note here is the difference between the method getDeclaredConstructors and the method getConstructors. Both methods return an array of constructors, but the getConstructors method only returns those constructors that are accessible to your class. This is useful if you want to know if you actually can invoke the constructor you've found, but it isn't useful for this application because I want to print out all of the constructors in the class, public or not. The field and method reflectors also have similar versions, one for all members and one only for public members.

Der letzte Schritt, der unten gezeigt wird, besteht darin, die Referenzen aller Methoden zu sammeln. Dieser Code muss Referenzen sowohl vom Typ der Methode (ähnlich den obigen Feldern) als auch von den Parametern (ähnlich den obigen Konstruktoren) erhalten.

mm = c.getDeclaredMethods (); für (int i = 0; i 0) {für (int j = 0; j <cx.Länge; j ++) {x = tName (cx [j] .getName (), classRef); }}}

Im obigen Code gibt es zwei Aufrufe an tName- einen zum Erfassen des Rückgabetyps und einen zum Erfassen des Typs jedes Parameters.