Mar 03

javadoc con UML

Hurgando por ahí me he encontrado con UMLGraph, una herramienta que genera gráficos UML a partir de unos textos que los describen. Lo bueno es que, al menos para el diagrama de clases, el texto que describe el diagrama UML es exactamente igual que un fuente java. Dicho de otra forma, si ponemos en el fichero "class UnaClase extends UnPadre {}", obtendremos un diagrama con una cajita UnaClase que hereda (flechita con triángulo) de una cajita UnPadre. Puedes ver el ejemplo aquí. Esto lo hace ideal para generar los gráficos UML de clases a partir de código fuente ya hecho.

Otro punto interesante es que viene con la posibilidad de invocar javadoc usando UMLGraph, de forma que en nuestro javadoc se generaría diagramas de clases incrustados. UMLGraph viene con un doclet que se puede usar desde el comando javadoc de java y de esta forma, javadoc generará la documentación de la forma habitual, pero incluyendo un gráfico de UML. En la descripción de un package, pondrá todas las clase incluidas en ese package y las relaciones entre ellas. En la descripción de una clase pondrá un dibujo de dicha clase con las relaciones (herencias, dependencias, etc) con otras clases del paquete o de otros paquetes. En la figura puedes ver un ejemplo de javadoc generado con el doclet de UMLGraph.

javadoc con grafico UML

Un detalle a tener en cuenta es que para que UMLGraph pueda generar los ficheros gráficos es necesario tener instalado previamente GraphViz.

En el diagrama de clases podrían configurarse muchas cosas, poner notas asociadas a las clases, poner otro tipo de cajas que no sean de clases, métodos que deben o no mostrarse, etc. La pega de ello es que iría configurado en código a base de anotaciones, por lo que el código quedaría algo "guarreado" para luego ver el dibujo bonito.

También pueden hacerse diagramas de secuencia, pero desgraciadamente la sintaxis del fichero de texto que lo describe ya no es java, así que no deja de ser una forma alternativa de hacer el diagrama. Puede ser interesante, por ejemplo, si guardamos los diagramas de secuencia en un sistema de control de versiones (como subversion). Siempre ocupa menos y es más interesante para ver diferencias con versiones anteriores un fichero de texto que no un gráfico o un proyecto entero de alguna herramienta compleja de generación de gráficos UML (Together, Rational,…).

Y otra cosa que a mí siempre me viene bien, es que UMLGraph está subido al repositorio ibiblio de maven y tiene plugin para el mismo. De esta forma, configurando el fichero pom.xml de nuestro proyecto maven (en concreto, configurando el plugin de javadoc para que use el doclet de UMLGraph), podemos generar el javadoc con gráficos UML directamente desde maven. La configuración sería algo parecido a esto

<reporting>
   <plugins>
      <plugin>
         <artifactId>maven-javadoc-plugin</artifactId>
         <configuration>
            <source>1.5</source>
            <aggregate>true</aggregate>
            <doclet>gr.spinellis.umlgraph.doclet.UmlGraphDoc</doclet>
            <docletArtifact>
               <groupId>gr.spinellis</groupId>
               <artifactId>UmlGraph</artifactId>
               <version>4.6</version>
             </docletArtifact>
             <additionalparam>
                    -inferrel -inferdep -quiet -hide java.*
                    -collpackages java.util.* -qualify
                    -postfixpackage -nodefontsize 9
                   -nodefontpackagesize 7
             </additionalparam>
          </configuration>
       </plugin>
    </plugins>
</reporting>

Con esto, un simple mvn javadoc:javadoc nos generaría la documentación javadoc de nuestro proyecto maven, gráficos UML incluidos.

 

Feb 24

OutOfMemoryError con test de maven

 

El otro día me salto un OutOfMemoryError al ejecutarse un test automático desde maven. Teóricamente, para evitar problemas de memoria con maven basta con poner la variable de entorno MAVEN_OPTS con los parámetros que le queremos pasar a la máquina virtual de java, en concreto, los de aumento de memoria

set MAVEN_OPTS=-Xmx512m

De hecho, tengo esa variable puesta por defecto en el entorno y estaba correctamente inicializada. Pero el OutOfMemoryException persiste. Así que a buscar en google.

Al final encuentro que maven arranca una máquina virtual java separada para ejecutar los test y que el parámetro MAVEN_OPTS sólo afecta a la máquina virtual en la que corre maven y no a la máquina virtual en la que se ejecutan los test. El plugin de maven que se encarga de ejecutar los test automáticos se llama maven-surefire-plugin y tiene su propia configuración. La variable argLine permite indicar, entre otras cosas, la cantidad de memoria que queremos que se asigne a la máquina virtual java en la que se ejecutan los test. Para ello, debemos ejecutar así

mvn -DargLine=-Xmx512m test

o bien, configurarlo en el mismo pom.xml del proyecto

<project>
<build>
<plugins>
   …
   <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-surefire-plugin</artifactId>               
      <configuration>
         <argLine>-Xmx512m</argLine>
      </configuration>
   </plugin>
 

Feb 22

Más sobre los test automáticos

 

Comenté ayer que estaba jugando con fest-swing y otras herramientas para test automáticos de pruebas de las interfaces gráficas de usuario. Pues bien, hoy he seguido un poco y he conseguido resolver la mayor parte de los problemas que se me presentaron. Eran, por supuesto, culpa mía, de no conocer la herramienta y de la mala costumbre de jugar el ensayo y error en vez de leerse la documentación.

Por un lado, en vez de bajarme fest-swing de la página de fest-swing, usé el repositorio maven que hay para ello, dejando que fuera maven el que se bajara el jar de fest-swing y todas sus dependencias. Entiendo que esto no tiene mucho que ver (más bien nada) con los problemas que se me presentaron.

El segundo punto, y este sí es importante, es que si hacemos el ejemplo tonto de test que viene con fest-swing no hay ningún problema, sale bien a la primera. Pero si ejecutamos una batería de test con fest-swing, es muy importante reiniciar los recursos que usa fest-swing. Esto se traduce que en el método tearDown() (que JUnit ejecuta después de cada test), debemos hacer la llamada correspondiente

private FrameFixture window; // Clase de Fest-Swing con la ventana principal.

public void tearDown() {
   window.cleanUp();  // reiniciar recursos
}

Y esto ha solucionado gran parte de los problemas que tenía. Ahora las ventanas de test sí salen cuando se ejecutan desde maven y no hacen tantas cosas raras cuando se ejecutan desde eclipse.

Otro tema importante es que aunque Fest-Swing se encarga de acceder a los componentes en el hilo de tratamiento de eventos de swing, es posible que el orden de estos eventos no sea el que esperamos. Si nuestro test manda visualizar una ventana, he encontrado útil esperar a que dicha ventana esté realmente visible antes de proseguir con el test. Esto se puede conseguir con la llamada a window.requireVisible() de fest-swing. Esta llamada hace fallar el test si la ventana no está visible, pero espera un tiempo prudencial antes de cantar el fallo. De esta forma, la llamada se queda bloqueada hasta que la ventana realmente está visible, o hasta que pasa un tiempo excesivo de espera (configurable, por supuesto).

Lo que no he conseguido es que desde eclipse los test se ejecuten dando resultados siempre. Sí es cierto que con todo esto parece que falla menos, pero siguen saliéndome barras grises o negras (en vez de verdes o rojas) de vez en cuando.

Una vez conseguido que todo funcione más o menos correctamente, lo he metido en Subversion y he esperado a que el sistema de integración continua (Hudson) compilara y ejecutara los test. A pesar de las advertencias en la documentación de fest-swing de que puede haber problemas con la integración continua, todo ha ido sobre ruedas y sin problemas.

Otra característica que me ha parecido interesante de fest-swing, aunque no la he probado, es que si un test falla, fest-swing puede capturar y almacenar la pantalla en el momento que se produce el fallo. De esta forma, viendo la foto de nuestra interfaz gráfica de usuario con el fallo, podemos ver si realmente hay algo incorrecto o el fallo se ha producido por otra circunstancia. Por ejemplo, que la ventana de que "windows se ha actualizado" haya salido justo encima de nuestra interfaz justo cuando la estamos testeando.

Así que de momento fest-swing queda incorporado en mi trabajo como herramienta para hacer test de interfaces gráficas de usuario. Sólo me falta ir convenciendo a mis "compis" para que también la usen.

Comenté también en el post anterior que quería probar Abbott. Bien, después del éxito con fest-swing, no lo he hecho. Pero leyendo la documentación de Abbott, he visto una característica también interesante. Abbott, ¿cómo no?, viene con Costello, una aplicación que permite ejecutar nuestra interfaz de usuario de forma normal (a través de su método main()) y actuar manualmente sobre ella. Costello se encargará de capturar toda nuestra interacción con ella (a modo de grabación), de forma que luego Costello puede reproducirla una y otra vez sobre nuestra aplicación, testeando que los resultados son los mismos. Es una forma interesante de hacer los test, sin necesidad de programar demasiado. A pesar de que usaremos fest-swing, probaré Abbott y Costello en algún momento, para ver si se puede testear el sistema completo en el entorno de pruebas.

Feb 20

Test automáticos de interfaces gráficas de usuario

 

Por fin, después de una larga temporada adaptando una y otra vez el mismo software ya hecho a distintos proyectos, ha llegado el momento de empezar a hacer algo nuevo. Vamos a pasar a java unas viejas interfaces gráficas de usuario que teníamos en C++. Por supuesto y con mis ganas de aplicar (y aprender) las buenas costumbres de programación, voy a intentar hacer la parte que me toca siguiendo TDD. Pero, amigo Sancho, con la iglesia hemos topado. Una de las cosas que tradicionalmente se reconoce que son difíciles de probar automáticamente son, precisamente, las interfaces gráficas de usuario.

En una aplicación java SWING, los test básicamente consisten en coger la ventana e ir buscando en los componentes que contienen, recursivamente, hasta que se encuentra el que se busca. Una vez encontrado, actuamos sobre él, haciendo click, metiendo un texto, leyendo su contenido o lo que sea que necesitemos hacer para realizar el test automáticamente. También la clase java.awt.Robot nos permite simular eventos de ratón y teclado sobre los componentes. Pero ni buscar componentes por las ventanas, ni usar la clase Robot es precisamente una tarea cómoda. Por fortuna, hay librerías que nos ayudan a hacer estas tareas y en definitiva, a realizar los test automáticos. Así que a probar esas librerías toca.

La primera que he probado ha sido FEST-Swing, quizás la más conocida para aplicaciones de escritorio con java. La librería está muy bien y contiene características muy interesantes. Las que más me han llamado la atención:

  • Según recomendación de java, todos los accesos a ventanas deben hacerse en el hilo de refresco de ventanas y tratamiento de eventos que nos proporciona java (EDT Event dispatch Thread). Pues bien, una simple configuración de FEST en el test hace que salte si accedemos a los componentes SWING fuera de ese hilo.
  • Precisamente ese hilo es un poco rollo. Nuestro test no se ejecuta en ese thread EDT y si el código que testeamos mete algo en un componente SWING usando el thread EDT (como se recomienda), nuestro test debe esperar a que el thread EDT termine antes de verificar el contenido del componente. Pues bien, FEST nos hace esto totalmente transparente. Cuando con FEST accedemos a un componente para ver su contenido, FEST espera que terminen los hilos EDT.
  • Aunque al principio parece raro, es realmente sencillo de usar. Podemos buscar en general cualquier componente con un método simple, indicando qué tipo de componente buscamos (JTextFiel, JLabel, etc), por su nombre (si hemos tenido la precaución de ponérselo con setName()) o haciéndonos un filtro a medida (un Matcher)

Pero no es oro todo lo que reluce. Me he encontrado con dos problemas que me van a dificultar seriamente su uso, más un pequeño problemilla.

  • El pequeño problemilla es que si ejecuto un test desde eclipse, a veces parece que no se ejecuta el test. Cuando ejecutamos un test en eclipse, al final sale una barra verde o roja, indicando si el test pasa o no. Pues resulta que, de forma aleatoria, el test termina y dicha barrita queda en negro o en gris, como si eclipse no detectara que el test ha terminado. Bueno, no es grave, puesto que no voy a ejecutar habitualmente los test con eclipse y lo peor que puede pasar es que tenga que darle dos o tres veces hasta obtener la barra verde o roja.
  • Un problema más serio lo he tenido con maven. Cuando ejecuto el test en eclipse, FEST visualiza las ventanas bajo test, se hace el test y pasa el test o falla (o se queda en gris/negro). Ejecutando el mismo test desde maven, ni siquiera sale la ventana y el test falla sistemáticamente. Investigaré en este tema, pero si no consigo solucionarlo, descartaré el uso de FEST en el entorno que trabajo.
  • Y un segundo posible problema es la integración continua. Hudson se encarga de compilar y pasar los test de nuestros proyectos todas las noches. FEST requiere que la ventana se visualice y por tanto, requiere que Hudson corra en un terminal abierto en sesión, sin salvapantallas ni nada que impida la correcta visualización de la ventana. En la documentación de FEST indican varias formas para solucionar problemas con la integración continua. Me pondré con ellas si consigo pasar el punto anterior, lo de ejecutar los test con maven, ya que si hudson ejecuta maven y maven no muestra las ventanas, me da igual que haya o no pantalla disponible.

Después de esto, fui a buscar otra herramienta y me topé con uispec4j. Mucho más simple que FEST de uso (aunque menos potente) y puede cumplir para test no excesivamente ambiciosos. A diferencia de FEST, no requiere que las ventanas se hagan visibles. Esto seguramente dé menos problemas a la hora de integración continua, pero posiblemente haga que los test sean menos reales. Tampoco controla el acceso a los componentes a través del thread EDT, por lo que las esperas por dicho thread debemos codificarlas en nuestro test. Así que nada, me puse a probarla y ¿cómo no?, me encontré con un par de problemillas.

  • Al igual que FEST, al ejecutar los test con eclipse, a veces la barra no queda ni verde ni roja, sino gris claro o negro. Esto ya hace pensar que no es problema ni de FEST ni de uispect4j, sino quizás un test mal hecho por mi parte o a algún problema con mi eclipse. Tengo que probar en casa.
  • Al ejecutar los test, me salta una fea excepción. Buscando en google, veo que hay un problema cuando juntas wndows xp, java 6 y uispec4j. ¡¡ qué casualidad !! ¡¡ justo mi configuración !!.

En fin, sigo peleándome a ver si consigo que alguna de estas herramientas me funcione correctamente. También me gustaría echar un ojo a abbott, herramienta similar aunque quizás menos conocida.

Por cierto, quiero dejar claro que no estoy diciendo que FEST o uispec4j estén mal o no funcionen correctamente. Simplemente estoy contando lo que me ha pasado trabajando dos o tres días con ellas mientras hacía el código real de mi aplicación. Pueden ser fallos de las librerías, de mi configuración concreta, de mi código que haga cosas raras o de que estoy pagando la novatada haciendo mis primeros test de este tipo con este tipo de herramientas.

 

Feb 03

Jugando con Openmap

 

A principios de semana me salió la necesidad de hacer un algoritmo que sobre la tierra me dijera lo siguiente: Supongamos que sobre un punto de la tierra trazamos un rumbo siguiendo un azimuth determinado (ángulo respecto al Norte, en sentido horario). Se trata de saber si ese rumbo pasa o no pasa por una determinada zona geográfica, definida como un polígono cerrado sobre la superficie de la tierra.

Si suponemos tierra plana, el tema es relativamente sencillo sabiendo un poco de matemáticas y de geometría. Pero si las distancias son algo grandes, no vale suponer tierra plana. Debemos meternos con trigonometría esférica, vectores o cualquier otro artilugio de matemáticas. Como me daba pereza desempolvar los apuntes de matemáticas, me dediqué a buscar por ahí y me encontré con Openmap.

Openmap es una librería java pensada para dibujar mapas y sobre ellos, dibujar lo que queramos. Podríamos, por ejemplo, dibujar nuestro polígono y el rumbo desde el punto en cuestión. La librería viene muy completita, de forma que con un simple new OpenMap(), sin parámetros, se lanza una aplicación con el mapa mundo por defecto, opciones de zoom, de dibujar día y noche, etc, etc. Pero por supuesto, también tiene todo el conjunto de clases necesario para que nosotros podamos hacer nuestro mapa a medida. Cargamos un mapa en formato shapefile, definimos nuestros menús y barras de herramientas, etc.

Pero eso no es lo que a mí me venía bien, puesto que no quiero pintar un mapa. Lo que a mí me venía bien es que tiene clases de sobra para el cálculo de "cosas" sobre la superficie de la tierra. Hay clases capaces de decirte la distancia en km de dos puntos sobre la tierra, de decirte si dos segmentos sobre la superficie de la tierra se cortan, hay clases de zonas geográficas poligonales … y clases para saber si un segmento corta a una zona poligonal.

Total, que lo que yo imaginaba iban a ser un par de días de echar cuentas arriba y abajo, probar, volver a probar y echar humo por la cabeza, al final ha sido una mañana de jugar con Openmap y aproximádamente diez líneas de código java para mi algoritmo, usando las clases de Openmap. La única "pega" que le veo es cargar con un Openmap.jar sólo para echar unas cuentas….

 

Feb 02

Cosillas con la clase File de Java

 

El otro día, tratando de hacer un test automático, me encontré con una cosa de la clase File de java que no sé si está bien o soy yo que soy demasiado enrevesado haciendo las cosas.

Supongamos que hay un fichero.txt que está en un directorio que existe, tanto el fichero como el directorio. Pero tenemos la mala suerte de que nuestro programa se está ejecutando en otro directorio que no es en el que está el fichero. Posiblemente no es la forma correcta de hacerlo, pero se me ocurrió que mi programa podía cambiarse de directorio usando la propiedad "user.dir" de la clase System.

System.setProperty("user.dir", "/directorio");

y ahora, para ver si el fichero existe, sólo tengo que hacer esto

File fichero = new File("fichero.txt");
if (fichero.exist()) {
    ….

Pues bien, resulta que NO existe. Bueno, será que File no lee la propiedad "user.dir" para ver el directorio de ejecución, o bien la lee muy al principio y luego no la cambia. No me gusta, pero puede ser así y tendría su razón de ser. Sin embargo, no es del todo así. Si en el código anterior hacemos esto

System.out.println(fichero.getCanonicalPath());
System.out.println(fichero.getAbsolutePath());

pues resulta que sale

/directorio/fichero.txt

o sea, que para los métodos getCanonicalPath() y getAbsolutePath() sí recarga la propiedad "user.dir".

Bueno, seguramente soy un poco enrevesado, pero esto me parece una pequeña incongruencia de File. O lee "user.dir" siempre, o no lo lee nunca. No parece lógico que el constructor o el método exist() no lo lea, pero los getCanonicalPath() y getAbsolutePath() sí.

Lo del enrevesamiento tiene una pequeña explicación. La aplicación en ejecución se ejecuta en un determinado directorio y busca los ficheros con path relativos, leídos de una propiedad (aplicacion.path.fichero=path/relativo/fichero.txt). Pero cuando desde maven compilo, los test se ejecutan en el directorio raíz del proyecto maven. Mi idea era hacer un test que leyera la propiedad y comprobara que el fichero existe. Para ello, tenía que llevar el directorio de ejecución del test al equivalente de la aplicación debajo de la estructura de directorios de maven (/proyecto_maven/src/main/config) y a partir de ahí usar la propiedad con el path relativo (directorio/fichero.txt)

Al final lo he solucionado con un new File("/proyecto_maven/src/main/config", "directorio/fichero"), que posiblemente sea una opción más correcta que andar jugando con el "user.dir"

Jan 29

Jugando con Apache Pivot

 

Esta mañana he estado jugando un poco con Apache Pivot. Es una librería puramente java para hacer aplicaciones ricas de internet (RIA). Normalmente yo me dedico a aplicaciones de escritorio, no de internet, pero tenía curiosidad por ver cómo era y si se podría aprovechar en una aplicación de escritorio.

La librería contiene básicamente todos los componentes habituales para hacer ventanas, clases java estilo Button, TableView, TextInput, etc. Las ventanas pueden construirse o bien usando un fichero xml en el que se indica qué componentes llevan dichas ventanas, o bien directamente desde código java, con un main(). Estos componentes tienen su aspecto propio, que no es el de SWING ni el del sistema operativo en el que corramos la aplicación. Aunque no lo he probado, aparentemente facilita mucho cosas como drag&drop o los efectos de aparición y desaparición de ventanas suavemente, degradados, transparencias, etc.

Pero lo que más me ha llamado la atención es la forma de rellenar u obtener datos de un formulario. Es algo detrás de lo que ando mucho tiempo y no he llegado a conseguir. Lo llaman "data binding" y consiste en lo siguiente:

  • Por un lado tienes tu formulario. A cada uno de los campos de dicho formulario lo identificas con una clave. Por ejemplo, si hacemos una ventana para pedir nombre y apellidos, podemos poner dos TextInput a los que identificaremos con las claves nombre y apellidos.
  • Por otro lado, podemos tener un Java Bean normalito, con dos atributos privados que se llamen igual que las claves del formulario, es decir, un atributo nombre y otro apellidos. Por supuesto, los correspondientes métodos get() y set().

Pues bien, el formulario (clase Form de Apache Pivot)  tiene métodos load() y store(), que directamente son capaces de extraer los atributos del Java Bean y meterlos en el formulario y al revés, haciendo coincidir clave con atributo. Permite además formularios anidados, que corresponden con Java Beans anidados.

En cuanto a las tablas (clase TableView), también es bastante sencilla. Al definir la tabla damos a cada columna un identificador, que luego correspoderá con el atributo del Java Bean. Así, siguiendo el ejemplo, nuestra tabla tendría dos columnas identificadas por nombre y apellidos. Nos basta ahora meter en el TableView usando el método setTableData() una List de nuestros Java Bean. La tabla se rellena solita.

Y más cosas interesantes. En las aplicaciones es importante validar los datos que se introducen. de forma que un TextInput, por ejemplo, sólo admita números entre 1 y 10,. Pues esto también está bien contemplado. Los TextInput tienen un setValidator() en el que podemos pasar una clase encargada de validar el dato según lo va escribiendo el usuario (el TextInput permanece rojo hasta que contiene un dato válido). La librería tiene ya hechos un montón de Validators habituales, como números en un rango determinado. También es interesante saber que si nuestro Java Bean tiene por ejemplo un atributo double (que no sea String), no tenemos que hacer nada especial, el formulario sabe convertirlo a texto para mostrarlo en el TextInput o reconstruirlo a partir del texto introducido por el usuario para devolver un double. Tanto los métodos load() y store() de la clase Form se "tragan" perfectamente atributos de varios tipos estándar de java, no sólo String.

Bueno, ahora sólo me queda buscar o hacer algo perecido a todo esto, pero con los componentes SWING normales de java, para poder empezar a usarlo sin tener que cambiar TODA la interface de un sólo golpe.

Nov 20

Sobre constructores, atributos y herencias.

 

Un pequeño "bug" con el que me he tropezado el otro día. Supón una clase padre abstracta en la que desde el constructor se llama al método abstracto.

public abstract ClasePadre {
   public ClasePadre() {
      …
      inicializa();
      …
   }

   public abstract void inicializa() {
   }
}

Ahora imagina que hacemos una clase hija, mal hecha, tal que así

public class ClaseHija extends ClasePadre {
   private UnAtributo atributo = null;

   @Override
   public void inicializa() {
         …
         atributo = new UnAtributo();
         …
      }
   }
}

Simplemente hemos sobreescrito el método inicializa() que nos obliga el padre y lo aprovechamos para inicializar un atributo que inicialmente es null. A partir de aquí, nuestro código se fia de que ese atributo esté inicializado. Pues bien, está mal. Veamos el orden de construcción cuando hacemos new ClaseHija()

  1. Primero java llama al constructor del padre. Este llama a inicializar() y se crea el atributo de la clase hija.
  2. Luego java asigna a los atributos de la clase hija los valores definidos al declararlos, o sea, pone atributo a null.
  3. Finalmente java llama al constructor de la clase hija.

El punto 2 es el que nos da los problemas, resulta que en el punto 1 se inicializa atributo dándole un valor y en el paso 2 se vuelve a poner a null. ¡¡ El atributo queda sin inicializar a pesar de que le hemos hecho un new !!. Nos costó un buen rato dilucidar por qué algo de lo que se hacía el new, un rato después era null.

Pues bien, esto nos ha pasado, y nos ha pasado por pasar de las métricas. Hay una que dice ConstructorCallOverridableMethod, en la que salta un error si un constructor llama a un método que no es final, es decir, que las clases hijas podrían sobreescribir y hacer que la clase padre no quedara bien inicializada. Esto no es exactamente así en este ejemplo, pero está claro que no es buena idea que un constructor llama a métodos que se pueda o, como en este ejemplo, se deban sobreescribir.

Nov 13

Problemilla con el compilador de java 6

 

Hace unos meses nos decidimos a pasarnos de java 5 a java 6. Tras hacer las pruebas correspondientes y ver que no había problemas en los sistemas en curso, correo a todo el mundo que se instalaran java 6.

Todo ha ido sin problemas durantes estos meses hasta que me vienen con una consulta. Hay un proyecto que tarda mucho en compilar en algunas máquinas y en otras no. Pruebo en mi PC y soy de los desafortunados. El tiempo de compilación normal de ese proyecto que suele ser unos 3 minutos, se pone alrededor de los 15 minutos.

Me pongo a investigar y veo que los PC donde compila rápido son los que todavía no se han actualizado a java 6 y siguen con java 5. Mirando en google y en la página de SUN, encuentro este bug. En ese bug se comenta que cuando el classpath de compilado es muy largo (habla de 56 jars en distintos directorios), el compilador tarda mucho más de la cuenta. Con un classpath menos grande, compila rápido. En java 5 eso no pasa, aunque el classpath sea largo, compila rápido.

Así que en esa página nos hemos registrado y hemos votado para que resuelvan el bug. Si alguien más tiene el problema, ya sabe qué hacer: registrarse y votar (en el lado izquierdo).

Jul 26

Jugando con PreparedStatement

 Hace tiempo que he oido que los PreparedStatement de java son más eficientes que los Statement, así que me he puesto a jugar con el tema y verlo por mi mismo.

Mi primera prueba un pequeño fracaso. Cogí una base de datos MySQL e hice 1000 inserciones seguidas con Statement, componiendo la SQL y luego otras 1000 con PreparedStatement. Por igualdad de condiciones, en ambos casos partía de la base de datos vacía. Pues bien, no solo no había diferencia apreciable, sino que además en ocasiones las Statement tardaban menos.

Me puse a investigar y descubro que hay dos requisitos indispensables para que los PreparedStatement sean más efectivos:

  1. El servidor de base de datos debe soportar los Prepared Statement, que realmente no son cosa de java, sino del servidor de base de datos.
  2. El conector/driver que se use también debe soportarlos.

Así que a ver si mi versión de driver y de servidor MySQL lo soportan … pues sí, lo soportan. Sigo investigando y descubro que al conector de MySQL hay que decirle en la configuración que use PreparedStatement del lado del servidor, cosa que por defecto no hace. Esto se consigue poniendo en la cadena de conexión algo como 

conexion = DriverManager.getConnection(
   "jdbc:mysql://localhost/basedatos?useServerPrepStmts=true", "usuario", "password");

Pues nada, pruebo otra vez y esta vez sí que es ligeramente más rápido el PreparedStatement, pero no tanto. El tiempo que tarda cada vez el programa en correr es distinto y aunque el PreparedStatement suele tardar algo menos que el Statement, a veces no ocurre así. Estamos hablando de entre 15 y 18 segundos, en el que el PreparedStatement apenas le saca un segundo al Statement y no siempre.

No puede ser que eso sea así. Algo debo estar haciendo mal. Sigo leyendo y encuentro un sitio (no recuerdo cual) que dice que algunas bases de datos no mantienen los PreparedStatement después del commit. No veo nada en MySQL que indique esto, pero me decido a hacer la prueba. Hago que la conexión no sea auto-commit y pongo el commit fuera del bucle de inserción. Las mil inserciones en cada caso quedan "en el aire" hasta que se hace un commit al finalizar cada bucle.

Esta vez el PreparedStatement sigue sacando su aproximadamente un segundo sobre el Statement … pero el tiempo total se reduce drásticamente, tanto para Statement como para PreparedStatement, a unos 3 ó 4 segundos. El hecho de no hacer commit hasta el final, independientemente del tipo de Statement usado, hace que el tiempo total baje de 15 segundos a 3. Y el segundo a favor de PreparedStatement se convierte ahora sí, en algo apreciable. Seguramente, todo lo que estaba midiendo antes eran los accesos reales a disco duro para realizar realmente las inserciones de una en una con el auto-commit. En el segundo caso, la medida ya sí debe ser mejor comparativa enrtre PreparedStatement y Statement.

Pero … ¿y en una aplicación real?. ¿Podemos permitirnos el lujo de no hacer commit hasta que hayamos hecho un conjunto más o menos grandes de inserciones?. Yo creo que normalmente no, así que ¿realmente merece la pena el uso de PreparedStatement por temas de eficiencia?. Pues supongo que depende entonces de los casos.

Por ejemplo, se me ocurre que si una sola inserción es más compleja que la mia e involucra varias tablas, podemos dejar el commit para cuando se haya hecho la inserción en todas las tablas implicadas. Esto tiene cierta lógica, la inserción se completa totalmente o no se completa en absoluto y el commit confirma varias inserciones, una en cada tabla. La diferencia de velocidad entre PreparedStatement y Statement será mayor cuantas más tablas haya implicadas.

También se me ocurre, aunque sólo sea una tabla, que si tenemos garantía de que van a ir insertándose muchos registros con mucha frecuencia, como para comprometer la velocidad con la que somos capaces de insertarlos en base de datos, podemos usar PreparedStatement y un pequeño Timer que haga commit cada cierto tiempo. Una idea rebuscada, pero sí se de casos donde podemos aplicarla. A veces tenemos equipos hardware "escupiendo" información a toda velocidad, información que necesitamos almacenar en base de datos.

En resumen, PreparedStatement posiblemente sí es más eficiente, pero para sacarle realmente rendimiento, no basta con usarlo sin más. Hay que tener en cuenta más cosas, como si tenemos el auto-commit a true o con qué frecuencia necesitamos hacer los commit. Si se investiga un poco, también ser verá que hay otras variables a tener en cuenta, como temas de caché en la configuración del servidor de base de datos o del conector, pero soy demasiado vago para investigar eso sin necesidad real.

Aparte de todo esto, a favor de los PreparedStatement, también está el tema de seguridad. Con PreparedStatement evitamos tener que chequear y "escapar" los caracteres conflictivos en las cadenas de texto que vamos a insertar en base de datos. Si vamos a insertar un nombre de usuario que pedimos al usuario y a este se le ocurre poner una comilla simple, por ejemplo "O’Donnell", podemos meterla tal cual en un PreparedStatement, pero necesitamos "escapar" la comilla antes de usarla en un Statement.