Programmering

Grundlæggende om Java-klasselæssere

Klasselæsserkonceptet, en af ​​hjørnestenene i den virtuelle Java-maskine, beskriver adfærden ved at konvertere en navngivet klasse til de bits, der er ansvarlige for implementeringen af ​​denne klasse. Da der findes klasselæssere, behøver Java-kørselstiden ikke at vide noget om filer og filsystemer, når Java-programmer køres.

Hvad klasselæssere gør

Klasser introduceres i Java-miljøet, når de henvises til ved navn i en klasse, der allerede kører. Der er lidt magi, der fortsætter med at få den første klasse i gang (det er derfor, du skal erklære hoved () metode som statisk, tager et strengarray som argument), men når denne klasse kører, udføres fremtidige forsøg på at indlæse klasser af klasselæsseren.

På sin enkleste måde opretter en klasselæsser et fladt navneområde for klassekroppe, der er henvist til med et strengnavn. Metodedefinitionen er:

Klasse r = loadClass (String className, boolsk resolutionIt); 

Variablen className indeholder en streng, der forstås af klasselæsseren og bruges til entydigt at identificere en klasseimplementering. Variablen løse det er et flag, der fortæller klasselæsseren, at klasser, der refereres til med dette klassenavn, skal løses (det vil sige, at enhver refereret klasse også skal indlæses).

Alle virtuelle Java-maskiner inkluderer en klasselæsser, der er indlejret i den virtuelle maskine. Denne indlejrede læsser kaldes primordial class loader. Det er noget specielt, fordi den virtuelle maskine antager, at den har adgang til et lager af pålidelige klasser som kan køres af VM uden verifikation.

Den primordial class loader implementerer standardimplementeringen af loadClass (). Således forstår denne kode, at klassens navn java.lang.Objekt gemmes i en fil med præfikset java / lang / Object.class et eller andet sted i klassestien. Denne kode implementerer også både klassesøgning og at kigge i zip-filer til klasser. Det rigtig seje ved den måde, dette er designet på, er at Java kan ændre sin klasselagringsmodel ved blot at ændre det sæt funktioner, der implementerer klasselæsseren.

Graver rundt i tarmen på den virtuelle Java-maskine, vil du opdage, at den oprindelige klasselæsser primært er implementeret i funktionerne FindClassFromClass og ResolveClass.

Så hvornår indlæses klasser? Der er nøjagtigt to tilfælde: når den nye bytecode udføres (f.eks. FooClassf = nyt FooClass ();) og når bytekoderne refererer til en statisk klasse til en klasse (f.eks. System.ud).

En ikke-primordial klasselæsser

"Og hvad så?" spørger du måske.

Den virtuelle Java-maskine har kroge i sig, så en brugerdefineret klasselæsser kan bruges i stedet for den oprindelige. Da brugerklasselæsseren får den første knæk på klassens navn, er brugeren desuden i stand til at implementere et vilkårligt antal interessante klasselagre, ikke mindst HTTP-servere - som i første omgang fik Java fra jorden.

Der er dog en pris, fordi klasselæsseren er så kraftig (for eksempel kan den erstatte java.lang.Objekt med sin egen version), er Java-klasser som applets ikke tilladt at instantiere deres egne loadere. (Dette håndhæves forresten af ​​klasselæsseren.) Denne kolonne vil ikke være nyttig, hvis du prøver at gøre disse ting med en applet, kun med et program, der kører fra det betroede klasselager (såsom lokale filer).

En brugerklasselæsser får chancen for at indlæse en klasse, før den oprindelige klasselæsser gør det. På grund af dette kan det indlæse klassens implementeringsdata fra en anden alternativ kilde, hvilket er, hvordan AppletClassLoader kan indlæse klasser ved hjælp af HTTP-protokollen.

Opbygning af en SimpleClassLoader

En klasselæsser starter med at være en underklasse af java.lang.ClassLoader. Den eneste abstrakte metode, der skal implementeres, er loadClass (). Strømmen af loadClass () er som følgende:

  • Bekræft holdets navn.
  • Kontroller, om den ønskede klasse allerede er indlæst.
  • Kontroller, om klassen er en "system" -klasse.
  • Forsøg at hente klassen fra denne klasselæssers lager.
  • Definer klassen til VM.
  • Løs klassen.
  • Returner klassen til den, der ringer op.

SimpleClassLoader vises som følger med beskrivelser af, hvad det gør ispolet med koden.

 offentlig synkroniseret Class loadClass (String className, boolean resolutionIt) kaster ClassNotFoundException {Class result; byte classData []; System.out.println (">>>>>> Load class:" + className); / * Tjek vores lokale cache af klasser * / resultat = (Klasse) klasser. Get (className); hvis (resultat! = null) {System.out.println (">>>>>> returnerer cachelagret resultat."); returresultat } 

Koden ovenfor er den første sektion af loadClass metode. Som du kan se, tager det et klassenavn og søger i en lokal hash-tabel, som vores klasselæsser opretholder af klasser, den allerede har returneret. Det er vigtigt at holde dette hashbord rundt siden dig skal returner den samme klasseobjektreference for det samme klassenavn hver gang du bliver bedt om det. Ellers vil systemet tro, at der er to forskellige klasser med samme navn og vil kaste et ClassCastException når du tildeler en objektreference mellem dem. Det er også vigtigt at holde en cache, fordi loadClass () metode kaldes rekursivt, når en klasse løses, og du bliver nødt til at returnere det cachelagrede resultat i stedet for at jage det ned til en anden kopi.

/ * Tjek med primordial class loader * / prøv {result = super.findSystemClass (className); System.out.println (">>>>>> returnerende systemklasse (i CLASSPATH)."); returresultat } fange (ClassNotFoundException e) {System.out.println (">>>>>> Ikke en systemklasse."); } 

Som du kan se i koden ovenfor, er det næste trin at kontrollere, om den oprindelige klasselæsser kan løse dette klassenavn. Denne kontrol er afgørende for både sundhed og sikkerhed i systemet. For eksempel, hvis du returnerer din egen forekomst af java.lang.Objekt til den, der ringer op, så deler dette objekt ingen fælles superklasse med noget andet objekt! Systemets sikkerhed kan blive kompromitteret, hvis din klasselæsser returnerede sin egen værdi af java.lang.SecurityManager, som ikke havde de samme kontroller som den rigtige havde.

 / * Prøv at indlæse det fra vores lager * / classData = getClassImplFromDataBase (className); hvis (classData == null) {kast ny ClassNotFoundException (); } 

Efter de indledende kontroller kommer vi til koden ovenfor, hvor den enkle klasselæsser får mulighed for at indlæse en implementering af denne klasse. Det SimpleClassLoader har en metode getClassImplFromDataBase () som i vores enkle eksempel blot forud for biblioteket "butik \" til klassens navn og tilføjer udvidelsen ".impl". Jeg valgte denne teknik i eksemplet, så der ikke var noget spørgsmål om, at den oprindelige klasselæsser skulle finde vores klasse. Bemærk, at sun.applet.AppletClassLoader præfikser kodebase-URL'en fra HTML-siden, hvor en applet lever til navnet, og derefter får en HTTP-anmodning om at hente bytekoder.

 / * Definer det (parse klassefilen) * / resultat = defineClass (classData, 0, classData.length); 

Hvis klasseimplementeringen blev indlæst, er det næstsidste trin at kalde defineClass () metode fra java.lang.ClassLoader, som kan betragtes som det første trin i klassificering. Denne metode er implementeret i den virtuelle Java-maskine og er ansvarlig for at kontrollere, at klassebytes er en juridisk Java-klassefil. Internt er den defineClass metode udfylder en datastruktur, som JVM bruger til at holde klasser. Hvis klassedataene er misdannede, forårsager dette opkald a ClassFormatError at blive kastet.

 if (resolutionIt) {resolClass (result); } 

Det sidste klasse læsserspecifikke krav er at ringe resolClass () hvis den boolske parameter løse det var sandt. Denne metode gør to ting: For det første får den alle klasser, der refereres eksplicit til denne klasse, indlæst, og der oprettes et prototypeobjekt til denne klasse; derefter påkalder det verifikatoren at foretage dynamisk verifikation af legitimiteten af ​​bytecodes i denne klasse. Hvis verifikationen mislykkes, kaster denne metodeopkald a LinkageError, hvor den mest almindelige er en VerifyError.

Bemærk, at for enhver klasse, du indlæser, løse det variabel vil altid være sand. Det er kun når systemet kalder rekursivt loadClass () at den kan indstille denne variabel falsk, fordi den ved, at den klasse, den beder om, allerede er løst.

 classes.put (className, result); System.out.println (">>>>>> Returnerer nyindlæst klasse."); returresultat } 

Det sidste trin i processen er at gemme den klasse, vi har indlæst og løst, i vores hash-tabel, så vi kan returnere den igen, hvis det er nødvendigt, og derefter returnere Klasse henvisning til den, der ringer op.

Selvfølgelig, hvis det var så simpelt, ville der ikke være meget mere at tale om. Der er faktisk to problemer, som bygherrer til klasselæssere skal beskæftige sig med, sikkerhed og tale med klasser, der er indlæst af den tilpassede klasselæsser.

Sikkerhedshensyn

Når du har en applikation, der indlæser vilkårlige klasser i systemet via din klasselæsser, er din applikations integritet i fare. Dette skyldes klasselæsserens styrke. Lad os tage et øjeblik på at se på en af ​​måderne, som en potentiel skurk kan bryde ind i din ansøgning, hvis du ikke er forsigtig.

I vores enkle klasselæsser, hvis den oprindelige klasselæsser ikke kunne finde klassen, indlæste vi den fra vores private lager. Hvad sker der, når dette arkiv indeholder klassen java.lang.FooBar ? Der er ingen klasse navngivet java.lang.FooBar, men vi kunne installere en ved at indlæse den fra klasselageret. Denne klasse i kraft af det faktum, at den ville have adgang til enhver pakkebeskyttet variabel i java.lang pakke, kan manipulere nogle følsomme variabler, så senere klasser kan undergrave sikkerhedsforanstaltninger. Derfor er et af opgaverne for enhver klasselæsser at beskyt systemnavnet.

I vores enkle klasselæsser kan vi tilføje koden:

 hvis (className.startsWith ("java.")) smider newClassNotFoundException (); 

lige efter opkaldet til findSystemClass over. Denne teknik kan bruges til at beskytte enhver pakke, hvor du er sikker på, at den indlæste kode aldrig har grund til at indlæse en ny klasse i en eller anden pakke.

Et andet risikoområde er, at det beståede navn skal være et bekræftet gyldigt navn. Overvej et fjendtligt program, der brugte klassenavnet ".. \ .. \ .. \ .. \ netscape \ temp \ xxx.class" som det klassenavn, det ønskede at blive indlæst. Det er klart, at hvis klasselæsseren simpelthen præsenterede dette navn for vores forenklede filsystemlæsser, kan det indlæse en klasse, som vores applikation faktisk ikke forventede. Før du søger i vores eget lager af klasser, er det således en god ide at skrive en metode, der verificerer integriteten af ​​dine klassenavne. Ring derefter til denne metode lige før du går til søgning i dit arkiv.

Brug af en grænseflade til at bygge bro over kløften

Det andet ikke-intuitive problem med at arbejde med klasselæssere er manglende evne til at kaste et objekt, der blev oprettet fra en indlæst klasse, til sin oprindelige klasse. Du skal kaste det returnerede objekt, fordi den typiske brug af en brugerdefineret klasselæsser er omtrent som:

 CustomClassLoader ccl = ny CustomClassLoader (); Objekt o; Klasse c; c = ccl.loadClass ("someNewClass"); o = c.newInstance (); ((SomeNewClass) o) .someClassMethod (); 

Du kan dog ikke caste o til SomeNewClass fordi kun den tilpassede klasselæsser "kender" til den nye klasse, den netop har indlæst.

Der er to grunde til dette. For det første betragtes klasserne i den virtuelle Java-maskine som castable, hvis de har mindst en fælles klassemarkør. Klasser, der er indlæst af to forskellige klasselæssere, har dog to forskellige klassemarkører og ingen klasser til fælles (undtagen java.lang.Objekt som regel). For det andet er ideen bag at have en brugerdefineret klasselæsser at indlæse klasser efter applikationen er distribueret, så applikationen ikke kender et priory om de klasser, den vil indlæse. Dette dilemma løses ved at give både applikationen og den indlæste klasse en klasse til fælles.

Der er to måder at oprette denne fælles klasse på, enten skal den indlæste klasse være en underklasse af en klasse, som applikationen har indlæst fra sit betroede lager, eller så skal den indlæste klasse implementere en grænseflade, der blev indlæst fra det betroede lager. På denne måde har den indlæste klasse og den klasse, der ikke deler det komplette navneområde for den tilpassede klasselæsser, en klasse til fælles. I eksemplet bruger jeg en interface, der hedder LocalModule, selvom du lige så let kunne gøre dette til en klasse og underklasse det.

Det bedste eksempel på den første teknik er en webbrowser. Den klasse, der er defineret af Java, der er implementeret af alle applets er java.applet.Applet. Når en klasse er lastet af AppletClassLoader, objektforekomsten, der oprettes, kastes til en forekomst af Applet. Hvis denne rollebesætning lykkes i det() metode kaldes. I mit eksempel bruger jeg den anden teknik, en grænseflade.

Leger med eksemplet

For at afrunde eksemplet har jeg oprettet et par mere

.java

filer. Disse er:

 offentlig grænseflade LocalModule {/ * Start modulet * / ugyldig start (strengindstilling); }