Compilar y cargar de forma dinámica una clase Java

Escrito por picodotdev el , actualizado el .
software programacion java planeta-codigo blog-stack
Comentarios

Java

Desde la versión 1.6 del JDK disponemos de una API para acceder al compilador desde un programa Java. En el ejemplo de este artículo utilizaré varias clases de esa API para conseguir compilar un archivo con la definición de una clase Java y posteriormente instanciarla y usarla. En posteriores artículos comentaré un ejemplo práctico y muy útil con el que podemos sacar provecho de esta funcionalidad.

Las clases que necesitaremos de esa API son:

  • JavaCompiler que es la interfaz para acceder al compilador desde un programa Java.
  • JavaFileManager que es una abstracción para gestionar los archivos fuente y las clases. Usaremos uno propio llamado ClassFileManager.
  • SimpleJavaFileObject clase que contiene el código fuente Java.

Y también necesitaremos redefinir algunas:

  • La clase ClassFileManager que extiende ForwardingJavaFileManager y se encargará de cargar los objetos JavaClassObject con un ClassLoader.
  • La clase JavaClassObject que extiende SimpleJavaFileObject y contendrá el código bytecode generado en memoria por el compilador.
  • CharSequenceJavaFileObject clase que extiende SimpleJavaFileObject y que contiene el código fuente en un objeto de tipo CharSequence.
  • La interfaz Configuracion es la interfaz que debe cumplir la clase Java que compilaremos, cargaremos de forma dinámica en la aplicación y posteriormente invocaremos sus métodos.

En el javadoc de las clases hay una descripción más amplia de cada una de ellas.

En el siguiente código suponiendo que disponemos en la variable source de un código Java a compilar y de la que crearemos mas tarde una instancia de la clase que define podemos hacerlo de la forma indicada continuación. Antes de mostrar el código código la clase a compilar y a cargar de forma dinámica en este ejemplo debe cumplir el contrato definido en una determinada interfaz de modo que una vez compilada y cargada sepamos que métodos podemos invocar de esa clase. En este caso el código fuente de la clase a compilar está hardcodeada en un String en el propio programa pero perfectamente podría haber obtenido su contenido de un archivo del disco duro o de una base de datos.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package io.github.picodotdev.blogbitix.config;

import java.util.ArrayList;
import java.util.List;

import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.ToolProvider;

public class Main1 {

    public static void main(String[] args) throws Exception {
        // Definir la clase
     String[] sources = new String[] { "package io.github.picodotdev.blogbitix.config;",
                "import java.util.HashMap;",
                "import java.util.Map;",
                "public class AppConfiguracion implements Configuracion {",
                    "private static Map config;",
                    "static {",
                        "config = new HashMap();",
                        "config.put(\"propiedad\", 11);",
                    "}",
                    "public Map get() {",
                        "return config;",
                    "}",
                "}" };

        String source = join(sources);
        String name = "io.github.picodotdev.blogbitix.config.AppConfiguracion";
    
        // Compilar la clase
     JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        JavaFileManager manager = new ClassFileManager(compiler.getStandardFileManager(null, null, null));

        List<JavaFileObject> files = new ArrayList<JavaFileObject>();
        files.add(new CharSequenceJavaFileObject(name, source));

        compiler.getTask(new NullWriter(), manager, null, null, null, files).call();

        // Cargar e instanciar la clase
     Configuracion configuracion = (Configuracion) manager.getClassLoader(null).loadClass(name).newInstance();
        
        // Invocar un método de la clase
     System.out.println(configuracion.get());
    }

    private static String join(String[] s) {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < s.length; i++) {
            sb.append(s[i]);
        }
        return sb.toString();
    }
}
1
2
3
4
5
6
7
8
package io.github.picodotdev.blogbitix.config;

import java.util.Map;

public interface Configuracion {

    Map get();
}

Con este ejemplo puede intuirse el ejemplo práctico que comentaré que no es más que utilizar código Java para definir la configuración de una aplicación, esto tiene varias ventajas sobre utilizar un xml u otro tipo de formato de archivo de configuración de la aplicación (una de ellas que utilizando un IDE el compilador nos informará de errores y nos ofrecerá asistencia al escribir código). Esta idea junto con la posibilidad de monitorizar un archivo para ver si se han producido cambios en él (también con la API de Java) y recargarlo puede darnos como resultado una funcionalidad en la que la configuración se basa en código Java y que la configuración pueda recargarse de forma dinámica, si la aplicación se utiliza en un servidor de aplicaciones podríamos cambiar la configuración sin tener que reiniciar la aplicación.

Casi para terminar las clases de utilidad:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package io.github.picodotdev.blogbitix.config;

import java.io.IOException;
import java.security.SecureClassLoader;
import java.util.HashMap;
import java.util.Map;

import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
import javax.tools.StandardJavaFileManager;

public class ClassFileManager extends ForwardingJavaFileManager {

    private Map<String, Object> classes;

    /**
     * Will initialize the manager with the specified standard java file manager
     * 
     * @param standardManger
     */
    public ClassFileManager(StandardJavaFileManager standardManager) {
        super(standardManager);
        classes = new HashMap<>();
    }

    /**
     * Will be used by us to get the class loader for our compiled class. It creates an anonymous
     * class extending the SecureClassLoader which uses the byte code created by the compiler and
     * stored in the JavaClassObject, and returns the Class for it
     */
    @Override
    public ClassLoader getClassLoader(Location location) {
        return new SecureClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                Object o = classes.get(name);
                if (o instanceof Class) {
                    return (Class) o;
                } else if (o instanceof JavaClassObject) {
                    JavaClassObject object = (JavaClassObject) o;
                    byte[] b = object.getBytes();
                    Class<?> clazz = defineClass(name, object.getBytes(), 0, b.length);
                    classes.put(name, clazz);
                    return clazz;
                } else {
                    return super.loadClass(name);                    
                } 
            }
        };
    }

    /**
     * Gives the compiler an instance of the JavaClassObject so that the compiler can write the byte
     * code into it.
     */
    @Override
    public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind, FileObject sibling) throws IOException {
        JavaClassObject object = new JavaClassObject(className, kind);
        classes.put(className, object);
        return object;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package io.github.picodotdev.blogbitix.config;

import java.net.URI;

import javax.tools.SimpleJavaFileObject;

public class CharSequenceJavaFileObject extends SimpleJavaFileObject {

    /**
     * CharSequence representing the source code to be compiled
     */
    private CharSequence content;

    /**
     * This constructor will store the source code in the internal "content" variable and register
     * it as a source code, using a URI containing the class full name
     * 
     * @param className
     *            name of the public class in the source code
     * @param content
     *            source code to compile
     */
    public CharSequenceJavaFileObject(String className, CharSequence content) {
        super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
        this.content = content;
    }

    /**
     * Answers the CharSequence to be compiled. It will give the source code stored in variable
     * "content"
     */
    @Override
    public CharSequence getCharContent(boolean ignoreEncodingErrors) {
        return content;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package io.github.picodotdev.blogbitix.config;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;

import javax.tools.SimpleJavaFileObject;

public class JavaClassObject extends SimpleJavaFileObject {

    /**
     * Byte code created by the compiler will be stored in this ByteArrayOutputStream so that we can
     * later get the byte array out of it and put it in the memory as an instance of our class.
     */
    protected final ByteArrayOutputStream bos = new ByteArrayOutputStream();

    /**
     * Registers the compiled class object under URI containing the class full name
     * 
     * @param name
     *            Full name of the compiled class
     * @param kind
     *            Kind of the data. It will be CLASS in our case
     */
    public JavaClassObject(String name, Kind kind) {
        super(URI.create("string:///" + name.replace('.', '/') + kind.extension), kind);
    }

    /**
     * Will be used by our file manager to get the byte code that can be put into memory to
     * instantiate our class
     * 
     * @return compiled byte code
     */
    public byte[] getBytes() {
        return bos.toByteArray();
    }

    /**
     * Will provide the compiler with an output stream that leads to our byte array. This way the
     * compiler will write everything into the byte array that we will instantiate later
     */
    @Override
    public OutputStream openOutputStream() throws IOException {
        return bos;
    }
}

El código fuente completo puede encontrarse en el siguiente repositorio de GitHub.

En el siguiente artículo comentaré como monitorizar un archivo con código fuente Java para ver si se han producido cambios en él. Y basándome en estos dos artículos comentaré como disponer de un archivo de configuración que se recargue al detectase cambios en él.