Ajoutez du code Java dynamique à votre application

JavaServer Pages (JSP) est une technologie plus flexible que les servlets car elle peut répondre aux changements dynamiques lors de l'exécution. Pouvez-vous imaginer une classe Java commune qui possède également cette capacité dynamique? Il serait intéressant de pouvoir modifier l'implémentation d'un service sans le redéployer et mettre à jour votre application à la volée.

L'article explique comment écrire du code Java dynamique. Il traite de la compilation du code source d'exécution, du rechargement de classe et de l'utilisation du modèle de conception Proxy pour apporter des modifications à une classe dynamique transparente à son appelant.

Un exemple de code Java dynamique

Commençons par un exemple de code Java dynamique qui illustre ce que signifie un véritable code dynamique et fournit également un contexte pour des discussions ultérieures. Veuillez trouver le code source complet de cet exemple dans les ressources.

L'exemple est une application Java simple qui dépend d'un service appelé Postman. Le service Postman est décrit comme une interface Java et ne contient qu'une seule méthode deliverMessage():

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

Une implémentation simple de ce service imprime les messages sur la console. La classe d'implémentation est le code dynamique. Cette classe,, PostmanImplest juste une classe Java normale, sauf qu'elle se déploie avec son code source au lieu de son code binaire compilé:

public class PostmanImpl implements Postman {

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

L'application qui utilise le service Postman apparaît ci-dessous. Dans la main()méthode, une boucle infinie lit les messages de chaîne à partir de la ligne de commande et les délivre via le service Postman:

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 } }

Exécutez l'application, entrez des messages et vous verrez des sorties dans la console telles que les suivantes (vous pouvez télécharger l'exemple et l'exécuter vous-même):

[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: 

Tout est simple sauf pour la première ligne, qui indique que la classe PostmanImplest compilée et chargée.

Nous sommes maintenant prêts à voir quelque chose de dynamique. Sans arrêter l'application, modifions PostmanImplle code source de. La nouvelle implémentation remet tous les messages dans un fichier texte, au lieu de la console:

// 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(); } }

Revenez à l'application et entrez plus de messages. Que va-t-il se passer? Oui, les messages vont maintenant dans le fichier texte. Regardez la console:

[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: 

Un avis [DynaCode] Init class sample.PostmanImplapparaît à nouveau, indiquant que la classe PostmanImplest recompilée et rechargée. Si vous vérifiez le fichier texte msg.txt (sous le répertoire de travail), vous verrez ce qui suit:

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

Incroyable, non? Nous sommes en mesure de mettre à jour le service Postman au moment de l'exécution et le changement est totalement transparent pour l'application. (Notez que l'application utilise la même instance de Postman pour accéder aux deux versions des implémentations.)

Quatre étapes vers un code dynamique

Laissez-moi vous révéler ce qui se passe dans les coulisses. Fondamentalement, il y a quatre étapes pour rendre le code Java dynamique:

  • Déployer le code source sélectionné et surveiller les modifications des fichiers
  • Compiler le code Java au moment de l'exécution
  • Charger / recharger la classe Java lors de l'exécution
  • Lier la classe à jour à son appelant

Déployer le code source sélectionné et surveiller les modifications des fichiers

Pour commencer à écrire du code dynamique, la première question à laquelle nous devons répondre est: "Quelle partie du code doit être dynamique - l'ensemble de l'application ou juste certaines des classes?" Techniquement, il y a peu de restrictions. Vous pouvez charger / recharger n'importe quelle classe Java au moment de l'exécution. Mais dans la plupart des cas, seule une partie du code a besoin de ce niveau de flexibilité.

L'exemple Postman illustre un modèle typique de sélection de classes dynamiques. Quelle que soit la composition d'un système, à la fin, il y aura des blocs de construction tels que des services, des sous-systèmes et des composants. Ces blocs de construction sont relativement indépendants et ils exposent des fonctionnalités les uns aux autres via des interfaces prédéfinies. Derrière une interface, c'est l'implémentation qui est libre de changer tant qu'elle est conforme au contrat défini par l'interface. C'est exactement la qualité dont nous avons besoin pour les classes dynamiques. En termes simples: choisissez la classe d'implémentation comme classe dynamique .

Pour le reste de l'article, nous ferons les hypothèses suivantes sur les classes dynamiques choisies:

  • La classe dynamique choisie implémente une interface Java pour exposer les fonctionnalités
  • L'implémentation de la classe dynamique choisie ne contient aucune information avec état sur son client (similaire au bean session sans état), donc les instances de la classe dynamique peuvent se remplacer

Veuillez noter que ces hypothèses ne sont pas des prérequis. Ils existent juste pour faciliter un peu la réalisation de code dynamique afin que nous puissions nous concentrer davantage sur les idées et les mécanismes.

En gardant à l'esprit les classes dynamiques sélectionnées, le déploiement du code source est une tâche facile. La figure 1 montre la structure des fichiers de l'exemple Postman.

Nous savons que «src» est la source et «bin» est binaire. Une chose à noter est le répertoire dynacode, qui contient les fichiers source des classes dynamiques. Ici, dans l'exemple, il n'y a qu'un seul fichier —PostmanImpl.java. Les répertoires bin et dynacode sont nécessaires pour exécuter l'application, tandis que src n'est pas nécessaire pour le déploiement.

La détection des modifications de fichiers peut être réalisée en comparant les horodatages de modification et la taille des fichiers. Pour notre exemple, une vérification de PostmanImpl.java est effectuée chaque fois qu'une méthode est appelée sur l' Postmaninterface. Vous pouvez également créer un thread démon en arrière-plan pour vérifier régulièrement les modifications du fichier. Cela peut entraîner de meilleures performances pour les applications à grande échelle.

Compiler le code Java au moment de l'exécution

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); 

Le renvoyé proxyest un objet d'une classe anonyme qui partage le même chargeur de classe avec l' Postmaninterface (le newProxyInstance()premier paramètre de la méthode) et implémente l' Postmaninterface (le deuxième paramètre). Un appel de méthode sur l' proxyinstance est envoyé à la méthode handlers invoke()(le troisième paramètre). handlerLa mise en œuvre de And peut ressembler à ceci: