Programmering

Se inden for Java-klasser

Velkommen til denne måneds rate af "Java In Depth." En af de tidligste udfordringer for Java var, hvorvidt det kunne stå som et dygtigt "system" -sprog. Spørgsmålets rod involverede Java's sikkerhedsfunktioner, der forhindrer en Java-klasse i at kende andre klasser, der kører ved siden af ​​den på den virtuelle maskine. Denne evne til at "se inde" i klasserne kaldes introspektion. I den første offentlige Java-udgivelse, kendt som Alpha3, kunne de strenge sprogregler vedrørende synlighed af de interne komponenter i en klasse omgås, selvom brugen af ObjectScope klasse. Derefter under beta, hvornår ObjectScope blev fjernet fra kørselstiden på grund af sikkerhedsproblemer, erklærede mange mennesker, at Java var uegnet til "seriøs" udvikling.

Hvorfor er introspektion nødvendig for at et sprog kan betragtes som et "systemsprog"? Én del af svaret er ret dagligdags: At komme fra "intet" (dvs. en uinitialiseret VM) til "noget" (dvs. en kørende Java-klasse) kræver, at en del af systemet er i stand til at inspicere de klasser, der skal være løb for at finde ud af, hvad man skal gøre med dem. Det kanoniske eksempel på dette problem er simpelthen følgende: "Hvordan begynder et program, skrevet på et sprog, der ikke kan se 'inde' i en anden sprogkomponent, at udføre den første sprogkomponent, som er startpunktet for udførelsen af ​​alle andre komponenter? "

Der er to måder at håndtere introspektion i Java: klasse filinspektion og den nye refleksions-API, der er en del af Java 1.1.x. Jeg dækker begge teknikker, men i denne kolonne vil jeg fokusere på førsteklasses filinspektion. I en fremtidig kolonne vil jeg se på, hvordan refleksions-API løser dette problem. (Links til den komplette kildekode til denne kolonne er tilgængelige i afsnittet Ressourcer.)

Se dybt ned i mine filer ...

I 1.0.x-udgivelserne af Java er en af ​​de største vorter på Java-kørselstiden den måde, hvorpå Java-eksekverbarheden starter et program. Hvad er problemet? Udførelse overføres fra domænet i værtsoperativsystemet (Win 95, SunOS osv.) Til domænet på den virtuelle Java-maskine. At skrive linjen "java MyClass arg1 arg2"sætter i gang en række begivenheder, der er hårdkodet af Java-tolk.

Som den første begivenhed indlæser operativsystemets kommandoskal Java-tolk og sender den strengen "MyClass arg1 arg2" som sit argument. Den næste begivenhed opstår, når Java-tolk forsøger at finde en klasse, der hedder Min klasse i et af de mapper, der er identificeret i klassestien. Hvis klassen findes, er den tredje begivenhed at finde en metode inden for den navngivne klasse vigtigste, hvis signatur har modifikatorerne "offentlig" og "statisk", og som tager en række af Snor objekter som argument. Hvis denne metode findes, konstrueres en urtråd, og metoden påberåbes. Java-tolken konverterer derefter "arg1 arg2" til en række strenge. Når denne metode er påberåbt, er alt andet rent Java.

Dette er alt godt og godt bortset fra at vigtigste metoden skal være statisk, fordi køretiden ikke kan påberåbe sig det med et Java-miljø, der ikke eksisterer endnu. Desuden skal den første metode navngives vigtigste fordi der ikke er nogen måde at fortælle tolken metodens navn på kommandolinjen. Selvom du fortæller tolken navnet på metoden, er der ingen generel måde at finde ud af, om den var i den klasse, du i første omgang havde nævnt. Endelig, fordi vigtigste metoden er statisk, du kan ikke erklære den i en grænseflade, og det betyder, at du ikke kan angive en grænseflade som denne:

offentlig grænseflade Application {public void main (String args []); } 

Hvis ovenstående interface blev defineret, og klasser implementerede det, kunne du i det mindste bruge forekomst af operatør i Java for at afgøre, om du havde et program eller ej, og dermed afgøre, om det var egnet til at påkalde fra kommandolinjen. Bundlinjen er, at du ikke kan (definere grænsefladen), det var det ikke (indbygget i Java-tolk), og så kan du ikke (afgøre, om en klassefil er et program let). Så hvad kan du gøre?

Faktisk kan du gøre en hel del, hvis du ved, hvad du skal kigge efter, og hvordan du bruger det.

Dekompilering af klassefiler

Java-klassefilen er arkitekturneutral, hvilket betyder, at det er det samme sæt bits, uanset om det indlæses fra en Windows 95-maskine eller en Sun Solaris-maskine. Det er også meget godt dokumenteret i bogen Specifikationen for Java Virtual Machine af Lindholm og Yellin. Klassens filstruktur blev til dels designet til let at blive indlæst i SPARC-adresseområdet. Dybest set kunne klassefilen kortlægges i det virtuelle adresserum, hvorefter de relative markører inde i klassen fikseres op og presto! Du havde øjeblikkelig klassestruktur. Dette var mindre nyttigt på Intel-arkitekturs maskiner, men arven efterlod klassens filformat let at forstå og endnu lettere at nedbryde.

I sommeren 1994 arbejdede jeg i Java-gruppen og byggede det, der er kendt som en "mindst privilegium" sikkerhedsmodel for Java. Jeg var lige færdig med at finde ud af, at hvad jeg virkelig ville gøre, var at se inde i en Java-klasse, afskære de stykker, der ikke var tilladt af det aktuelle privilegieniveau, og derefter indlæse resultatet gennem en brugerdefineret klasselæsser. Det var da, jeg opdagede, at der ikke var nogen klasser i hovedkørselstiden, der vidste om konstruktionen af ​​klassefiler. Der var versioner i kompilatortræet (som skulle generere klassefiler fra den kompilerede kode), men jeg var mere interesseret i at opbygge noget til at manipulere allerede eksisterende klassefiler.

Jeg startede med at opbygge en Java-klasse, der kunne nedbryde en Java-klassefil, der blev præsenteret for den på en inputstrøm. Jeg gav det det mindre end originale navn ClassFile. Begyndelsen på denne klasse er vist nedenfor.

