Ficheros de configuración en aplicaciones Java, un enfoque sencillo

Detalles de calidad

  A la hora de desarrollar un aplicación siempre nos encontramos con constates y valores por defecto. Cablearlos en el código es un grave error. Cada modificación implicaría una recompilación del código. Puede que en el jurásico. Cuando las computadoras se programaban con tarjetas perforadas, eso fuese aceptable; hoy en día no.

  Bromas aparte, un buen código no puede permitirse esos lujos y se hace imprescindible utilizar mecanismos que nos permitan modificar la configuración de nuestros programas de manera cómoda y efectiva. Tampoco es cuestión de hacer nada complejo: basta con usar un fichero de configuración.

  En este artículo aprenderemos como podemos usar ficheros de configuración de manera sencilla en nuestras aplicaciones Java.

Arte vs. ingeniería

  Mucha gente – entre los cuales me incluyo – proviene del mundo de la programación a medio-bajo nivel, donde se siente por lo general un rechazo hacia las APIs de programación, escudándose en tópicos como: “es más lento”, “no es exactamente lo que necesito”, etc.

  Lo cierto es que hoy en día el uso de APIs de programación, salvo casos excepcionales, es el modo ideal para desarrollar. Intentar realizar tareas complejas basándose en las posibilidades básicas del lenguaje que estemos usando, aunque factible, suele ser una gran pérdida de tiempo. La programación actual tiene más de ingeniería que de arte; seamos ingenieros y usemos soluciones que ya funcionan para crear otras nuevas.

  Supongo que todavía habrá gente que no crea lo que aquí lee, así que haremos un experimento práctico. Intentaremos crear un sistema para acceder a variables de configuración. Primero lo haremos a mano, después usaremos una clase del extenso API de Java directamente y por último, basándonos en lo anterior, afinaremos esta solución. Espero que al llegar al final del artículo, el lector se decante sin dudarlo por la última opción.

Haciéndolo a mano

  Supongamos que tenemos un fichero de configuración sencillo que contiene pares del tipo clave=valor, donde almacenamos constantes relevantes a nuestra aplicación. Un fichero como este:


depuracion=True
alto=480
ancho=640

  Ahora debemos crear un código que nos permita acceder a sus contenidos en cualquier parte de la aplicación. Por tanto, debemos leer los pares y guardarlos en una estructura en memoria que permita un acceso rápido. Para nuestro ejemplo realizamos una sencilla clase con un método main donde experimentaremos.


import java.util.HashMap;
import java.util.StringTokenizer;
import java.io.BufferedReader;
import java.io.FileReader;

public class Main {

  public static void main(String[] args) {

    String FICHERO_CONFIGURACION = "Configuracion.conf";
    HashMap propiedades;

    try {

      propiedades = new HashMap();
      BufferedReader br = new BufferedReader(
        new FileReader(FICHERO_CONFIGURACION));
      String s;

      while ((s = br.readLine())!= null) {
        StringTokenizer st = new StringTokenizer(s, "=");
        propiedades.put(st.nextToken(),st.nextToken());
      }
      br.close();

      System.out.println(propiedades);

      /* Buscamos algunos valores
       */
      int alto = Integer.parseInt(
        (String)propiedades.get("alto"));
      boolean depuracion = Boolean.valueOf(
        (String)propiedades.get("depuracion")).booleanValue();

      System.out.println(alto);
      System.out.println(depuracion);
      System.out.println(
        (String)propiedades.get("DevuelveNull"));

    } catch (Exception e) {
      /* Manejo de excepciones
       */
    }
  }
}

  Parece sencillo, leemos secuencialmente el fichero y usando un StringTokenizer – que usa el igual como separador – almacenamos los pares clave valor obtenidos en un mapa hash, que nos permite un acceso eficiente a los valores.

  Ahora probemos a recuperar valores. Esto no debería representar mayor problema, unos cuantos castings, algún parsing y listo. La salida del programa por consola es la siguiente:


{depuracion=True, alto=480, ancho=640}
480
true
null

  Aunque todo parece funcionar se observan varios problemas.

    1. Si solicitamos un valor que no existe, obtenemos un null. Esto puede provocar problemas – null pointer exceptions – así que debemos tener cuidado y comprobar que el valor no es null antes de utilizarlo tan alegremente.
    2. La sintaxis del fichero debe ser muy estricta. No es lo mismo "alto=480" que "alto =480", los espacios importan. Si queremos tener una sintaxis más libre deberíamos hacer una interpretación más exhaustiva del fichero. Un claro ejemplo de esto es que si añadimos líneas en blanco al final del fichero – meros retornos de carro – necesitaríamos modificar el código.
    3. Como programadores de esta clase, sabemos para que vale cada parámetro del fichero de configuración, pero un usuario lo tiene difícil. Algún comentario no vendría mal.

  ¡Vaya, vaya! Esto empieza a complicarse.

El API al rescate

  Francamente, yo no estoy dispuesto a tirar líneas de código para hacer algo que otros ya han hecho y que está a mi disposición como programador Java.

  El API de Java contiene la clase Properties que nos permite hacer todo lo que queremos, que es crear ficheros de configuración complejos y manejarlos con facilidad. Tanto la estructura de la clase como la sintaxis de los ficheros de propiedades están comentadas en la documentación del API y puede ser consultada online.

  Un fichero .properties nos permite, sin grandes alardes, hacer casi todo lo que deseamos mientras respetemos el formato clave = valor (o valores). Pero lo veremos mejor con un ejemplo:


############################################
#
# Fichero de configuracion
#
############################################

# Parametro 1
depuracion = True

# Otros parametros
alto = 480
ancho = 640

  Hagamos ahora un código como nuestra versión a mano pero usando la clase Properties.


import java.util.Properties;
import java.io.FileInputStream;

public class Main {

  public static void main(String[] args) {

    String FICHERO_CONFIGURACION = "Configuracion.properties";
    Properties propiedades;

    /* Carga del fichero de propiedades
     */
    try {
      FileInputStream f =
        new FileInputStream(FICHERO_CONFIGURACION);

      propiedades = new Properties();
      propiedades.load(f);
      f.close();

      /* Imprimimos los pares clave = valor
       */
      propiedades.list(System.out);

      /* Buscamos algunos valores
       */
      int alto = Integer.parseInt(
        propiedades.getProperty("alto"));
      boolean depuracion = Boolean.valueOf(
        propiedades.getProperty("depuracion")).booleanValue();

      System.out.println(alto);
      System.out.println(depuracion);
      System.out.println(
        propiedades.getProperty("DevuelveNull"));

    } catch (Exception e) {

      /* Manejo de excepciones
       */
    }

  }
}

  Para leer los datos del fichero solo necesitamos crear un InputStream. Este será usado por la clase Properties para leer y parsear el fichero almacenándolo como un conjunto de pares clave-valor. – evidentemente, la extensión del fichero no es obligatoriamente .properties aunque nunca está de más para identificarlos rápidamente – La clase Properties es una subclase de HashTable lo que asegura un acceso eficiente usando técnicas de dispersión.

  Una vez más realizamos unas pruebas, pero esta vez accedemos a los valores mediante los métodos de acceso de Properties.


-- listing properties --
depuracion=True
alto=480
ancho=640
480
true
null

  La diferencia más apreciable, es sin duda que Properties ya sabe que trabaja con Strings, por lo que no es necesario realizar castings. Sin embargo, seguimos encontrando el problema del null. La solución ahora está en nuestras manos.

Algo más realista

  Las bases ya están sentadas, ahora es cuestión de ponerse a jugar un poco y buscar una manera interesante de usar lo que acabamos de aprender, solucionando los pequeños detalles que nos hemos ido dejando por el camino. Para ello os propongo un pequeño código compuesto de tres clases que veremos a continuación.

  La primera es una excepción personalizada, que nos va a permitir manejar el caso en el que el parámetro buscado está ausente.


public class FaltaPropiedadException extends Exception {

  private String nombreParametro;

  public FaltaPropiedadException(String nombreParametro) {
    super("Falta parametro de configuracion: '" + nombreParametro + "'");
    this.nombreParametro = nombreParametro;
  }

  public String getNombreParametro() {
    return nombreParametro;
  }
}

  Ahora crearemos una clase estática AlmacenPropiedades para gestionar los valores de configuración. Es deseable que está clase sea de fácil acceso en cualquier parte de nuestra aplicación. Podíamos haber optado también por usar un singleton – siguiendo el patrón de diseño clásico de la instancia única -.

  La idea es añadir una capa alrededor de las properties para controlar el acceso que se hace a ellas:

    a. Preferimos usar un HashMap en vez de Properties, que es una HashTable, por motivos de eficiencia, ya que los métodos de acceso de una HashTable son sincronizados – es decir, soportan concurrencia-. Como se trata de valores de solo lectura esto es innecesario. Sin embargo, seguimos necesitamos la clase Properties para interpretar el fichero.
    b. El método de acceso a las claves realiza el casting a String y además gestiona el caso en que no se encuentra la clave, lanzando nuestra excepción personalizada para que sean las capas superiores las que se ocupen de las acciones a tomar ante el fallo.


import java.util.Properties;
import java.util.HashMap;
import java.io.FileInputStream;

public class AlmacenPropiedades {

  private static final String CONFIGURATION_FILE = "Configuracion.properties";
  private static HashMap propiedades;

  /* Bloque de inicializacion
   */
  static {

    try {
      FileInputStream f =
        new FileInputStream(CONFIGURATION_FILE);

      Properties propiedadesTemporales = new Properties();
      propiedadesTemporales.load(f);
      f.close();

      propiedades = new HashMap(propiedadesTemporales);

      /* Imprimimos los pares clave = valor
       */
      System.out.println(propiedades);

    } catch (Exception e) {
      /* Manejo de excepciones
       */
    }
  }

  private AlmacenPropiedades() { }

  public static String getPropiedad(String nombre)
        throws FaltaPropiedadException {

    String valor = (String) propiedades.get(nombre);

    if (valor == null)
      throw new FaltaPropiedadException(nombre);

    return valor;
  }
}

  Por último creamos una clase Main para realizar las pruebas de siempre. Como única diferencia, vamos a gestionar la excepción. Por ejemplo, en este caso, la mostramos por consola y damos un valor por defecto a la variable.


public class Main {

  public static void main(String[] args) {

    String devuelveNull;

    try {

      /* Buscamos algunos valores
       */
      int alto = Integer.parseInt(
        AlmacenPropiedades.getPropiedad("alto"));
      boolean depuracion = Boolean.valueOf(
        AlmacenPropiedades.getPropiedad("depuracion")).
          booleanValue();

      System.out.println(alto);
      System.out.println(depuracion);

      devuelveNull = AlmacenPropiedades.
        getPropiedad("DevuelveNull");

    } catch (FaltaPropiedadException e) {
      System.out.println(e);
      devuelveNull = "Valor por defecto";
    }
  }
}

  Una vez compilado y ejecutado obtenemos la siguiente salida, como era de esperar:


{depuracion=True, alto=480, ancho=640}
480
true
FaltaPropiedadException: Falta parametro de configuracion: 'DevuelveNull'

Una última mejora

  El lector observador habrá notado que el uso de un FileInputStream, para cargar el fichero de propiedades, nos obliga a cablear la ruta del mismo – absoluta o relativa – en la propia clase como un static String. Esto es incomodo si pensamos cambiar esa ruta, ya que nos obliga a modificar dicha cadena y a recompilar. Lo ideal sería que fuese otro parámetro configurable… ¡vaya!, parece que hemos llegado a un callejón sin salida. Afortunadamente, hay una puerta por donde podemos escapar del problema.

  Esta puerta es el cargador de clases. Gracias al uso del mismo y a la variable de entorno CLASSPATH podemos implementar un mecanismo flexible y eficaz para localizar el fichero de configuración.

  Con esta aproximación, solo cableamos el nombre del fichero – algo aceptable – y añadimos al CLASSPATH el directorio que lo contiene. Ahora será el cargador de clases el que se ocupe de encontrarlo. Para ello modificaremos el bloque static de AlmacenPropiedades.


static {
  try {

  Class almacenPropiedadesClass = AlmacenPropiedades.class;
  ClassLoader classLoader =
    almacenPropiedadesClass.getClassLoader();
  InputStream inputStream =
    classLoader.getResourceAsStream(CONFIGURATION_FILE);
  Properties propiedadesTemporales = new Properties();
  propiedadesTemporales.load(inputStream);
  inputStream.close();

  propiedades = new HashMap(propiedadesTemporales);

  /* Imprimimos los pares clave = valor
   */
  System.out.println(propiedades);

  } catch (Exception e) {
  /* Manejo de excepciones
   */
  }
}

  Entender como actúa en este caso el cargador de clases se escapa de los objetivos de este artículo, pero recomiendo al lector interesado una visita a la documentación del API de Java. Y con esto terminamos.

Un detalle final: escribiendo valores

  Aunque no se ha mencionado, existe la posibilidad de modificar las propiedades y volcarlas a fichero. Sin embargo, hay que tener cuidado con la escritura de valores.

    1. Si nos encontramos en un entorno multihilo, debemos controlar la concurrencia mediante métodos sincronizados.
    2. Si tenemos varios cargadores de clases debemos recordar que el concepto de instancia única no existe, por tanto, no podemos suponer sin más que en dos puntos de nuestra aplicación no se estén usando dos clases distintas, con lo que podemos tener problemas de consistencia. Esto es clásico en aplicaciones web corriendo sobre servidores de aplicaciones que usan varios cargadores de clases – como Jakarta Tomcat -. En este caso se buscan otras soluciones.
Despedida

  Como ya dije, muchas líneas arriba, espero que a estas alturas el lector ya se haya decantado por algo similar a la última opción que planteamos. Las posibilidades son muchas, pero la base es siempre la misma. Ahora todo depende del buen hacer de cada uno.

  Esta claro que se puede soñar con cosas mucho más complejas, como usar XML o acceder a un directorio activo a través de una red, pero esa, amigos, es otra historia.

David Barral
http://www.elrincondelprogramador.com/default.asp?pag=articulos/leer.asp&id=30

Leave A Comment?