Eine Klasse dynamisch generieren, kompilieren, instanziieren und benutzen

Motivation

In letzter Zeit habe ich viel darüber nachgedacht, wie man die besseren Eigenschaften der Technologien JSP und Templates zu etwas Neuem und Besserem verschmelzen könnte. Die Details dazu möchte ich mir für einen zukünftigen Artikel aufsparen.

Jedenfalls kam ich mit meinen Gedanken irgendwann an den Punkt, wo ich mich fragte, ob es möglich sei, aus einem laufenden Java Programm den source code fuer eine Java Klasse dynamisch erzeugen, kompilieren, eine Instanz davon erzeugen und benutzen zu können.

Bei JSPs wird so etwas hinter den Kulissen nämlich auch gemacht. Ich könnte also zum Beispiel im Tomcat code nachschauen, wie das dort gemacht wird. Ich habe mich für den Anfang dann aber doch entschieden, erst mal google anzuwerfen und wurde dann auch relativ schnell fündig.

Einfache Demoklasse

Die folgende Klasse zeigt, wie einfach es ist so etwas zu machen. Da es mir darum geht die Technik zu demonstrieren enthält sie nur den nötigsten Code. Die Klasse exp.Main erzeugt eine Klasse exp.DynamicallyGeneratedClass. Beide source Dateien leben im Verzeichnis src. Danach kompiliert Main diese dynamisch erzeugte Quelldatei wobei das Zielverzeichnis output benutzt wird. Danach erzeugt Main eine Instanz dieser dynamisch erzeugten Klasse und ruft die Funktion doit() dieser Instanz auf.

package exp;

import java.io.*;
import java.lang.reflect.*;

/**
 * Create a java source dynamically, compile and call it.
 * 
 * Found at:
 *
 *     http://www.rgagnon.com/javadetails/java-0039.html
 *
 * See also:
 *
 *     http://www.javaworld.com/javatips/jw-javatip131.html
 */
public class Main
{
    String dynClassClassname = "DynamicallyGeneratedClass";
    String dynClassSourceFilename = dynClassClassname + ".java";

    public static void main(String args[]) throws Exception
    {
        Main main = new Main();
        main.createSourceFile();
        if (main.compileSourceFile())
        {
            System.out.println("Running " + main.dynClassClassname + ":\n\n");
            main.runDynClass();
        }
        else
        {
            System.out.println(main.dynClassSourceFilename + " could not be compiled.");
        }
    }

    public void createSourceFile()
    {
        try
        {
            String filename = "src/exp/" + dynClassSourceFilename;
            File f = new File(filename);
            if (f.exists())
            {
                f.delete();
            }
            FileWriter aWriter = new FileWriter(filename, true);
            aWriter.write("package exp;\n");
            aWriter.write("public class " + dynClassClassname + "\n");
            aWriter.write("{\n");
            aWriter.write("\tpublic void doit()\n");
            aWriter.write("\t{\n");
            aWriter.write("\t\tSystem.out.printf(\"Hello world from '\" + getClass().getName() + \"'\");\n");
            aWriter.write("\t}\n");
            aWriter.write("}\n");
            aWriter.flush();
            aWriter.close();
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }

    public boolean compileSourceFile() throws Exception
    {
        String[] javacCmdLineArgs = {"-d", "output", "src/exp/" + dynClassSourceFilename};
        PrintWriter pw = new PrintWriter(System.out);
        int status = com.sun.tools.javac.Main.compile(javacCmdLineArgs, pw);
        System.out.println("Compilation done. Status: " + status);System.out.flush();

        return status == 0;
    }

    public void runDynClass()
    {
        try
        {
            Class params[] = {};
            Object paramsObj[] = {};
            System.out.println("trying to load class");System.out.flush();
            Class clazz = Class.forName("exp." + dynClassClassname);
            System.out.println("loaded");System.out.flush();
            Object instance = clazz.newInstance();
            Method thisMethod = clazz.getDeclaredMethod("doit", params);
            thisMethod.invoke(instance, paramsObj);
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }
}

Ein paar Dinge gibt es noch zu beachten, wenn man diese Idee weitertreiben möchte:

  • Angenommen wir befinden uns in einer webapplikation und der sourcecode von DynamicallyGeneratedClass verändert sich ab und zu. Dann muss man dafür sorgen, dass der Classloader die Klasse wieder neu lädt. Dies kann man zum Beispiel erreichen, indem man entweder seinen eigenen Classloader schreibt oder, dass der Klassenname einen dynamischen Teil, wie zum Beispiel eine Datum-Uhrzeitkombination enthält.
  • In unserem Beispiel ist es wichtig, dass sich DynamicallyGeneratedClass.class, im Klassenpfad des Classloader's befindet, der Main geladen hat. Ansonsten würde die neue Klasse nicht gefunden werden.