offentlig klasse ClassFile {int magi; kort større version; kort mindre version ConstantPoolInfo constantPool []; kort adgangFlags; ConstantPoolInfo thisClass; ConstantPoolInfo superklasse; ConstantPoolInfo grænseflader []; FieldInfo felter []; MethodInfo-metoder []; AttributeInfo attributter []; boolsk isValidClass = false; offentlig statisk endelig int ACC_PUBLIC = 0x1; offentlig statisk endelig int ACC_PRIVATE = 0x2; offentlig statisk endelig int ACC_PROTECTED = 0x4; offentlig statisk endelig int ACC_STATIC = 0x8; offentlig statisk endelig int ACC_FINAL = 0x10; offentlig statisk endelig int ACC_SYNCHRONIZED = 0x20; offentlig statisk endelig int ACC_THREADSAFE = 0x40; offentlig statisk endelig int ACC_TRANSIENT = 0x80; offentlig statisk endelig int ACC_NATIVE = 0x100; offentlig statisk endelig int ACC_INTERFACE = 0x200; offentlig statisk endelig int ACC_ABSTRACT = 0x400; 

Som du kan se, forekommer variablerne for klasse ClassFile definere hovedkomponenterne i en Java-klassefil. Især er den centrale datastruktur for en Java-klassefil kendt som den konstante pool. Andre interessante klumper af klassefiler får deres egne klasser: MethodInfo til metoder, FieldInfo for felter (som er variabeldeklarationerne i klassen), AttributInfo at holde klasse filattributter og et sæt konstanter, der blev taget direkte fra specifikationen på klassefiler for at afkode de forskellige modifikatorer, der gælder for felt-, metode- og klassedeklarationer.

Den primære metode i denne klasse er Læs, som bruges til at læse en klassefil fra disken og oprette en ny ClassFile eksempel fra dataene. Koden til Læs metoden er vist nedenfor. Jeg har blandet beskrivelsen med koden, da metoden har tendens til at være temmelig lang.

1 offentlig boolsk læsning (InputStream in) 2 kaster IOException {3 DataInputStream di = ny DataInputStream (in); 4 int-antal 5 6 magi = di.readInt (); 7 hvis (magi! = (Int) 0xCAFEBABE) {8 returnerer (falsk); 9} 10 11 majorVersion = di.readShort (); 12 mindre version = di.readShort (); 13 tæller = di.readShort (); 14 constantPool = ny ConstantPoolInfo [count]; 15 hvis (debug) 16 System.out.println ("læs (): Læs 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 typer optager "to" pletter i tabellen 24 hvis ((constantPool [i] .type == ConstantPoolInfo.LONG) || 25 (constantPool [i] .type == ConstantPoolInfo.DOUBLE)) 26 i ++; 27} 

Som du kan se, begynder koden ovenfor med først at indpakke a DataInputStream omkring inputstrømmen, der er refereret til af variablen i. Yderligere er i linie 6 til 12 alle de nødvendige oplysninger til at bestemme, at koden rent faktisk ser på en gyldig klassefil, til stede. Denne information består af den magiske "cookie" 0xCAFEBABE, og version nummer 45 og 3 for henholdsvis de store og mindre værdier. Derefter læses den konstante pool i linie 13 til 27 i en række af ConstantPoolInfo genstande. Kildekoden til ConstantPoolInfo er umærkelig - den læser simpelthen ind data og identificerer dem ud fra deres type. Senere elementer fra den konstante pool bruges til at vise oplysninger om klassen.

Efter ovenstående kode, Læs metode scanner den konstante pool igen og "retter" referencer i den konstante pool, der henviser til andre elementer i den konstante pool. Fix-up-koden er vist nedenfor. Denne fix-up er nødvendig, da referencerne typisk er indekser i den konstante pool, og det er nyttigt at have disse indekser allerede løst. Dette giver også en kontrol for læseren at vide, at klassefilen ikke er korrupt på det konstante poolniveau.

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 hvis (dumpConstants) {38 for (int i = 1; i <constantPool.length; i ++) {39 System.out.println ("C" + i + "-" + constantPool [i]); 30} 31} 

I ovenstående kode bruger hver konstante poolindgang indeksværdierne til at finde ud af henvisningen til en anden konstant poolindgang. Når den er færdig i linje 36, dumpes hele poolen eventuelt.

Når koden er scannet forbi den konstante pool, definerer klassefilen den primære klasseinformation: dens klassenavn, superklassens navn og implementeringsgrænseflader. Det Læs kode scanner efter disse værdier som vist nedenfor.

32 accessFlags = di.readShort (); 33 34 thisClass = constantPool [di.readShort ()]; 35 superClass = constantPool [di.readShort ()]; 36 if (debug) 37 System.out.println ("læs (): Læs klasseinfo ..."); 38 39 / * 30 * Identificer alle de grænseflader, der er implementeret i denne klasse 31 * / 32 count = di.readShort (); 33 if (count! = 0) {34 if (debug) 35 System.out.println ("Klasse implementerer" + count + "interfaces."); 36 grænseflader = ny 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 grænseflader [i] = constantPool [iindex]; 42 if (debug) 43 System.out.println ("I" + i + ":" + grænseflader [i]); 44} 45} 46 if (debug) 47 System.out.println ("læs (): Læs grænsefladeinformation ..."); 

Når denne kode er færdig, Læs metoden har opbygget en ret god idé om klassens struktur. Det eneste, der er tilbage, er at samle feltdefinitionerne, metodedefinitionerne og måske vigtigst af alt at filfilens attributter.

Klassefilformatet opdeler hver af disse tre grupper i en sektion bestående af et tal efterfulgt af det antal forekomster af den ting, du leder efter. Så for felter har klassefilen antallet af definerede felter og derefter så mange feltdefinitioner. Koden, der skal scannes i felterne, vises nedenfor.

48 optælling = di.readShort (); 49 if (debug) 50 System.out.println ("Denne klasse har" + count + "felter."); 51 if (count! = 0) {52 fields = new FieldInfo [count]; 53 for (int i = 0; i <count; i ++) {54 felter [i] = ny FieldInfo (); 55 hvis (! Felter [i] .læs (di, konstantPool)) {56 retur (falsk); 57} 58 if (debug) 59 System.out.println ("F" + i + ":" + 60 felter [i] .toString (constantPool)); 61} 62} 63 if (debug) 64 System.out.println ("læs (): Læs feltinfo ..."); 

Ovenstående kode starter med at læse en optælling i linje # 48, og mens optællingen ikke er nul, læser den i nye felter ved hjælp af FieldInfo klasse. Det FieldInfo klasse udfylder simpelthen data, der definerer et felt til den virtuelle Java-maskine. Koden til at læse metoder og attributter er den samme ved blot at erstatte referencerne til FieldInfo med henvisninger til MethodInfo eller AttributeInfo som passende. Denne kilde er ikke inkluderet her, men du kan se på kilden ved hjælp af linkene i afsnittet Ressourcer nedenfor.