Migrar de JSP a Velocity

Puedes encontrar la versión original de este artículo en Inglés en:

http://www.onjava.com/

Introducción

Hay una gran variedad de posibles opciones cuando se trata de producir contenido web dinámico. Normalmente, la mayoría de los proyectos caen en lo ya aceptado como estándar, es decir si la plataforma elegida para tu proyecto es Java, tendrás que utilizar JSPs. Esto tiene sentido por varias razones: el personal nuevo suele tener más experiencia con algo estándar; muchos servidores web soportan JSP, dandote la oportunidad de elegir entre muchas plataformas Web; hay mucha documentación disponible porque la gente normalmente "ya lo ha hecho antes"; etc. Sin embargo, algunas veces utilizar el estándar no es la mejor elección. Quizás necesites o quieras un control más directo sobre el código que produce tu contenido; quizás tienes problemas de rendimiento con las JSPs; o quizás la nueva dirección técnica de tu compañia está intentando salir de este paradigma.

Velocity es un sencillo, pero poderoso, motor de plantilla que se puede utilizar como generador de contenido dinámico como alternativa al estándar impuesto. En algunos casos proporciona mejores rendimientos que las páginas JSP, y ciertamente fuerza a los desarrolladores web a pensar de forma diferente en la forma de separar el contenido del código.

El mayor problema es que si ya tienes un proyecto basado en JSP, o si tienes un equipo poco experimentado que ya domina JSP, cambiar a Velocity representa un gran reto. En este artículo, intentaremos eliminar algunos de esos obstáculos, utilizando algunas sencillas extensiones de Velocity que espero que reduzcan el dolor que podrías experimentar durante la migración.

Un poco de Historia

Hace casi dos años heredé un sistema de tamaño medio basado en servlets bastante mal escrito. El software utilizaba un motor de plantillas bastante básico, pero también había mucho HTML embebido en el código. De hecho, había una lista enorme de decisiones de diseño poco agraciadas, y partes del código manejadas con ineptitud (incluyendo el estilo de desarrollo del 'metodo-enorme-que-lo-hace-todo') esperando a algún desarrollador en una pesadilla de código enredado como espaguetis.

Considerando las limitaciones del sistema y los requerimientos que tenía mi compañía de un sistema con más capacidades, tomé la decicisión de eliminar el 99 por ciento del código y empezar desde cero, diseñando un sistema J2EE totalmente nuevo. El nuevo sistema ha estado trabajando durante casi un año, con muy pocos incovenientes. Es, en un orden de magnitud, más mantenible, y con una excepción, más divertido de trabajar con él.

Más tarde, y a pesar del hecho de que podría considerarse un punto de vista inadecuado, me sentí insatisfecho con mi decisión original de trabajar con JSPs para los interfaces Web y WAP. La decisión orginal era estar lo más cerca posible del patrón MVC, que no es una consideración tan simple como intentar embeber código Java en tus páginas Web. La tarea se hizo más fácil con el uso de JSTL (standard tag library), pero inevitablemente, encuentras que hay algo que simplemente no puedes hacer con JSTL, por eso escribes tu propia librería para hacer la tarea. Y hay objetos que necesitas utilizar en la página que, simplemente no funcionan con JSTL a menos que se les haya llamado y, debido a las restricciones de tiempo en esa parte del desarrollo, fue más fácil llamarlos directamente desde Java. Es suficiente decir que el resultado final, aunque perfectamente funcional, es menos elegante de lo que uno podría esperar al principio del desarrollo.

Aceptar el Reto

La re-evaluación de la parte web de nuestro conjunto tecnológico ha ganado en importancia recientemente debido a un problema con el contenedor servlet y la forma en que éste maneja los JSPs bajo ciertas condiciones de carga (el infame problema "Too Many Open Files"), y después de examinar todas las opciones disponibles, hemos seleccionado Velocity debido a un par de consideraciones importantes:

  • Open Source — tanto si es un concepto importante para otros como si no, nosotros ejecutamos una tienda Linux, por eso el software de código abierto es normalmente nuestra primera opción.
  • Rendimiento — mis tests iniciales indicaron que en páginas simples, Velocity mejora el rendimiento de JSP entre un 35 y un 45 por ciento. Para páginas más complicadas, la diferencia empieza a disminuir (y después de añadir todos los otros factores que entran en una página web dinámica media, algunas veces cae hasta sólo un 5 por ciento), pero la diferencia existe.
  • Elegancia — La sintaxis de Velocity es simple y efectiva. También es bastante fácil añadir nuevos comandos (o directivas como se las llama) sin violar las convenciones de simplicidad.

Hay también un punto de vista que sugiere que si entregas una página JSP/JSTL a un diseñador empelará mucho más tiempo que si le entregas una plantilla Velocity (la desventaja de la aproximación JSP/JSTL es que están viendo XHTML, tener etiquetas XML para el contenido dinámico no hace precisamente la vida más fácil a los diseñadores web no-técnicos).

¿Cómo Funciona?

En Velocity, por cada página (o si lo prefieres, por cada grupo de páginas), escribes un servlet. Tu servlet extrae todos los datos que necesitas para esa página, los sitúa en un contexto, y finalmente, le dice a Velocity que dibuje ese contexto utilizando una plantilla. La plantilla contiene etiquetas HTML y directivas Velocity - no código Java.

Por supuesto, por esto es tan efectivo; forzándote a limitar severamente el código de la plantilla, simplificas tus páginas - permitiendo de forma efectiva que se puedan entregar a un diseñador web, como no se puede hacer con JSP.

Por el contrario, en nuestras páginas JSP utilizamos JavaBeans para ocultar el código Java lo más posible. Luego incluimos estos beans en nuestras páginas utilizando la etiqueta useBean, y seleccionando las propiedades de esos beans antes de recuperarlas utilizando etiquetas JSTL:

<jsp:useBean id="comp" scope="session" 
    class="com.lateralnz.comp.view.CompDefBean" />
<jsp:setProperty name="comp" 
    property="someprop" value="someval" />

...
...

<c:forEach var="comp" items="${comps.iterator}">
  <tr>
    <td class="list">
      <c:out value="${comp.compCode}" />
    </td>
...

Como puedes imaginar, una conversión correcta de JSP a Velocity representa una signficante cantidad de trabajo, sin mencionar la curva de aprendizaje implicada si estás liderando un equipo relativamente inexperto. El tiempo siempre es un problema en cualquier proyecto, y aunque nuestros sistemas no están limitados a la imaginación, no tenemos los recursos para convertir unos cientos de páginas JSP a una alternativa tecnológica, especialmente cuando esa tecnología requiere que trabajes con métodos muy diferentes. Según esto, tomé la decisión de hacer una migración gradual a Velocity, adoptando una aproximación más comedida.

Añadir la Funcionalidad Perdida por JSP

El paso uno es permitir que las plantillas de Velocity sean llamadas de la misma forma que una JSP. Para empezar con ello he creado un servlet básico (VMServlet) y luego lo he mapeado con el url-pattern: "*.vm":

<servlet-mapping>
    <servlet-name>vm</servlet-name>
    <url-pattern>*.vm</url-pattern>
</servlet-mapping>

Esto significa que http://mydomain.com/mypath/test.vm se pasará como una llamada de servlet estándar a VMServlet. Al método principal de VMServlet, processRequest, se le llama desde los métodos servlet estándars(doGet, doPost), y realiza las operaciones requeridas para añadir varios objetos implícitos al contexto de Velocity, cargar la plantilla, y producir la salida real:

protected void processRequest(
    HttpServletRequest request, 
    HttpServletResponse response) 
  throws ServletException, IOException {

Aquí se carga la plantilla utilizando los métodos estándar de Velocity (las plantillas son chacheadas por el motor):

  String templateName = request.getServletPath();
  Template tmp = ve.getTemplate(templateName);
  VelocityContext ctx = new VelocityContext();

El contextpath, la session, la jsessionid, y request van dentro del contexto (estos son los objetos más comunes utilizados en JSPs):

  HttpSession session = request.getSession(true);      
  ctx.put("contextPath", request.getContextPath());
  ctx.put("session", session);
  ctx.put("jsessionid", JSESSIONID_STR + session.getId());
  ctx.put("request", request);

El paso final es dibujar la salida, utilizando un PrintWriter, el Context de Velocity, y la plantilla:

  response.setContentType(contentType);
  PrintWriter out = response.getWriter();
  tmp.merge(ctx, out);

Duplicar una JSP

Echando un vistado a una JSP básica, todavía necesitamos algunas características básicas para poder facilitar nuestra migración.

En el ejemplo de abajo, seleccionamos el tipo de contenido, ejemplarizamos un JavaBean (un objeto Date), incluimos un par de ficheros JSP "fragmentados", y utilizamos la librería JSTL fmt para poder mostrar una cabecera y un mensaje de un ResourceBundle. La última línea llama al método getTime del Date (esto se podría hacer más fácilmente llamando a la etiqueta JSTL c:out para obtener el mismo dato):

<%@ page contentType="text/html; charset=UTF-8" %>
<fmt:setBundle basename="com.lateralnz.messages" var="sysm" />
<jsp:useBean id="test" scope="session" class="java.util.Date" />
<html xmlns="http://www.w3.org/1999/xhtml">

<%@ include file="htmlhead.jsf" %>

<body>
<%@ include file="header.jsf" %>

<h1><fmt:message key="app.welcome_title" bundle="${sysm}" /></h1>

<p><fmt:message key="app.welcome_text" bundle="${sysm}" /></p>

<p><%= test.getTime() %></p>

</div>
</body>
</html>

Velocity proporciona directivas para publicar algo de esta JSP, pero no toda. En una instalación típica de Velocity, un servlet situaría los objetos Date y ResourceBundle en el contexto para que pudieran ser llamados desde dentro de la plantilla. Luego el paso dos, es evitar esta necesidad (ignora por el momento que hacer esto equivale a dar un paso atrás en el objetivo MVC mencionao antes). He crado mi propia directiva llamada #usebean (puedes ver su código fuente en UseBeanDirective.java), a la que se le pasa un nombre, el nombre de la clase a utilizar, y su ámbito. Entonces UseBeanDirective crea el JavaBean y lo pone en el contexto. Si el ámbito es "application", almacena el bean en un HashMap estático para su uso posterior; si el ámbito es "session" se almacena en la sesión del usuario.

También se necesita un método en VMServlet para seleccionar el tipo de contenido de la plantilla, de una forma similar a como se hace en una página JSP. Mi elección actual es almacenar los tipos de contenidos registrados por su requestURI, y utilizar el método getData de la plantilla de Velocity para buscar una variable ($contentType) en la página:

private final String determineContentType(Node node) {
  for (int i = 0; i < node.jjtGetNumChildren(); i++) {
    StringBuffer sb = new StringBuffer();
    Node n = node.jjtGetChild(i);
    
    if (n instanceof org.apache.velocity.runtime.parser.node.ASTSetDirective) {
      Token t = findTokenWithPrefix(n.getFirstToken(), "$");
      if (t == null || !t.toString().equalsIgnoreCase("$contentType")) {
        continue;
      }
      else {
        t = findTokenWithPrefix(t, """);
        String ct = StringUtils.remove(t.toString(), """);
        return ct;
      }
    }
  }

  return "text/html";
}

private final Token findTokenWithPrefix(Token t,String prefix) {
    
  if (t == null) {
    return null;
  }
  else if (t.toString().startsWith(prefix)) {
    return t;
  }
  else {
    return findTokenWithPrefix(t.next, prefix);
  }
}

Admitámoslo, esto es un atajo, pero hace el trabajo. Por lo tanto, he añadido lo siguiente a processRequest:

String contentType;
if (contentTypes.containsKey(requestURI)) {
  contentType = (String)contentTypes.get(requestURI);
}
else {
  contentType = determineContentType((Node)tmp.getData());
  contentTypes.put(requestURI, contentType);
}

La plantilla final de Velocity, se debería parecer a esto:

#set ( $contentType = "text/html" )
<html xmlns="http://www.w3.org/1999/xhtml">

#usebean ( test "java.util.Date" page )
#usebean ( sysm "com.lateralnz.MyMessagesBundle" application )

#parse ( "htmlhead.vm" )

<body>
#include ( "header.vm" )

<h1>
$sysm.getString('app.welcome_title')
</h1>

<p>
$sysm.getString('app.welcome_text')
</p>

<p>$test.getTime()</p>

</body>
</html>

No hay una gran diferencia con la página JSP original, como puedes ver. Hemos mostrado el ResourceBundle cargado como un JavaBean personalizado, pero en nuestro sistema real, he almacenado el paquete en VMServlet, y lo he cargado automáticamente como un objeto implícito.

Hay algunas cosas que no hemos mencionado. Si estás utilizando JSTL, probablemente también utilizarás las otras librerías de etiquetas de Jakarta. Si este es el caso, deberías realizar alguna codificación adicional. Afortunadamente, es bastante fácil extender Velocity creando nuevas directivas. Hasta ahora yo sólo he necesitado crear un par de directivas extras: una para formatear fechas y otra para formatear números, y una sencilla directiva que deja de renderizar la plantilla:

public class ExitDirective extends Directive {
  private static final String EXIT = "exit";
  private static final 
    StopRenderingException STOP_RENDER_EXCEPTION = new StopRenderingException();
  
  public boolean render(InternalContextAdapter context, Writer writer, Node node) 
        throws IOException, ResourceNotFoundException, ParseErrorException, 
           MethodInvocationException {
    throw STOP_RENDER_EXCEPTION;
  }
  
  public int getType() {
    return DirectiveConstants.LINE;
  }
  
  public String getName() {
    return EXIT;
  }
}

Dentro del método processRequest de VMServlet, se captura StopRenderingException, y efectivamente se detiene el proceso de renderizado de la plantilla. En nuestro sistema, utilizamos esta directiva particular en la cabecera de una página WAP para detenerla, en el caso de que el usuario no haya realizado correctamente el logging en el sistema:

#if ( !$session.getAttribute(USER_ID) )
<card id="message" title="Invalid Session">
<p>Sorry, but your session has 
timed out.</p>
</card>
</wml>
#exit ( )
#end

Conclusión

Velocity proporciona a los desarrolladores Web un poderoso mecanismo de plantillas - sin embargo, esto puede presentar una gran barrera de entrada para aquellos que están interesados en migrar desde JSPs y las librerías de etiquetas. Utilizando un sencillo servlet, y extendiendoVelocity, con su concepto de directivas, espero que sea posible imitar una porción considerable de las funcionalidades de tus páginas JSP, eso sí haciendo la migración a pequeños pasos.

Referencias

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP