Dai un'occhiata alle classi Java

Benvenuti alla puntata di questo mese di "Java In Depth". Una delle prime sfide per Java era se potesse o meno rappresentare un valido linguaggio di "sistema". La radice della domanda riguardava le funzionalità di sicurezza di Java che impediscono a una classe Java di conoscere altre classi in esecuzione insieme a essa nella macchina virtuale. Questa capacità di "guardare dentro" le classi è chiamata introspezione . Nella prima versione pubblica di Java, nota come Alpha3, le rigide regole del linguaggio riguardanti la visibilità dei componenti interni di una classe potevano essere aggirate attraverso l'uso della ObjectScopeclasse. Quindi, durante la beta, quando è ObjectScopestato rimosso dal runtime a causa di problemi di sicurezza, molte persone hanno dichiarato che Java non era adatto per uno sviluppo "serio".

Perché l'introspezione è necessaria affinché una lingua sia considerata una lingua "di sistema"? Una parte della risposta è abbastanza banale: passare da "niente" (cioè una VM non inizializzata) a "qualcosa" (cioè una classe Java in esecuzione) richiede che una parte del sistema sia in grado di ispezionare le classi per essere corri in modo da capire cosa fare con loro. L'esempio canonico di questo problema è semplicemente il seguente: "Come fa un programma, scritto in un linguaggio che non può guardare" dentro "un altro componente del linguaggio, iniziare a eseguire il primo componente del linguaggio, che è il punto di partenza dell'esecuzione di tutti gli altri componenti? "

Esistono due modi per gestire l'introspezione in Java: l'ispezione dei file di classe e la nuova API di riflessione che fa parte di Java 1.1.x. Tratterò entrambe le tecniche, ma in questa colonna mi concentrerò sull'ispezione di file di prima classe. In una colonna futura vedrò come l'API di riflessione risolve questo problema. (I collegamenti al codice sorgente completo per questa colonna sono disponibili nella sezione Risorse).

Guarda a fondo nei miei file ...

Nelle versioni 1.0.x di Java, una delle più grandi verruche sul tempo di esecuzione di Java è il modo in cui l'eseguibile Java avvia un programma. Qual è il problema? L'esecuzione sta transitando dal dominio del sistema operativo host (Win 95, SunOS e così via) al dominio della Java virtual machine. Digitando la riga " java MyClass arg1 arg2" si avvia una serie di eventi completamente codificati dall'interprete Java.

Come primo evento, la shell dei comandi del sistema operativo carica l'interprete Java e gli passa la stringa "MyClass arg1 arg2" come argomento. L'evento successivo si verifica quando l'interprete Java tenta di individuare una classe denominata MyClassin una delle directory identificate nel percorso classe. Se la classe viene trovata, il terzo evento è individuare un metodo all'interno della classe denominata main, la cui firma ha i modificatori "public" e "static" e che prende un array di Stringoggetti come argomento. Se viene trovato questo metodo, viene costruito un thread primordiale e il metodo viene richiamato. L'interprete Java converte quindi "arg1 arg2" in un array di stringhe. Una volta richiamato questo metodo, tutto il resto è Java puro.

Tutto ciò va bene, tranne che il mainmetodo deve essere statico perché il runtime non può richiamarlo con un ambiente Java che non esiste ancora. Inoltre, il primo metodo deve essere nominato mainperché non c'è alcun modo per dire all'interprete il nome del metodo sulla riga di comando. Anche se hai detto all'interprete il nome del metodo, non c'è alcun modo generale per scoprire se era nella classe che avevi nominato in primo luogo. Infine, poiché il mainmetodo è statico, non puoi dichiararlo in un'interfaccia e ciò significa che non puoi specificare un'interfaccia come questa:

interfaccia pubblica Application {public void main (String args []); }

Se l'interfaccia precedente è stata definita e le classi l'hanno implementata, almeno è possibile utilizzare l' instanceofoperatore in Java per determinare se si dispone di un'applicazione o meno e quindi determinare se è adatta o meno per essere richiamata dalla riga di comando. La linea di fondo è che non puoi (definire l'interfaccia), non lo era (integrato nell'interprete Java) e quindi non puoi (determinare facilmente se un file di classe è un'applicazione). Che cosa si può fare?

In realtà, puoi fare un bel po 'se sai cosa cercare e come usarlo.

Decompilazione di file di classe

Il file di classe Java è indipendente dall'architettura, il che significa che è lo stesso insieme di bit sia che venga caricato da una macchina Windows 95 che da una macchina Sun Solaris. È anche molto ben documentato nel libro The Java Virtual Machine Specification di Lindholm e Yellin. La struttura del file di classe è stata progettata, in parte, per essere facilmente caricata nello spazio degli indirizzi SPARC. Fondamentalmente, il file di classe potrebbe essere mappato nello spazio degli indirizzi virtuali, quindi i relativi puntatori all'interno della classe potrebbero essere riparati e presto! Avevi una struttura di classe istantanea. Questo era meno utile sulle macchine con architettura Intel, ma l'eredità lasciava il formato del file di classe facile da comprendere e ancora più facile da scomporre.

Nell'estate del 1994, lavoravo nel gruppo Java e creavo quello che è noto come un modello di sicurezza "con privilegi minimi" per Java. Avevo appena finito di capire che ciò che volevo veramente fare era guardare all'interno di una classe Java, eliminare quei pezzi che non erano consentiti dal livello di privilegio corrente e quindi caricare il risultato tramite un caricatore di classi personalizzato. Fu allora che scoprii che non c'erano classi durante l'esecuzione principale che sapessero della costruzione dei file di classe. C'erano versioni nell'albero delle classi del compilatore (che doveva generare file di classe dal codice compilato), ma ero più interessato a creare qualcosa per manipolare file di classe preesistenti.

Ho iniziato creando una classe Java che potesse decomporre un file di classe Java che le veniva presentato su un flusso di input. Gli ho dato il nome meno che originale ClassFile. L'inizio di questa classe è mostrato di seguito.

classe pubblica ClassFile {int magic; short majorVersion; short minorVersion; ConstantPoolInfo constantPool []; short accessFlags; ConstantPoolInfo thisClass; ConstantPoolInfo superClass; ConstantPoolInfo interfacce []; Campi FieldInfo []; Metodi MethodInfo []; AttributeInfo attributi []; boolean isValidClass = false; public static final int ACC_PUBLIC = 0x1; int finale statico pubblico ACC_PRIVATE = 0x2; int finale statico pubblico ACC_PROTECTED = 0x4; int finale statico pubblico ACC_STATIC = 0x8; int finale statico pubblico ACC_FINAL = 0x10; int finale statico pubblico ACC_SYNCHRONIZED = 0x20; int finale statico pubblico ACC_THREADSAFE = 0x40; int finale statico pubblico ACC_TRANSIENT = 0x80; int finale statico pubblico ACC_NATIVE = 0x100; int finale statico pubblico ACC_INTERFACE = 0x200; int finale statico pubblico ACC_ABSTRACT = 0x400;

Come puoi vedere, le variabili di istanza per classe ClassFiledefiniscono i componenti principali di un file di classe Java. In particolare, la struttura dati centrale per un file di classe Java è nota come pool di costanti. Altre parti interessanti del file di classe ottengono classi proprie: MethodInfoper i metodi, FieldInfoper i campi (che sono le dichiarazioni di variabili nella classe), AttributeInfoper contenere gli attributi del file di classe e un insieme di costanti che è stato preso direttamente dalla specifica sui file di classe per decodificare i vari modificatori che si applicano alle dichiarazioni di campo, metodo e classe.

Il metodo principale di questa classe è read, che viene utilizzato per leggere un file di classe dal disco e creare una nuova ClassFileistanza dai dati. Il codice per il readmetodo è mostrato di seguito. Ho intervallato la descrizione con il codice poiché il metodo tende ad essere piuttosto lungo.

1 lettura booleana pubblica (InputStream in) 2 genera IOException {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 (): Leggi intestazione ..."); 17 ConstantPool [0] = nuovo ConstantPoolInfo (); 18 for (int i = 1; i <constantPool.length; i ++) {19 constantPool [i] = new ConstantPoolInfo (); 20 if (! ConstantPool [i] .read (di)) {21 return (false); 22} 23 // Questi due tipi occupano "due" punti nella tabella 24 if ((constantPool [i] .type == ConstantPoolInfo.LONG) || 25 (constantPool [i] .type == ConstantPoolInfo.DOUBLE)) 26 i ++; 27}

Come puoi vedere, il codice sopra inizia avvolgendo prima un DataInputStreamintorno al flusso di input a cui fa riferimento la variabile in . Inoltre, nelle righe dalla 6 alla 12, sono presenti tutte le informazioni necessarie per determinare che il codice sta effettivamente guardando un file di classe valido. Queste informazioni sono costituite dal "cookie" magico 0xCAFEBABE e dai numeri di versione 45 e 3 rispettivamente per i valori maggiore e minore. Successivamente, nelle righe dalla 13 alla 27, il pool di costanti viene letto in un array di ConstantPoolInfooggetti. Il codice sorgente di ConstantPoolInfoè insignificante: legge semplicemente i dati e li identifica in base al tipo. Gli elementi successivi del pool di costanti vengono utilizzati per visualizzare le informazioni sulla classe.

Dopo il codice precedente, il readmetodo esegue nuovamente la scansione del pool di costanti e "risolve" i riferimenti nel pool di costanti che fanno riferimento ad altri elementi nel pool di costanti. Il codice di correzione è mostrato di seguito. Questa correzione è necessaria poiché i riferimenti in genere sono indici nel pool di costanti ed è utile che tali indici siano già risolti. Ciò fornisce anche un controllo affinché il lettore sappia che il file di classe non è danneggiato a livello di pool costante.

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

Nel codice precedente ogni voce di pool di costanti utilizza i valori di indice per calcolare il riferimento a un'altra voce di pool di costanti. Una volta completato nella riga 36, ​​l'intero pool viene facoltativamente scaricato.

Una volta che il codice ha superato il pool di costanti, il file di classe definisce le informazioni sulla classe primaria: nome della classe, nome della superclasse e interfacce di implementazione. Il codice di lettura esegue la scansione di questi valori come mostrato di seguito.

32 accessFlags = di.readShort (); 33 34 thisClass = constantPool [di.readShort ()]; 35 superClass = constantPool [di.readShort ()]; 36 if (debug) 37 System.out.println ("read (): Leggi informazioni sulla classe ..."); 38 39 / * 30 * Identifica tutte le interfacce implementate da questa classe 31 * / 32 count = di.readShort (); 33 if (count! = 0) {34 if (debug) 35 System.out.println ("Class implements" + count + "interfaces."); 36 interfacce = new ConstantPoolInfo [count]; 37 for (int i = 0; i <count; i ++) {38 int iindex = di.readShort (); 39 if ((iindex constantPool.length - 1)) 40 return (false); 41 interfacce [i] = constantPool [iindex]; 42 if (debug) 43 System.out.println ("I" + i + ":" + interfacce [i]); 44} 45} 46 if (debug) 47 System.out.println ("read (): Leggi informazioni sull'interfaccia ...");

Una volta che questo codice è completo, il readmetodo ha costruito un'idea abbastanza buona della struttura della classe. Non resta che raccogliere le definizioni dei campi, le definizioni dei metodi e, forse la cosa più importante, gli attributi del file di classe.

Il formato del file di classe suddivide ciascuno di questi tre gruppi in una sezione composta da un numero, seguito da quel numero di istanze dell'oggetto che stai cercando. Quindi, per i campi, il file di classe ha il numero di campi definiti e quindi molte definizioni di campo. Il codice da scansionare nei campi è mostrato di seguito.

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..."); 

Il codice precedente inizia leggendo un conteggio nella riga # 48, quindi, mentre il conteggio è diverso da zero, legge in nuovi campi utilizzando la FieldInfoclasse. La FieldInfoclasse compila semplicemente i dati che definiscono un campo nella macchina virtuale Java. Il codice per leggere metodi e attributi è lo stesso, semplicemente sostituendo i riferimenti a FieldInfocon riferimenti a MethodInfoo AttributeInfocome appropriato. Quella fonte non è inclusa qui, tuttavia puoi guardare la fonte utilizzando i collegamenti nella sezione Risorse di seguito.