Programmering

Ta en titt i Java-klasser

Velkommen til denne månedens del av "Java In Depth." En av de tidligste utfordringene for Java var om det kunne stå som et dyktig "systemspråk". Roten til spørsmålet involverte Javas sikkerhetsfunksjoner som hindrer en Java-klasse i å kjenne til andre klasser som kjører ved siden av den i den virtuelle maskinen. Denne evnen til å "se på innsiden" klassene kalles introspeksjon. I den første offentlige Java-utgivelsen, kjent som Alpha3, kunne de strenge språkreglene om synlighet av de interne komponentene i en klasse omgåes, selv om bruken av ObjectScope klasse. Så, under beta, når ObjectScope ble fjernet fra kjøretiden på grunn av sikkerhetsproblemer, mange erklærte Java for å være uegnet for "seriøs" utvikling.

Hvorfor er introspeksjon nødvendig for at et språk skal betraktes som et "systemspråk"? En del av svaret er ganske dagligdags: Å komme fra "ingenting" (det vil si en ikke-initialisert VM) til "noe" (det vil si en Java-klasse som kjører) krever at en del av systemet kan inspisere klassene som skal være løp for å finne ut hva du skal gjøre med dem. Det kanoniske eksemplet på dette problemet er ganske enkelt følgende: "Hvordan begynner et program, skrevet på et språk som ikke kan se" inne "i en annen språkkomponent, å utføre den første språkkomponenten, som er utgangspunktet for utførelsen for alle andre komponenter? "

Det er to måter å håndtere introspeksjon i Java: klasse filinspeksjon og den nye refleksjon API som er en del av Java 1.1.x. Jeg vil dekke begge teknikkene, men i denne kolonnen vil jeg fokusere på førsteklasses filinspeksjon. I en fremtidig spalte vil jeg se på hvordan refleksjon API løser dette problemet. (Koblinger til fullstendig kildekode for denne kolonnen er tilgjengelig i Ressurser-delen.)

Se dypt inn i filene mine ...

I 1.0.x-utgivelsene av Java er en av de største vortene på Java-kjøretiden måten Java-kjørbarheten starter et program på. Hva er problemet? Utførelse overføres fra domenet til vertsoperativsystemet (Win 95, SunOS og så videre) til domenet til den virtuelle Java-maskinen. Skriv linjen "java MyClass arg1 arg2"setter i gang en serie hendelser som er helt hardkodet av Java-tolk.

Som den første hendelsen laster operativsystemets kommandoskall Java-tolk og sender den strengen "MyClass arg1 arg2" som argument. Den neste hendelsen inntreffer når Java-tolk prøver å finne en klasse som heter Klassen min i et av katalogene som er identifisert i klassebanen. Hvis klassen blir funnet, er den tredje hendelsen å finne en metode inne i klassen som heter hoved-, hvis signatur har modifikatorene "offentlig" og "statisk" og som tar en rekke String objekter som argument. Hvis denne metoden blir funnet, konstrueres en urtråd og metoden påberopes. Java-tolken konverterer deretter "arg1 arg2" til en rekke strenger. Når denne metoden er påkalt, er alt annet rent Java.

Dette er vel og bra, bortsett fra at hoved- metoden må være statisk fordi kjøretiden ikke kan påberope den med et Java-miljø som ikke eksisterer ennå. Videre må den første metoden navngis hoved- fordi det ikke er noen måte å fortelle tolken metodens navn på kommandolinjen. Selv om du fortalte tolken navnet på metoden, er det ikke noen generell måte å finne ut om den var i klassen du i utgangspunktet hadde nevnt. Endelig, fordi hoved- metoden er statisk, du kan ikke erklære den i et grensesnitt, og det betyr at du ikke kan spesifisere et grensesnitt som dette:

offentlig grensesnitt Application {public void main (String args []); } 

Hvis grensesnittet ovenfor ble definert, og klasser implementerte det, kan du i det minste bruke tilfelle av operatør i Java for å avgjøre om du hadde et program eller ikke, og dermed avgjøre om det var egnet for å påkalle fra kommandolinjen. Poenget er at du ikke kan (definere grensesnittet), det var det ikke (innebygd i Java-tolk), og så kan du ikke (avgjøre om en klassefil er et program lett). Så hva kan du gjøre?

Egentlig kan du gjøre ganske mye hvis du vet hva du skal se etter og hvordan du bruker den.

Dekompilering av klassefiler

