Programmering

Legg til dynamisk Java-kode i applikasjonen

JavaServer Pages (JSP) er en mer fleksibel teknologi enn servlets fordi den kan svare på dynamiske endringer ved kjøretid. Kan du forestille deg en vanlig Java-klasse som også har denne dynamiske muligheten? Det ville være interessant hvis du kunne endre implementeringen av en tjeneste uten å distribuere den på nytt og oppdatere applikasjonen din på farten.

Artikkelen forklarer hvordan du skriver dynamisk Java-kode. Den diskuterer kildekodekompilering av kjøretid, klasselading og bruk av Proxy-designmønsteret for å gjøre endringer i en dynamisk klasse gjennomsiktig for den som ringer.

Et eksempel på dynamisk Java-kode

La oss starte med et eksempel på dynamisk Java-kode som illustrerer hva ekte dynamisk kode betyr, og som også gir litt sammenheng for videre diskusjoner. Vennligst finn dette eksemplets komplette kildekode i Resources.

Eksemplet er et enkelt Java-program som er avhengig av en tjeneste som heter Postman. Postman-tjenesten er beskrevet som et Java-grensesnitt og inneholder bare en metode, deliverMessage ():

offentlig grensesnitt Postmann {void deliverMessage (String msg); } 

En enkel implementering av denne tjenesten skriver ut meldinger til konsollen. Implementeringsklassen er den dynamiske koden. Denne klassen, PostbudImpl, er bare en vanlig Java-klasse, bortsett fra at den distribueres med kildekoden i stedet for den kompilerte binære koden:

offentlig klasse PostmanImpl implementerer Postman {

privat PrintStream-utgang; offentlig PostmanImpl () {output = System.out; } public void deliverMessage (String msg) {output.println ("[Postman]" + msg); output.flush (); }}

Søknaden som bruker Postmann-tjenesten vises nedenfor. I hoved() metode, en uendelig løkke leser strengmeldinger fra kommandolinjen og leverer dem gjennom Postman-tjenesten:

offentlig klasse PostmanApp {

offentlig statisk ugyldig hoved (String [] args) kaster Unntak {BufferedReader sysin = new BufferedReader (new InputStreamReader (System.in));

// Få en postmann-instans Postbud postbud = getPostman ();

while (true) {System.out.print ("Skriv inn en melding:"); Streng msg = sysin.readLine (); postman.deliverMessage (msg); }}

privat statisk Postmann getPostman () {// Utelat for nå, kommer tilbake senere}}

Utfør applikasjonen, skriv inn noen meldinger, og du vil se utganger i konsollen som følgende (du kan laste ned eksemplet og kjøre det selv):

[DynaCode] Eksempel på første klasse.PostmanImpl Skriv inn en melding: hei verden [Postmann] hei verden Skriv inn en melding: for en fin dag! [Postbud] for en fin dag! Skriv inn en melding: 

Alt er greit bortsett fra første linje, noe som indikerer at klassen PostbudImpl blir samlet og lastet.

Nå er vi klare til å se noe dynamisk. La oss endre uten å stoppe applikasjonen PostbudImplkildekoden. Den nye implementeringen leverer alle meldingene til en tekstfil, i stedet for konsollen:

// MODIFISERT VERSJON offentlig klasse PostmanImpl implementerer Postman {

privat PrintStream-utgang; // Start av modifikasjon offentlig PostmanImpl () kaster IOException {output = ny PrintStream (ny FileOutputStream ("msg.txt")); } // Slutt på endringen

public void deliverMessage (String msg) {output.println ("[Postman]" + msg);

output.flush (); }}

Skift tilbake til applikasjonen og skriv inn flere meldinger. Hva vil skje? Ja, meldingene går til tekstfilen nå. Se på konsollen:

[DynaCode] Innledende klasseeksempel.PostmanImpl Skriv inn en melding: hallo verden [Postmann] hei verden Skriv inn en melding: for en fin dag! [Postbud] for en fin dag! Skriv inn en melding: Jeg vil gå til tekstfilen. [DynaCode] Eksempel på første klasse.PostmanImpl Skriv inn en melding: jeg også! Skriv inn en melding: 

Legge merke til [DynaCode] Innledende klasseprøve.PostmanImpl vises igjen, noe som indikerer at klassen PostbudImpl blir kompilert og lastet inn på nytt. Hvis du sjekker tekstfilen msg.txt (under arbeidskatalogen), vil du se følgende:

[Postbud] Jeg vil gå til tekstfilen. [Postbud] meg også! 

Utrolig, ikke sant? Vi er i stand til å oppdatere Postman-tjenesten ved kjøretid, og endringen er helt gjennomsiktig for applikasjonen. (Legg merke til at applikasjonen bruker samme Postman-forekomst for å få tilgang til begge versjonene av implementeringene.)

Fire trinn mot dynamisk kode

La meg avsløre hva som skjer bak kulissene. I utgangspunktet er det fire trinn for å gjøre Java-kode dynamisk:

  • Distribuere valgt kildekode og overvåke filendringer
  • Kompilere Java-kode ved kjøretid
  • Last inn / last inn Java-klassen på kjøretid
  • Koble den oppdaterte klassen til den som ringer

Distribuere valgt kildekode og overvåke filendringer

For å begynne å skrive en dynamisk kode, er det første spørsmålet vi må svare på: "Hvilken del av koden skal være dynamisk - hele applikasjonen eller bare noen av klassene?" Teknisk sett er det få begrensninger. Du kan laste inn / laste hvilken som helst Java-klasse på kjøretid. Men i de fleste tilfeller trenger bare en del av koden dette fleksibilitetsnivået.

Postman-eksemplet viser et typisk mønster for valg av dynamiske klasser. Uansett hvordan et system er sammensatt, til slutt vil det være byggesteiner som tjenester, delsystemer og komponenter. Disse byggesteinene er relativt uavhengige, og de utsetter funksjonalitet for hverandre via forhåndsdefinerte grensesnitt. Bak et grensesnitt er det implementeringen som er gratis å endre så lenge den er i samsvar med kontrakten definert av grensesnittet. Dette er akkurat den kvaliteten vi trenger for dynamiske klasser. Så enkelt sagt: Velg implementeringsklassen for å være den dynamiske klassen.

For resten av artikkelen tar vi følgende antagelser om de valgte dynamiske klassene:

  • Den valgte dynamiske klassen implementerer noe Java-grensesnitt for å avsløre funksjonalitet
  • Implementeringen av den valgte dynamiske klassen inneholder ingen stateful informasjon om klienten (ligner på den statsløse øktbønnen), slik at forekomster av den dynamiske klassen kan erstatte hverandre

Vær oppmerksom på at disse forutsetningene ikke er en forutsetning. De eksisterer bare for å gjøre realiseringen av dynamisk kode litt enklere, slik at vi kan fokusere mer på ideene og mekanismene.

Med de valgte dynamiske klassene i tankene er det enkelt å distribuere kildekoden. Figur 1 viser filstrukturen til Postman-eksemplet.

Vi vet at "src" er kilde og "bin" er binært. En ting som er verdt å merke seg er dynacode-katalogen, som inneholder kildefilene til dynamiske klasser. Her i eksemplet er det bare én fil— PostmanImpl.java. Det er nødvendig med bin og dynacode-katalogene for å kjøre applikasjonen, mens src ikke er nødvendig for distribusjon.

Å oppdage filendringer kan oppnås ved å sammenligne modifikasjonstidsstempler og filstørrelser. For vårt eksempel utføres en sjekk til PostmanImpl.java hver gang en metode påkalles på Postbud grensesnitt. Alternativt kan du gyte en demonstråd i bakgrunnen for regelmessig å sjekke filendringene. Dette kan gi bedre ytelse for store applikasjoner.

Kompilere Java-kode ved kjøretid

Etter at en kildekodeendring er oppdaget, kommer vi til kompileringsproblemet. Ved å delegere den virkelige jobben til en eksisterende Java-kompilator, kan runtime-kompilering være et stykke kake. Mange Java-kompilatorer er tilgjengelige for bruk, men i denne artikkelen bruker vi Javac-kompilatoren som er inkludert i Suns Java Platform, Standard Edition (Java SE er Suns nye navn for J2SE).

I det minste kan du kompilere en Java-fil med bare ett utsagn, forutsatt at tools.jar, som inneholder Javac-kompilatoren, er på klassestien (du finner tools.jar under / lib /):

 int errorCode = com.sun.tools.javac.Main.compile (ny streng [] {"-classpath", "bin", "-d", "/ temp / dynacode_classes", "dynacode / sample / PostmanImpl.java" }); 

Klassen com.sun.tools.javac.Main er programmeringsgrensesnittet til Javac-kompilatoren. Det gir statiske metoder for å kompilere Java-kildefiler. Å utføre uttalelsen ovenfor har samme effekt som å kjøre javac fra kommandolinjen med de samme argumentene. Den kompilerer kildefilen dynacode / sample / PostmanImpl.java ved hjelp av den angitte klassesti-kassen og sender ut klassefilen til destinasjonskatalogen / temp / dynacode_classes. Et helt tall returnerer som feilkoden. Null betyr suksess; ethvert annet tall indikerer at noe har gått galt.

De com.sun.tools.javac.Main klasse gir også en annen kompilere() metode som godtar en ekstra PrintWriter som vist i koden nedenfor. Detaljerte feilmeldinger vil bli skrevet til PrintWriter hvis kompilering mislykkes.

 // Definert i com.sun.tools.javac.Main public static int compile (String [] args); public static int compile (String [] args, PrintWriter out); 

Jeg antar at de fleste utviklere er kjent med Javac-kompilatoren, så jeg stopper her. For mer informasjon om hvordan du bruker kompilatoren, se Ressurser.

Last inn / last inn Java-klassen på kjøretid

Den kompilerte klassen må lastes før den trer i kraft. Java er fleksibelt når det gjelder klasseinnlasting. Den definerer en omfattende klasselastningsmekanisme og gir flere implementeringer av klasselastere. (For mer informasjon om klasseinnlasting, se Ressurser.)

Eksempelkoden nedenfor viser hvordan du laster og laster en klasse på nytt. Den grunnleggende ideen er å laste den dynamiske klassen ved hjelp av vår egen URLClassLoader. Hver gang kildefilen endres og kompileres på nytt, forkaster vi den gamle klassen (for søppeloppsamling senere) og oppretter en ny URLClassLoader for å laste klassen igjen.

// Dir inneholder de sammensatte klassene. FilklasserDir = ny fil ("/ temp / dynacode_classes /");

// Overordnet klasselaster ClassLoader parentLoader = Postman.class.getClassLoader ();

// Last klasse "sample.PostmanImpl" med vår egen klasselaster. URLClassLoader loader1 = ny URLClassLoader (ny URL [] {classesDir.toURL ()}, parentLoader); Klasse cls1 = loader1.loadClass ("sample.PostmanImpl"); Postbud postbud1 = (Postbud) cls1.newInstance ();

/ * * Påkalle postmann1 ... * Da blir PostmanImpl.java endret og kompilert på nytt. * /

// Last klassen "sample.PostmanImpl" på nytt med en ny klasselaster. URLClassLoader loader2 = ny URLClassLoader (ny URL [] {classesDir.toURL ()}, parentLoader); Klasse cls2 = loader2.loadClass ("sample.PostmanImpl"); Postbud postmann2 = (Postman) cls2.newInstance ();

/ * * Arbeid med postman2 fra nå av ... * Ikke bekymre deg for loader1, cls1 og postman1 * de blir søppeloppsamlet automatisk. * /

Vær oppmerksom på parentLoader når du lager din egen klasselaster. I utgangspunktet er regelen at den overordnede klasselasteren må oppgi alle avhengigheter barnet klasselasteren krever. Så i eksempelkoden, den dynamiske klassen PostbudImpl avhenger av grensesnittet Postbud; det er derfor vi bruker Postbudsin klasselaster som overordnet klasselaster.

Vi er fortsatt et skritt unna for å fullføre den dynamiske koden. Husk eksemplet som ble introdusert tidligere. Der er dynamisk klasselading gjennomsiktig for den som ringer. Men i eksemplet ovenfor, må vi fortsatt endre tjenesteinstansen fra postbud1 til postbud2 når koden endres. Det fjerde og siste trinnet vil fjerne behovet for denne manuelle endringen.

Koble den oppdaterte klassen til den som ringer

Hvordan får du tilgang til den oppdaterte dynamiske klassen med en statisk referanse? Tilsynelatende vil en direkte (normal) referanse til objektet til en dynamisk klasse ikke gjøre susen. Vi trenger noe mellom klienten og den dynamiske klassen - en proxy. (Se den berømte boka Design mønstre for mer om proxy-mønsteret.)

Her er en proxy en klasse som fungerer som en dynamisk klasses tilgangsgrensesnitt. En klient påkaller ikke den dynamiske klassen direkte; fullmakten gjør i stedet. Fullmakten videresender deretter anropene til den dynamiske klassen for backend. Figur 2 viser samarbeidet.

Når den dynamiske klassen lastes inn på nytt, trenger vi bare å oppdatere koblingen mellom proxyen og den dynamiske klassen, og klienten fortsetter å bruke den samme proxyinstansen for å få tilgang til den omlastede klassen. Figur 3 viser samarbeidet.

På denne måten blir endringer i den dynamiske klassen gjennomsiktig for den som ringer.

API for Java-refleksjon inkluderer et praktisk verktøy for å opprette fullmakter. Klassen java.lang.reflect.Proxy gir statiske metoder som lar deg opprette proxy-forekomster for ethvert Java-grensesnitt.

Eksempelkoden nedenfor oppretter en proxy for grensesnittet Postbud. (Hvis du ikke er kjent med java.lang.reflect.Proxy, ta en titt på Javadoc før du fortsetter.)

 InvocationHandler handler = ny DynaCodeInvocationHandler (...); Postman proxy = (Postman) Proxy.newProxyInstance (Postman.class.getClassLoader (), new Class [] {Postman.class}, handler); 

De returnerte fullmektig er et objekt av en anonym klasse som deler den samme klasselasteren med Postbud grensesnitt ( newProxyInstance () metodens første parameter) og implementerer Postbud grensesnitt (den andre parameteren). En metodeinnkallelse på fullmektig forekomst sendes til behandlers påkalle () metode (den tredje parameteren). Og behandlerimplementeringen kan se slik ut:

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