This document contains a proposal to extend the Servlet API as defined by JSR 154 with asynchronous concerns to meet the goals of JSR 315. At this stage, this proposal is only a contribution from the author (Greg Wilkins) and not an official proposal from the JSR.
JSR 315 nominates "Async and Comet Support" as a targeted feature, described as:
The ability to receive data from a client without blocking if the data is slow arriving.
The ability to send data to a client without blocking if the client or network is slow.
The comet style of Ajax web application can require that a request handling is delayed until either a timeout or an event has occurred. Delaying request handling is also useful if a remote/slow resource must be obtained before servicing the request or if access to a specific resource needs to be throttled to prevent too many simultaneous accesses.
The comet style of Ajax web application can require that a response is held open to allow additional data to be sent when asynchronous events occur.
The ability to notify push blocking or non-blocking events. Channels concept - The ability to subscribe to a channel and get asynchronous events from that channel. This implies being able to create, subscribe, unsubscribe and also apply some security restriction on who can join and who cannot.
The following additional requirements have been used to guide the design of this proposal. These requirements are only from the experience of the author and should themselves be subject to review and discussion:
All substantive
handling of asynchronous requests and generation of asynchronous
responses should take place within the existing calling chain through
Filter.doFilter(..)
to Servlet.service(...).
It is only within this calling chain
that we have a well defined environment for authentication,
authorization, JNDI, and access to other JEE services. The creation
of additional request handling or response generating methods will
require substantial redefinition of the servlet environment and will
impede frame work compatibility.
An asynchronous servlet should be a first class servlet and not limited in any significant way. To this effect an asynchronous servlet should be able to:
Be filtered by name or URL path.
Be secured by standard security mechanisms
The initiator or target of a RequestDispatchers
Framework compatibility
Handling of asynchronous requests and responses should be able to be done by existing frameworks with little or no modification. For example, JSP and/or JSF should be able to be used to handle a comet request and to generate the comet response.
The servlet
container should be empowered to handle common or difficult IO tasks
that are currently handled by the servlet developer. With 2.5
servlets, the only content-type that is automatically handed by the
container is "application/x-www-form-urlencoded".
The servlet developer (or framework) must handle common tasks such
as:
Parsing multi-part mime
Parsing XML to DOM from request input streams
Writing DOM to response output streams
Moving data from requests to Files or Files to responses.
If asynchronous IO is enabled within the servlet container, then these common tasks will need to be reimplemented and will be more complex and bug-prone as a result. The remove this source of errors and duplicated effort, the servlet container should instead be empowered to implement common IO tasks on behalf of the servlet developer. The container could then be free to use efficient asynchronous techniques (or even native optimizations) to perform IO on behalf of the servlet developer.
This proposal aims to meet the JSR315 and additional asynchronous requirements by extending the servlet container in the following two areas:
The ServletRequest
and ServletResponse
classes are extended with methods to allow HTTP message content to be
retrieved and set as complex Java objects. The container will be
responsible for the IO, parsing and generation of the content using
converters supplied either by the container or the application.
A compliant container will be expected to provide a standard set of converters, including the following:
|
Mime Type |
Java Type |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The container supplied converters may operate asynchronously or synchronously. There will be an API to allow applications to provide additional, replacement, or optional converters.
The current API proposal uses the Object class for content, however it should be possible to extend this proposal to use generics for type safe content handling.
The request handling lifecycle is extended so that the handling of a single request can span multiple dispatches to the filter chain and the servlet service method. This allows the handling of a request and the completion of a response to be delayed until resources are available or asynchronous events occur or complete.
A request life cycle is started when a request/response pair is first dispatched. Normally the request life cycle will complete when a the dispatch returns and the response will be committed, flushed and completed. However, the extension allows for a request to be suspended, so that when the dispatch returns to the container, the request lifecycle is not completed.
A suspended request is held by the container until either it is resumed or a timeout expires. The request is then retried by being dispatched to the normal filter chain and servlet service method.
This cycled can repeat more that once and the suspension effectively turns the existing dispatch mechanism into an asynchronous callback.
To allow this mechanism to work with existing frameworks and code that is unaware of suspension, when a request is suspended, the response object is disabled so that headers and content may not be written. Additional methods are provided to access a "NonStop" output stream or writer, that can be used at any time during the request lifecycle and is not disabled by request suspend.
The existing
ServletRequest
interface is extended with methods to access parsed content and to
suspend request handling:
public interface ServletRequest
{
/**
* Get the request body content as an Object.
*
* <p>The bytes received byte the server will be parsed and converted to the Object
* by a {@link RequestContentConverter} provided either by the container or the
* application and configured in the deployment descriptor or servlet annotations.
* Examples of the type of Object that could be returned include
* {@link java.nio.ByteBuffer}, {@link String}, {@link java.io.File} and
* {@link org.w3c.dom.Document}. If the request specifies a character encoding or
* Locale, then this will be used for the parsing of the content, otherwise default
* encodings may be specified in the deployment descriptor or servlet annotations.
* </p>
*
* <p>If the deployment descriptor or servlet annotations indicates that the request
* URL is asynchronous, then the container will parse the content before the request
* is dispatched to the filter chain and/or servlet and this call will never block.
* Otherwise, the content is parsed during this call and may block waiting for the
* complete content to be received.</p>
*
* @return an object containing parsed body of the request
*
* @exception IllegalStateException if the {@link #getReader} or
* {@link #getInputStream} methods has already been called for this request
*
* @exception ObjectSTreamException if there was a problem parsing the content.
*
* @exception IOException if an input or output exception occurred
*/
Object getContent() throws IllegalStateException, ObjectStreamException;
/**
* Suspend the processing of the request and associated {@link ServletResponse}.
*
* <p>After this method has been called, the lifecycle of the request will be
* extended beyond the return to the container from the
* {@link Servlet#service(ServletRequest, ServletResponse)} method and
* {@link Filter#doFilter(ServletRequest, ServletResponse, FilterChain)} calls. If a
* request is suspended, then the container will not commit the associated response
* when the call to the filter chain and/or servlet service method returns to the
* container. Instead the container will wait until either
* {@link ServletRequest#resume()} is called or the passed timeout expires, at
* which time the request will be redispatched via the normal filter chain to the
* servlet.
* </p>
*
* <p>The associated {@link ServletResponse} object is disabled by this call, so
* that all calls to modify or generate a response are silently ignored, until
* such time as the request is retried. With the exception that the objects
* returned by {@link ServletResponse#getNonStopOutputStream()} and
* {@link ServletResponse#getNonStopWriter} remain active throughout the request
* lifecycle.</p>
*
* <p>If a request is already suspended, any subsequent calls to suspend will set
* the timeout to the minimum of the previous timeout and the newly passed
* timeout</p>
*
* <p>Suspend may only be called by a thread that is within the service calling
* stack of {@link Filter#doFilter(ServletRequest, ServletResponse, FilterChain)}
* and/or {@link Servlet#service(ServletRequest, ServletResponse)}</p>
*
* @see {@link #resume()}
*
* @param timeoutMs The time in milliseconds to wait before retrying this request.
*
* @exception IllegalStateException If the calling thread is not within the calling
* stack of {@link Filter#doFilter(ServletRequest, ServletResponse, FilterChain)}
* and/or {@link Servlet#service(ServletRequest, ServletResponse)}
*/
void suspend(long timeoutMs);
/**
* Resume a suspended request.
*
* <p>This method can be called by any thread that has been passed a reference to
* the request to request that the request be redispatched</p>
*
* <p>If resume is called before a suspended request is returned to the container
* (ie the thread that called {@link #suspend(long)} is still within the filter
* chain and/or servlet service method), then the resume does not take effect until
* the call to the filter chain and/or servlet returns to the container. In this
* case, until the service call returns, both {@link #isSuspended()} and
* {@link isResumed()} to return true.</p>
*
* <p>If resume is called on an unsuspended request, then the resume will apply to
* any subsequent calls to suspend. In this case, {@link #isResumed()} will true
* and {@link #isSuspended()} will return false. </p>
*
* <p>If resume is called on a request after the request lifecycle is complete,
* then the results are undefined</p>
*
* <p>Multiple calls to resume are ignored</p>
*
* @see {@link #response()}
*
*/
void resume();
/**
* @return true after {@link #suspend(long)} has been called and before the request
* has been resumed or timedout.
*/
boolean isSuspended();
/**
* @return true after {@link #resume()} has been called and before any subsequent
* call to suspend
*/
boolean isResumed();
/**
* @return true after a request has been retried as the result of a resume or a
* timeout
*/
boolean isRetry();
/**
* @return true after a request has been retried as the result of a timeout
*/
boolean isTimeout();
}
|
The
existing ServletResponse
interface is extended with methods to asynchronously send content and
to obtain non stop streams:
public interface ServletResponse
{
// existing response methods omitted
/**
* Set the response content body
*
* <p>Set the response body to be sent as an Object. The Object will be
* converted to bytes byte a {@link ResponseContentConverter} instance
* provided either by the container or application and configured in the
* deployment descriptor or servlet annotations.</p>
*
* <p>The converter will set the content length, content type and content
* encoding of the response.</p>
*
* <p>A call to {@link #flushBuffer()} will block until the entire content
* is converted to bytes and flushed. If {@link #flushBuffer()} is not called,
* then the container may generate and write the byte content asynchronously
* after the call to the filter chain and servlet service method return
* to the container.
*
* @exception IllegalStateException if any of the {@link #getWriter()},
* {@link #getNonStopWriter(), {@link #getOutputStream()} or
* {@link #getNonStopOutputStream()} methods have been called on
* this response
*
*/
void setContent(Object content);
/**
* Returns a {@link PrintWriter} suitable for writing character text
* in the response. Unlike the writer returned by {@link #getWriter()},
* the writer returned by this method is not affected by a call to
* {@link ServletRequest#suspend(long)} and may be used by any thread
* during the request lifecycle. </p>
*
* <p>The writer is not guaranteed to be thread safe, and if multiple
* threads are using the stream, they must use some form of mutual
* exclusion.</p>
*
* <p>Calling flush() on the Writer 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 UnsupportedEncodingException
* if the character encoding returned
* by <code>getCharacterEncoding</code> cannot be used
*
*
*/
PrintWriter getNonStopWriter();
/**
*
* Returns a {@link ServletOutputStream} suitable for writing binary
* data in the response. The servlet container does not encode the
* binary data. Unlike the stream returned by {@link #getOutputStream()},
* the stream returned by this method is not affected by a call to
* {@link ServletRequest#suspend(long)} and may be used by any thread
* during the request lifecycle. </p>
*
* <p>The stream is not guaranteed to be thread safe, and if multiple
* threads are using the stream, they must use some form of mutual
* exclusion.</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();
}
|
A new
RequestContentConverter
interface is provided for applications to convert request content
into parsed Objects. The container provided converters are not
required to use this interface and may use alternate (and more
efficient) mechanisms to convert content.
/**
* A converter for Request content.
*
* <p>Implementations of this interface are responsible for converting
* bytes received from a request to and Object instance that will be
* made available via the {@link ServletRequest#getContent()} method.
*
* <p>The container will create as many instances of a converter as needed
* and an instance will be associated with a request by a call to
* {@link #init(ServletRequest)}. The container will call
* {@link #convert(byte[])} as content is received until either an object
* is returned or an exception is throw.
*
* <p>Container provided converters are not required to implement this
* interface.
*
*/
public interface RequestContentConverter
{
/**
* Initialize this converter for the request.
* @param request The request that contains the content. The request
* headers may be inspected, but not methods that consume content may
* be called (eg {@link ServletRequest#getInputStream()}.
* @throws IllegalStateException If this converter cannot convert the
* request.
*/
void init(ServletRequest request);
/**
* Convert a bytes of content.
*
* <p>This method is called when the container receives some bytes of
* content. The content may not be complete and subsequent calls to
* convert will be required to deliver the complete content.
* A subsequent call will not be made until after the current call
* to convert has returned.
*
* @param chunk Either a partial or complete content as an
* array of bytes.
* @return The converted object or null if more content is required.
* @throws IOException If the content cannot be parsed.
*/
Object convert(byte[] chunk) throws IOException;
}
|
A new
ResponseContentConverter
interface is provided for applications to convert request content
into parsed Objects. The container provided converters are not
required to use this interface and may use alternate (and more
efficient) mechanisms to convert content.
/* ------------------------------------------------------------ */
/**
* A converter for Response content.
*
* <p>Implementations of this interface are responsible for converting
* Objects passed in the {@link ServletResponse#setContent(Object)} method
* to bytes to be sent as the response content.
*
* <p>The container will create as many instances of a converter as needed
* and an instance will be associated with a response by a call to
* {@link #init(ServletResponse)}. The container will call
* {@link #convert(byte[], int, int)} until -1 is returned.
*
* <p>Container provided converters are not required to implement this
* interface.
*
*/
public interface ResponseContentConverter
{
/**
* Associate this converter with a response.
* <p>The response headers may be set to indicate the content
* size, type and encoding.
*
* @param response The response
* @param content The content to convert.
* @throws IllegalStateException If the content cannot be converted
* by this converter.
*/
void init(HttpServletResponse response, Object content);
/**
* Convert the content to bytes. The converted bytes are written into
* the passed buffer starting at the given offset and not exceeding the
* given length. The actual number of bytes written is returned.
* The bytes written my represent all of the content or a part of the
* content. Subsequent calls to convert may be required to convert the
* entire content.
*
* <p>The container will not call convert until a previous call to convert
* returns. The container may delay calling convert until it is ready to
* send more content to the client.
*
* @param buffer The byte array into which content should be written
* @param offset The offset into the array at which content should be written
* @param length The maximum number of bytes that may be written.
* @return The number of bytes written or -1 to indicate that the entire
* content has been written.
* @throws IOException If the content cannot be converted.
*/
int convert(byte[] buffer,int offset, int length) throws IOException;
}
|
Request content converters may be defined in the deployment descriptor and may contain the following elements:
url-pattern
elements.servlet-name
elements.ServletRequest.getContent()
method.asynchronous
element is present in a converter, then the converter will run
(asynchronously if possible) before the initial dispatch. If it is
not present, then the converter will run only if and when
ServletRequest.getContent()
is called.mime-type
and content-class.
If the container does not provide a matching converter, and there is
no converter-class
element, then the web application will not be able to be deployed.RequestContentConverter
interface that will be used to convert content. If only a
converter-class
element is present, then it will be used. If both a converter-class
and provided
elements are present, then provided takes precedence if the
container does provide such a converter.An example request converter descriptor is:
<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>
|
This descriptor defines a converter that applies to requests to /comet/* that have a content type header indicating a text/json mime type. The content is converted to a java Map object asynchronously before the request is dispatched. If the container provides a suitable converter, it will be used, otherwise the application provided class will be used.
Request content converters may be defined in the deployment descriptor and may contain the following elements:
url-pattern
elements.servlet-name
elements.ServletResponse.setContent(Object).mime-type
and content-class.
If the container does not provide a matching converter, and there is
no converter-class
element, then the web application will not be able to be deployed.RequestContentConverter
interface that will be used to convert content. If only a
converter-class
element is present, then it will be used. If both a converter-class
and provided
elements are present, then provided takes precedence if the
container does provide such a converter.An example response converter descriptor is:
<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>
|
This converter
applies to all responses generated by resources
at URIs matching /comet/* that use the
ServletResponse.setContent(Object)
to pass an object of type org.w3c.dom.Document. The passed content
will be converted to XML encoded as utf-8 using a converter provided
by the container. If no such converter is available, the web
application will not be able to be deployed.
Appropriate annotations can be defined for filters and servlets to effectively create request and converter entries with the same semantics as the deployment descriptor.
This proposal will require some minor changes or enhancements to existing features of the servlet spec:
Currently a Fitler
map be mapped to a dispatcher of REQUEST,
FORWARD,
INCLUDE
or ERROR.
The REQUEST
dispatcher should apply to both initial and retried requests.
Additional dispatcher types of INITIAL-REQUEST
and RETRY-REQUEST
should be defined to allow filters to map initial or retired
requests.
This example shows how asynchronous input may be achieved within a JSP.
This example uses
a JSP to generate an multipart form for uploading file content. The
ServletRequest.getContent()
API is used to allow the container to upload large files
asynchronously and save them to a temporary file. The upload request
is dispatched to the JSP only once the entire file is uploaded. The
JSP then simply renames the temporary file to a permanent location
before making it available via a link.
<%@ 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=".">
Description: <input type='text' name='TextField' value='comment'/><br/>
File: <input type='file' name='file' /><br/>
<input type='submit' name='Action' value='Submit'><br/>
</form>
<%
}
else
{
String description = (String)content.get("description");
File file = (File)content.get("file");
String path="/store/"+file.getName();
file.renameTo(new File(request.getRealPath(path)));
%>
<h2>UPLOADED content</h2>
Description: <%= description%><br/>
File: <a href="<%=path%>"><%=file.getName()%></a><br/>
<%
}
%>
|
<request-converter>
<url-pattern>/upload.jsp</url-pattern>
<mime-type>multipart/form-data</mime-type>
<content-class>java.util.Map</content-class>
<asynchronous/>
<provided/>
</request-converter>
|
This example shows how delayed request handling can be applied to an existing web application using a Filter. This filter ensures quality of service by ensuring that no more than 20 requests are simultaneously being handled. Any additional requests are suspended and delayed until previous requests complete. Moreover, authentication and authorization are used to maintain two priority queues of delayed requests. The requests in the higher priority queue are preferentially resumed.
Because the delayed request handling can be implemented in a standard filter, it would be possible for this filter to be configured after application supplied filters. Thus if the application provided it's own authentication and authorization mechanisms in a filter, these would be able to be used by this filter.
public class QosFilter implements Filter
{
int _passes = 20;
Queue<ServletRequest> _lowPriority = new LinkedList<ServletRequest>();
Queue<ServletRequest> _highPriority = new LinkedList<ServletRequest>();
public void init(FilterConfig filterConfig) {}
public void destroy(){}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException
{
boolean has_pass=Boolean.TRUE.equals(request.getAttribute("PASS"));
if (!has_pass)
{
synchronized(this)
{
if (request.isRetry())
{
_lowPriority.remove(request);
_highPriority.remove(request);
((HttpServletResponse)response)
.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
return;
}
if (_passes>0)
{
has_pass=true;
_passes--;
}
else
{
if (((HttpServletRequest)request).isUserInRole("priority"))
_highPriority.add(request);
else
_lowPriority.add(request);
request.suspend(30000L);
return;
}
}
}
try
{
assert has_pass;
chain.doFilter(request,response);
}
finally
{
synchronized(this)
{
ServletRequest waiting = _highPriority.poll();
if (waiting==null)
waiting = _lowPriority.poll();
if (waiting==null)
_passes++;
else
{
waiting.setAttribute("PASS",Boolean.TRUE);
waiting.resume();
}
}
}
}
}
|
This example shows how a non-stop writer can be used to delay the close of a response and stream data. Note that while such streaming servlets are widely deployed, the approach is not guaranteed to pass proxies, which may buffer content until the response is complete.
public class StreamServlet extends GenericServlet
{
public void service(final ServletRequest request, ServletResponse response)
throws ServletException, IOException
{
if (request.isRetry())
return;
final PrintWriter writer = response.getNonStopWriter();
request.suspend(30000);
new Thread(new Runnable()
{
public void run()
{
while (request.isSuspended())
{
try{Thread.sleep(1000);}catch(Exception e){}
writer.write("The time is "+new Date()+"\n");
writer.flush();
}
}
}).start();
}
}
|