viernes, 7 de octubre de 2011

Configuración de entorno en Java con ayuda de Groovy

JavaGroovy
Una necesidad habitual en prácticamente todas las aplicaciones y proyectos es tener una forma de poder configurar una aplicación en función del entorno en el que se vaya a ejecutar. Cosas habituales que cambian dependiendo del entorno son la configuración de logging, conexión a la base de datos, tal vez la configuración de hibernate si lo usamos, parámetros, etc... Habitualmente necesitaremos un entorno con su configuración para la máquina en la que desarrolla cada programador del proyecto que será el entorno de desarrollo, también es habitual tener un entorno de pruebas independiente del entorno de cada desarrollador y el entorno de producción que es donde se ejecuta la aplicación.

La configuración del multientorno la podemos la podemos hacer de diferentes formas, mediante archivos de propiedades, con xml u otras formas. Aqui vamos a ver como hacerlo con la ayuda de groovy de una forma sencilla sin tener que pelearnos con procesar la forma del archivo, que con xml puede llegar a ser laborioso y con archivos .properties un tanto limitado en cuanto a posibilidades.

Veámos primero lo que sería el archivo de configuración:

[
    entorno: 'desarrollo',
    
 //
 valores: [
  'valor1', 
  'valor2', 
  'valor3'
 ] as String[],

 // Redefinición para configuración para entornos
    entornos: [
        desarrollo: [
      log4j:      '/cfg/log4j-desarrollo.properties',
      hibernate:  '/cfg/hibernate-desarrollo.cfg.xml',
      quartz:     '/cfg/quartz-desarrollo.properties',
  ],
        pruebas: [
   log4j:      '/cfg/log4j-pruebas.properties',
   hibernate:  '/cfg/hibernate-pruebas.cfg.xml',
   quartz:     '/cfg/quartz-pruebas.properties',
  ],
        produccion: [
   log4j:      '/cfg/log4j-produccion.properties',
   hibernate:  '/cfg/hibernate-produccion.cfg.xml',
   quartz:     '/cfg/quartz-produccion.properties',

   valor: [
    'valor-produccion1', 
    'valor-produccion2', 
    'valor-produccion3'
   ] as String[],
  ]
    ]
]

El archivo de configuración es código groovy que define un mapa con una serie de propiedades y valores. La propiedad entorno indica el entorno que será usado en tiempo de ejecución por la aplicación. La propiedad entornos tiene las propiedades específicas de cada entorno. Las propiedad «valores» está definda de forma global independiente de entorno y es común para los entornos de desarrollo y pruebas pero que se redefine para el entorno de producción.

La clase Entorno nos permitirá procesar el archivo de configuración groovy y la clase Utilidades tiene unos métodos de utilidad para facilitar la tarea.

// Entorno.java
package com.blogspot.elblogdepicodev.misc;

import java.io.InputStream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Entorno {

 private static Logger logger = LoggerFactory.getLogger(Entorno.class);
 
 public enum Ent {
  DESARROLLO, PRUEBAS, PRODUCCION
 }
 
 public static final String PROP_LOG4J = "log4j";
 public static final String PROP_HIBERNATE = "hibernate";
 public static final String PROP_QUARTZ = "quartz";

 private static Object conf;
 private static Ent entorno;
 private static Object configuracion;
 
 public static void initialize(Object conf) throws Exception {
  initialize(conf, null);
 }
 
 public static void initialize(Object conf, Ent ent) throws Exception {
  Entorno.conf = conf;
  Entorno.entorno = (ent != null)?ent:Ent.valueOf(((String) getPropiedadGlobal("entorno")).toUpperCase());
  Entorno.configuracion = getPropiedadGlobal("entornos." + Entorno.entorno.toString().toLowerCase());
  
  logger.debug("Entorno inicializado (" + ent + ")");
 }
 
 public static Object getPropiedad(String propiedad) {
  try {
   String p = propiedad.replaceAll("\\.", "?.");
   // Ver si es una propiedad del entorno
   Object o = getPropiedadEntorno(p);
   if (o == null) {
    // No es una propiedad del entorno, ver si es una propiedad global
    o = getPropiedadGlobal(p);
   }
   return o;
  } catch (Exception e) {
   throw new RuntimeException(e);
  }
 }
 
 public static String getString(String propiedad) {
  return (String) getPropiedad(propiedad);
 }
 
 public static InputStream getResourceAsStream(String propiedad) {
  return (InputStream) Entorno.class.getResourceAsStream(getString(propiedad));
 }
 
 public static Ent getEntorno() {
  return entorno;
 }
 
 public static boolean isEntorno(Ent e) {
  return entorno.equals(e);
 }
 
 public static boolean isDesarrollo() {
  return isEntorno(Ent.DESARROLLO);
 }
 
 public static boolean isPruebas() {
  return isEntorno(Ent.PRUEBAS);  
 }
 
 public static boolean isProduccion() {
  return isEntorno(Ent.PRODUCCION);
 } 
 
 public static Object getPropiedadGlobal(String propiedad) throws Exception {
  return Utilidades.groovy("conf." + propiedad, Utilidades.map("conf", conf));
 }
 
 public static Object getPropiedadEntorno(String propiedad) throws Exception {
  return Utilidades.groovy("configuracion." + propiedad, Utilidades.map("configuracion", configuracion));
 }
}

// Utilidades.java
package com.blogspot.elblogdepicodev.misc;

import groovy.text.SimpleTemplateEngine;
import groovy.text.TemplateEngine;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Type;
import java.text.MessageFormat;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;

import javax.script.Bindings;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.blogspot.elblogdepicodev.misc.Entorno.Ent;

public class Utilidades {

 private static Logger logger = LoggerFactory.getLogger(Utilidades.class);

 private static ScriptEngine engine;

 static {
  ScriptEngineManager manager = new ScriptEngineManager();
  engine = manager.getEngineByName(Constantes.GROOVY_ENGINE_NAME);
 }

 public static void initializeEntorno() throws Exception {
  initializeEntorno(null);
 }

 public static void initializeEntorno(Ent ent) throws Exception {
  StringBuffer sb = new StringBuffer();

  InputStream is = Utilidades.class.getResourceAsStream("/cfg/Configuracion.groovy");
  Reader r = new InputStreamReader(is);

  char[] b = new char[4096];
  int i = r.read(b);
  while (i != -1) {
   sb.append(b, 0, i);
   i = r.read(b);
  }
  Object configuracion = Utilidades.groovy(sb.toString(), Collections.EMPTY_MAP);
  Entorno.initialize(configuracion, ent);
 }

 public static Object groovy(String script, Map bindings) throws ScriptException {
  Bindings b = engine.createBindings();
  if (bindings != null) {
   b.putAll(bindings);
  }
  return engine.eval(script, b);
 }
}

Los métodos importantes de esta clase son el initializeEntorno de clase Utilidades que se encarga de leer el archivo de configuración, evaluarlo como una expresión groovy y pasárselo al método initialize de la clase Entorno. La llamada a initializeEntorno la haremos cuando se arranque la aplicación posiblemente en un ContextLitener si se trata de una aplicación web. Una vez tengamos en entorno inicializado podremos llamar a getPropiedad de Entorno para obtener el valor de una propiedad, este método primeramente buscará entre la propiedades específicas entorno en ejecución y si no existe en él buscará entre las propiedades globales, devolverá lo primero en lo que encuentre algo.

Con estas dos clases tenemos una solución sencilla pero a la vez muy potente de configuración de entornos. Dado que las propiedades las estamos de definiendo con groovy las propiedades devueltas no será solo datos String que luego tendremos que parsear en nuestra aplicación sino que con la ayuda de groovy las propiedades devueltas podrán ser objetos, Double, Long, Date, listas, mapas, etc .... Y esta no es la unica forma de definir el archivo de configuración podríamos hacer que fuese un objeto en vez de un mapa de propiedades y podríamos definir métodos con alguna lógica y llamarlos al obtener una propiedad. ¡Las posibilidades son muchas para estas pocas líneas de código!

Referencia:
Obtener información del entorno de una aplicación web en Java