Programování

Podívejte se do tříd Java

Vítejte u splátky „Java In Depth“ z tohoto měsíce. Jednou z prvních výzev pro Javu bylo, zda může nebo nemůže obstát jako schopný „systémový“ jazyk. Kořen otázky zahrnoval bezpečnostní prvky Java, které brání třídě Java ve znalosti dalších tříd, které běží vedle ní ve virtuálním stroji. Tato schopnost „nahlédnout do“ tříd se nazývá introspekce. V prvním veřejném vydání Java, známém jako Alpha3, bylo možné obejít přísná jazyková pravidla týkající se viditelnosti interních komponent třídy, ačkoli použití ObjectScope třída. Pak, během beta, kdy ObjectScope byl odstraněn z běhu kvůli bezpečnostním obavám, mnoho lidí prohlásilo Javu za nevhodnou pro „vážný“ vývoj.

Proč je introspekce nezbytná, aby byl jazyk považován za „systémový“ jazyk? Jedna část odpovědi je celkem světská: Přechod od „ničeho“ (tj. Neinicializovaného virtuálního počítače) k „něčemu“ (tj. Běžící třídě Java) vyžaduje, aby některá část systému mohla kontrolovat třídy, které mají být běžte, abyste zjistili, co s nimi bude dělat. Kanonický příklad tohoto problému je jednoduše následující: „Jak program, napsaný v jazyce, který se nemůže dívat„ dovnitř “jiné jazykové komponenty, začne provádět první jazykovou komponentu, která je výchozím bodem provedení pro všechny ostatní komponenty? "

Existují dva způsoby, jak se vypořádat s introspekcí v Javě: kontrola souborů třídy a nové reflexní API, které je součástí Java 1.1.x. Obejdu obě techniky, ale v tomto sloupci se zaměřím na prvotřídní kontrolu spisů. V budoucím sloupci se podívám na to, jak tento problém řeší reflexní API. (Odkazy na kompletní zdrojový kód pro tento sloupec jsou k dispozici v sekci Zdroje.)

Podívejte se hlouběji do mých souborů ...

Ve verzích Java 1.0.x je jednou z největších bradavic v době běhu prostředí Java způsob, jakým spustitelný program Java spouští program. Co je za problém? Provádění probíhá z domény hostitelského operačního systému (Win 95, SunOS atd.) Do domény virtuálního počítače Java. Psaní řádku "java MyClass arg1 arg2"uvádí do pohybu řadu událostí, které jsou zcela pevně zakódovány interpretem Java.

Jako první událost načte příkazový shell operačního systému tlumočníka Java a předá mu jako argument řetězec „MyClass arg1 arg2“. K další události dojde, když se interpret jazyka Java pokusí najít třídu s názvem Moje třída v jednom z adresářů identifikovaných v cestě ke třídě. Pokud je třída nalezena, třetí událostí je vyhledání metody uvnitř pojmenované třídy hlavní, jehož podpis má modifikátory „public“ a „static“ a který má pole Tětiva objekty jako argument. Pokud je tato metoda nalezena, je vytvořeno primordiální vlákno a metoda je vyvolána. Interpret Java poté převede „arg1 arg2“ na pole řetězců. Jakmile je tato metoda vyvolána, všechno ostatní je čistá Java.

To je všechno dobré a dobré kromě toho, že hlavní metoda musí být statická, protože za běhu ji nelze vyvolat v prostředí Java, které dosud neexistuje. Dále je třeba pojmenovat první metodu hlavní protože neexistuje žádný způsob, jak sdělit tlumočníkovi název metody na příkazovém řádku. I kdybyste tlumočníkovi sdělili název metody, neexistuje žádný obecný způsob, jak zjistit, zda se jednalo o třídu, kterou jste pojmenovali. Nakonec, protože hlavní metoda je statická, nemůžete ji deklarovat v rozhraní, a to znamená, že nemůžete určit rozhraní takto:

veřejné rozhraní Aplikace {public void main (String args []); } 

Pokud bylo definováno výše uvedené rozhraní a třídy jej implementovaly, pak byste alespoň mohli použít instanceof operátor v Javě, aby zjistil, zda jste aplikaci měli nebo neměli, a určili tak, zda je nebo není vhodná pro vyvolání z příkazového řádku. Závěrem je, že nemůžete (definovat rozhraní), nebylo (zabudováno do interpretu Java), a proto nemůžete (snadno určit, zda je soubor třídy aplikací). Co tedy můžete dělat?

Ve skutečnosti toho můžete udělat docela dost, pokud víte, co hledat a jak to používat.

Dekompilace souborů třídy

Soubor třídy Java je neutrální vůči architektuře, což znamená, že jde o stejnou sadu bitů, ať už je načten ze stroje se systémem Windows 95 nebo ze stroje Sun Solaris. Je to také velmi dobře zdokumentováno v knize Specifikace Java Virtual Machine Lindholm a Yellin. Struktura souboru třídy byla zčásti navržena pro snadné načtení do adresního prostoru SPARC. V zásadě lze soubor třídy namapovat do virtuálního adresního prostoru, poté opravit relativní ukazatele uvnitř třídy a presto! Měl jsi okamžitou strukturu třídy. To bylo na počítačích s architekturou Intel méně užitečné, ale dědictví ponechalo formát souboru třídy snadno srozumitelný a ještě snazší rozebrat.

V létě roku 1994 jsem pracoval ve skupině Java a budoval takzvaný model zabezpečení „nejméně privilegovaných“ pro Javu. Právě jsem dokončil zjišťování, že to, co jsem opravdu chtěl, bylo podívat se do třídy Java, vyříznout ty kousky, které nebyly povoleny aktuální úrovní oprávnění, a poté načíst výsledek pomocí vlastního zavaděče tříd. Tehdy jsem zjistil, že v hlavním běhu nebyly žádné třídy, které by věděly o konstrukci souborů tříd. Ve stromu tříd kompilátoru byly verze (které musely generovat soubory třídy z kompilovaného kódu), ale více mě zajímalo budování něčeho pro manipulaci s již existujícími soubory tříd.

Začal jsem vytvořením třídy Java, která by dokázala rozložit soubor třídy Java, který mu byl předložen na vstupním proudu. Dal jsem tomu méně než původní název ClassFile. Začátek této třídy je uveden níže.

veřejná třída ClassFile {int magie; krátká hlavní verze; krátká menší verze; ConstantPoolInfo constantPool []; short accessFlags; ConstantPoolInfo thisClass; ConstantPoolInfo superClass; Rozhraní ConstantPoolInfo []; FieldInfo pole []; MethodInfo metody []; AttributeInfo atributy []; 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; 

Jak vidíte, proměnné instance pro třídu ClassFile definovat hlavní součásti souboru třídy Java. Zejména je centrální datová struktura pro soubor třídy Java známá jako konstantní fond. Další zajímavé bloky souboru třídy získávají vlastní třídy: MethodInfo pro metody, FieldInfo pro pole (což jsou deklarace proměnných ve třídě), AttributeInfo k uložení atributů souboru třídy a sady konstant, které byly převzaty přímo ze specifikace souborů třídy k dekódování různých modifikátorů, které se vztahují na deklarace polí, metod a tříd.

Primární metoda této třídy je číst, který slouží k načtení souboru třídy z disku a vytvoření nového ClassFile instance z dat. Kód pro číst metoda je uvedena níže. Proložil jsem popis kódem, protože metoda má tendenci být docela dlouhá.

1 veřejné booleovské čtení (InputStream in) 2 vyvolá IOException {3 DataInputStream di = new DataInputStream (in); 4 int počet; 5 6 magie = 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 = nový ConstantPoolInfo [počet]; 15 if (debug) 16 System.out.println ("read (): Číst záhlaví ..."); 17 constantPool [0] = nový 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 // Tyto dva typy zabírají „dvě“ místa v tabulce 24 if ((constantPool [i] .type == ConstantPoolInfo.LONG) || 25 (constantPool [i] .type == ConstantPoolInfo.DOUBLE)) 26 i ++; 27} 

Jak vidíte, výše uvedený kód začíná prvním zabalením a DataInputStream kolem vstupního proudu odkazovaného proměnnou v. Dále na řádcích 6 až 12 jsou k dispozici všechny informace nezbytné k určení, že se kód skutečně dívá na platný soubor třídy. Tyto informace se skládají z magického „cookie“ 0xCAFEBABE a čísel verzí 45 a 3 pro hlavní a vedlejší hodnoty. Dále se na řádcích 13 až 27 načte konstantní fond do pole ConstantPoolInfo předměty. Zdrojový kód do ConstantPoolInfo je pozoruhodný - jednoduše načte data a identifikuje je na základě jejich typu. Pozdější prvky z konstantního fondu se používají k zobrazení informací o třídě.

V návaznosti na výše uvedený kód se číst metoda znovu prohledá konstantní fond a "opraví" odkazy v konstantním fondu, které odkazují na další položky ve fondu konstant. Kód opravy je uveden níže. Tato oprava je nutná, protože odkazy jsou obvykle indexy do fondu konstant a je užitečné mít tyto indexy již vyřešené. To také poskytuje kontrolu, aby čtenář věděl, že soubor třídy není poškozen na úrovni konstantního fondu.

28 pro (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} 

Ve výše uvedeném kódu používá každá položka konstantního fondu hodnoty indexu k určení odkazu na jinou položku konstantního fondu. Po dokončení v řádku 36 je celý fond volitelně vypsán.

Jakmile je kód naskenován kolem fondu konstant, soubor třídy definuje informace o primární třídě: její název třídy, název nadtřídy a implementace rozhraní. The číst kód vyhledá tyto hodnoty, jak je uvedeno níže.

32 accessFlags = di.readShort (); 33 34 thisClass = constantPool [di.readShort ()]; 35 superClass = constantPool [di.readShort ()]; 36 if (debug) 37 System.out.println ("read (): Číst informace o třídě ..."); 38 39 / * 30 * Určete všechna rozhraní implementovaná touto třídou 31 * / 32 count = di.readShort (); 33 if (count! = 0) {34 if (debug) 35 System.out.println ("Třída implementuje" + count + "rozhraní."); 36 rozhraní = nový ConstantPoolInfo [počet]; 37 for (int i = 0; i <count; i ++) {38 int iindex = di.readShort (); 39 if ((iindex constantPool.length - 1)) 40 return (false); 41 rozhraní [i] = constantPool [iindex]; 42 if (debug) 43 System.out.println ("I" + i + ":" + rozhraní [i]); 44} 45} 46 if (debug) 47 System.out.println ("read (): Číst informace o rozhraní ..."); 

Jakmile je tento kód kompletní, číst metoda vytvořila docela dobrou představu o struktuře třídy. Zbývá jen shromáždit definice polí, definice metod a, možná nejdůležitější, atributy souboru třídy.

Formát souboru třídy rozdělí každou z těchto tří skupin na část skládající se z čísla, následovaného tímto počtem instancí hledané věci. Takže pro pole má soubor třídy počet definovaných polí a potom tolik definic polí. Kód pro skenování do polí je uveden níže.

48 count = di.readShort (); 49 if (debug) 50 System.out.println ("Tato třída má pole" + count + "."); 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 polí [i] .toString (constantPool)); 61} 62} 63 if (debug) 64 System.out.println ("read (): Číst informace o poli ..."); 

Výše uvedený kód začíná načtením počtu v řádku # 48, poté, když je počet nenulový, čte v nových polích pomocí FieldInfo třída. The FieldInfo třída jednoduše vyplní data, která definují pole do virtuálního stroje Java. Kód pro čtení metod a atributů je stejný, jednoduše nahradí odkazy na FieldInfo s odkazy na MethodInfo nebo AttributeInfo podle potřeby. Tento zdroj zde není zahrnut, ale můžete se na něj podívat pomocí odkazů v sekci Zdroje níže.