sábado, 21 de agosto de 2010

Uso avanzado de Enumeration

No parece que el tema de las cachés interese demasiado, así que mientras decido si continúo o no con el tema, otro consejo breve, en este caso sobre el uso de las enumeraciones en Java.
El uso básico que todo el mundo aprende rápidamente es el de simplemente hacer una lista de elementos para luego, por ejemplo, poderlos pasar como parámetros con verificación de tipos. Algunos desarrolladores, si embargo, se quedan ahí y piensan que su utilidad es limitada puesto que se puede sacar el valor con name(), se puede obtener el índice con ordinal() pero si necesitamos más, tenemos que complicar mucho la cosa... y total para pasar una cadena...
Nada más lejos de la realidad, puesto que extender una enumeración para que nos sea más útil es tremendamente sencillo.
Pongamos un caso parecido a uno con el que trabajé hace poco: Queremos mostrar una lista de empleados de forma ordenada, pudiendo pasar el criterio de ordenación como parámetro, o sea una cadena. Para ordenar una lista de elementos necesitamos un Comparator y no queremos ni que el parámetro a pasar sea un nombre muy largo ni que el nombre de la enumeración sea demasiado corto ni se aparte de las convenciones de código que usamos, así que... ¿como lo podemos hacer?
Muy sencillo:
  • Añadimos dos campos a la enumeración: acrónimo y comparador y los inicializamos al crear cada elemento.
  • Dado que querremos obtener el elemento correcto a partir del acrónimo, no del nombre completo, creamos una tabla de búsquedas inversa, indexada por acrónimos. La creamos estática e inicializada al cargar la clase y siempre la tendremos disponible y preparada, para no tener que buscar en un bucle.
Y el código es igualmente sencillo:
  static Comparator ComparadorPorNombre = new Comparator()
  {
   //...
  };
  static Comparator ComparadorPorSueldo = new Comparator()
  {
   //...
  };
  static Comparator ComparadorPorCargo = new Comparator()
  {
   //...
  };

  public enum Ordenacion
  {
    ORDENAR_POR_NOMBRE(ComparadorPorNombre, "nombre")
    ,ORDENAR_POR_SUELDO(ComparadorPorSueldo, "sueldo")
    ,ORDENAR_POR_CARGO(ComparadorPorCargo, "cargo");
    
    private static Map<String, Ordenacion> mapaAcronimoOrdenacion;

    private final String acronimo;
    private final Comparator comparador;

    private Ordenacion(Comparator comparador, String acronimo)
    {
      this.comparador = comparador;
      this.acronimo = acronimo;
    }

    public Comparator getComparador()
    {
      return comparador;
    }

    public String getAcronimo()
    {
      return acronimo;
    }

    // Método para obtener una ordenación según el acrónimo que nos hayan pasado.
    // Devuelve null si la ordenación no existe.
    public static Ordenacion getPorAcronimo(String acronimo)
    {
      return mapaAcronimoOrdenacion.get(acronimo);
    }

    // Inicializamos el mapa inverso por acronimo al cargar la clase, también
    // podríamos hacerlo de forma "lazy" en el primer acceso, si quisiéramos.
    static
    {
      mapaAcronimoOrdenacion = new HashMap();
      for (Ordenacion rt : Ordenacion.values())
      {
        mapaAcronimoOrdenacion.put(rt.getAcronimo(), rt);
      }
    }
  }

  // Un breve ejemplo de como sería el uso.
  public static void main(String[] arg) throws Exception
  {
    //...
    String param_acronimo = ""; // lo obtenemos de alguna forma
    Ordenacion ord = Ordenacion.getPorAcronimo(param_acronimo);
    Set listaOrdenada = new TreeSet(ord.getComparador());
    //...
  }

De esta forma, podemos añadir nuevas ordenaciones "tranquilamente" añadiendo elementos a la enumeración y podemos saber que en todos los métodos a los que le pasemos una Ordenacion como parámetro seguirán funcionando, y que el control de si existe o no una Ordenacion con ese acrónimo se produce en un punto único, lo cual facilita el mantenimiento.

Lo que he mostrado con un acrónimo y un comparador se puede aplicar a cualquier cosa que se os ocurra: Enumeración con tipos de datos admitidos y expresiones regulares para comprobar si el parámetro es correcto traducciones de acrónimos/códigos numéricos a nombres completos para mostrar al usuario en listas cortas que no estén en BDD...

La cuestión es que si tenéis una lista de elementos y estáis pasando int o String y luego haciendo if/else o switch en varias partes de vuestro código, re-pensadlo a ver si podéis usar una enumeración y hacer el código "type safe" con la traducción de input -> valor correcto en un sólo punto.

Espero que os sirva y...
Happy coding! EJ

viernes, 20 de agosto de 2010

Reflexiones sobre el uso de cachés I

Hoy un par de reflexiones sobre un tema interesante y complejo, aunque menos de lo que parece a primera vista: el uso de cachés* (me disculpo por adelantado del uso incorrecto de memoria caché y sus derivados, pero todos nos entenderemos y no encuentro sinónimo breve y adecuado).
La utilización de cachés es una técnica muy útil para mejorar el rendimiento de nuestras aplicaciones, aunque sufre del mismo tipo de problemas que otras técnicas avanzadas para mejorar el rendimiento, como la programación multi-hilo: Es tremendamente útil pero a la vez peligrosa si no se usa adecuadamente, por lo que mucha gente no se encuentra cómoda usándola y prefiere optar por la prudencia y abstenerse. Sin embargo, en el fondo no es tan complicada de utilizar si sabemos lo que estamos haciendo, así que intentaremos explicar aquí algunos conceptos para ver si ayudan a entenderla mejor.

Qué es y por qué

Lo primero es dejar claro qué es lo que hacemos y por qué lo hacemos: La idea básica es que para obtener el resultado que necesitamos, nuestros programas siguen una serie de pasos que hay que repetir cada vez que pedimos ese resultado. Usar una cache no es más que almacenar alguno de los resultados intermedios para no tener que volver a calcularlo, evitando tener que repetir los pasos que llevan a calcular el resultado intermedio. Así de simple. La razón de hacerlo es aun más sencilla: realizar los pasos cuesta recursos (principalmente tiempo) y si podemos evitar tener que hacerlos, evitamos consumir esos recursos. Esta perogrullada de explicación cobra sentido cuando debamos tomar decisiones sobre si vale la pena usar cachés, dónde, cómo...

Como atacar el problema

