Fügen Sie Ihrer Anwendung dynamischen Java-Code hinzu

JavaServer Pages (JSP) ist eine flexiblere Technologie als Servlets, da sie zur Laufzeit auf dynamische Änderungen reagieren kann. Können Sie sich eine gemeinsame Java-Klasse vorstellen, die auch diese dynamische Fähigkeit besitzt? Es wäre interessant, wenn Sie die Implementierung eines Dienstes ändern könnten, ohne ihn erneut bereitzustellen, und Ihre Anwendung im laufenden Betrieb aktualisieren könnten.

Der Artikel erklärt, wie dynamischer Java-Code geschrieben wird. Es wird die Kompilierung des Laufzeit-Quellcodes, das erneute Laden von Klassen und die Verwendung des Proxy-Entwurfsmusters erläutert, um Änderungen an einer dynamischen Klasse für den Aufrufer transparent zu machen.

Ein Beispiel für dynamischen Java-Code

Beginnen wir mit einem Beispiel für dynamischen Java-Code, das veranschaulicht, was echter dynamischer Code bedeutet, und einen Kontext für weitere Diskussionen bietet. Den vollständigen Quellcode dieses Beispiels finden Sie unter Ressourcen.

Das Beispiel ist eine einfache Java-Anwendung, die von einem Dienst namens Postman abhängt. Der Postman-Dienst wird als Java-Schnittstelle beschrieben und enthält nur eine Methode deliverMessage():

public interface Postman { void deliverMessage(String msg); } 

Eine einfache Implementierung dieses Dienstes druckt Nachrichten an die Konsole. Die Implementierungsklasse ist der dynamische Code. Diese Klasse PostmanImplist nur eine normale Java-Klasse, außer dass sie mit ihrem Quellcode anstelle ihres kompilierten Binärcodes bereitgestellt wird:

public class PostmanImpl implements Postman {

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

Die Anwendung, die den Postman-Dienst verwendet, wird unten angezeigt. Bei der main()Methode liest eine Endlosschleife Zeichenfolgennachrichten von der Befehlszeile und übermittelt sie über den Postman-Dienst:

public class PostmanApp {

public static void main(String[] args) throws Exception { BufferedReader sysin = new BufferedReader(new InputStreamReader(System.in));

// Obtain a Postman instance Postman postman = getPostman();

while (true) { System.out.print("Enter a message: "); String msg = sysin.readLine(); postman.deliverMessage(msg); } }

private static Postman getPostman() { // Omit for now, will come back later } }

Führen Sie die Anwendung aus, geben Sie einige Meldungen ein, und in der Konsole werden folgende Ausgaben angezeigt (Sie können das Beispiel herunterladen und selbst ausführen):

[DynaCode] Init class sample.PostmanImpl Enter a message: hello world [Postman] hello world Enter a message: what a nice day! [Postman] what a nice day! Enter a message: 

Bis auf die erste Zeile, die angibt, dass die Klasse PostmanImplkompiliert und geladen ist, ist alles unkompliziert .

Jetzt sind wir bereit, etwas Dynamisches zu sehen. Lassen Sie uns PostmanImplden Quellcode ändern , ohne die Anwendung zu stoppen . Die neue Implementierung liefert alle Nachrichten in eine Textdatei anstelle der Konsole:

// MODIFIED VERSION public class PostmanImpl implements Postman {

private PrintStream output; // Start of modification public PostmanImpl() throws IOException { output = new PrintStream(new FileOutputStream("msg.txt")); } // End of modification

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

output.flush(); } }

Wechseln Sie zurück zur Anwendung und geben Sie weitere Nachrichten ein. Was wird passieren? Ja, die Nachrichten werden jetzt in die Textdatei verschoben. Schauen Sie sich die Konsole an:

[DynaCode] Init class sample.PostmanImpl Enter a message: hello world [Postman] hello world Enter a message: what a nice day! [Postman] what a nice day! Enter a message: I wanna go to the text file. [DynaCode] Init class sample.PostmanImpl Enter a message: me too! Enter a message: 

Der Hinweis wird [DynaCode] Init class sample.PostmanImplerneut angezeigt und zeigt an, dass die Klasse PostmanImplneu kompiliert und neu geladen wird. Wenn Sie die Textdatei msg.txt (unter dem Arbeitsverzeichnis) überprüfen, wird Folgendes angezeigt:

[Postman] I wanna go to the text file. [Postman] me too! 

Erstaunlich, richtig? Wir können den Postman-Dienst zur Laufzeit aktualisieren, und die Änderung ist für die Anwendung vollständig transparent. (Beachten Sie, dass die Anwendung dieselbe Postman-Instanz verwendet, um auf beide Versionen der Implementierungen zuzugreifen.)

Vier Schritte in Richtung dynamischer Code

Lassen Sie mich zeigen, was sich hinter den Kulissen abspielt. Grundsätzlich gibt es vier Schritte, um Java-Code dynamisch zu machen:

  • Stellen Sie den ausgewählten Quellcode bereit und überwachen Sie Dateiänderungen
  • Kompilieren Sie Java-Code zur Laufzeit
  • Laden / Laden der Java-Klasse zur Laufzeit
  • Verknüpfen Sie die aktuelle Klasse mit ihrem Aufrufer

Stellen Sie den ausgewählten Quellcode bereit und überwachen Sie Dateiänderungen

Um mit dem Schreiben von dynamischem Code zu beginnen, müssen wir zunächst die Frage beantworten: "Welcher Teil des Codes sollte dynamisch sein - die gesamte Anwendung oder nur einige der Klassen?" Technisch gibt es nur wenige Einschränkungen. Sie können jede Java-Klasse zur Laufzeit laden / neu laden. In den meisten Fällen benötigt jedoch nur ein Teil des Codes diese Flexibilität.

Das Postman-Beispiel zeigt ein typisches Muster für die Auswahl dynamischer Klassen. Unabhängig davon, wie ein System aufgebaut ist, wird es am Ende Bausteine ​​wie Dienste, Subsysteme und Komponenten geben. Diese Bausteine ​​sind relativ unabhängig und setzen sich über vordefinierte Schnittstellen gegenseitig Funktionen zur Verfügung. Hinter einer Schnittstelle kann sich die Implementierung ändern, solange sie dem von der Schnittstelle definierten Vertrag entspricht. Dies ist genau die Qualität, die wir für dynamische Klassen benötigen. Einfach ausgedrückt: Wählen Sie die Implementierungsklasse als dynamische Klasse aus .

Für den Rest des Artikels werden wir die folgenden Annahmen über die ausgewählten dynamischen Klassen treffen:

  • Die ausgewählte dynamische Klasse implementiert eine Java-Schnittstelle, um die Funktionalität verfügbar zu machen
  • Die Implementierung der ausgewählten dynamischen Klasse enthält keine statusbehafteten Informationen über ihren Client (ähnlich der zustandslosen Sitzungs-Bean), sodass sich die Instanzen der dynamischen Klasse gegenseitig ersetzen können

Bitte beachten Sie, dass diese Annahmen keine Voraussetzungen sind. Sie existieren nur, um die Realisierung von dynamischem Code ein wenig zu vereinfachen, damit wir uns mehr auf die Ideen und Mechanismen konzentrieren können.

Angesichts der ausgewählten dynamischen Klassen ist die Bereitstellung des Quellcodes eine einfache Aufgabe. Abbildung 1 zeigt die Dateistruktur des Postman-Beispiels.

Wir wissen, dass "src" Quelle und "bin" binär ist. Bemerkenswert ist das Dynacode-Verzeichnis, das die Quelldateien dynamischer Klassen enthält. Hier im Beispiel gibt es nur eine Datei - PostmanImpl.java. Die Verzeichnisse bin und dynacode sind erforderlich, um die Anwendung auszuführen, während src für die Bereitstellung nicht erforderlich ist.

Das Erkennen von Dateiänderungen kann durch Vergleichen von Änderungszeitstempeln und Dateigrößen erreicht werden. In unserem Beispiel wird jedes Mal, wenn eine Methode auf der PostmanSchnittstelle aufgerufen wird, eine Überprüfung von PostmanImpl.java durchgeführt . Alternativ können Sie einen Daemon-Thread im Hintergrund erzeugen, um die Dateiänderungen regelmäßig zu überprüfen. Dies kann zu einer besseren Leistung für Großanwendungen führen.

Kompilieren Sie Java-Code zur Laufzeit

After a source code change is detected, we come to the compilation issue. By delegating the real job to an existing Java compiler, runtime compilation can be a piece of cake. Many Java compilers are available for use, but in this article, we use the Javac compiler included in Sun's Java Platform, Standard Edition (Java SE is Sun's new name for J2SE).

At the minimum, you can compile a Java file with just one statement, providing that the tools.jar, which contains the Javac compiler, is on the classpath (you can find the tools.jar under /lib/):

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

The class com.sun.tools.javac.Main is the programming interface of the Javac compiler. It provides static methods to compile Java source files. Executing the above statement has the same effect as running javac from the command line with the same arguments. It compiles the source file dynacode/sample/PostmanImpl.java using the specified classpath bin and outputs its class file to the destination directory /temp/dynacode_classes. An integer returns as the error code. Zero means success; any other number indicates something has gone wrong.

The com.sun.tools.javac.Main class also provides another compile() method that accepts an additional PrintWriter parameter, as shown in the code below. Detailed error messages will be written to the PrintWriter if compilation fails.

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

I assume most developers are familiar with the Javac compiler, so I'll stop here. For more information about how to use the compiler, please refer to Resources.

Load/reload Java class at runtime

The compiled class must be loaded before it takes effect. Java is flexible about class loading. It defines a comprehensive class-loading mechanism and provides several implementations of classloaders. (For more information on class loading, see Resources.)

The sample code below shows how to load and reload a class. The basic idea is to load the dynamic class using our own URLClassLoader. Whenever the source file is changed and recompiled, we discard the old class (for garbage collection later) and create a new URLClassLoader to load the class again.

// The dir contains the compiled classes. File classesDir = new File("/temp/dynacode_classes/");

// The parent classloader ClassLoader parentLoader = Postman.class.getClassLoader();

// Load class "sample.PostmanImpl" with our own classloader. URLClassLoader loader1 = new URLClassLoader( new URL[] { classesDir.toURL() }, parentLoader); Class cls1 = loader1.loadClass("sample.PostmanImpl"); Postman postman1 = (Postman) cls1.newInstance();

/* * Invoke on postman1 ... * Then PostmanImpl.java is modified and recompiled. */

// Reload class "sample.PostmanImpl" with a new classloader. URLClassLoader loader2 = new URLClassLoader( new URL[] { classesDir.toURL() }, parentLoader); Class cls2 = loader2.loadClass("sample.PostmanImpl"); Postman postman2 = (Postman) cls2.newInstance();

/* * Work with postman2 from now on ... * Don't worry about loader1, cls1, and postman1 * they will be garbage collected automatically. */

Pay attention to the parentLoader when creating your own classloader. Basically, the rule is that the parent classloader must provide all the dependencies the child classloader requires. So in the sample code, the dynamic class PostmanImpl depends on the interface Postman; that's why we use Postman's classloader as the parent classloader.

We are still one step away to completing the dynamic code. Recall the example introduced earlier. There, dynamic class reload is transparent to its caller. But in the above sample code, we still have to change the service instance from postman1 to postman2 when the code changes. The fourth and final step will remove the need for this manual change.

Link the up-to-date class to its caller

How do you access the up-to-date dynamic class with a static reference? Apparently, a direct (normal) reference to a dynamic class's object will not do the trick. We need something between the client and the dynamic class—a proxy. (See the famous book Design Patterns for more on the Proxy pattern.)

Here, a proxy is a class functioning as a dynamic class's access interface. A client does not invoke the dynamic class directly; the proxy does instead. The proxy then forwards the invocations to the backend dynamic class. Figure 2 shows the collaboration.

When the dynamic class reloads, we just need to update the link between the proxy and the dynamic class, and the client continues to use the same proxy instance to access the reloaded class. Figure 3 shows the collaboration.

In this way, changes to the dynamic class become transparent to its caller.

The Java reflection API includes a handy utility for creating proxies. The class java.lang.reflect.Proxy provides static methods that let you create proxy instances for any Java interface.

The sample code below creates a proxy for the interface Postman. (If you aren't familiar with java.lang.reflect.Proxy, please take a look at the Javadoc before continuing.)

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

Das zurückgegebene proxyObjekt ist ein Objekt einer anonymen Klasse, die denselben Klassenladeprogramm mit der PostmanSchnittstelle (dem newProxyInstance()ersten Parameter der Methode) teilt und die PostmanSchnittstelle (den zweiten Parameter) implementiert . Ein Methodenaufruf auf der proxyInstanz zu geschickter handler‚s - invoke()Methode (der dritte Parameter). Und handler‚s Implementierung kann wie folgt aussehen: