miércoles, 12 de enero de 2011

Componente cache para Tapestry 5

En el desarrollo de un proyecto web suele ser interesante tener alguna forma de cachear el contenido de ciertas regiones dinámicas de la página que puede ser costoso generarlas pero que es poco habitual que varíen su contenido, como por ejemplo, el menú, el pie de página u otras partes comunes. El cachear estas regiones ayuda a generar la página más rápidamente y supone un ahorro de recursos del servidor con lo que conseguimos dos importantes cosas: evitar que el usuario pierda el interés y se vaya de nuestra web porque la página tarda mucho en cargarse y poder atender a más usuarios con los mismos recursos del servidor.

Para Tapestry 4 había disponible un componente cache en la librería tapfx pero estos componentes no son compatibles con Tapestry 5. Con lo que he tenido la necesidad de desarrollar uno compatible con esta versión (y me ha sorprendido lo sencillo que me ha resultado como veréis por el número de lineas del mismo).

El componente Cache que he desarrollado hace uso de la librería estándar de facto para esta funcionalidad en Java ehcache, a la que si tenemos necesidad posteriormente podemos añadirle características de cache distribuida con terracotta de forma simple y sin afectar al código del componente.

Sin más vayamos a ver el código del componente:

package com.blogspot.elblogdepicodev.tapestry.components;

import java.util.Collections;

import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Ehcache;

import org.apache.tapestry5.BindingConstants;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.dom.Element;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.RequestGlobals;

public class Cache {

    @Inject
    @Property
    private RequestGlobals requestGlobals;
    
    @Parameter(required = true, allowNull = false, defaultPrefix = BindingConstants.LITERAL)
    private String cacheName;
    
    @Parameter(required = true, allowNull = false, defaultPrefix = BindingConstants.LITERAL)
    private String key;
    
    @Parameter(value = "false", defaultPrefix = BindingConstants.PROP)
    private boolean disabled;
    
    @Inject
    private CacheManager cacheManager;

    boolean beforeRenderBody(MarkupWriter writer) {
        if (!disabled) {
            Ehcache cache = cacheManager.getEhcache(cacheName);
            net.sf.ehcache.Element element = (net.sf.ehcache.Element) cache.get(key + "-" + requestGlobals.getRequest().getLocale().toString());
            if (element != null) {
                String value = (String) element.getValue();
                writer.writeRaw(value);
                return false;
            }
            writer.element("cache", Collections.EMPTY_LIST.toArray());
        }
        return true;        
    }
    
    void afterRenderBody(MarkupWriter writer) {
        if (!disabled) {
            Element e = writer.getElement();
            if (e.getName().equals("cache")) {
                String value = e.getChildMarkup();
                writer.end();
                e.pop();
            
                net.sf.ehcache.Element element = new net.sf.ehcache.Element(key + "-" + requestGlobals.getRequest().getLocale().toString(), value);
                Ehcache cache = cacheManager.getEhcache(cacheName);
                cache.put(element);
            }
        }
    }
}

Como se puede ver en el código el componente tiene 3 parámetros: cacheName para saber en que cache de ehcache se cacheará el contenido html, key para identificar el contenido en la cache y disabled para habilitar la cache o deshabilitarla según alguna condición.

La funcionanilidad está dividida en dos métodos: beforeRenderBody y afterRenderBody. En el primero lo que se hace es comprobar si se habilita el uso de la cache, si es que no se devuelve true para que se procesen el cuerpo del componente, si está habilitada la cache (disabled == false) se comprueba si en la cache indicada por el parámetro cacheName existe una clave indicada por el parámetro key, si existe el contenido html del cuerpo del componente se ha generado anteriormente y ya está cachedo por lo que no es necesario volver a procesarlo y se escribe directamente el contenido, finalmente se devuelve false para que no se procese el cuerpo del componente. Si se llama al método afterRenderBody es que se ha procesado el cuerpo del componente, si la cache está habilitada tendremos que almacenar el contenido html generado por los componentes del cuerpo del componente cache para posteriores usos, se obtiene la cache y en la clave indicada junto con el locale en el que se ha generado el contenido se guarda el html del cuerpo.

Otra parte del uso de este componente es definir el servicio CacheManager que se inyecta en el componente. Para ello tendremos que definir un método (buildCacheManager) en la clase del módulo de la aplicación para construir el servicio y poder inyectarlo en el componente.

package com.blogspot.elblogdepicodev.tapestry.services;

import java.util.Collection;

import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.config.CacheConfiguration;
import net.sf.ehcache.constructs.blocking.BlockingCache;

import org.apache.tapestry5.SymbolConstants;
import org.apache.tapestry5.ioc.Configuration;
import org.apache.tapestry5.ioc.MappedConfiguration;
import org.apache.tapestry5.ioc.annotations.SubModule;
import org.apache.tapestry5.ioc.services.Coercion;
import org.apache.tapestry5.ioc.services.CoercionTuple;
import org.apache.tapestry5.util.StringToEnumCoercion;

...

public class AppModule {

    ...
    
    public static CacheManager buildCacheManager() {
        CacheConfiguration defaultCacheConfig = new CacheConfiguration("defaultCache", 10000);
        defaultCacheConfig.setDiskStorePath("java.io.tmpdir");
        defaultCacheConfig.setEternal(false);
        defaultCacheConfig.setTimeToIdleSeconds(120);
        defaultCacheConfig.setTimeToLiveSeconds(120);
        defaultCacheConfig.setOverflowToDisk(false);
        defaultCacheConfig.setDiskPersistent(false);
        defaultCacheConfig.setMemoryStoreEvictionPolicy("LRU");
        Cache defaultCache = new Cache(defaultCacheConfig);

        CacheConfiguration fragmentosTapestryConfig = new CacheConfiguration("fragmentos-tapestry", 50);
        fragmentosTapestryConfig.setEternal(false);
        fragmentosTapestryConfig.setTimeToIdleSeconds(1800);
        fragmentosTapestryConfig.setTimeToLiveSeconds(3600);
        fragmentosTapestryConfig.setOverflowToDisk(false);
        fragmentosTapestryConfig.setDiskPersistent(false);
        fragmentosTapestryConfig.setMemoryStoreEvictionPolicy("LRU");
        Cache fragmentosTapestryCache = new Cache(fragmentosTapestryConfig);        
        
        CacheManager cacheManager = new CacheManager();
        cacheManager.addCache(defaultCache);
        cacheManager.addCache(fragmentosTapestryCache);
        
        Cache cache = cacheManager.getCache("fragmentos-tapestry");
        BlockingCache fragmentosTapestryBlockingCache = new BlockingCache(cache);
        cacheManager.replaceCacheWithDecoratedCache(cache, fragmentosTapestryBlockingCache);
        
        return cacheManager;
    }
}

La configuración del ehcache puede hacerse definiendo un archivo ehcache.xml o como he preferido en este caso de forma programática, aquí se definen dos cache: la cache por defecto y una cache para los fragmentos html que cacheará el componente Cache.

El uso del componente cache sería tan sencillo como:

<t:cache cacheName="fragmentos-tapestry" key="ejemplo">
    [Componentes de tapestry que generarán en contenido HTML que se cacheará]
</t:cache>

Y esto es todo, unas 65 líneas de código que pueden mejorar notablemente el tiempo de respuesta de nuestras aplicaciones del ya de por si excelente rendimiento que ofrece Tapestry. Si este componente te ha resultado interesante o puede serte útil para tu proyecto siente libre de usarlo, modificarlo o proponer mejoras, solo pido que dejes un comentario en esta entrada para conocer a otras personas que usan Tapestry o tienen interés en él.

Referencia:
Documentación sobre Apache Tapestry
http://ehcache.org/
http://www.terracotta.org/
http://andyhot.di.uoa.gr/tapfx/app
http://tapfx.sourceforge.net/