Uno de los errores comunes a la hora de implementar técnicas de cachés es empezar pensando cómo vamos a implementar nuestra caché, que librerías utilizar etc. Error. La implementación es importante, pero hay otros factores tanto o más importantes sobre los que merece la pena decidir antes:
  • ¿Merece la pena introducir una caché? Ésta es la primera pregunta que deberíamos hacernos y no es trivial. Volviendo al primer punto, queremos introducir el uso de cachés para obtener unos resultados (ahorrar tiempo, consumo de CPU etc.) y si no lo vamos a conseguir, no merece la pena hacerlo. Aunque no lo parezca, hay muchos tipos de aplicaciones donde el uso de cachés no produce suficientes beneficios para el coste invertido. Para contestar a esta pregunta, es importante estudiar las respuestas a las preguntas que la siguen, ya que nos darán muchas pistas.
  • ¿A que nivel vamos a introducir la caché? En el diseño típico de una aplicación hay varias capas que tendremos que recorrer hasta llegar el resultado que deseamos, y muchas veces el resultado va sufriendo transformaciones por el camino, así que decidir cuál es el resultado intermedio que vamos a cachear es también una de las decisiones importantes.
    Por ejemplo, en una típica aplicación web multi-capa, no es lo mismo guardar el resultado de la consulta a la BDD, que los objetos Java que hayamos construido a partir del resultado de la consulta, que el trozo de JSP (p.e.) donde los hayamos pintado, que toda la página HTML resultado de la petición.
    Hay que pensar que cuanto más cerca estemos del origen del resultado, más fácil será de reutilizar, ya que habrá sufrido menos transformaciones, pero menos estaremos ganando ya que nos ahorraremos menos pasos. Al mismo tiempo, cuanto más complejo sea el resultado que guardemos en caché, más nos ahorraremos pero mayor será la probabilidad de que no nos sirva la próxima vez que lo queramos usar.
    Por ejemplo, pintar la fecha/hora actual en la página resultado, o el nombre del usuario, o tener filtros sobre las búsquedas puede hacer que la página HML entera no la podamos reutilizar para otras peticiones de otros usuarios, pero si bajamos un nivel y guardamos la información antes de añadir esa información personalizada y aplicar el filtro, ahorraremos menos pasos pero podremos reutilizar mucho más ese resultado intermedio.
  • ¿Cómo vamos a mantener actualizada la caché? Esta es la tercera pregunta importante y también nos da una pista muy importante sobre si merece la pena o no introducir una caché. Si hay algo mucho peor que un programa con bajo rendimiento por que no usa cachés, es un programa que devuelve resultados obsoletos por que no actualiza su caché cuando toca. Para poder mantener actualizada adecuadamente nuestra caché necesitamos tener claras dos cosas:
    • Identificar claramente cada elemento que guardamos en la cache para saber cuando lo podemos usar en vez de calcular de nuevo el resultado y cuando no.
    • Cuales son las circunstancias en las cuales el resultado intermedio que tenemos en cachés es inválido y cómo invalidarlo. Por ejemplo, si se actualizan datos en una de nuestras tablas... ¿que objetos de la caché se ven afectados y cómo podemos invalidarlos? ¿Lo haremos por tiempo, usaremos eventos, lo hará el usuario de forma manual?
    No poder responder claramente a estas preguntas o tener una respuesta muy compleja son signos de que quizá usar una caché no sea tan buena idea. Por ejemplo, si actualizar los datos de la tabla X nos invalida toda la caché y esa tabla se actualiza muy a menudo... quizá no nos sirva de nada tener una caché.

  • ¿Cual es el perfil de nuestra aplicación? Una vez visto lo anterior, hay que añadir que no es lo mismo una aplicación donde los datos cambian solamente una vez al año pero el cambio ha de ser visible inmediatamente, que una aplicación donde los datos se actualizan continuamente pero únicamente mostramos resultados consolidados que no cambian. No es lo mismo una aplicación donde el 90% de accesos son a un par de páginas que han de estar siempre actualizadas, que otra donde los accesos están repartidos semi-aleatoriamente entre miles de páginas, aunque cambien poco.
    Siguiendo con los ejemplos, una aplicación como la última mencionada donde se accede aleatoriamente a miles de páginas... o tenemos una caché donde poder tener las miles de páginas pre-calculadas o al no poder guardarlas todas y ser el acceso aleatorio, el uso de la caché puede ser muy bajo así que o nos caben todas en caché o quizá no merezca la pena. En cambio, en la anterior donde un 90% de acceso se producen a un par de páginas que han de estar siempre actualizadas, si montamos un sistema de eventos para tener la caché siempre actualizada podemos sacarle un gran partido a la caché, pero si el sistema para mantenerla actualizada es demasiado complejo e inestable quizá debamos descartar el uso de una caché a ese nivel.

Así pues, el primer paso es responder a estas preguntas y decidir si merece la pena o no implementar una caché. Obviamente habrá muchos casos donde la respuesta no se decantara claramente en un sentido y podemos pasar a hacer pruebas y decidir con datos en la mano, pero como mínimo nos habrá servidor para tener claro cómo queremos hacerlo y qué cosas queremos comprobar en nuestra fase de evaluación.

Hay mucho más que hablar sobre el tema, aunque espero que con esto os de alguna idea de como empezar a estudiar el tema. No quiero hacer este tocho más extenso de lo que es y lo dejaré aquí de momento. Si hay interés, continuaré con el tema explicando algunos perfiles típicos de aplicaciones y las soluciones técnicas más habituales.

Happy coding! EJ

miércoles, 18 de agosto de 2010

TreeSet y java.io.NotSerializableException

Después de un periodo sabático, un truco para solucionar un problemilla habitual por culpa de como está montado el JDK, sin ser realmente culpa de nadie... El problema en sí aparece al tratar de serializar un TreeSet, o cualquier otra colección ordenada, en el cual hemos especificado un Comparator creado como instancia anónima. En código:
static Comparator miComparador =
    new Comparator()
    {
      @Override
      public int compare(Object arg0, Object arg1)
      {
        //...
        return valorAdecuado;
      }
    };

  public static void main(String[] arg)
    throws Exception
    {
      Set miSetOrdenado = new TreeSet(miComparador);
      // ... rellenamos el Set
      ByteArrayOutputStream theBOS = new ByteArrayOutputStream();
      ObjectOutputStream theOOS = new ObjectOutputStream(theBOS);
      theOOS.writeObject(miSetOrdenado);
      theOOS.close();
    }
Si intentamos ejecutar este código, nos saltará una excepción:
java.io.NotSerializableException: test.App$1

Después de volverse tonto mirando por que el los elementos que uno pone en el TreeSet no son serializables, para normalmente descubrir que sí lo son, al final resulta que el problema es el Comparator. Entonces buscamos por Internet y las recomendaciones más habituales son hacer el campo static o transient, implementar Serializable... pero resulta que el Comparator lo tenemos declarado como static, por lo tanto no debería afectar puesto que la clase App sería Serializable... ¿o no? La cuestión sin embargo es... ¿cual es realmente la instancia que no es Serializable?

Para hacer la historia breve, el problema es que la instancia no Serializable es el TreeSet y que el campo que habría que marcar como transient es donde se guarda el Comparator de la clase TreeSet, cosa imposible, así que sólo nos queda declarar que el Comparator implementa Serializable, pero... ¿Cómo lo hacemos si es una instancia anónima? La solución pasa por no usar una instancia de clase anónima y hacer una clase interna, la cual sí podemos declarar que es Serializable y luego crear una instancia. Modificando ligeramente el código:
static class MiClaseComparadora
    implements Comparator, Serializable
  {
    @Override
    public int compare(Object arg0, Object arg1)
    {
      //...
      return valorAdecuado;
    }
  }
  static Comparator miComparador = new MiClaseComparadora();

  public static void main(String[] arg)
    throws Exception
    {
      Set miSetOrdenado = new TreeSet(miComparador);
      // ... rellenamos el Set
      ByteArrayOutputStream theBOS = new ByteArrayOutputStream();
      ObjectOutputStream theOOS = new ObjectOutputStream(theBOS);
      theOOS.writeObject(miSetOrdenado);
      theOOS.close();
    }
Y listo. Un detalle tonto que puede impedir que podamos serializar algunos objetos necesarios para nuestra aplicación y cuyo diagnóstico no es sencillo.

En mi caso, necesitaba almacenar en OSCache una lista ordenada de elementos para no tener que re-ordenarlos cada vez y al pasar la cache de memoria a disco... zas!

Espero que esto le evite a alguien tener que dar tantas vueltas por Internet como a mí, para acabar sin encontrar una solucón clara y concisa.

Happy coding!
EJ

martes, 22 de junio de 2010

Maven: como hacer fácil lo difícil y difícil lo fácil

//RANT ON

Debo confesar que mi primera idea para el titulo, llevado por la frustración, era "Mierda de Maven", y eso era lo más fino que se me ocurría, pero teniendo en cuenta que por mi parte odio los titulares amarillistas en los posts de los blogs, he decidido ser coherente y poner algo más neutro, o casi :).

La cuestión es que llevo unos días intentando transformar un proyecto de infraestructura desde Ant a Maven. Lo reconozco, nunca he sido fan de Maven pero resulta que quiero poder distribuir este proyecto en repositorios Maven y los buenos chicos de SonaType ofrecen hospedaje gratutito de repositorio a los proyectos open source. Eso sí, usando Maven para crear y subir las "releases" etc.

Así que después de mucho resistirme, me dije "¡amos a probar, hombre!" y como dicen los anglos decidí "morder la bala" (bite the bullet) o agarrarme los machos, como más os guste. Por el camino tuve que abandonar algunas cosas, como los productos del proyecto que no son .jar (tengo/tenía un .zip y un .xpi), y plegarme a las convenciones que los creadores de Maven consideran que son las buenas. Todo sea por la causa.

Pero bueno, después de unas cuantas peleas, unos cuantos insultos al aire y mucho Google + prueba y error, consigo tener el proyecto montado más o menos como quiero. Los otros artefactos los hago con Ant y al final dejo un proyecto padre y un subproyecto. Sigo las instrucciones para desplegar los artefactos en el repositorio de Sonatype (ver Sonatype OSS Maven Repository Usage Guide) y después de unas cuantas maldiciones más consigo desplegar unos SNAPSHOT e incluso una release. Y aquí llega el detalle: Como repositorio de versiones uso Mercurial, para poder tener siempre el histórico conmigo, cosa muy útil hoy en día donde los repositoros públicos te pueden hacer alguna jugada, y para colmo de males desarrollo en Windows (ya, ya, nadie es perfecto) así que cuanto intento perfeccionar mi pom.xml para que no tenga ningún path absoluto, me encuentro con que la propiedad ${basedir}devuelve siempre el path en Windows con barras invertidas (backslahes) y que el modulo SCM no entiende la URL tal como se la pasa esa propiedad. Bueno,un detallito, nada que Google no pueda arreglar, ¿verdad? Pues no, Google nos responde que ese problema es muy, pero que muy, común y que realmente "no hay solución" que no sea dar trescientas vueltas al mundo, constuir un plugin hacer unas cosas extrañísimas. Madre mía.

Entonces decido que si no lo puedo sacar automáticamente, quizá si se lo paso en un fichero de propiedades y que cada usuario se apañe configurando.... ¡MEEEC! ¡Error! ¿A quien se le ocurriría poder leer algo que no esté en el pom.xml? Debe ser únicamente a los extraterrestres por que Maven lo vuelve a poner complicadísimo simplemente para leer un p|@#@# fichero de propiedades y usarlas en el pom.xml. Lo más aproximado a lo que quiero requiere un plugin que he encontrado, el cual está en estado alpha y ni siquiera en los repositorios oficiales...

Así que sí, Maven permite hacer cosas "complicadísimas" de forma fácil por que te vienen hechas, pero algo tan "simple" como producir un artefacto que no sea .jar/.rar/.war/.ear, obtener un path normalizado o leer un fichero de propiedades es como para sacarse un master.

¿Por qué es el mundo tan cruel?... ¿por qué?... ¿por qué? :(

//RANT OFF

Happy coding! EJ

miércoles, 26 de mayo de 2010

Escogiendo números al azar o RTFA

Hoy un mensaje breve y al grano, para ilustrar la importancia de perder un rato investigando el API de Java, que es muy rico y uno de las mejores cosas que tiene el lenguaje (vale, no es perfecto y tiene algunas cagadas *ehem* Date *ehem* pero te da muchas cosas hechas).

El problema planteado es el típico que sale habitualmente en foros de "escoger X números entre Y posibles de forma aleatoria y sin repeticiones". Una especie de Lotería Primitiva, vamos. Las soluciones habituales suelen tender a usar Random para generar números de forma aleatoria y comprobar si el número ya lo tenemos o no, solución que por probabilidad no podemos asegurar cuanto tiempo se tirará ejecutándose, o meter todos los números en una lista, escogerlos aleatoriamente e irlos borrando etc.

La segunda opción no es mala y al menos es "determinista*", pero si perdemos 5 minutos en el API de las colecciones, podemos llegar a esto (como extra devolvemos los números ordenados):

/**
   * Devuelve numberCount números de entre 1 y
   *  maxNumber escogidos de forma aleatoria y
   * ordenados.
   * 
   * @param maxNumber
   * @param numberCount
   * @return La lista ordenada con los números seleccionados aleatoriamente.
   */
  private static List selectRandomNumbers(int maxNumber, int numberCount)
  {
    List numberList = new LinkedList();
    for (int i = 1; i <= maxNumber; i++)
    {
      numberList.add(i); //**
    }
    Collections.shuffle(numberList);
    numberList =
      numberList.subList(0, numberCount);
    Collections.sort(numberList);
    return numberList;
  }
Y eso es todo. Dos métodos de Collections y uno de List hacen todo el trabajo y no tenemos que preocuparnos de nada. Los ingenieros del JDK se preocuparán de optimizar ese código y nosotros podemos dedicarnos al “core” de nuestro negocio. La moraleja es... no hagas tú el trabajo si alguien ya lo ha hecho por ti. O como decía Mulder cuando programaba en Java... "La verdad, está en el API" ;P

*: Determinista en cuanto sabemos el flujo de ejecución que va a seguir sin depender del azar.
**: Sí, estoy metiendo primitivas en una lista, cosa que solo funciona gracias al mágico autoboxing de las últimas versiones de Java y que no me gusta, pero para este caso el compilador escribiría el mismo código, así que...

Happy coding! EJ

viernes, 7 de mayo de 2010

Los criterios que NO deberías utilizar para escoger una solución

Hola,
Hoy toca hablar de un tema interesante aunque menos técnico que los anteriores: Criterios a la hora de escoger una solución. O mejor dicho, criterios que no hay que seguir para escoger una solución.
Me explico:
En Internet y en Java en particular suele haber montones de herramientas, librerías y frameworks que se solapan en funciones y que se venden como "la solución a tu problema". Muchos desarrolladores tienen alergia a escoger cuando hay mucho donde elegir, siempre es más fácil cuando otra persona carga con la responsabilidad, y tienden a dejarse llevar por razonamientos simples que no suelen tener mucha base. Aunque es complicado decir cuales son las buenas razones para escoger una opción sobre otras, es relativamente fácil descartar algunas opciones populares, aunque no por ello menos erroneas.

Así que sin más dilación, pasaremos una serie de argumentos en los cuales NO te tendrías que basar para escoger una solución sobre otra:
  • Por que es nuevo: Éste es uno de los argumentos favoritos de los “entusiastas por las novedades”, para los cuales todo lo nuevo es muchísimo mejor que lo anterior. La demostración de la estupidez de semejante argumento es muy sencilla: Cualquier cosa, repito: cualquier cosa, cuando sale es nueva. Así que ser novedad es algo que todo el mundo tiene y que cura el tiempo independientemente de lo adecuada que sea o no una solución.
  • Por que lo usa "todo el mundo": Éste, en cambio, es el argumento favorito de la gente sin criterio que prefiere ir en medio de la manada, sin importarle si la manada se dirige a un pozo o no. Los males compartidos parecen menos males, ¿no? Pues no. Si lo usa todo el mundo, efectivamente hay mayores probabilidades de que te sirva, si eres como todo el mundo, pero no es una garantía y como dice el chiste: “¡Come mierda! ¡Millones de moscas no pueden estar equivocadas!” :). Ten en cuenta que hubo un tiempo donde “todo el mundo” hacia JSPs con EJB1.1 y CMP... auch.


  • Por que lo usa Google/EBay/Amazon...: Éste es un argumento parecido al anterior pero por el lado opuesto, apto para mentes que se sienten mejor si escogen lo mismo que “los grandes”. Desafortunadamente para nosotros, la mayoría no somos Google/Ebay etc. ni tenemos sus necesidades, recursos ni prioridades, así que usar una arquitectura pensada para recibir millones de visitas por segundo puede ser muy “cool”, pero seguramente sea una perdida de tiempo y un ejemplo claro de sobre-ingeniería. No hay nada de malo en no ser una de esas grandes compañías, millones de compañías no lo son, así que es mejor usar la cabeza y adaptarnos a NUESTRA situación.


  • Por que es caro:... y como es caro es bueno. Sí, sí, ríete pero yo he estado presente en reuniones donde ese ha sido el único criterio de selección. Por otro lado, sólo hace falta estar como oyente, atónito pero oyente, en una reunión donde se fija el precio de un producto para darse cuenta de lo que éste significa: Era una herramienta que se vendió por cifras con 6 ceros y en € y el precio fue fijado después de un estudio sobre “el precio que los potenciales clientes estarían dispuestos a pagar”. Ni más ni menos.


  • Por que es diferente a lo anterior: Un argumento muy común, utilizado para “diferenciarse de la competencia” pero que, al fin y al cabo, por si mismo no dice nada sobre lo adecuado o no de una solución. Cabe preguntarse si el resto de soluciones que lo hacen de la otra forma son todas parte de una confabulación judeo-masónica, son tontos... ¿o quizá es por que si lo hacen así es por que es más adecuado? En fin, que la evolución es necesaria pero diferente no es necesariamente mejor.


  • Por que su publicidad dice que...: Ésta es un clásico y siempre me sorprende que gente por otro lado totalmente racional e inteligente sea tan “tonta” de creerse lo que dice la auto-publicidad. ¿Que esperan? ¿Que ellos mismos digan que su solución no es adecuada para todo el mundo o que es un asco? No debería ser necesario mencionarla... pero es que todavía la gente sigue cayendo en ella.


De momento esa es la lista de las razones más flagrantemente inválidas que se me ocurren, seguro que hay más pero estas son unos buenos ejemplos. Y repito, no es que esas razones no signifiquen nada, es simplemente que sin otras razones para sustentar la elección, no significan mucho.

Así que no os dejéis llevar y haced vuestras propias elecciones basadas en razonamientos un poco lógicos. Al fin y al cabo se supone que intentamos que ésto sea una ciencia, así que un poco de método científico no viene mal de vez en cuando.

¡Hasta la próxima!

EJ
Happy Coding!

viernes, 16 de abril de 2010

Cláusulas dinámicas en Groovy: versión a prueba de Inyección SQL

Hola,

En una entrada anterior comentamos cómo crear dinámicamente una sentencia SQL partiendo de una lista de valores para una columna.
Ya advertimos que debido al uso de la concatenación de cadenas, el método sólo era válido si teníamos totalmente controlados los parámetros de entrada, ya que si no podíamos sufrir un ataque de Inyección SQL, pero con un poco más de trabajo, podemos crear una solución que nos proteja contra estos ataques, y es la que presentamos ahora:
Suponiendo la misma tabla que en la entrada anterior, lo que haremos en este caso es que la cadenas que vamos a concatenar colocará interrogantes (?) donde deberían ir los valores y luego pasaremos la lista de valores a la sentencia SQL para que la ejecute. Haciéndolo así Groovy usará un PreparedStatement para asignar los valores de los parámetros y estaremos protegidos contra un posible ataque de Inyección SQL. Veámoslo:

def parametrosMultiples = 'ABE,MUL,TAC,FRO' // o podría ser sólo 'ABE' o null
def listaParametrosMultiples = parametrosMultiples.split(',')
def condicionOR = listaParametrosMultiples.collect({'?'}).join(' OR TAB_CAMPO=')
def clausulaWhere = parametrosMultiples ? "WHERE TAB_CAMPO=${condicionOR}" : ''
def sentencia = "SELECT * FROM TAPP_TABLA ${clausulaWhere}" 
println sentencia
Resultado:
SELECT * FROM TAPP_TABLA WHERE TAB_CAMPO=? OR TAB_CAMPO=? OR TAB_CAMPO=? OR TAB_CAMPO=?

Y al ejecutarlo, le pasamos la lista de valores como parámetros:
//...
def sql = new Sql(ds); // ds es un DataSource que habremos obtenido de algún lado
sql.eachRow( sentencia
     ,Arrays.asList(listaParametrosMultiples)
     ,{
        ... // Aquí haremos algo para
        // cada fila resultado (it)
      }
     );
//...

Básicamente lo único que hemos hecho es usar la función collect para sustituir los valores del array de parámetros por interrogantes, y luego convertir el array en una lista, con Arrays.asList() para poder pasárselo al método eachRow.

Con estos sencillos cambios, cerramos un posible agujero en la seguridad.

Eso es todo de momento. ¡Feliz fin de semana!

EJ
Happy Coding!