Programmering

Føj dynamisk Java-kode til din applikation

JavaServer Pages (JSP) er en mere fleksibel teknologi end servlets, fordi den kan reagere på dynamiske ændringer under kørsel. Kan du forestille dig en fælles Java-klasse, der også har denne dynamiske kapacitet? Det ville være interessant, hvis du kunne ændre implementeringen af ​​en tjeneste uden at omplacere den og opdatere din applikation i farten.

Artiklen forklarer, hvordan man skriver dynamisk Java-kode. Det diskuterer kørsel af kildekodekompilering, genindlæsning af klassen og brugen af ​​Proxy-designmønsteret til at gøre ændringer i en dynamisk klasse gennemsigtig for dens opkald.

Et eksempel på dynamisk Java-kode

Lad os starte med et eksempel på dynamisk Java-kode, der illustrerer, hvad ægte dynamisk kode betyder, og som også giver en vis kontekst til yderligere diskussioner. Find dette eksemples komplette kildekode i Ressourcer.

Eksemplet er et simpelt Java-program, der afhænger af en tjeneste, der hedder Postman. Postbudtjenesten beskrives som en Java-grænseflade og indeholder kun en metode, deliverMessage ():

offentlig grænseflade Postmand {void deliverMessage (String msg); } 

En simpel implementering af denne service udskriver beskeder til konsollen. Implementeringsklassen er den dynamiske kode. Denne klasse, Postbudmand, er bare en normal Java-klasse, bortset fra at den implementeres med sin kildekode i stedet for sin kompilerede binære kode:

public class PostmanImpl implementerer Postman {

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

Den applikation, der bruger postbudtjenesten, vises nedenfor. I hoved () metode, en uendelig løkke læser strengbeskeder fra kommandolinjen og leverer dem gennem Postman-tjenesten:

offentlig klasse PostmanApp {

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

// Få en postbudsforfatter Postbudmand = getPostman ();

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

privat statisk postbud getPostman () {// udelad for øjeblikket, kommer tilbage senere}}

Udfør applikationen, indtast nogle meddelelser, og du vil se output i konsollen som følgende (du kan downloade eksemplet og køre det selv):

[DynaCode] Eksempel på første klasse.PostmanImpl Indtast en besked: hej verden [Postbud] hej verden Indtast en besked: hvad en dejlig dag! [Postmand] hvad en dejlig dag! Indtast en besked: 

Alt er ligetil undtagen den første linje, hvilket indikerer, at klassen Postbudmand er samlet og indlæst.

Nu er vi klar til at se noget dynamisk. Lad os ændre uden at stoppe applikationen Postbudmandkildekode. Den nye implementering leverer alle meddelelserne til en tekstfil i stedet for konsollen:

// ÆNDRET VERSION public class PostmanImpl implementerer Postman {

privat PrintStream-output; // Start af ændring offentlig PostmanImpl () kaster IOException {output = ny PrintStream (ny FileOutputStream ("msg.txt")); } // Afslutning på ændringen

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

output.flush (); }}

Skift tilbage til applikationen, og indtast flere beskeder. Hvad vil der ske? Ja, meddelelserne går til tekstfilen nu. Se på konsollen:

[DynaCode] Eksempel på første klasse.PostmanImpl Indtast en besked: hej verden [Postbud] hej verden Indtast en besked: hvad en dejlig dag! [Postmand] hvad en dejlig dag! Indtast en besked: Jeg vil gå til tekstfilen. [DynaCode] Indledende klasseeksempel.PostmanImpl Indtast en besked: også mig! Indtast en besked: 

Varsel [DynaCode] Indledende klasseeksempel.PostmanImpl vises igen, hvilket indikerer, at klassen Postbudmand bliver kompileret og genindlæst. Hvis du kontrollerer tekstfilen msg.txt (under arbejdsmappen), vil du se følgende:

[Postbudmand] Jeg vil gerne gå til tekstfilen. [Postmand] også mig! 

Fantastisk, ikke? Vi er i stand til at opdatere Postbudtjenesten ved kørsel, og ændringen er fuldstændig gennemsigtig for applikationen. (Bemærk, at applikationen bruger den samme Postman-instans til at få adgang til begge versioner af implementeringerne.)

Fire trin mod dynamisk kode

Lad mig afsløre, hvad der foregår bag kulisserne. Dybest set er der fire trin til at gøre Java-kode dynamisk:

  • Implementer den valgte kildekode og overvåg filændringer
  • Kompilér Java-kode ved kørsel
  • Indlæs / genindlæs Java-klasse ved kørsel
  • Knyt den opdaterede klasse til den, der ringer op

Implementer den valgte kildekode og overvåg filændringer

For at begynde at skrive en eller anden dynamisk kode er det første spørgsmål, vi skal besvare, "Hvilken del af koden skal være dynamisk - hele applikationen eller bare nogle af klasserne?" Teknisk set er der få begrænsninger. Du kan indlæse / genindlæse enhver Java-klasse ved kørsel. Men i de fleste tilfælde har kun en del af koden brug for dette niveau af fleksibilitet.

Postmand-eksemplet viser et typisk mønster ved valg af dynamiske klasser. Uanset hvordan et system er sammensat, i sidste ende vil der være byggesten som tjenester, undersystemer og komponenter. Disse byggesten er relativt uafhængige, og de udsætter funktionaliteter for hinanden via foruddefinerede grænseflader. Bag en grænseflade er det implementeringen, der er fri til at ændre, så længe den er i overensstemmelse med den kontrakt, der er defineret af grænsefladen. Dette er nøjagtigt den kvalitet, vi har brug for til dynamiske klasser. Så enkelt sagt: Vælg implementeringsklassen for at være den dynamiske klasse.

For resten af ​​artiklen antager vi følgende antagelser om de valgte dynamiske klasser:

  • Den valgte dynamiske klasse implementerer nogle Java-grænseflader for at afsløre funktionalitet
  • Implementeringen af ​​den valgte dynamiske klasse indeholder ingen statefulde oplysninger om dens klient (svarende til den statsløse sessionsbønne), så forekomsterne af den dynamiske klasse kan erstatte hinanden

Bemærk, at disse antagelser ikke er forudsætninger. De eksisterer bare for at gøre realiseringen af ​​dynamisk kode lidt lettere, så vi kan fokusere mere på idéer og mekanismer.

Med de valgte dynamiske klasser i tankerne er det let at implementere kildekoden. Figur 1 viser filstrukturen i Postman-eksemplet.

Vi ved, at "src" er kilde, og "bin" er binært. En ting, der er værd at bemærke, er dynacode-biblioteket, der indeholder kildefilerne til dynamiske klasser. Her i eksemplet er der kun én fil - PostmanImpl.java. Bin- og dynacode-mapperne kræves for at køre applikationen, mens src ikke er nødvendigt for implementering.

Detektering af filændringer kan opnås ved at sammenligne ændringstidstempler og filstørrelser. For vores eksempel udføres en kontrol til PostmanImpl.java hver gang en metode påberåbes på Postbud interface. Alternativt kan du gyde en dæmontråd i baggrunden for regelmæssigt at kontrollere filændringerne. Det kan resultere i bedre ydeevne til store applikationer.

Kompilér Java-kode ved kørsel

Når en kildeændring er registreret, kommer vi til kompileringsproblemet. Ved at delegere det rigtige job til en eksisterende Java-kompilator kan runtime-kompilering være et stykke kage. Mange Java-compilere er tilgængelige til brug, men i denne artikel bruger vi Javac-compileren, der er inkluderet i Suns Java Platform, Standard Edition (Java SE er Suns nye navn til J2SE).

I det mindste kan du kompilere en Java-fil med kun en sætning, forudsat at tools.jar, som indeholder Javac-kompilatoren, er på klassestien (du kan finde 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 programmeringsgrænsefladen til Javac-kompilatoren. Det giver statiske metoder til at kompilere Java-kildefiler. At udføre ovenstående erklæring har samme effekt som at køre javac fra kommandolinjen med de samme argumenter. Den kompilerer kildefilen dynacode / sample / PostmanImpl.java ved hjælp af den angivne klassesti og outputter sin klassefil til destinationsmappen / temp / dynacode_classes. Et heltal returneres som fejlkoden. Nul betyder succes; ethvert andet tal angiver, at noget er gået galt.

Det com.sun.tools.javac.Main klasse giver også en anden udarbejde() metode, der accepterer en ekstra PrintWriter som vist i koden nedenfor. Detaljerede fejlmeddelelser vil blive skrevet til PrintWriter hvis kompilering mislykkes.

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

Jeg antager, at de fleste udviklere er fortrolige med Javac-kompilatoren, så jeg stopper her. For mere information om, hvordan du bruger kompilatoren, se Ressourcer.

Indlæs / genindlæs Java-klasse ved kørsel

Den kompilerede klasse skal indlæses, før den træder i kraft. Java er fleksibel med hensyn til klasseindlæsning. Det definerer en omfattende klasselastningsmekanisme og giver flere implementeringer af klasselæssere. (For mere information om klasseindlæsning, se Ressourcer.)

Eksempelkoden nedenfor viser, hvordan man indlæser og genindlæser en klasse. Grundideen er at indlæse den dynamiske klasse ved hjælp af vores egen URLClassLoader. Når kildefilen ændres og kompileres igen, kasserer vi den gamle klasse (til affaldsindsamling senere) og opretter en ny URLClassLoader for at indlæse klassen igen.

// Dir indeholder de kompilerede klasser. FilklasserDir = ny fil ("/ temp / dynacode_classes /");

// Den overordnede klasselæsser ClassLoader parentLoader = Postman.class.getClassLoader ();

// Indlæs klasse "sample.PostmanImpl" med vores egen classloader. URLClassLoader loader1 = ny URLClassLoader (ny URL [] {classesDir.toURL ()}, parentLoader); Klasse cls1 = loader1.loadClass ("sample.PostmanImpl"); Postbudmand postbud1 = (Postbudmand) cls1.newInstance ();

/ * * Påkald postmand1 ... * Derefter modificeres og kompileres PostmanImpl.java. * /

// Genindlæs klasse "sample.PostmanImpl" med en ny klasselæsser. URLClassLoader loader2 = ny URLClassLoader (ny URL [] {classesDir.toURL ()}, parentLoader); Klasse cls2 = loader2.loadClass ("sample.PostmanImpl"); Postbudmand postbud2 = (Postbudmand) cls2.newInstance ();

/ * * Arbejd med postman2 fra nu af ... * Du skal ikke bekymre dig om loader1, cls1 og postman1 *, de bliver automatisk indsamlet skrald. * /

Vær opmærksom på parentLoader når du opretter din egen klasselæsser. Grundlæggende er reglen, at den overordnede klasselæsser skal levere alle de afhængigheder, som barnelasteladeren kræver. Så i prøvekoden, den dynamiske klasse Postbudmand afhænger af grænsefladen Postbud; det er derfor, vi bruger Postbud's classloader som den overordnede classloader.

Vi er stadig et skridt væk for at fuldføre den dynamiske kode. Husk eksemplet, der blev introduceret tidligere. Der er dynamisk klasselæsning gennemsigtig for den, der ringer op. Men i ovenstående eksempelkode skal vi stadig ændre serviceinstansen fra postbud 1 til postbud2 når koden ændres. Det fjerde og sidste trin fjerner behovet for denne manuelle ændring.

Link den opdaterede klasse til dens opkaldende

Hvordan får du adgang til den opdaterede dynamiske klasse med en statisk reference? Tilsyneladende gør en direkte (normal) henvisning til en dynamisk klasses objekt ikke tricket. Vi har brug for noget mellem klienten og den dynamiske klasse - en proxy. (Se den berømte bog Designmønstre for mere om Proxy-mønster.)

Her er en proxy en klasse, der fungerer som en dynamisk klasses adgangsgrænseflade. En klient påkalder ikke den dynamiske klasse direkte; fuldmægtigen gør i stedet. Proxyen videresender derefter opkaldene til den dynamiske backend-klasse. Figur 2 viser samarbejdet.

Når den dynamiske klasse genindlæses, skal vi bare opdatere linket mellem proxyen og den dynamiske klasse, og klienten fortsætter med at bruge den samme proxyinstans for at få adgang til den genindlæste klasse. Figur 3 viser samarbejdet.

På denne måde bliver ændringer i den dynamiske klasse gennemsigtige for den, der ringer op.

Java-refleksions-API'en indeholder et praktisk værktøj til oprettelse af proxyer. Klassen java.lang.reflect.Proxy indeholder statiske metoder, der giver dig mulighed for at oprette proxy-forekomster til enhver Java-grænseflade.

Eksempelkoden nedenfor opretter en proxy til grænsefladen Postbud. (Hvis du ikke er bekendt med java.lang.reflect.Proxy, kig venligst på Javadoc inden du fortsætter.)

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

De vendte tilbage fuldmagt er et objekt fra en anonym klasse, der deler den samme klasselæsser med Postbud grænseflade ( newProxyInstance () metodens første parameter) og implementerer Postbud interface (den anden parameter). En metodeopkald på fuldmagt eksempel sendes til handler's påkalde () metode (den tredje parameter). Og handlerimplementering kan se ud som følger: