Ohjelmointi

Tutustu Java-luokkiin

Tervetuloa tämän kuukauden erään "Java In Depth". Yksi Java: n varhaisimmista haasteista oli, pystyykö se seisomaan kykenevänä "järjestelmäkielenä". Kysymyksen ytimessä olivat Java-turvaominaisuudet, jotka estävät Java-luokkaa tuntemasta muita virtuaalikoneessa sen vieressä olevia luokkia. Tätä kykyä luokkien "katseluun" kutsutaan itsetarkastelu. Ensimmäisessä julkisessa Java-julkaisussa, joka tunnetaan nimellä Alpha3, tiukat kielisäännöt, jotka koskevat luokan sisäisten komponenttien näkyvyyttä, voitaisiin kiertää, vaikka ObjectScope luokassa. Sitten beetan aikana, milloin ObjectScope poistettiin ajoajasta turvallisuuteen liittyvien huolenaiheiden vuoksi, monet ihmiset julistivat Javan soveltumattomaksi "vakavaan" kehitykseen.

Miksi itsetarkastelu on tarpeen, jotta kieltä voidaan pitää "järjestelmäkielenä"? Yksi osa vastauksesta on melko arkipäivää: Siirtyminen "tyhjästä" (eli alustamattomaan virtuaalikoneeseen) "johonkin" (ts. Käynnissä olevaan Java-luokkaan) edellyttää, että jokin järjestelmän osa pystyy tarkastamaan luokat juosta niin selvittääksesi mitä tehdä heidän kanssaan. Kanoninen esimerkki tästä ongelmasta on yksinkertaisesti seuraava: "Kuinka ohjelma, joka on kirjoitettu kielellä, joka ei voi katsoa toisen kielikomponentin sisälle, alkaa suorittaa ensimmäisen kielikomponentin, joka on kaikkien muiden komponenttien suorittamisen lähtökohta? "

On olemassa kaksi tapaa käsitellä itsetarkastusta Javassa: luokkatiedostojen tarkastus ja uusi heijastus-sovellusliittymä, joka on osa Java 1.1.x: tä. Käsittelen molemmat tekniikat, mutta tässä sarakkeessa keskityn ensiluokkaiseen tiedostotarkastukseen. Tulevassa sarakkeessa tarkastelen, miten heijastus-API ratkaisee tämän ongelman. (Linkit tämän sarakkeen täydelliseen lähdekoodiin ovat saatavilla Resurssit-osiossa.)

Katso syvällisesti tiedostoni ...

Java: n 1.0.x-julkaisuissa yksi Java-käyntiajan suurimmista syylistä on tapa, jolla Java-suoritettava tiedosto käynnistää ohjelman. Mikä on ongelma? Suoritus on siirtymässä isäntäkäyttöjärjestelmän (Win 95, SunOS ja niin edelleen) toimialueelta Java-virtuaalikoneen toimialueelle. Kirjoittamalla viivan "java MyClass arg1 arg2"käynnistää sarjan tapahtumia, jotka Java-tulkki koodaa täysin kovasti.

Ensimmäisenä tapahtumana käyttöjärjestelmän komentokuori lataa Java-tulkin ja välittää sen argumentiksi merkkijonon "MyClass arg1 arg2". Seuraava tapahtuma tapahtuu, kun Java-tulkki yrittää paikantaa luokan nimeltä Luokkani yhdessä luokan polulla tunnistetuista hakemistoista. Jos luokka löytyy, kolmanneksi tapahtumaksi on löydettävä menetelmä nimeltä mainitun luokan sisältä tärkein, jonka allekirjoituksessa on muuttujat "public" ja "staattinen" ja johon tarvitaan joukko Merkkijono esineet sen argumenttina. Jos tämä menetelmä löydetään, muodostetaan alkulanka ja menetelmään vedotaan. Java-tulkki muuntaa sitten "arg1 arg2" merkkijonoiksi. Kun tämä menetelmä on käytetty, kaikki muu on puhdasta Java.

Kaikki on hyvin ja hyvää paitsi että tärkein menetelmän on oltava staattinen, koska ajoaika ei voi kutsua sitä Java-ympäristössä, jota ei vielä ole olemassa. Lisäksi ensimmäinen menetelmä on nimettävä tärkein koska ei ole mitään tapaa kertoa tulkille menetelmän nimeä komentorivillä. Vaikka sanoitkin tulkille menetelmän nimen, ei ole mitään yleistä tapaa saada selville, kuuluuko se ensin nimettyyn luokkaan. Lopuksi, koska tärkein menetelmä on staattinen, et voi ilmoittaa sitä käyttöliittymässä, ja se tarkoittaa, että et voi määrittää tällaista käyttöliittymää:

julkinen käyttöliittymä Sovellus {public void main (String args []); } 

Jos yllä oleva käyttöliittymä oli määritelty ja luokat toteuttivat sen, ainakin voit käyttää esiintymä operaattori Java-sovelluksessa selvittääksesi, onko sinulla sovellusta vai ei, ja selvittää siten, sopiiko se komentoriviltä. Tärkeintä on, että et voi (määritellä käyttöliittymää), sitä ei ollut (sisäänrakennettu Java-tulkkiin), joten et voi (selvittää, onko luokkatiedosto sovellus helposti). Joten mitä voit tehdä?

Itse asiassa voit tehdä melko vähän, jos tiedät mitä etsiä ja miten sitä käytetään.

Luokkatiedostojen purkaminen

Java-luokan tiedosto on arkkitehtuurineutraali, mikä tarkoittaa, että se on sama joukko bittejä riippumatta siitä, ladataanko se Windows 95- tai Sun Solaris -laitteelta. Se on myös hyvin dokumentoitu kirjassa Java-virtuaalikoneiden määrittely kirjoittaneet Lindholm ja Yellin. Luokkatiedostorakenne on suunniteltu osittain ladattavaksi helposti SPARC-osoitetilaan. Pohjimmiltaan luokkatiedosto voidaan yhdistää virtuaaliseen osoiteavaruuteen, sitten luokan sisäiset viitteet korjataan ja presto! Sinulla oli välitön luokan rakenne. Tämä oli vähemmän hyödyllistä Intel-arkkitehtuurikoneissa, mutta perintö jätti luokan tiedostomuodon helposti ymmärrettäväksi ja vielä helpommin hajoavaksi.

Kesällä 1994 työskentelin Java-ryhmässä ja rakensin ns. "Vähiten etuoikeuksia" käyttävän Java-suojausmallin. Olin juuri päättänyt selvittää, että mitä todella halusin tehdä, oli katsoa Java-luokan sisälle, valmistella ne kappaleet, joita nykyinen etuoikeustaso ei salli, ja ladata sitten tulos mukautetun luokan kuormaajan kautta. Silloin huomasin, että pääajoaikana ei ollut luokkia, jotka tietäisivät luokkatiedostojen rakentamisesta. Kääntäjän luokan puussa oli versioita (joiden oli luotava luokkatiedostot käännetystä koodista), mutta olin kiinnostunut rakentamaan jotain olemassa olevien luokkatiedostojen manipuloimiseksi.

Aloitin rakentamalla Java-luokan, joka voisi hajottaa sille syötevirrassa esitetyn Java-luokan tiedoston. Annoin sille vähemmän kuin alkuperäinen nimi LuokkaTiedosto. Tämän luokan alku on esitetty alla.

julkinen luokka ClassFile {int magic; lyhyt pääversio; lyhyt molliversio; ConstantPoolInfo constantPool []; lyhyt pääsyLiput; ConstantPoolInfo thisClass; ConstantPoolInfo superClass; ConstantPoolInfo-liitännät []; FieldInfo-kentät []; MethodInfo-menetelmät []; AttributeInfo-määritteet []; totuusarvo onValidClass = false; julkinen staattinen lopullinen int ACC_PUBLIC = 0x1; julkinen staattinen lopullinen int ACC_PRIVATE = 0x2; julkinen staattinen lopullinen int ACC_PROTECTED = 0x4; julkinen staattinen lopullinen int ACC_STATIC = 0x8; julkinen staattinen lopullinen int ACC_FINAL = 0x10; julkinen staattinen lopullinen int ACC_SYNCHRONIZED = 0x20; julkinen staattinen lopullinen int ACC_THREADSAFE = 0x40; julkinen staattinen lopullinen int ACC_TRANSIENT = 0x80; julkinen staattinen lopullinen int ACC_NATIVE = 0x100; julkinen staattinen lopullinen int ACC_INTERFACE = 0x200; julkinen staattinen lopullinen int ACC_ABSTRACT = 0x400; 

Kuten näette, luokan esiintymämuuttujat LuokkaTiedosto määritellä Java-luokan tiedoston pääkomponentit. Erityisesti Java-luokan tiedoston keskeinen tietorakenne tunnetaan vakiopoolina. Muut mielenkiintoiset luokkatiedoston palat saavat omat luokat: MethodInfo menetelmiä varten, FieldInfo kentille (jotka ovat luokan muuttujan ilmoituksia), AttributeInfo pitää sisällään luokkatiedostoattribuutit ja vakiojoukko, joka otettiin suoraan luokkatiedostojen spesifikaatiosta erilaisten kenttien, menetelmien ja luokkailmoitusten muokkaajien dekoodaamiseksi.

Tämän luokan ensisijainen menetelmä on lukea, jota käytetään luokkatiedoston lukemiseen levyltä ja uuden luomiseen LuokkaTiedosto esimerkiksi tiedoista. Koodin lukea menetelmä on esitetty alla. Olen leikannut kuvauksen koodin kanssa, koska menetelmä on yleensä melko pitkä.

1 julkinen looginen luettu (InputStream sisään) 2 heittää IOException {3 DataInputStream di = new DataInputStream (in); 4 int-laskenta; 5 6 taikuutta = di.readInt (); 7 if (taika! = (Int) 0xCAFEBABE) {8 palaa (väärä); 9} 10 11 majorVersion = di.readShort (); 12 minorVersion = di.readShort (); 13 määrä = di.readShort (); 14 constantPool = uusi ConstantPoolInfo [count]; 15 if (virheenkorjaus) 16 System.out.println ("read (): Lue otsikko ..."); 17 constantPool [0] = uusi ConstantPoolInfo (); 18 for (int i = 1; i <vakioPool.length; i ++) {19 vakioPool [i] = uusi ConstantPoolInfo (); 20 if (! ConstantPool [i]. Read (di)) {21 return (false); 22} 23 // Nämä kaksi tyyppiä vievät "kaksi" kohtaa taulukossa 24 if ((constantPool [i] .type == ConstantPoolInfo.LONG) || 25 (constantPool [i] .type == ConstantPoolInfo.DOUBLE)) 26 i ++; 27} 

Kuten näette, yllä oleva koodi alkaa käärimällä ensin a DataInputStream muuttujan viittaaman tulovirran ympärillä sisään. Riveillä 6 - 12 on lisäksi kaikki tarvittavat tiedot sen määrittämiseksi, että koodi todella etsii kelvollista luokkatiedostoa. Nämä tiedot koostuvat maagisesta "evästeestä" 0xCAFEBABE ja versionumeroista 45 ja 3 pää- ja pieniarvoista. Seuraavaksi rivillä 13 - 27 vakiopooli luetaan joukoksi ConstantPoolInfo esineitä. Lähdekoodi ConstantPoolInfo on huomaamaton - se vain lukee tietoja ja tunnistaa ne tyypin perusteella. Myöhemmin vakioaltaan elementtejä käytetään luokan tietojen näyttämiseen.

Edellä olevan koodin jälkeen lukea method skannaa vakiopoolin uudelleen ja "korjaa" vakiopoolin viitteet, jotka viittaavat muihin vakiopoolin kohteisiin. Korjauskoodi näkyy alla. Tämä korjaus on välttämätöntä, koska viitteet ovat tyypillisesti hakemistoja vakiopooliin, ja on hyödyllistä, että nämä indeksit on jo ratkaistu. Tämä varmistaa myös, että lukija tietää, että luokkatiedosto ei ole vioittunut jatkuvalla poolitasolla.

28 (int i = 1; i 0) 32: lle vakio-allas [i] .arg1 = vakio-allas [vakio-allas [i] indeksi1]; 33 if (vakioPool [i] .index2> 0) 34 vakioPool [i] .arg2 = vakioPool [vakioPool [i] .index2]; 35} 36 37 if (dumpConstants) {38 arvolle (int i = 1; i <vakioUima.pituus; i ++) {39 System.out.println ("C" + i + "-" + vakioPool [i]); 30} 31} 

Yllä olevassa koodissa kukin vakiopoolimerkintä käyttää indeksiarvoja selvittääkseen viittauksen toiseen vakiopoolimerkintään. Kun se on valmis rivillä 36, koko allas valinnaisesti jätetään pois.

Kun koodi on skannattu vakioalueen ohi, luokkatiedosto määrittelee ensisijaisen luokan tiedot: sen luokan nimen, yliluokan nimen ja käyttöliittymät. lukea koodi skannaa nämä arvot alla esitetyllä tavalla.

32 accessFlags = di.readShort (); 33 34 thisClass = vakio-allas [di.readShort ()]; 35 superluokka = vakio-allas [di.readShort ()]; 36 if (virheenkorjaus) 37 System.out.println ("read (): Lue luokan tiedot ..."); 38 39 / * 30 * Tunnista kaikki tämän luokan toteuttamat rajapinnat 31 * / 32 count = di.readShort (); 33 if (count! = 0) {34 if (debug) 35 System.out.println ("Luokan toteutus" + laskenta + "rajapinnat"); 36 rajapintaa = new ConstantPoolInfo [count]; 37 (int i = 0; i <count; i ++) {38 int iindex = di.readShort (); 39 if ((iindex constantPool.length - 1)) 40 return (väärä); 41 rajapintaa [i] = vakio-allas [iindex]; 42 if (virheenkorjaus) 43 System.out.println ("I" + i + ":" + rajapinnat [i]); 44} 45} 46 if (virheenkorjaus) 47 System.out.println ("read (): Lue käyttöliittymän tiedot ..."); 

Kun tämä koodi on valmis, lukea method on rakentanut melko hyvän kuvan luokan rakenteesta. Ainoa mitä on jäljellä, on kerätä kenttämääritykset, metodimääritykset ja, mikä tärkeintä, luokkatiedoston määritteet.

Luokkatiedostomuoto jakaa nämä kolme ryhmää osaan, joka koostuu luvusta, jota seuraa etsittävän määrän esineitä. Joten kenttien osalta luokkatiedostossa on määriteltyjen kenttien määrä ja sitten monet kenttämääritykset. Kentissä skannattava koodi näkyy alla.

48 count = di.readShort (); 49 if (debug) 50 System.out.println ("Tässä luokassa on" + count + "kenttiä."); 51 if (count! = 0) {52 kenttää = uusi FieldInfo [count]; 53 for (int i = 0; i <count; i ++) {54 kenttää [i] = uusi FieldInfo (); 55 if (! Kentät [i]. Lue (di, constantPool)) {56 return (väärä); 57} 58 if (virheenkorjaus) 59 System.out.println ("F" + i + ":" + 60 kenttää [i] .toString (constantPool)); 61} 62} 63 if (virheenkorjaus) 64 System.out.println ("read (): Lue kentän tiedot ..."); 

Yllä oleva koodi alkaa lukemalla määrä riviltä # 48, sitten vaikka luku ei ole nolla, se lukee uudet kentät käyttämällä FieldInfo luokassa. FieldInfo luokka yksinkertaisesti täyttää tiedot, jotka määrittelevät kentän Java-virtuaalikoneelle. Menetelmien ja määritteiden lukemiseen tarkoitettu koodi on sama, yksinkertaisesti korvaamalla viitteet FieldInfo viitteillä MethodInfo tai AttributeInfo tarvittaessa. Lähde ei sisälly tähän, mutta voit tarkastella lähdettä alla olevien Resurssit-osion linkkien avulla.