Werfen Sie einen Blick in Java-Klassen

Willkommen zur diesmonatigen Ausgabe von "Java In Depth". Eine der frühesten Herausforderungen für Java war, ob es als fähige "System" -Sprache gelten kann oder nicht. Die Wurzel der Frage lag in den Sicherheitsfunktionen von Java, die verhindern, dass eine Java-Klasse andere Klassen kennt, die in der virtuellen Maschine daneben ausgeführt werden. Diese Fähigkeit, in die Klassen "hineinzuschauen", wird als Selbstbeobachtung bezeichnet . In der ersten öffentlichen Java-Version, bekannt als Alpha3, konnten die strengen Sprachregeln bezüglich der Sichtbarkeit der internen Komponenten einer Klasse durch die Verwendung der ObjectScopeKlasse umgangen werden . Während der Beta-Phase, als ObjectScopesie aus Sicherheitsgründen aus der Laufzeit entfernt wurde, erklärten viele Leute Java für ungeeignet für eine "ernsthafte" Entwicklung.

Warum ist Selbstbeobachtung notwendig, damit eine Sprache als "Systemsprache" betrachtet werden kann? Ein Teil der Antwort ist ziemlich banal: Um von "nichts" (dh einer nicht initialisierten VM) zu "etwas" (dh einer laufenden Java-Klasse) zu gelangen, muss ein Teil des Systems in der Lage sein, die zu prüfenden Klassen zu überprüfen laufen, um herauszufinden, was mit ihnen zu tun ist. Das kanonische Beispiel für dieses Problem ist einfach das folgende: "Wie beginnt ein Programm, das in einer Sprache geschrieben ist, die nicht in eine andere Sprachkomponente hineinschauen kann, die erste Sprachkomponente auszuführen, die der Ausgangspunkt für die Ausführung aller anderen Komponenten ist? ""

Es gibt zwei Möglichkeiten, mit Introspektion in Java umzugehen: die Überprüfung von Klassendateien und die neue Reflection-API, die Teil von Java 1.1.x ist. Ich werde beide Techniken behandeln, aber in dieser Spalte werde ich mich auf die erstklassige Dateiprüfung konzentrieren. In einer zukünftigen Spalte werde ich untersuchen, wie die Reflection-API dieses Problem löst. (Links zum vollständigen Quellcode für diese Spalte finden Sie im Abschnitt Ressourcen.)

Schau tief in meine Akten ...

In den Java-Versionen 1.0.x ist eine der größten Warzen in der Java-Laufzeit die Art und Weise, wie die ausführbare Java-Datei ein Programm startet. Worin besteht das Problem? Die Ausführung erfolgt von der Domäne des Host-Betriebssystems (Win 95, SunOS usw.) in die Domäne der virtuellen Java-Maschine. Durch Eingabe der Zeile " java MyClass arg1 arg2" wird eine Reihe von Ereignissen in Gang gesetzt, die vom Java-Interpreter vollständig fest codiert werden.

Als erstes Ereignis lädt die Betriebssystem-Befehlsshell den Java-Interpreter und übergibt ihm die Zeichenfolge "MyClass arg1 arg2" als Argument. Das nächste Ereignis tritt auf, wenn der Java-Interpreter versucht, eine Klasse zu finden, die MyClassin einem der im Klassenpfad angegebenen Verzeichnisse benannt ist. Wenn die Klasse gefunden wird, besteht das dritte Ereignis darin, eine Methode innerhalb der benannten Klasse zu finden main, deren Signatur die Modifikatoren "public" und "static" enthält und die ein Array von StringObjekten als Argument verwendet. Wenn diese Methode gefunden wird, wird ein Ur-Thread erstellt und die Methode aufgerufen. Der Java-Interpreter konvertiert dann "arg1 arg2" in ein Array von Zeichenfolgen. Sobald diese Methode aufgerufen wird, ist alles andere reines Java.

Dies ist alles gut und schön, außer dass die mainMethode statisch sein muss, da die Laufzeit sie nicht mit einer Java-Umgebung aufrufen kann, die noch nicht existiert. Außerdem muss die erste Methode benannt werden, mainda es keine Möglichkeit gibt, dem Interpreter den Namen der Methode in der Befehlszeile mitzuteilen. Selbst wenn Sie dem Interpreter den Namen der Methode mitgeteilt haben, gibt es keine allgemeine Möglichkeit, herauszufinden, ob es sich um die Klasse handelt, die Sie ursprünglich benannt haben. Da die mainMethode statisch ist, können Sie sie nicht in einer Schnittstelle deklarieren. Dies bedeutet, dass Sie eine Schnittstelle wie diese nicht angeben können:

öffentliche Schnittstelle Anwendung {public void main (String args []); }}

Wenn die obige Schnittstelle definiert und von Klassen implementiert wurde, können Sie zumindest den instanceofOperator in Java verwenden, um festzustellen, ob Sie eine Anwendung hatten oder nicht, und um festzustellen, ob sie zum Aufrufen über die Befehlszeile geeignet ist oder nicht. Die Quintessenz ist, dass Sie nicht (definieren Sie die Schnittstelle), es war nicht (in den Java-Interpreter integriert), und Sie können nicht (bestimmen, ob eine Klassendatei leicht eine Anwendung ist). Also, was kannst du machen?

Eigentlich können Sie einiges tun, wenn Sie wissen, wonach Sie suchen und wie Sie es verwenden müssen.

Klassendateien dekompilieren

Die Java-Klassendatei ist architekturneutral. Dies bedeutet, dass es sich um denselben Bitsatz handelt, unabhängig davon, ob er von einem Windows 95-Computer oder einem Sun Solaris-Computer geladen wird. Es ist auch sehr gut in dem Buch The Java Virtual Machine Specification von Lindholm und Yellin dokumentiert . Die Klassendateistruktur wurde teilweise so konzipiert, dass sie problemlos in den SPARC-Adressraum geladen werden kann. Grundsätzlich könnte die Klassendatei in den virtuellen Adressraum abgebildet werden, dann die relativen Zeiger innerhalb der Klasse fixiert werden und presto! Sie hatten sofort Klassenstruktur. Dies war auf den Intel-Architekturmaschinen weniger nützlich, aber das Erbe ließ das Klassendateiformat leicht verständlich und noch einfacher zu zerlegen.

Im Sommer 1994 arbeitete ich in der Java-Gruppe und baute ein sogenanntes Sicherheitsmodell mit dem geringsten Privileg für Java auf. Ich hatte gerade herausgefunden, dass ich wirklich in eine Java-Klasse schauen wollte, die Teile herausschneiden wollte, die von der aktuellen Berechtigungsstufe nicht zugelassen wurden, und dann das Ergebnis über einen benutzerdefinierten Klassenlader laden wollte. Zu diesem Zeitpunkt stellte ich fest, dass es in der Hauptlaufzeit keine Klassen gab, die über die Erstellung von Klassendateien Bescheid wussten. Es gab Versionen im Compiler-Klassenbaum (die Klassendateien aus dem kompilierten Code generieren mussten), aber ich war mehr daran interessiert, etwas zum Bearbeiten bereits vorhandener Klassendateien zu erstellen.

Ich begann mit dem Erstellen einer Java-Klasse, die eine Java-Klassendatei zerlegen konnte, die ihr in einem Eingabestream präsentiert wurde. Ich gab ihm den weniger als ursprünglichen Namen ClassFile. Der Beginn dieser Klasse ist unten dargestellt.

öffentliche Klasse ClassFile {int magic; kurze Hauptversion; kurze MollVersion; ConstantPoolInfo constantPool []; kurze accessFlags; ConstantPoolInfo thisClass; ConstantPoolInfo superClass; ConstantPoolInfo-Schnittstellen []; FieldInfo fields []; MethodInfo Methoden []; AttributeInfo Attribute []; boolean isValidClass = false; public static final int ACC_PUBLIC = 0x1; public static final int ACC_PRIVATE = 0x2; public static final int ACC_PROTECTED = 0x4; public static final int ACC_STATIC = 0x8; public static final int ACC_FINAL = 0x10; public static final int ACC_SYNCHRONIZED = 0x20; public static final int ACC_THREADSAFE = 0x40; public static final int ACC_TRANSIENT = 0x80; public static final int ACC_NATIVE = 0x100; public static final int ACC_INTERFACE = 0x200; public static final int ACC_ABSTRACT = 0x400;

Wie Sie sehen können, ClassFiledefinieren die Instanzvariablen für die Klasse die Hauptkomponenten einer Java-Klassendatei. Insbesondere wird die zentrale Datenstruktur für eine Java-Klassendatei als Konstantenpool bezeichnet. Andere interessante Teile von Klassendateien erhalten eigene Klassen: MethodInfofür Methoden, FieldInfofür Felder (die die Variablendeklarationen in der Klasse sind), AttributeInfoum Klassendateiattribute zu enthalten, und eine Reihe von Konstanten, die direkt aus der Spezifikation für Klassendateien in übernommen wurden Dekodieren Sie die verschiedenen Modifikatoren, die für Feld-, Methoden- und Klassendeklarationen gelten.

Die primäre Methode dieser Klasse ist readdas Lesen einer Klassendatei von der Festplatte und das Erstellen einer neuen ClassFileInstanz aus den Daten. Der Code für die readMethode ist unten dargestellt. Ich habe die Beschreibung mit dem Code durchsetzt, da die Methode in der Regel ziemlich lang ist.

1 öffentliches boolesches Lesen (InputStream in) 2 löst IOException aus {3 DataInputStream di = new DataInputStream (in); 4 int count; 5 6 magic = di.readInt (); 7 if (magic! = (Int) 0xCAFEBABE) {8 return (false); 9} 10 11 majorVersion = di.readShort (); 12 minorVersion = di.readShort (); 13 count = di.readShort (); 14 constantPool = new ConstantPoolInfo [count]; 15 if (debug) 16 System.out.println ("read (): Header lesen ..."); 17 constantPool [0] = new ConstantPoolInfo (); 18 für (int i = 1; i <KonstantePool.Länge; i ++) {19 KonstantePool [i] = neue ConstantPoolInfo (); 20 if (! ConstantPool [i] .read (di)) {21 return (false); 22} 23 // Diese beiden Typen nehmen "zwei" Stellen in der Tabelle 24 ein, wenn ((constantPool [i] .type == ConstantPoolInfo.LONG) || 25 (constantPool [i] .type == ConstantPoolInfo.DOUBLE)) 26 i ++; 27}

Wie Sie sehen können, beginnt der obige Code damit, zuerst DataInputStreamden Eingabestream zu umbrechen, auf den die Variable in verweist . Ferner sind in den Zeilen 6 bis 12 alle Informationen vorhanden, die erforderlich sind, um festzustellen, ob der Code tatsächlich eine gültige Klassendatei betrachtet. Diese Informationen bestehen aus dem magischen "Cookie" 0xCAFEBABE und den Versionsnummern 45 und 3 für die Haupt- bzw. Nebenwerte. Als nächstes wird in den Zeilen 13 bis 27 der konstante Pool in ein Array von ConstantPoolInfoObjekten eingelesen . Der Quellcode von ConstantPoolInfoist unauffällig - er liest einfach Daten ein und identifiziert sie anhand ihres Typs. Spätere Elemente aus dem Konstantenpool werden verwendet, um Informationen über die Klasse anzuzeigen.

Nach dem obigen Code readscannt die Methode den Konstantenpool erneut und "korrigiert" Referenzen im Konstantenpool, die auf andere Elemente im Konstantenpool verweisen. Der Korrekturcode wird unten angezeigt. Diese Korrektur ist erforderlich, da die Referenzen normalerweise Indizes für den Konstantenpool sind und es nützlich ist, diese Indizes bereits aufgelöst zu haben. Auf diese Weise kann der Leser auch überprüfen, ob die Klassendatei auf der Ebene des konstanten Pools nicht beschädigt ist.

28 für (int i = 1; i 0) 32 KonstantePool [i] .arg1 = KonstantePool [KonstantePool [i] .index1]; 33 if (KonstantePool [i] .index2> 0) 34 KonstantePool [i] .arg2 = KonstantePool [KonstantePool [i] .index2]; 35} 36 37 if (dumpConstants) {38 for (int i = 1; i <KonstantePool.Länge; i ++) {39 System.out.println ("C" + i + "-" + KonstantePool [i]); 30} 31}

Im obigen Code verwendet jeder konstante Pooleintrag die Indexwerte, um den Verweis auf einen anderen konstanten Pooleintrag herauszufinden. Wenn dies in Zeile 36 abgeschlossen ist, wird der gesamte Pool optional ausgegeben.

Sobald der Code über den konstanten Pool hinaus gescannt wurde, definiert die Klassendatei die primären Klasseninformationen: ihren Klassennamen, den Namen der Oberklasse und die implementierenden Schnittstellen. Der gelesene Code sucht nach diesen Werten, wie unten gezeigt.

32 accessFlags = di.readShort (); 33 34 thisClass = constantPool [di.readShort ()]; 35 superClass = constantPool [di.readShort ()]; 36 if (debug) 37 System.out.println ("read (): Klasseninformationen lesen ..."); 38 39 / * 30 * Identifizieren Sie alle von dieser Klasse implementierten Schnittstellen. 31 * / 32 count = di.readShort (); 33 if (count! = 0) {34 if (debug) 35 System.out.println ("Klasse implementiert" + count + "interfaces."); 36 Schnittstellen = new ConstantPoolInfo [count]; 37 für (int i = 0; i <count; i ++) {38 int iindex = di.readShort (); 39 if ((iindex constantPool.length - 1)) 40 return (false); 41 Schnittstellen [i] = constantPool [iindex]; 42 if (debug) 43 System.out.println ("I" + i + ":" + Schnittstellen [i]); 44} 45} 46 if (debug) 47 System.out.println ("read (): Schnittstelleninformationen lesen ...");

Once this code is complete, the read method has built up a pretty good idea of the structure of the class. All that remains is to collect the field definitions, the method definitions, and, perhaps most importantly, the class file attributes.

The class file format breaks each of these three groups into a section consisting of a number, followed by that number of instances of the thing you are looking for. So, for fields, the class file has the number of defined fields, and then that many field definitions. The code to scan in the fields is shown below.

48 count = di.readShort(); 49 if (debug) 50 System.out.println("This class has "+count+" fields."); 51 if (count != 0) { 52 fields = new FieldInfo[count]; 53 for (int i = 0; i < count; i++) { 54 fields[i] = new FieldInfo(); 55 if (! fields[i].read(di, constantPool)) { 56 return (false); 57 } 58 if (debug) 59 System.out.println("F"+i+": "+ 60 fields[i].toString(constantPool)); 61 } 62 } 63 if (debug) 64 System.out.println("read(): Read field info..."); 

Der obige Code beginnt mit dem Lesen einer Zählung in Zeile 48 und liest dann, während die Zählung nicht Null ist, neue Felder unter Verwendung der FieldInfoKlasse ein. Die FieldInfoKlasse füllt einfach Daten aus, die ein Feld für die Java Virtual Machine definieren. Der Code zum Lesen von Methoden und Attributen ist derselbe. Ersetzt einfach die Verweise auf FieldInfodurch Verweise auf MethodInfooder AttributeInfonach Bedarf. Diese Quelle ist hier nicht enthalten. Sie können die Quelle jedoch über die Links im Abschnitt Ressourcen unten anzeigen.