lunes, 6 de abril de 2009

Solución al Problema de Flex con el Cache de los Navegadores de Internet

Introducción:

Es sabido que el uso de aplicaciones desarrolladas en Flex, traen consigo un problema asociado con la Cache de los navegadores de Internet, provocando que los usuarios accedan a información desactualizada e incluso lleguen a utilizar versiones obsoletas de las aplicaciones que se construyen.

Esto último fue lo que me motivo a buscar una solución eficiente ante el problema del cacheo de las páginas.

Voy a comentar aquí con detalle que es lo que intente solucionar específicamente.

Es muy común que las aplicaciones Web corporativas sean accedidas por muchos clientes diariamente, y también es frecuente que estas estén en un constante proceso de desarrollo continuo, otorgándoles a los clientes nuevas funcionalidades agregadas en cada iteración dentro del ciclo de vida del proceso de desarrollo del programa.

El problema se presenta luego de haber desplegado una nueva versión de la aplicación en el servidor. Los clientes siguen accediendo a la versión antigua, ya que esta se mantiene almacenada en la cache del navegador. Y como esto depende de la configuración particular del navegador, pude que dure almacenada solo por una sesión o puede que quede eternamente mientras no se realice ninguna acción manual como es el limpiado de los archivos temporales y cache del navegador.

Es natural que la primera ocurrencia del desarrollador sea deshabilitar o impedir que se almacene en cache la pagina. Por supuesto que la solución anda bien, pero es bastante molesto para los usuarios tener que recargar toda la aplicación cada vez que se necesite accederla. Sobre todo porque las paginas desarrolladas en Flex suelen tener gran tamaño y llegan a demorar en algunos casos varios minutos en cargarse completamente.

La otra alternativa es forzar que la Url de la página cambie en cada ingreso. En este punto pueden existir dos variantes. La primera es la de agregar un parámetro de fecha (timestanp, la pagina se recarga siempre), y la segunda es acceder a una página cuyo nombre se corresponde con una versión en particular que se va incrementando (Ej:aplicación_v1.html).

La solución que presento se basa en esta última alternativa, pero teniendo en cuenta un nuevo problema. Suponiendo que el cliente accedió a la aplicación mediante un index.html y esta fue direccionada a la versión actual aplicación_v1.html, puede suceder que un cliente que este familiarizado con los favoritos, desee guardar la pagina para ingresar en otra ocasión. Va a producirse que este cliente, ingrese “siempre” a la versión 1 de la aplicación. Este es el problema fundamental que se quiere resolver.


Aproximación:

La solución consta de cuatro cosas fundamentales:

  1. Mantener un registro actualizado de versión en alguna base de datos.
  2. Crear un archivo dinámico que busque la última versión, construya la Url correcta y cargue la pagina. Esta página debe ser obligada a que no sea almacenada en cache.
  3. La Url de la página no debe variar nunca. (Explicado anteriormente: para que, si se guarda en favoritos no se haga referencia a una versión incorrecta.)
  4. Compilar la última versión en el servidor, incrementar la versión y renombrar la pagina versionada.


Implementación:

Nota:

El servidor de aplicaciones utilizado es Glassfish.

La Base de Datos es MySQL.

Entorno de programación J2EE.

Tecnologías Usadas: Faces

La aplicación Flex genera un archivo llamado Aplicación.html y Aplicación.swf.


Paso 1:

Creamos una tabla de configuraciones generales e insertamos los parámetros necesarios:

CREATE TABLE `test`.`configuracion` (

`Id_parametro` bigint(20) NOT NULL auto_increment,

`parametro` varchar(50) NOT NULL,

`valor` varchar(50) NOT NULL,

`descripcion` varchar(200) default NULL,

PRIMARY KEY (`Id_parametro`)

) ENGINE=InnoDB DEFAULT CHARSET=latin1;


INSERT INTO Configuracion (parametro, valor, descripcion)

values ('APP_VERSION', '1', 'Version de la aplicacion flex');

INSERT INTO Configuracion (parametro, valor, descripcion)

values ( 'APP_NOMBRE', 'aplicacion', 'Nombre de la aplicacion flex');


Paso 2:

Configuramos Faces en nuestro Servidor Web:

Agregamos las siguentes lineas en el archivo web.xml.




<servlet>

<servlet-name>Faces Servlet</servlet-name>

<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>

<load-on-startup>1</load-on-startup>

</servlet>



<servlet-mapping>

<servlet-name>Faces Servlet</servlet-name>

<url-pattern>*.faces</url-pattern>


</servlet-mapping>



<welcome-file-list>

<welcome-file>index.jsp</welcome-file>

</welcome-file-list>






Configuramos las reglas de navegación y el bean en el archivo principal de Faces.

Lo que hacemos es agregar una regla de navegación que permita desde el archivo Index.jsp manejar una acción invocada con el nombre findNextPage. Por otro lado Registramos el Bean (LocatorBean) que va a manejar esta acción.

Agregamos las siguentes lineas al archivo faces-config.xml:



<navigation-rule>

<from-view-id>/index.jsp</from-view-id>

<navigation-case>

<from-outcome>findNextPage</from-outcome>

<to-view-id>/*</to-view-id>

</navigation-case>

</navigation-rule>



<managed-bean>

<managed-bean-name>locator</managed-bean-name>

<managed-bean-class>com.Bean.LocatorBean</managed-bean-class>

<managed-bean-scope>session</managed-bean-scope>

</managed-bean>






Creamos un archivo dinámico Jsp llamado Index.jsp

En esta página se importa el Bean para poder acceder al metodo “getNextPage”.
Luego mediante una acción forward se carga la página que nos devuelve este método.

Agregamos las siguientes líneas:


<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%

response.setHeader( "Pragma", "no-cache" );

response.setHeader( "Cache-Control", "no-cache" );

response.setDateHeader( "Expires", 0 );

%>

<html>

<head>

</head>

<body>

<jsp:useBean id="locator" class="com.Bean.LocatorBean" scope="page"/>

<jsp:forward page="<%=locator.getNextPage()%>"/>

</body>

</html>





Creamos una Clase Java llamada LocatorBean.java dentro del paquete com.Bean.

Esta clase es la encargada de consultar los parámetros que tienen el nombre y versión de la aplicación y devuelven la próxima Url que se debe cargar.

Agregamos las siguientes lineas:


/*

Se omiten sentencias de Import y Package

*/

public class LocatorBean extends HibernateDaoSupport {

private String nextPage;

public String getNextPage() throws ConfiguracionException {

/*

No se explicara en este momento el método de conexión con la base de datos.

Pero se deberá representar una conexión que por medio de un DAO, consulte los parámetros “APP_VERSION” y “APP_NOMBRE” en la base de datos.

*/

DaoRegistry.getConfiguracionDao();

ApplicationContext ctx = AppContext.getApplicationContext();

ConfiguracionSpringDao configuracionDao = (ConfiguracionSpringDao) ctx.getBean("configuracionDao");

String appNombre;

String appVersion;

appNombre = configuracionDao.findByParametro("APP_NOMBRE");

appVersion = configuracionDao.findByParametro("APP_VERSION");

nextPage = appNombre + appVersion + ".html";

return nextPage;

}

public void setNextPage(String newValue) {

nextPage = newValue;

}

}




Paso 3:

El paso tres se cumple con en la siguiente sentencia dentro del archivo index.jsp.

Este comando hace que se cargue la página pero no cambie la Url, como puede llegar a suceder con algún otro tipo de redirección.



<jsp:forward page="<%=locator.getNextPage()%>"/>



Paso 4:

Se crea un script bash para hacer el despliegue de la aplicación en el servidor de producción, montado sobre Linux.

(De igual forma debería hacerse en el caso que el servidor este montado sobre windows)

Este script es el encargado de realizar el despliegue contra el servidor de aplicaciones, también debe incrementar el parámetro de versión en uno “1”, y por ultimo, renombrar el archivo generado en Flex para que quede versionado.

Se crea el archivo despliegue.sh y se agregan las siguientes líneas:



#! /bin/bash



#####################

# Se omiten los comandos para realizar el despliegue de la aplicación

# Aquí es donde debería realizarse el despliegue.

# Por ejemplo para un servidor glassfish debería quedar lo siguiente:

# $GLASSFISH_HOME/bin/asadmin deploy aplicación.war

#####################

#

# RUTA_APLICACION: debe apuntar al directorio donde se encuentra

# el archivo a versionar. En este ejemplo es aplicación.html

#

#####################



$RUTA_APLICACION=/algunpath

#INICIO VERSIONADO



mysql -u root --password=mi-clave base_datos <<!!

UPDATE base_datos.configuracion

SET valor=valor+1

WHERE parametro = 'APP_VERSION';

quit

!!



mysql -u root --password=mi-clave base_datos > /tmp/app_version <<!!

SELECT valor FROM base_datos.configuracion where parametro = 'APP_VERSION';

quit

!!



APP_VERSION=`head -2 /tmp/app_version | tail -1`

echo "Version Index: "$APP_VERSION



mysql -u root --password=mi-clave base_datos > /tmp/app_nombre <<!!

SELECT valor FROM base_datos.configuracion where parametro = 'APP_NOMBRE';

quit

!!



APP_NOMBRE=`head -2 /tmp/app_nombre | tail -1`

echo "Nombre Index: "$APP_NOMBRE



mv $RUTA_APLICACION/$APP_NOMBRE.html \ $RUTA_APLICACION/$APP_NOMBRE$APP_VERSION.html



#FINAL DE VERSIONADO



Prueba:
Se comprobó que la página la primera vez se carga completamente, luego quedaba almacenada en cache y fue accedida rápidamente en las siguientes cargas.
Luego se compilo una nueva versión y se desplegó la misma corriendo el script despliegue.sh. Por ultimo se verificó que al ingresar nuevamente a la página, se descargaba automáticamente la última versión de la aplicación.


Notas Adicionales:

No presenta problemas en tiempo de desarrollo:

En entornos productivos esta aplicación va a ser versionada teniendo en cuenta los parámetros “APP_VERSION” y “APP_NOMBRE”, de la base de datos. Pero para los esquemas donde se realicen el desarrollo y las pruebas, no es necesario correr el script despliegue.sh. Simplemente realizamos la compilación de la aplicación naturalmente, y dejamos el parámetro “APP_VERSION” sin ningún valor asignado. Con esto lograremos que la Url que se genera en las pruebas sea /url/aplicación.html y no /url/aplicación<version>.html


Para los que quieran usar Struts, u otra tecnología similar:

No existen inconvenientes para implementar esta solución usando Struts, solo será necesario construir la clase Action correctamente para redirigir a la nueva pagina.


Para los que quieran usar PHP:

También sería posible realizar la consulta desde PHP y redirigir la pagina.

Lo único a tener en cuenta es que la nueva página debe ser cargada sin que la Url cambie.


Se puede hacer una solución que funcione solo desde Flex?

Si, también probé realizar una pequeña aplicación desde Flex para que desde una página se llame a la otra. Encontrando las siguientes dificultades:

1- La primera vez que se invoca la pagina, se realizan dos cargas de página. (No es para nada una buena práctica). La segunda vez, se cachean las dos páginas. Cuando se crea una nueva versión, se carga solo la segunda página como se esperaba.

2- La Url cambia si se realiza una redirección, con lo cual perdemos toda utilidad para esta solución.

3- Existe un método para que no cambie la Url, que es cargar la pagina versionada como módulo dentro de la primera. Pero es sabido que los módulos traen consigo otros problemas que obligan a los desarrolladores en muchos casos a no usarlos.

4- La página incrementa considerablemente su tamaño.



Referencias en la Web del Problema:

http://shanky.org/es/2008/08/02/flex-httpservice-browser-cache-and-ie/

http://flexonblog.wordpress.com/2008/01/24/another-solution-for-cache-problem-in-ie/

http://www.flexpasta.com/index.php/2008/11/24/firefox-swf-cache/