Java-klassefilen er arkitekturnøytral, noe som betyr at den er det samme settet med bits, enten den er lastet fra en Windows 95-maskin eller en Sun Solaris-maskin. Det er også veldig godt dokumentert i boka Java Virtual Machine Specification av Lindholm og Yellin. Klassens filstruktur var delvis designet for å enkelt kunne lastes inn i SPARC-adresserommet. I utgangspunktet kan klassefilen være kartlagt i det virtuelle adresserommet, så fikser de relative pekerne inne i klassen, og presto! Du hadde umiddelbar klassestruktur. Dette var mindre nyttig på Intel-arkitekturmaskiner, men arven etterlot klassefilformatet lett å forstå, og enda lettere å bryte ned.

Sommeren 1994 jobbet jeg i Java-gruppen og bygde det som er kjent som en "minst privilegium" sikkerhetsmodell for Java. Jeg hadde akkurat funnet ut at det jeg virkelig ønsket å gjøre var å se inn i en Java-klasse, avgifte de brikkene som ikke var tillatt av det nåværende privilegienivået, og deretter laste resultatet gjennom en tilpasset klasselaster. Det var da jeg oppdaget at det ikke var noen klasser i hovedkjøringstiden som visste om konstruksjon av klassefiler. Det var versjoner i kompilatorklassetreet (som måtte generere klassefiler fra den kompilerte koden), men jeg var mer interessert i å bygge noe for å manipulere eksisterende klassefiler.

Jeg startet med å bygge en Java-klasse som kunne spalte en Java-klassefil som ble presentert for den på en inngangsstrøm. Jeg ga det det mindre enn originale navnet ClassFile. Begynnelsen på denne klassen er vist nedenfor.

