Programování

Přidejte do své aplikace dynamický kód Java

JavaServer Pages (JSP) je flexibilnější technologie než servlety, protože dokáže reagovat na dynamické změny za běhu. Dokážete si představit běžnou třídu Java, která má také tuto dynamickou schopnost? Bylo by zajímavé, kdybyste mohli upravit implementaci služby bez jejího opětovného nasazení a aktualizovat svoji aplikaci za běhu.

Článek vysvětluje, jak psát dynamický kód Java. Pojednává o kompilaci zdrojového kódu za běhu, opětovném načítání tříd a použití návrhového vzoru Proxy k provedení úprav dynamické třídy transparentních pro jeho volajícího.

Příklad dynamického kódu Java

Začněme příkladem dynamického kódu Java, který ilustruje, co znamená skutečný dynamický kód, a také poskytuje kontext pro další diskuse. Kompletní zdrojový kód tohoto příkladu najdete ve zdrojích.

Příkladem je jednoduchá aplikace Java, která závisí na službě s názvem Postman. Služba Postman je popsána jako rozhraní Java a obsahuje pouze jednu metodu, deliverMessage ():

veřejné rozhraní Postman {void deliverMessage (String msg); } 

Jednoduchá implementace této služby vytiskne zprávy do konzoly. Třída implementace je dynamický kód. Tato třída, Pošťák, je jen normální třída Java, kromě toho, že nasazuje zdrojový kód namísto kompilovaného binárního kódu:

veřejná třída PostmanImpl implementuje Postmana {

soukromý výstup PrintStream; public PostmanImpl () {output = System.out; } public void deliverMessage (String msg) {output.println ("[Postman]" + msg); output.flush (); }}

Níže se zobrazí aplikace, která používá službu Postman. V hlavní() metoda, nekonečná smyčka čte zprávy řetězce z příkazového řádku a doručuje je prostřednictvím služby Postman:

veřejná třída PostmanApp {

public static void main (String [] args) vyvolá výjimku {BufferedReader sysin = nový BufferedReader (nový InputStreamReader (System.in));

// Získání instance Postman Postman postman = getPostman ();

while (true) {System.out.print ("Zadejte zprávu:"); Řetězec msg = sysin.readLine (); postman.deliverMessage (msg); }}

soukromý statický pošťák getPostman () {// Prozatím vynecháme, vrátím se později}}

Spusťte aplikaci, zadejte některé zprávy a v konzole uvidíte výstupy, například následující (můžete si stáhnout příklad a spustit jej sami):

[DynaCode] Ukázka třídy Init. PostmanImpl Zadejte zprávu: ahoj svět [Postman] ahoj svět Zadejte zprávu: jaký krásný den! [Pošťák], krásný den! Zadejte zprávu: 

Všechno je jednoduché, s výjimkou prvního řádku, který naznačuje, že třída Pošťák je zkompilován a načten.

Nyní jsme připraveni vidět něco dynamického. Bez zastavení aplikace, pojďme upravit Pošťákzdrojový kód. Nová implementace doručuje všechny zprávy do textového souboru namísto do konzoly:

// MODIFIKOVANÁ VERZE veřejná třída PostmanImpl implementuje Postman {

soukromý výstup PrintStream; // Začátek úpravy public PostmanImpl () vyvolá IOException {output = new PrintStream (new FileOutputStream ("msg.txt")); } // Konec modifikace

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

output.flush (); }}

Přejděte zpět do aplikace a zadejte další zprávy. Co se bude dít? Ano, zprávy nyní přejdou do textového souboru. Podívejte se na konzolu:

[DynaCode] Ukázka třídy Init.PostmanImpl Zadejte zprávu: ahoj svět [Pošťák] ahoj svět Zadejte zprávu: jaký krásný den! [Pošťák], krásný den! Zadejte zprávu: Chci přejít do textového souboru. [DynaCode] Ukázka třídy Init.PostmanImpl Zadejte zprávu: já taky! Zadejte zprávu: 

Oznámení [DynaCode] Ukázka třídy Init.PostmanImpl se znovu objeví, což naznačuje, že třída Pošťák je překompilován a znovu načten. Pokud zkontrolujete textový soubor msg.txt (v pracovním adresáři), uvidíte následující:

[Pošťák] Chci jít do textového souboru. [Pošťák] i já! 

Úžasné, že? Jsme schopni aktualizovat službu Postman za běhu a změna je pro aplikaci zcela transparentní. (Všimněte si, že aplikace používá stejnou instanci Postmana pro přístup k oběma verzím implementací.)

Čtyři kroky k dynamickému kódu

Dovolte mi odhalit, co se děje v zákulisí. V zásadě existují čtyři kroky k tomu, aby byl kód Java dynamický:

  • Nasadit vybraný zdrojový kód a sledovat změny souborů
  • Kompilace kódu Java za běhu
  • Načíst / znovu načíst třídu Java za běhu
  • Propojte aktuální třídu se svým volajícím

