Configuración usando código Java

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

Java

En las dos últimas entradas he explicado como compilar un archivo de código fuente Java desde una aplicación y como cargar esa clase compilada de forma dinámica para ser utilizada en un programa, la segunda entrada trataba el como monitorizar un directorio o archivo para ver si han tenido cambios con la nueva API que a partir de Java 7 disponemos.

En esta entrada quiero explicar un ejemplo de como aprovechar estas dos funcionalidades diferentes en un caso práctico y que nos puede ser útil en algún caso. La idea del ejemplo es definir la configuración de una aplicación como podría ser una aplicación web en un archivo de código fuente Java y que cuando se produjese algún cambio se recargase de forma dinámica.

Algunas ventajas de definir la configuración de la aplicación de esta manera son las siguientes:

  • Al ser el archivo de configuración código Java que se compila podemos aprovecharnos de la validación que hace el compilador para estar seguros de que está libre de errores léxicos y sintácticos, el archivo solo se cargará cuando está libre de errores de compilación. Al compilarlo el compilador nos advertirá de los errores que contenga de forma precisa.
  • Por otra parte al ser código en el archivo de configuración podemos usar el lenguaje Java para hacer ciertas operaciones que en un xml u otro formato de archivo de texto plano no podemos hacer. Podríamos hacer un cálculo o conectarnos a la base de datos u otro sistema para recuperar cierta información. En algunos casos el lenguaje Java puede ser mejor opción para describir la configuración que los archivos de texto, son los mismos problemas de ant y maven comparados con gradle. También el código Java puede ser la forma más breve y útil para describir la configuración de la aplicación que archivos de texto, usando código Java podremos devolver objetos, listas, … en vez de Strings o números.
  • La recarga del archivo de configuración cuando se produzcan cambios en él nos evitará tener que reiniciar la aplicación, simplemente haremos el cambio y la configuración se aplicaría. Esto puede ser útil en las aplicaciones web evitándonos tener que hacer un reinicio de la aplicación.

Una de las razones de la existencia de los archivos de configuración es tener esa configuración de forma externalizada a la aplicación de tal forma que podamos cambiar la configuración sin tener que modificar la aplicación ni tener que recompilarla. Con la compilación y carga dinámica de la clase Java de la configuración podemos tener estas mismas propiedades de los archivos de configuración. Si a esto le sumamos la recarga dinámica evitamos tener caídas de servicio en la aplicación por modificaciones en el archivo de configuración.

Todo esto es algo que se comenta en el libro The Pragmatic Programmer con las siguientes lineas:

Many programs will scan such things only at startup, which is unfortunate. If you need to change the configuration, this forces you to restart the application. A more flexible approach is to write programs that can reload their configuration while they’re running. This flexibility comes at a cost: it is more complex to implement. If it is a long-running server process, you will want to provide some way to reread and apply metadata while the program is running.

Esta es la teoría, veamos el código del ejemplo de configuración en Java con recarga dinámica. La mayor parte del código está en la clase ConfiguracionManager. Esta tiene dos métodos que son usados en la clase Main de la aplicación, el método load carga la clase y la compila, y el método monitor que monitoriza el archivo en busca de cambios y llama al método load cuando los detecte.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package io.github.picodotdev.blogbitix.config;

import java.nio.file.FileSystems;
import java.nio.file.Path;

public class Main {

    public static void main(String[] args) throws Exception {
        Path path = FileSystems.getDefault().getPath("src/main/java/io/github/picodotdev/blogbitix/config/AppConfiguracion.java");
        ConfiguracionManager manager = new ConfiguracionManager("io.github.picodotdev.blogbitix.config.AppConfiguracion", path).load().monitor();
        
        int n = 0;
        while (n < 20) {
            Thread.sleep(2000);
            System.out.println(manager.get());
        }
    }
}
  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
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
package io.github.picodotdev.blogbitix.config;

import java.io.FileReader;
import java.io.Reader;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

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

public class ConfiguracionManager {

    private String fullName;
    private Path path;
    private Configuracion configuracion;

    private Thread thread;
    private boolean closed;

    public ConfiguracionManager(String fullName, Path path) {
        this.fullName = fullName;
        this.path = path;
    }

    public Configuracion getConfiguracion() {
        return configuracion;
    }

    public Map get() {
        return configuracion.get();
    }

    public ConfiguracionManager load() throws Exception {
        List<String> l = Arrays.asList(fullName.split("\\."));
        String name = l.get(l.size() - 1);
        String source = loadSource();

        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        JavaFileManager manager = new ClassFileManager(compiler.getStandardFileManager(null, null, null));

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

        compiler.getTask(new NullWriter(), manager, null, null, null, files).call();
        configuracion = (Configuracion) manager.getClassLoader(null).loadClass(fullName).newInstance();

        return this;
    }

    public ConfiguracionManager monitor() throws Exception {
        closed = false;

        Runnable task = new Runnable() {
            @Override
            public void run() {
                while (!closed) {
                    try (WatchService watchService = FileSystems.getDefault().newWatchService()) {
                        path.getParent().register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
                        WatchKey watchKey = watchService.take();
                        if (watchKey == null) {
                            return;
                        }
                        for (WatchEvent<?> event : watchKey.pollEvents()) {
                            Path p = (Path) event.context();                         
                            Path pp = path.getParent().resolve(p);
                            if (path.equals(pp)) {
                                load();
                            }
                        }
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };

        thread = new Thread(task);
        thread.setDaemon(true);
        thread.start();

        return this;
    }

    public void close() throws Exception {
        closed = true;
    }

    private String loadSource() throws Exception {
        StringBuffer source = new StringBuffer();
        char[] buffer = new char[128 * 1024];
        Reader reader = new FileReader(path.toFile());
        int n = reader.read(buffer);
        while (n != -1) {
            source.append(buffer, 0, n);
            n = reader.read(buffer);
        }
        return source.toString();
    }
}

Esta idea de no utilizar archivos de configuración sino emplear código como la mejor forma y más breve de definirla es algo que hace gradle con los archivos de configuración del proyecto y apache tapestry para definir los módulos y la configuración del contenedor de inversión de control, parece una tendencia por el hecho de tener las propiedades y ventajas comentadas sobre otro tipo de archivos ya sean xml o sus sustitutos más recientes como yaml, json, … que son más compactos y legibles que xml pero que siguen adoleciendo de algunos de los mismos defectos.

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