offentlig klasse ClassFile {int magi; kort majorversjon; kort mindreversjon; ConstantPoolInfo constantPool []; kort tilgangFlagger; ConstantPoolInfo thisClass; ConstantPoolInfo superklasse; ConstantPoolInfo grensesnitt []; FieldInfo felt []; MethodInfo metoder []; AttributeInfo attributter []; boolsk isValidClass = false; offentlig statisk sluttint ACC_PUBLIC = 0x1; offentlig statisk endelig int ACC_PRIVATE = 0x2; offentlig statisk slutt int ACC_PROTECTED = 0x4; offentlig statisk sluttint ACC_STATIC = 0x8; offentlig statisk sluttint ACC_FINAL = 0x10; offentlig statisk sluttint ACC_SYNCHRONIZED = 0x20; offentlig statisk sluttint ACC_THREADSAFE = 0x40; offentlig statisk slutt int ACC_TRANSIENT = 0x80; offentlig statisk sluttint ACC_NATIVE = 0x100; offentlig statisk sluttint ACC_INTERFACE = 0x200; offentlig statisk sluttint ACC_ABSTRACT = 0x400; 

Som du kan se, forekomstvariablene for klassen ClassFile definere hovedkomponentene i en Java-klassefil. Spesielt er den sentrale datastrukturen for en Java-klassefil kjent som det konstante bassenget. Andre interessante biter av klassefilen får egne klasser: MethodInfo for metoder, FieldInfo for felt (som er variabeldeklarasjonene i klassen), AttributeInfo å ha klassefilattributter, og et sett med konstanter som ble hentet direkte fra spesifikasjonen på klassefiler for å dekode de forskjellige modifikatorene som gjelder felt-, metode- og klassedeklarasjoner.

Den primære metoden i denne klassen er lese, som brukes til å lese en klassefil fra disk og opprette en ny ClassFile forekomst fra dataene. Koden for lese metoden er vist nedenfor. Jeg har ispedd beskrivelsen med koden siden metoden har en tendens til å være ganske lang.

1 offentlig boolsk lesing (InputStream in) 2 kaster IOException {3 DataInputStream di = ny DataInputStream (in); 4 int teller; 5 6 magi = di.readInt (); 7 hvis (magi! = (Int) 0xCAFEBABE) {8 retur (usann); 9} 10 11 majorVersion = di.readShort (); 12 mindre versjon = di.readShort (); 13 count = di.readShort (); 14 constantPool = ny ConstantPoolInfo [count]; 15 hvis (feilsøking) 16 System.out.println ("les (): Les overskrift ..."); 17 constantPool [0] = ny ConstantPoolInfo (); 18 for (int i = 1; i <constantPool.length; i ++) {19 constantPool [i] = ny ConstantPoolInfo (); 20 if (! ConstantPool [i] .read (di)) {21 return (false); 22} 23 // Disse to typene tar opp "to" flekker i tabellen 24 hvis ((constantPool [i] .type == ConstantPoolInfo.LONG) || 25 (constantPool [i] .type == ConstantPoolInfo.DOUBLE)) 26 i ++; 27} 

Som du kan se, begynner koden ovenfor med å først pakke inn a DataInputStream rundt inngangsstrømmen det refereres til av variabelen i. Videre, i linjene 6 til 12, er all informasjon som er nødvendig for å bestemme at koden faktisk ser på en gyldig klassefil, til stede. Denne informasjonen består av den magiske "cookien" 0xCAFEBABE, og versjon nummer 45 og 3 for henholdsvis de store og mindre verdiene. Deretter leses det konstante bassenget i linje 13 til 27 i en rekke ConstantPoolInfo gjenstander. Kildekoden til ConstantPoolInfo er lite bemerkelsesverdig - den leser ganske enkelt inn data og identifiserer dem basert på typen. Senere elementer fra det konstante bassenget brukes til å vise informasjon om klassen.

Etter koden ovenfor, vil lese metoden skanner det konstante bassenget på nytt og "fikser opp" referanser i det konstante bassenget som refererer til andre elementer i det konstante bassenget. Opprettingskoden er vist nedenfor. Denne reparasjonen er nødvendig siden referansene vanligvis er indekser i den konstante puljen, og det er nyttig å ha disse indeksene allerede løst. Dette gir også en sjekk for leseren om at klassefilen ikke er skadet på konstant bassengnivå.

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} 

I koden ovenfor bruker hver konstante oppføring indeksverdiene til å finne ut referansen til en annen konstant oppføring. Når du er ferdig i linje 36, blir hele bassenget dumpet ut.

Når koden har skannet forbi det konstante bassenget, definerer klassefilen den primære klasseinformasjonen: klassens navn, superklassens navn og implementeringsgrensesnitt. De lese kode skanner etter disse verdiene som vist nedenfor.

32 accessFlags = di.readShort (); 33 34 thisClass = constantPool [di.readShort ()]; 35 superClass = constantPool [di.readShort ()]; 36 if (feilsøking) 37 System.out.println ("les (): Les klasseinfo ..."); 38 39 / * 30 * Identifiser alle grensesnittene implementert av denne klasse 31 * / 32 count = di.readShort (); 33 if (count! = 0) {34 if (debug) 35 System.out.println ("Klasse implementerer" + count + "grensesnitt."); 36 grensesnitt = nye ConstantPoolInfo [count]; 37 for (int i = 0; i <count; i ++) {38 int iindex = di.readShort (); 39 hvis ((iindex constantPool.length - 1)) 40 returnerer (false); 41 grensesnitt [i] = constantPool [iindex]; 42 hvis (feilsøking) 43 System.out.println ("I" + i + ":" + grensesnitt [i]); 44} 45} 46 if (feilsøking) 47 System.out.println ("les (): Les informasjon om grensesnitt ..."); 

Når denne koden er fullført, vil lese metoden har bygget opp en ganske god ide om klassens struktur. Alt som gjenstår er å samle feltdefinisjonene, metodedefinisjonene, og, kanskje viktigst, klassefilattributtene.

Klassefilformatet deler hver av disse tre gruppene i en seksjon som består av et tall, etterfulgt av det antall forekomster av tingen du leter etter. Så for felt har klassefilen antall definerte felt, og så mange feltdefinisjoner. Koden som skal skannes i feltene er vist nedenfor.

48 teller = di.readShort (); 49 hvis (feilsøking) 50 System.out.println ("Denne klassen har" + count + "felt."); 51 if (count! = 0) {52 field = new FieldInfo [count]; 53 for (int i = 0; i <count; i ++) {54 felt [i] = ny FieldInfo (); 55 hvis (! Felt [i] .les (di, constantPool)) {56 retur (usann); 57} 58 if (feilsøking) 59 System.out.println ("F" + i + ":" + 60 felt [i] .toString (constantPool)); 61} 62} 63 if (feilsøking) 64 System.out.println ("les (): Les feltinfo ..."); 

Ovennevnte kode starter med å lese en telling i linje # 48, og mens tellingen ikke er null, leser den i nye felt ved hjelp av FieldInfo klasse. De FieldInfo klasse fyller ganske enkelt ut data som definerer et felt til den virtuelle Java-maskinen. Koden for å lese metoder og attributter er den samme, bare å erstatte referansene til FieldInfo med referanser til MethodInfo eller AttributeInfo som hensiktsmessig. Denne kilden er ikke inkludert her, men du kan se på kilden ved hjelp av koblingene i Ressurser-delen nedenfor.

$config[zx-auto] not found$config[zx-overlay] not found