Pogledajte unutar Java satova

Dobrodošli u ovomjesečni dio "Java u dubini." Jedan od najranijih izazova za Javu bio je može li ona postojati kao sposoban jezik "sustava". Korijen pitanja uključivao je Java-ove sigurnosne značajke koje sprečavaju Java klasu da poznaje druge klase koje se izvode uz nju u virtualnom stroju. Ta sposobnost "gledanja iznutra" u nastavu naziva se introspekcija . U prvom javnom izdanju Jave, poznatom kao Alpha3, stroga jezična pravila u pogledu vidljivosti unutarnjih komponenata klase mogla bi se zaobići upotrebom ObjectScopeklase. Zatim, tijekom beta verzije, kada ObjectScopeje uklonjena iz vremena izvođenja zbog sigurnosnih razloga, mnogi su ljudi proglasili Javu neprikladnom za "ozbiljan" razvoj.

Zašto je introspekcija potrebna da bi se jezik smatrao jezikom "sustava"? Jedan dio odgovora prilično je svakodnevan: prelazak s "ničega" (tj. Neinicijalizirane VM) do "nečega" (odnosno pokrenute Java klase) zahtijeva da neki dio sustava može pregledati klase koje treba trčite kako biste shvatili što učiniti s njima. Kanonski primjer ovog problema jednostavno je sljedeći: "Kako program, napisan na jeziku koji ne može gledati 'iznutra' drugu komponentu jezika, započinje s izvršavanjem komponente prvog jezika, što je početna točka izvršenja svih ostalih komponenata? "

Postoje dva načina za rješavanje introspekcije u Javi: inspekcija datoteka klase i novi API za refleksiju koji je dio Jave 1.1.x. Objasnit ću obje tehnike, ali u ovoj ću se koloni usredotočiti na prvoklasni pregled datoteka. U sljedećem ću stupcu pogledati kako API refleksije rješava ovaj problem. (Veze do potpunog izvornog koda za ovaj stupac dostupne su u odjeljku Resursi.)

Zaviri duboko u moje datoteke ...

U izdanjima Java 1.0.x, jedna od najvećih bradavica u vremenu izvođenja Java način je na koji Java izvršna datoteka pokreće program. U čemu je problem? Izvršenje je tranzit iz domene glavnog operativnog sustava (Win 95, SunOS i tako dalje) u domenu Java virtualnog stroja. Upisivanje retka java MyClass arg1 arg2pokreće niz događaja koji su potpuno interpretirani Java interpretatorom.

Kao prvi događaj, ljuska naredbe operativnog sustava učitava interpretator Jave i predaje mu niz "MyClass arg1 arg2" kao svoj argument. Sljedeći se događaj događa kada Java interpreter pokušava pronaći klasu imenovanu MyClassu jednom od direktorija identificiranih na putu do klase. Ako je klasa pronađena, treći je događaj locirati metodu unutar imenovane klase main, čiji potpis ima modifikatore "public" i "static" i koja Stringkao argument uzima niz objekata. Ako se pronađe ova metoda, konstruira se primordijalna nit i metoda se poziva. Tada Java interpreter pretvara "arg1 arg2" u niz nizova. Jednom kada se ova metoda pozove, sve ostalo je čista Java.

To je sve u redu, osim što mainmetoda mora biti statična jer se vrijeme izvođenja ne može pozvati s Java okruženjem koje još ne postoji. Dalje, prva metoda mora biti imenovana mainjer ne postoji način da se interpretatoru kaže ime metode u naredbenom retku. Čak i ako ste tumaču rekli ime metode, ne postoji općenit način na koji možete saznati je li to uopće bilo u klasi koju ste imenovali. Konačno, budući da je mainmetoda statična, ne možete je deklarirati u sučelju, a to znači da ne možete odrediti sučelje poput ovog:

javno sučelje Aplikacija {public void main (String args []); }

Ako je gornje sučelje definirano, a klase su ga implementirale, tada biste barem mogli upotrijebiti instanceofoperator u Javi da biste utvrdili imate li aplikaciju ili ne i tako odredili je li prikladno za pozivanje iz naredbenog retka. Dno svega je da ne možete (definirati sučelje), nije (nije ugrađeno u Java interpreter), pa tako i ne možete (lako odrediti je li datoteka klase aplikacija). Pa što možete učiniti?

Zapravo, možete učiniti prilično puno ako znate na što treba paziti i kako to koristiti.

Dekompiliranje datoteka klase

Datoteka klase Java arhitekturno je neutralna, što znači da se radi o istom skupu bitova bilo da je učitana sa stroja Windows 95 ili Sun Solaris. Također je vrlo dobro dokumentirano u knjizi Java Virtual Machine Specification od Lindholma i Yellina. Struktura datoteke klase dizajnirana je, dijelom, za lako učitavanje u adresni prostor SPARC-a. U osnovi bi se datoteka klase mogla preslikati u virtualni adresni prostor, zatim popraviti relativne pokazivače unutar klase i presto! Imali ste trenutnu strukturu klase. Ovo je bilo manje korisno na Intelovim arhitekturnim strojevima, ali nasljeđe je ostavilo format datoteke klase lako razumljivim, a još lakše razgradljivim.

U ljeto 1994. radio sam u Java grupi i gradio ono što je poznato kao "najmanji privilegij" sigurnosni model za Javu. Upravo sam završio sa shvaćanjem da ono što zapravo želim učiniti jest pogledati unutar Java klase, izrezati one dijelove koji nisu dopušteni trenutnom razinom privilegija, a zatim učitati rezultat kroz prilagođeni učitavač klasa. Tada sam otkrio da u glavnom vremenu izvođenja nije bilo klasa koje su znale o konstrukciji datoteka klasa. Postojale su verzije u stablu klase kompajlera (koje su morale generirati datoteke klase iz kompajliranog koda), ali mene je više zanimalo da napravim nešto za manipulaciju već postojećim datotekama klase.

Počeo sam s izgradnjom Java klase koja bi mogla rastaviti datoteku Java klase koja joj je predstavljena na ulaznom toku. Dao sam mu manje originalno ime ClassFile. Početak ove klase prikazan je u nastavku.

javna klasa ClassFile {int magic; kratka glavna verzija; kratka molskaVerzija; ConstantPoolInfo constantPool []; kratki pristupFlags; ConstantPoolInfo thisClass; SuperClass ConstantPoolInfo; Sučelja ConstantPoolInfo []; FieldInfo polja []; MethodInfo metode []; AtributiInfo atributi []; boolean isValidClass = false; javni statički konačni int ACC_PUBLIC = 0x1; javni statički konačni int ACC_PRIVATE = 0x2; javni statički konačni int ACC_PROTECTED = 0x4; javni statički konačni int ACC_STATIC = 0x8; javni statički konačni int ACC_FINAL = 0x10; javni statički konačni int ACC_SYNCHRONIZED = 0x20; javni statički konačni int ACC_THREADSAFE = 0x40; javni statički konačni int ACC_TRANSIENT = 0x80; javni statički konačni int ACC_NATIVE = 0x100; javni statički konačni int ACC_INTERFACE = 0x200; javni statički konačni int ACC_ABSTRACT = 0x400;

Kao što vidite, varijable instance za klasu ClassFiledefiniraju glavne komponente datoteke Java klase. Osobito je središnja struktura podataka za datoteku klase Java poznata kao konstantno spremište. Ostali zanimljivi dijelovi datoteke klase dobivaju vlastite klase: MethodInfoza metode, FieldInfoza polja (koja su deklaracije varijabli u klasi), AttributeInfoda sadrže atribute datoteke klase i skup konstanti koji je preuzet izravno iz specifikacije na datotekama klase u dekodirati razne modifikatore koji se primjenjuju na deklaracije polja, metode i klase.

Primarna metoda ove klase je read, koja se koristi za čitanje datoteke klase s diska i stvaranje nove ClassFileinstance iz podataka. Šifra readmetode prikazana je u nastavku. Prošarao sam opis kodom jer je metoda često prilično duga.

1 javno logičko očitanje (InputStream in) 2 baca IOException {3 DataInputStream di = new DataInputStream (in); 4 int brojanje; 5 6 magija = di.readInt (); 7 if (magic! = (Int) 0xCAFEBABE) {8 return (false); 9} 10 11 majorVersion = di.readShort (); 12 minorVersion = di.readShort (); 13 brojanje = di.readShort (); 14 constantPool = novi ConstantPoolInfo [count]; 15 if (otklanjanje pogrešaka) 16 System.out.println ("read (): Read header ..."); 17 constantPool [0] = novi ConstantPoolInfo (); 18 za (int i = 1; i <constantPool.length; i ++) {19 constantPool [i] = novi ConstantPoolInfo (); 20 if (! ConstantPool [i] .read (di)) {21 return (false); 22} 23 // Ove dvije vrste zauzimaju "dva" mjesta u tablici 24 ako ((constantPool [i] .type == ConstantPoolInfo.LONG) || 25 (constantPool [i] .type == ConstantPoolInfo.DOUBLE)) 26 i ++; 27}

Kao što vidite, gornji kod započinje tako što se prvo omota a DataInputStreamoko ulaznog toka na koji se odnosi varijabla u . Nadalje, u redovima od 6 do 12 prisutni su svi podaci potrebni da bi se utvrdilo da li kôd zaista gleda valjanu datoteku klase. Ove se informacije sastoje od čarobnog "kolačića" 0xCAFEBABE, odnosno verzije brojeva 45 i 3 za glavne i male vrijednosti. Dalje, u redovima 13 do 27, konstantno spremište čita se u niz ConstantPoolInfoobjekata. Izvorni kôd ConstantPoolInfonije značajan - on jednostavno čita podatke i identificira ih na temelju njihove vrste. Kasniji elementi iz konstantnog spremišta koriste se za prikaz informacija o klasi.

Slijedeći gornji kod, readmetoda ponovno skenira konstantni bazen i "popravlja" reference u konstantnom spremištu koje se odnose na druge stavke u konstantnom spremištu. Kôd za popravljanje prikazan je u nastavku. Ovo je popravak neophodno jer su reference obično indeksi u konstantnom spremištu, a korisno je imati te indekse već riješene. Ovo također pruža provjeru da čitač zna da datoteka razreda nije oštećena na konstantnoj razini spremišta.

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

U gornjem kodu svaki unos konstantnog spremišta koristi vrijednosti indeksa da bi utvrdio referencu na drugi unos konstantnog spremišta. Kada se završi u retku 36, cijeli se bazen po želji izbacuje.

Jednom kada je kôd skeniran pored konstantnog spremišta, datoteka klase definira podatke o primarnoj klasi: naziv klase, naziv superklase i implementacijska sučelja. Kôd za čitanje skenira ove vrijednosti kako je prikazano u nastavku.

32 accessFlags = di.readShort (); 33 34 thisClass = constantPool [di.readShort ()]; 35 superClass = constantPool [di.readShort ()]; 36 if (otklanjanje pogrešaka) 37 System.out.println ("read (): Pročitajte informacije o predavanju ..."); 38 39 / * 30 * Identificirajte sva sučelja koja implementira ova klasa 31 * / 32 count = di.readShort (); 33 if (count! = 0) {34 if (debug) 35 System.out.println ("Implementacije klase" + count + "sučelja."); 36 sučelja = novi ConstantPoolInfo [count]; 37 za (int i = 0; i <count; i ++) {38 int iindex = di.readShort (); 39 if ((iindex constantPool.length - 1)) 40 return (false); 41 sučelje [i] = constantPool [iindex]; 42 if (otklanjanje pogrešaka) 43 System.out.println ("I" + i + ":" + sučelja [i]); 44} 45} 46 if (otklanjanje pogrešaka) 47 System.out.println ("read (): Pročitajte informacije o sučelju ...");

Jednom kada je ovaj kôd završen, readmetoda je stvorila prilično dobru ideju o strukturi klase. Preostaje samo prikupiti definicije polja, definicije metoda i, možda najvažnije, atribute datoteke klase.

Format datoteke klase razbija svaku od ove tri skupine u odjeljak koji se sastoji od broja, nakon čega slijedi taj broj primjeraka stvari koju tražite. Dakle, za polja datoteka klase ima broj definiranih polja, a zatim toliko definicija polja. Kôd za skeniranje u poljima prikazan je ispod.

48 brojač = di.readShort (); 49 if (otklanjanje pogrešaka) 50 System.out.println ("Ova klasa ima polja" + count + ".); 51 if (count! = 0) {52 polja = novo FieldInfo [count]; 53 za (int i = 0; i <count; i ++) {54 polja [i] = novo FieldInfo (); 55 if (! Polja [i] .read (di, constantPool)) {56 return (false); 57} 58 if (otklanjanje pogrešaka) 59 System.out.println ("F" + i + ":" + 60 polja [i] .toString (constantPool)); 61} 62} 63 if (otklanjanje pogrešaka) 64 System.out.println ("read (): Pročitajte informacije o polju ...");

Gornji kôd započinje čitanjem brojača u retku # 48, a zatim, dok brojanje nije nula, on čita u novim poljima pomoću FieldInfoklase. FieldInfoKlasa jednostavno ispunjava podatke koji definiraju polje za Java virtualni stroj. Kôd za čitanje metoda i atributa je isti, jednostavno zamjenjujući reference na FieldInforeference MethodInfoili AttributeInfoprema potrebi. Taj izvor ovdje nije uključen, no izvor možete pogledati pomoću poveznica u odjeljku Resursi u nastavku.