Nasadit vybraný zdrojový kód a sledovat změny souborů

Abychom mohli začít psát nějaký dynamický kód, první otázkou, na kterou musíme odpovědět, je: „Která část kódu by měla být dynamická - celá aplikace nebo jen některé třídy?“ Technicky existuje několik omezení. Za běhu můžete načíst / znovu načíst libovolnou třídu Java. Ve většině případů však tuto úroveň flexibility potřebuje pouze část kódu.

Příklad Postmana ukazuje typický vzor při výběru dynamických tříd. Bez ohledu na to, jak je systém složen, na konci budou stavební bloky, jako jsou služby, subsystémy a komponenty. Tyto stavební bloky jsou relativně nezávislé a navzájem si vystavují funkce prostřednictvím předdefinovaných rozhraní. Za rozhraním je implementace, kterou lze libovolně měnit, pokud odpovídá smlouvě definované rozhraním. Přesně tuto kvalitu potřebujeme pro dynamické třídy. Jednoduše řečeno: Vyberte třídu implementace jako dynamickou třídu.

Ve zbývající části článku provedeme následující předpoklady o vybraných dynamických třídách:

  • Zvolená dynamická třída implementuje určité rozhraní Java, aby odhalila funkčnost
  • Implementace vybrané dynamické třídy neobsahuje žádné stavové informace o klientovi (podobně jako fazole bezstavové relace), takže instance dynamické třídy se mohou navzájem nahradit

Vezměte prosím na vědomí, že tyto předpoklady nejsou předpoklady. Existují jen proto, aby realizace dynamického kódu byla o něco jednodušší, abychom se mohli více soustředit na nápady a mechanismy.

S ohledem na vybrané dynamické třídy je nasazení zdrojového kódu snadný úkol. Obrázek 1 ukazuje strukturu souborů příkladu Postmana.

Víme, že „src“ je zdroj a „bin“ je binární. Za zmínku stojí jeden adresář dynacode, který obsahuje zdrojové soubory dynamických tříd. Tady v příkladu je pouze jeden soubor - PostmanImpl.java. Pro spuštění aplikace jsou vyžadovány adresáře bin a dynacode, zatímco src není pro nasazení nutný.

Zjišťování změn souborů lze dosáhnout porovnáním časových značek modifikací a velikostí souborů. Pro náš příklad se kontrola na PostmanImpl.java provádí pokaždé, když je metoda vyvolána na Listonoš rozhraní. Alternativně můžete na pozadí založit vlákno démona, abyste pravidelně kontrolovali změny souboru. To může mít za následek lepší výkon u rozsáhlých aplikací.

Kompilace kódu Java za běhu

Po zjištění změny zdrojového kódu se dostáváme k problému s kompilací. Delegováním skutečné úlohy na existující kompilátor Java může být runtime kompilace hračkou. Mnoho překladačů Java je k dispozici k použití, ale v tomto článku používáme překladač Javac obsažený v platformě Sun Java Standard Edition (Java SE je nový název Sunu pro J2SE).

Minimálně můžete zkompilovat soubor Java s pouhým jedním příkazem za předpokladu, že soubor tools.jar, který obsahuje kompilátor Javac, je na cestě ke třídě (soubor toolss.jar najdete pod / lib /):

 int errorCode = com.sun.tools.javac.Main.compile (nový řetězec [] {"-classpath", "bin", "-d", "/ temp / dynacode_classes", "dynacode / sample / PostmanImpl.java" }); 

Třída com.sun.tools.javac.Hlavní je programovací rozhraní kompilátoru Javac. Poskytuje statické metody pro kompilaci zdrojových souborů Java. Provedení výše uvedeného příkazu má stejný účinek jako spuštění javac z příkazového řádku se stejnými argumenty. Kompiluje zdrojový soubor dynacode / sample / PostmanImpl.java pomocí zadaného koše třídy a odešle svůj soubor třídy do cílového adresáře / temp / dynacode_classes. Celé číslo se vrací jako kód chyby. Nula znamená úspěch; jakékoli jiné číslo znamená, že se něco pokazilo.

The com.sun.tools.javac.Hlavní třída také poskytuje další kompilovat() metoda, která přijímá další PrintWriter parametr, jak je uvedeno v níže uvedeném kódu. Podrobné chybové zprávy budou zapsány do PrintWriter pokud kompilace selže.

 // Definováno v com.sun.tools.javac.Hlavní veřejná statická kompilace int (String [] args); public static int compile (String [] args, PrintWriter out); 

Předpokládám, že většina vývojářů je obeznámena s překladačem Javac, takže se zastavím zde. Další informace o tom, jak používat kompilátor, najdete v části Zdroje.

Načíst / znovu načíst třídu Java za běhu

Zkompilovaná třída musí být načtena, než se projeví. Java je flexibilní při načítání tříd. Definuje komplexní mechanismus načítání tříd a poskytuje několik implementací třídních nakladačů. (Další informace o načítání tříd najdete v článku Zdroje.)

Ukázkový kód níže ukazuje, jak načíst a znovu načíst třídu. Základní myšlenkou je načíst dynamickou třídu pomocí naší vlastní URLClassLoader. Kdykoli se zdrojový soubor změní a znovu zkompiluje, zahodíme starou třídu (pro pozdější odvoz odpadu) a vytvoříme novou URLClassLoader znovu načíst třídu.

// Adresář obsahuje kompilované třídy. Třídy souborůDir = nový soubor ("/ temp / dynacode_classes /");

// Nadřazený třídní zavaděč ClassLoader parentLoader = Postman.class.getClassLoader ();

// Načtení třídy „sample.PostmanImpl“ pomocí našeho vlastního načítače tříd. URLClassLoader loader1 = nový URLClassLoader (nová URL [] {classesDir.toURL ()}, parentLoader); Třída cls1 = loader1.loadClass ("sample.PostmanImpl"); Pošťák pošťák1 = (Pošťák) cls1.newInstance ();

/ * * Vyvolání na postman1 ... * Potom je upravena a znovu zkompilována PostmanImpl.java. * /

// Znovu načíst třídu „sample.PostmanImpl“ pomocí nového načítače tříd. URLClassLoader loader2 = nový URLClassLoader (nová URL [] {classesDir.toURL ()}, parentLoader); Třída cls2 = loader2.loadClass ("sample.PostmanImpl"); Pošťák pošťák2 = (Pošťák) cls2.newInstance ();

/ * * Od nynějška pracujte s postman2 ... * Nedělejte si starosti s loader1, cls1 a postman1 * budou automaticky shromažďovány odpadky. * /

Věnujte pozornost parentLoader při vytváření vlastního Classloaderu. V zásadě platí pravidlo, že nadřazený Classloader musí poskytnout všechny závislosti, které podřízený Classloader vyžaduje. V ukázkovém kódu tedy dynamická třída Pošťák záleží na rozhraní Listonoš; proto používáme Listonoš'classloader jako nadřazený classloader.

K dokončení dynamického kódu jsme ještě jeden krok. Připomeňme si příklad představený dříve. Tam je opětovné načtení dynamické třídy pro volajícího transparentní. Ale ve výše uvedeném ukázkovém kódu musíme ještě změnit instanci služby pošťák1 na pošťák2 když se kód změní. Čtvrtý a poslední krok odstraní potřebu této ruční změny.

Propojte aktuální třídu se svým volajícím

Jak přistupujete k aktuální dynamické třídě se statickým odkazem? Zdá se, že přímý (normální) odkaz na objekt dynamické třídy nebude stačit. Potřebujeme něco mezi klientem a dynamickou třídou - proxy. (Viz slavná kniha Designové vzory Další informace o vzoru Proxy.)

Zde je proxy třída fungující jako přístupové rozhraní dynamické třídy. Klient nevyvolá dynamickou třídu přímo; místo toho dělá proxy. Proxy pak předá vyvolání do dynamické třídy back-endu. Obrázek 2 ukazuje spolupráci.

Když se dynamická třída znovu načte, musíme jen aktualizovat propojení mezi proxy a dynamickou třídou a klient nadále používá stejnou instanci proxy pro přístup k znovu načtené třídě. Obrázek 3 ukazuje spolupráci.

Tímto způsobem se změny dynamické třídy stanou pro volajícího transparentní.

Rozhraní API pro reflexi Java obsahuje užitečný nástroj pro vytváření serverů proxy. Třída java.lang.reflect.Proxy poskytuje statické metody, které vám umožňují vytvářet instance proxy pro jakékoli rozhraní Java.

Ukázkový kód níže vytvoří proxy pro rozhraní Listonoš. (Pokud nejste obeznámeni s java.lang.reflect.Proxy, než budete pokračovat, podívejte se prosím na Javadoc.)

 Obslužná rutina InvocationHandler = nový DynaCodeInvocationHandler (...); Postman proxy = (Postman) Proxy.newProxyInstance (Postman.class.getClassLoader (), nová třída [] {Postman.class}, obsluha); 

Vrátil se proxy je objekt anonymní třídy, který sdílí stejný zavaděč tříd s Listonoš rozhraní ( newProxyInstance () první parametr metody) a implementuje Listonoš rozhraní (druhý parametr). Vyvolání metody na proxy instance je odeslána do psovodje vyvolat () metoda (třetí parametr). A psovodImplementace může vypadat takto: