Este documento contiene una propuesta para extender la API de Servlet como se define en la JSR 154 con asincronÃa para cumplir los objetivos de la JSR 315.
En este momento, esta propuesta es sólo una contribución de su autor (Greg Wilkins) y no una propuesta oficial de la JSR.
JSR 315 nombra “Async and Comet Support” como una caracterÃstica objetivo as a targeted feature, desacrita como:
La habilidad de recibir datos de un cliente sin bloquear si los datos tardan en llegar.
La habilidad de enviar datos a un cliente sin bloquear si el cliente o la red es lenta.
El “estilo cometa” de una aplicación web AJAX puede requerir que la gestión de una petición sea retrasada hasta que ocurra un timeout o un evento.
Retrasar la gestión de una petición también es útil si hay que obtener un recurso remoto/lento antes de servir la petición o si el acceso a un recurso
especÃfico necesita ser suprimido para prevenir demasiados accesos simultáneos.
El “estilo comet” de una aplicación web AJAX puede requerir que una respuesta se mantenga abierta para permitir que datos adicionales sean enviados cuando ocurran eventos asÃncronos.
La habilidad de notificar eventos bloqueantes o no bloqueantes. El concepto de canales - la habliddad de suscribirse a un canal y obtener eventos asÃncronos de ese canal. Esto implica poder crear, suscribirse, desuscribirse y también aplicar algunas restricciones de seguridad sobre quién puede añadirse o quién no puede añadirse a un canal.
Los siguientes requerimientos adicionales han sido usados para guiar el diseño de esta propuesta. Estos requerimientos son sólo desde la experiencia del autor y deben ser sujeto de revisión y discusión:
Toda gestión relevante de peticiones asÃncronas y generación de respuestas asÃncronas debe tener lugar dentro de la cadena de llamadas existente de Filter.doFilter(..) a Servlet.service(…). Sólo dentro de esta cadena de llamadas tenemos un entorno bien definido para autenticación, autorización, JNDI, y acceso a otros servicios J2EE. La creación de gestores de peticiones adicionales o métodos de respues requerirá una redefinición sustancial del entorno del servlet e impedirá compatibilidad de marcos de trabajo.
Un servlet asÃncrono debe ser una first class servlet y no debe estar limitado de un modo significativo. Para ello, un servlet asÃncrono debe poder:
Compatibilidad de framework
La gestión de peticiones y respuestas asÃncronas debe poder hacerse por frameworks existentes con pocas o ninguna modificación. por ejemplo, JSP y/o JSF deben poder ser usados para gestionar una petición asÃncrona y para generar una respuesta asÃncrona.
El contenedor del servlet deberÃa estar capacitado para gestionar tareas de entrada/salida tanto comunes como complicadas que actualmente las gestionar el desarrollador del servlet. Con los servlets 2.5, el único tipo de contenido gestionado autmáticamnete por el contenedor es “application/x-www-form-urlencoded“. El desarrollador del servlet (o framework) debe gestionar tares corrientes como:
Si la e/s asÃncrona está habilitada dentro de un contenedor de un servlet, estas tareas necesitarán ser reimplementadas y serán más complejas y con más posibilidad de fallos. Eliminar esta fuente de errores y de esfuerzo innecesario implica dar poder a los contenedores de servlets para implementar tareas comunes de e/s en lugar del desarrollador del servlet. El contenedor entonces puede ser libre de usar técnicas asÃncronas eficientes (o incluso optimizaciones nativas) para hacer e/s en lugar del desarrollador.
Esta propuesta pretende cumplir los requerimientos de la JSR315, además de otros, extendiendo el contenedor de servlets en las siguientes dos areas:
Las clases ServletRequest y ServletResponse se extienden con métodos para permitir que el contenido de los mensajes HTTP sean devueltos y establecidos como objetos complejos Java. El contenedor será responsable de la entrada/salida, parseo y generación del contenido usando conversores suministrados por el contenedor o por la aplicación.
Se esperará de un contenedor compatible que suministre un conjunto de conversores estándar, incluyendo los siguientes:
| Tipo Mime | Tipo Java |
|---|
El ciclo de vida de una petición se extiende de modo que la gestión de una única petición puede englobar múltiples envÃos a la cadena de filtros y al método service del servlet. Esto permite que la gestión de una petición y la generación de una respuesta se retrasen hasta que los recursos estén disponibles u ocurran o acaben eventos asÃncronos.
El ciclo de vida de una petición empieza cuando una pareja petición/respuesta sea despachada por primera vez. Normalmente, se completa el ciclo de via de una petición cuando retorna el despacho y se confirma, se lanza y se finaliza la respuesta. Sin embargo, la extensión permite que una petición sea suspendida, tal que cuando el dispatch vuelve al contenedor, el ciclo de vida de la petición no sea completado.
El contenedor mantiene la petición suspendida hasta que o se reanuda o expira por sobrepasar el tiempo de espera. Entonces la petición se devuelve siendo despachada a la cadena de filtros normal y al método service del servlet. Este ciclo se puede repetri más de una vez y la suspensión transforma el mecanismo existente de despacho en un callback asÃncrono. Para permitir que este mecanismo funcione con los frameworks y códigos existentes, se puede usar en cualquier momento durante el ciclo de vida de la petición y no se deshabilita por la suspensión de la petición.
request suspend.
La interfaz ServletRequest existente se extiende cin métoso para acceder al contenido parseado y para suspender la gestión de la petición:
public interface ServletRequest { /** * Obtiene el contenido del cuerpo de la petición como un Object. * * <p>Los bytes recibidos por el servidor serán parseados y * convertidos a un Object por un {@link RequestContentConverter} * del contenedor o de la aplicación configurado en el descriptor * de despliegue o las anotaciones del servlet. * Ejemplos del tipo de Object que pueden ser retornados incluyen * {@link java.nio.ByteBuffer}, {@link String}, {@link java.io.File} y * {@link org.w3c.dom.Document}. Si la petición especifica una * codificación de caracteres o un Locale, entonces esto se usará en el * parseo del contenido, sinó se deben especificar codificaciones por * defecto en el descriptor de despliegue o en las anotaciones del servlet. * </p> * * <p>Si el descriptor de despliegue o las anotaciones del servlet indican que la * URL de la petición es asÃncrona, entonces el contenedor parseará en contenido * antes de que la petición sea servida a la cadena de filtros o/y al servlet y * esta llamada nunca se bloqueará. De otro modo, el contenido es parseado * durante esta llamada y puede bloquearse esperando a recibir el contenido completo.</p> * * @return un objeto que contiene el cuerpo parseado de la petición * * @exception IllegalStateException si los métodos {@link #getReader} o * {@link #getInputStream} ya han sido llamados por esta para esta * petición * * @exception ObjectSTreamException si hubo algún problema parseando el contenido. * * @exception IOException si ha ocurrido un excepción de entrada o salida. */ Object getContent() throws IllegalStateException, ObjectStreamException; /** * Suspende el procesador de la petición y de la {@link ServletResponse} asociada. * * <p>Despues de que este método sea llamado, el ciclo de vida de la petición será extendida * más allá del retorno al contenedor desde el método * {@link Servlet#service(ServletRequest, ServletResponse)} y las llamadas * {@link Filter#doFilter(ServletRequest, ServletResponse, FilterChain)}. Si una * petición es suspendida, entonces el contenedor no confirmará la respuesta asociada * cuando la llamada a la cadena de filtros y/o al service del servlet vuelve al * contenedor. En su lugar, el contenedor esperará hasta que se llame a * {@link ServletRequest#resume()} o expire el timeout, y entonces la petición se * volverá a despachar via la cadena de filtros al servlet. * </p> * * <p>El objeto {@link ServletResponse} es deshabilitado por esta llamada, * de modo que todas las llamada para modificar o generar una respuesta son * ignoradas, hasta que una petición sea devuelta. Con la excepción de que * los objetos devueltos por {@link ServletResponse#getNonStopOutputStream()} y * {@link ServletResponse#getNonStopWriter} permanecen activos durante todo * el ciclo de vida de la petición.</p> * * <p>Si ya hay una petición suspendida, cualquiera llamada subsiguiente a * suspender establecerá el timeout al mÃnimo del timeout previo y del nuevo * timeout pasado</p> * * <p>Suspender sólo debe ser llamado por un thread que esté dentro de la pila de * llamadas al servicio de {@link Filter#doFilter(ServletRequest, ServletResponse, FilterChain)} * y/o {@link Servlet#service(ServletRequest, ServletResponse)}</p> * * @see {@link #resume()} * * @param timeoutMs El tiempo en milisegundos a esperar antes de devolver esta petición. * * @exception IllegalStateException Si el thread ejecutor no está dentro de la pila de llamadas * de {@link Filter#doFilter(ServletRequest, ServletResponse, FilterChain)} * y/o {@link Servlet#service(ServletRequest, ServletResponse)} */ void suspend(long timeoutMs); /** * Reanudar una request suspendida. * * <p>Este método puede ser llamado por cualquier thread al que se le haya pasado una referencia a * a la petición para pedir que la request sea reenviada</p> * * <p>Si resume se llama antes de que una petición suspendida sea devuelta al contenedor * (ie el thread que llamó a {@link #suspend(long)} está todavÃa dentro de la cadena de filtros * y/o en el método service del servlet), entonces la reanudación no se lleva a cabo hasta que * la llamada a la filter chain y/o al método service vuelvan al contenedor. En este caso, hasta * que vuelva la llamada a service, tanto {@link #isSuspended()} como {@link isResumed()} * devuelvan true.</p> * * <p>Si se llama a resume en una petición no suspendida, se aplicará la reanudación a * cualquier llamada subsiguiente a suspend. En este caso, {@link #isResumed()} será true * y {@link #isSuspended()} false. </p> * * <p>Si resume es llamado en una petición después de que el ciclo de vida de una petición * se complete, los resultados quedan indefinidos</p> * * <p>Multiples llamadas a resume son ignoradas</p> * * @see {@link #response()} * */ void resume(); /** * @return true después de que {@link #suspend(long)} haya sido llamado y antes de que * la petición haya sido reanudada o expirada. */ boolean isSuspended(); /** * @return true despues de que {@link #resume()} haya sido llamado y antes de cualquier llamada * subsiguiente a suspend */ boolean isResumed(); /** * @return true después de que una petición haya sido devuelta como el resultado de un resume o * timeout */ boolean isRetry(); /** * @return true después de que una petición haya sido devuelta como resultado de timeout */ boolean isTimeout(); } |
La interfaz ServletResponse existente es extendida con métodos para enviar contenidos asÃncronamente y para obtener streams sin final:
public interface ServletResponse { // Se han omitido los métodos de response ya existentes /** * Establece el cuerpo del contenido de la respuesta (response) * * <p>Establece el cuerpo de la response para ser enviada como un Object. * El Object será convertido una instancia de bytes {@link ResponseContentConverter} * suministrada por el contenedor o la aplicación y configurado en el descriptor * de despliegue o las annotations del servlet.</p> * * <p>El conversor establecerá la longitud del contenido, el content type y el * content encoding de la response.</p> * * <p>Una llamada a {@link #flushBuffer()} bloqueará hasta que todo el contenido sea * convertido a bytes y sea enviado. Si no se llama a {@link #flushBuffer()}, el * contenedor tiene que generar y escribir el contenido asÃncronamente después de * de que la llamada a la cadena de filtros y al método service vuelva al contenedor. * * @exception IllegalStateException si cualquiera de los métodos {@link #getWriter()}, * {@link #getNonStopWriter(), {@link #getOutputStream()} o * {@link #getNonStopOutputStream()} han sido llamados en esta response * */ void setContent(Object content); /** * Devuelve un {@link PrintWriter} adecuado para escribir texto en la response. * A diferencia del writer retornado por {@link #getWriter()}, este writer no * está afectado por una llamada a {@link ServletRequest#suspend(long)} y * puede ser usado por cualquier thread durante el ciclo de vida de la * request. </p> * * <p>No está garantizado que el writer sea thread safe, y si varios threads * usan el stream, deben usar alguna forma de exclusión mutua.</p> * * <p>Calling flush() on the Writer commits the response.</p> * * @return a {@link ServletOutputStream} para escribir datos binarios * * @exception IllegalStateException si cualquiera de los métodos {@link #getWriter()}, * {@link #getNonStopWriter() o {@link #setContent(Object)} han sido llamados * en esta response * * @exception UnsupportedEncodingException * si el char encoding devuelto por <code>getCharacterEncoding</code> * no se puede usar * * */ PrintWriter getNonStopWriter(); /** * * Devuelve un {@link ServletOutputStream} para escribir datos binarios en la * response. El contenedor no codifica los datos binarios. A diferencia del * stream devuelto por {@link #getOutputStream()}, el stream devuelto por este * método no está afectado por una llamada a {@link ServletRequest#suspend(long)} * y puede ser usado por cualquier thread durante el ciclo de vida de la * request. </p> * * <p>No está garantizado que el stream sea thread safe, y si varios threads están * el stream, deben usar alguna forma de exclusión mutua.</p> * * <p>Calling flush() on the ServletOutputStream commits the response.</p> * * * @return a {@link ServletOutputStream} for writing binary data * * @exception IllegalStateException if any of the {@link #getWriter()}, * {@link #getNonStopWriter() or {@link #setContent(Object)} methods * have been called on this response * * @exception IOException if an input or output exception occurred * */ ServletOutputStream getNonStopOutputStream(); } |
Se suministra una nueva interfaz RequestContentConverter para las aplicaciones para convertir el contenido de una request en Objects. Los conversores del contenedor no son requeridos para usar esta interfaz y se deben usar mecanismos alternativos (y más eficientes) para convertir contenidos.
/** * Un conversor para el contenido de la petición. * * <p>Las implementaciones de esta interficie son responsables de * convertir los bytes recibidos de una request a un Object que * estará disponible via el método.</p> * * <p>El contenedor creará tantas instancias de un conversors como * sean necesarias y una instancia será asociada con una request * por una llamada a {@link #init(ServletRequest)}. El contenedor * llamará a {@link #convert(byte[])} a medida que reciba contenido * hasta que se devuelva un objeto o se lance una excepción.</p> * * <p>Para implementar esta interfaz no se requieren los conversores * provistos por el contenedor.</p> * */ public interface RequestContentConverter { /** * Inicializa este converter para la request. * @param request La request que contiene el contenido. Se pueden * inspeccionar las cabeceras de esta petición, pero no se puede * consumir contenido de la petición (ejemplo {@link ServletRequest#getInputStream()}). * @throws IllegalStateException Si este converter no puede convertir la petición. */ void init(ServletRequest request); /** * Convierte unos bytes del contenido. * * <p>Este método se llama cuando un contenedor recibe algunos bytes de * del contenido. El contenido puede que no esté completo y será necesario * más llamadas a convert para tratar todo el contenido. * No se hará una llamada subsiguiente hasta después de que la llamada * actual haya retornado.</p> * * @param chunk Contenido parcial o completo como una array de bytes. * @return El objeto convertido o null si se requiere más contenido. * @throws IOException Si no se puede analizar el contenido. */ Object convert(byte[] chunk) throws IOException; } |
Se suministra una nueva interfaz ResponseContentConverter para las aplicaciones para convertir el contenido de una request en Objects. Los conversores del contenedor no son requeridos para usar esta interfaz y se deben usar mecanismos alternativos (y más eficientes) para convertir el contenido.
/* ------------------------------------------------------------ */ /** * Un converter para el contenido de Response. * * <p>Las implementaciones de esta interficie son responsables de convertir * los Objects pasados en el método {@link ServletResponse#setContent(Object)} * a bytes para ser enviados como contenido de la respuesta.</p> * * <p>El contenedor creará tantas instancias como necesite y una instancia * se asociará con una response por una llamada a {@link #init(ServletResponse)}. * El contenedor llamará a {@link #convert(byte[], int, int)} hasta que se * devuelva un -1.</p> * * <p>Los conversores del contenedor no son requeridos para implementar esta intarficie. * */ public interface ResponseContentConverter { /** * Asocia este converter con una response. * <p>Se pueden establecer cabeceras de la response para indicar el tamaño * del contenido, el tipo y la codificación.</p> * * @param response La respuesta. * @param content El contenido a convertir. * @throws IllegalStateException Si el contenido no se puede the content cannot be converted * by this converter. */ void init(HttpServletResponse response, Object content); /** * Convierte el contenido a bytes. Los bytes convertidos se escriben en * el buffer pasado comenzando en el offset dado y sin exceder la * longitud dada. Devuelve el número real de bytes escritos. * Los bytes escritos puede representar todo el contenido o una parte * de él. Pueden ser necesarias más llamadas a converte para convertir * el contenido completo. * * <p>El contenedor no llamará a converte hasta que una llamada previa * a convert termine. El contenedor puede retrasar la llamada a convert * hasta que esté listo para enviar más contenido al cliente.</p> * * @param buffer El array de bytes dónde se debe escribir el contenido. * @param offset El offset dentro del array dónde se debe escribir el * contenido. * @param length El número máximo de bytes que pueden ser escritos. * @return El número de bytes escritos o -1 para indicar que se ha * escrito todo el contenido. * @throws IOException Si el contenido no se puede convertir. */ int convert(byte[] buffer,int offset, int length) throws IOException; } |
Los conversores de contenidos de peticiones deben definirse en el descriptor de despliegue y tiene que contener los siguientes elementos:
url-pattern. servlet-name.ServletRequest.getContent() se podrá obtener una instancia de esta clase.asynchronous está presente en un converter, el conversor se ejecutará (asÃncronamente si es posible) antes del dispatch inicial. Si no está presente, el conversor sólo se ejecutará cuando se llame a ServletRequest.getContent().mime-type y content-class. Si el contenedor no te da un conversor adecuado con el matching, y no hay elemento converter-class, entonces la aplicación no se podrá desplegar. RequestContentConverter y que se usará para convertir el contenido de la petición. Si sólo hay un elemento converter-class será éste el que se use. Si tanto el elemento converter-class como el provided están presentes, entonces el provided tiene preferencia si el contenedor te da una conversor.Descriptor de conversor de request de ejemplo:
<request-converter> <url-pattern>/comet/*</url-pattern> <mime-type>text/json</mime-type> <content-class>java.util.Map</content-class> <asynchronous/> <provided/> <converter-class>com.acme.ajax.JsonContentParser</converter-class> </request-converter> |
Los conversores del contenido de la request se pueden definir en el descriptor de despliegue y pueden contener los siguientes elementos:
url-pattern.servlet-name. ServletResponse.setContent(Object).mime-type y content-class. Si el contenedor no da un conversor correcto, y no hay un elemento converter-class, entonces la aplicación web no se podrá desplegar. RequestContentConverter que se usará para convertir el contenido. Si sólo hay presente un elemento converter-class, entonces se usará éste. Si tanto converter-class como provided están presenet, el elemento provided tiene preferencia si el contenedor da un converter.Ejemplo de un descriptor de converter para la response:
<response-converter> <url-pattern>/comet/*</url-pattern> <content-class>org.w3c.dom.Document</content-class> <content-type>text/xml; charset=utf-8</content-type> <provided/> </request-converter> |
ServletResponse.setContent(Object) para pasar un objeto del tipo org.w3c.dom.Document. El contenido pasado se convertirá a XML codificado en utf-8, usando un conversor dado por el contenedor. Si tal conversor no está disponible, no se podrá desplegar la aplicación web.
Annotations apropiadas se pueden definir para filtros y servlets para crear efectivamente entradas de request y converter con la misma semántica de una descriptor de despliegue.
Esta propuesta requerirá algunos cambios menores o mejoras de las caracterÃsticas existentes de la especificación de servlet:
Actualmente un Filter se puede mapear en un dispatcher de REQUEST, FORWARD, INCLUDE o ERROR. El dispatcher de REQUEST se deben aplicar tanto a las peticiones de initial y retried. Se deben definir tipos de dispatcher adicionales como INITIAL-REQUEST y RETRY-REQUEST para permitir mapear las peticiones initial y retried.
Este ejemplo muestra como una entrada asÃncrona puede ser tratada dentro de una JSP.
Este ejemplo usa JSP para generar un formulario multipart para subir ficheros. Se usa la API de ServletRequest.getContent() para permitir al contenedor subir grandes ficheros asÃncronamente y guardarlos en u fichero temporal. La petición de upload se despacha a la JSP sólo cuando el fichero enter se ha subido. Entonces la JSP simplemente renombra el fichero temporal en una localización permanente antes de hacerlo disponible vÃa enlace.
<%@ page contentType=‘text/html; charset=UTF-8′ import=‘java.util.Map,java.io.File’ %> <% Map content=(Map)request.getContent(); if (content==null) { %> <h2>UPLOAD content</h2> <form method=‘POST’ enctype=‘multipart/form-data’ accept-charset=‘utf-8′ action=“.” |