On Servlets and Async Servlets
The Servlet API, part of the Java EE standard, has long been (since 1998, when the 2.1 specification was officially released) an important building block of Java-based enterprise architectures.
It is an opinionated API to serve request/response protocols built around a few fundamental concepts:
- A compliant container, that is a dedicated runtime that is either a standalone server (more common in the past) or a library-based embedded runtime (more common nowadays). It can support hosting several web applications at once and segregating class-loading between them. It can also provide management features such as application deployment, start, stop, resources allocation, JNDI naming contexts, JDBC datasources with connection pooling, HTTP adapters, thread pooling and so on. It’s basically a centrally managed package of Java EE features where it’s possible to drop compliant applications.
- One or more servlets, that is classes implementing the
Servlet
interface, which is not specific to HTTP as the Servlet specification was designed for request/response protocols in general. Implementing the interface means dealing with servlet configuration information, which most containers already handle, so it’s far more common (and convenient) to extend abstract classes that are part of the specification such asGenericServlet
or evenHttpServlet
. Apart from lifecycle management, the remaining methods to be implemented are request handlers that will be called by the container when requests come in, and they are supposed to serve them. They’ll do so by manipulating mutable request and response objects (standard interfaces too) that they receive as arguments from the container itself or by raising an exception if some unexpected condition happens, which the container will manage appropriately depending on how it’s been configured, for example by redirecting to a JSP page. They can also include and delegate (part of) processing to an entirely new handling chain mapped to some different URLs through theRequestDispatcher
. This was meant as a mechanism to chain servlets and was is use mainly before filters were introduced in 2.3. - One or more filters, which extend the
Filter
interface and are similar to servlets except they support chaining, that is they are arranged in a sequence and can delegate (part of) request processing to the next filter in the chain, as well as perform post-processing when it finishes. A servlet is always located at the end of a filter chain. - Setup information, such as mapping of requests and filters to HTTP requests that can be provided in several ways, from XML descriptors to class annotations to actual initialisation code.
- Request-serving threads: each request is served by a dedicated thread that will run the whole filter chain the request itself has been mapped to and will block on network I/O operations associated with the HTTP request and response, as well as any other thread-blocking call necessary to complete the request processing.
Dissecting the Servlet API
If we were trying to characterize the long-lived Servlet API, we could qualify it as:
- Definitely object-oriented as every concept involved, no matter how abstract, has been objectified and translated into an interface or a class. “Servlet”, “Filter”, “RequestDispatcher” are all examples of this modeling style. The only exception is the container itself, which is an ubiquitous actor behind the scenes but has no unique representation and is dealt with indirectly, either through explicit actors or secondary objects such as contexts.
- It is (object-oriented) patterns-based and we can identify several many of them.
- It has state machine semantics, it is stateful and it is mutable because, assuming the request handling process is in some state (which is the sum of all the API’s objectified actors’ state, including the container), there are operations that transition it into a new, partially inspectable and different state while other transitions are forbidden (e.g. forwarding a request after the response has been committed).
- It is handler-based as you, the developer, don’t ask for incoming requests when you feel comfortable doing so but they are pushed on your servlets, which you are forced to code as objectified request handlers.
- It is low-level as it doesn’t provide, for example, routing mechanisms nor it fosters specific paradigms such as MVC.
- It was originally born synchronous as the handler is supposed to complete request processing within the calling computational context (stack) and not at all to defer it.
- It is explicitly thread-based as the specification states that the handler’s computational context is a servlet container thread. Synchronous and thread-based together mean basically that the servlet API was originally designed to be thread-blocking.
In sum it is a very complex and opinionated API, although based on opinions that were very commonplace, and with a very long backward compatibility history.
Incidentally: Clojure’s Ring, a new lucid and minimalistic view of HTTP servers
Although Clojure community is very plural and there are plenty of very interesting choices in every area, the “de-facto” Clojure low-level, basic standard framework for the web is Ring.
Given HTTP is pretty much a stateless request-response protocol, HTTP request serving is naturally a domain that lends itself very well to a functional, input-output modeling style. In fact, Ring thinks of HTTP requests serving as a total of 3 functional entities with straightforward relationships:
- A handler is a function receiving as its only input a Clojure map with well-known key names and value types, representing the HTTP request, and producing as its output another Clojure map that must have a specific structure, representing the HTTP response (this is an over-simplification though, as Ring allows returning simpler data structures for convenience).
- A middleware is a function receiving a handler function and producing another handler function. A middleware is thus an higher-order function which is meant to enrich any handler’s logic in some specific way, such as intercepting and serving filesystem requests or enriching the request itself with multi-part pre-processing information, and it’s thus akin to Servlet filters, although made much simpler through functional programming ideas such as first-class functions. Please note that middlewares can be chained in a specific order by the straightforward means of functional composition, because what we get by applying a middleware to a handler is another handler, to which then several more middleware functions can be applied.
- An adapter is a function receiving as its main input a handler function and returning nothing. Its purpose is purely the side-effect of spawning some HTTP server that will serve requests using the provided handler and is thus really an adapter to some pre-existing (or new) HTTP server technology. Its functional interface is not standard as the inputs it can receive are very much technology-dependent, but a common pattern is for many adapters to receive as a first argument the handler and then an implementation-dependent options map (or other sequence) as a second one. Furthermore the most common options, such as listening interfaces and ports, tend to have the same key names in most adapters.
Ring is an opinionated API too and in some ways it doesn’t depart from popular concepts, for example it is still handler-based, although the idea of the adapter as just a function makes it very straightforward to use it as the embedded HTTP “boundary” of an otherwise completely traditional application; plus it is synchronous, which is good as it makes for straightforward and maintainable code. Still it takes a joyfully fresh, lucid and minimalistic view on the subject, trying to remove incidental complexity altogether and to provide the least number of orthogonal concepts needed to deal concisely and effectively with the domain’s intrinsic complexity, leveraging functional programming ideas and dynamic language flexibility to this avail; this approach very much conforms to the spirit of the Clojure language itself.
Please note that Ring says nothing about execution contexts: it is perfectly ok for someone to implement an adapter for its blocking API based on lightweight fibers rather than on heavyweight threads: this is exactly was Comsat offers and Ring’s lucidity and minimalism has greatly simplified writing such an integration.
Servlet 3.0 Async
The Async addition to the servlet specification serves as an example of the fact that OOP doesn’t necessarily simplify the complexities of stateful APIs; sometimes instead it only provides the dangerous illusion of doing so, by spreading state all over the table, splitting it and putting it in objects.
This illusion can actually worsen the situation as it can make us think that a seemingly very simple idea to evolve our stateful APIs can indeed work without unintended consequences.
The “simple” idea behind the Async feature in Servlet 3.0 is that of a new request mode, the asynchronous one. When the request is switched to async through the startAsync method call, we are “simply” telling the container that whenever the request handling chain (filters and servlets) returns and its associated container thread completes, we’re not meaning at all that request processing has finished and thus the response shouldn’t be shipped back to the HTTP client. Instead, it should be held back until some other execution context signals that request processing has indeed been completed, and it will do so through either a complete
or dispatch
method call on the AsyncContext
object returned by the startAsync
call.
Needless to say, there are several possible interactions of the async mode with the stateful moving parts of the pre-existing Servlet API: we’re going to have a look at some of them next.
Error handling in Async mode
AsyncContext
offers the ability to register listeners about request handling progress and abnormal conditions but outside of the container’s thread we’ll be running in a self-managed execution context, so the container can’t catch and handle exceptions for us.
Instead AsyncContext
does offer a new form of processing delegation that will transfer control back to a container-managed thread, which is the purpose of the dispatch
method. By using it after setting the error condition (and any other relevant information) in request attributes and by checking the request’s dispatcher type we can verify we’re indeed handling an abnomal condition originated in an async processing flow and choose to re-throw the exception, this time being able to rely on the container’s ability to manage it.
This approach is a bit convoluted and it basically requires using a revamped flavour of forward/dispatch features previously deprecated, as a matter of fact, by filters. Yet it works and it is a able to mimic the error handling flow that would take place in a synchronous setting; it would certainly be interesting to measure how efficient it is though.
The servlet API has also been offering a sendError
facility but as of today it is unclear whether (and how) it is supposed to work in async mode and this situation can easily result in open issues even in most popular servlet containers such as Jetty and Tomcat.
Filters in Async mode
The Servlet 3.0 spec explicitly disallows running filter chaining in an execution context different from a container’s thread. This limitation means that the only the handler at the end of the request processing chain, that is a servlet, can put the request in async mode, while pre-processing filter logic can only execute in the container’s thread.
This is quite unfortunate as filters, since their introduction, have been used by many popular framework and application developers to carry out substantial amounts of request processing that could benefit from running in separate execution contexts, such as fibers, without blocking expensive container threads.
In fact there are several open issues in popular servlet containers about this limitation.
Servlet 3.1: Async HTTP I/O
Servlet 3.0 enables detaching the container’s thread and the servlet’s handling code from the request processing completion, but I/O to read the request and write the response was still thread-blocking.
Servlet 3.1 adds asynchronous I/O capabilities to requests, provided they are already in asynchronous mode, through the setReadListener
and setWriteListener
methods.
There a few shortcomings about this new set of APIs:
- At most one read and one write listener can be registered.
- The API doesn’t enforce registering them only after the request has been put into asynchronous mode.
- The listener interfaces are brand-new and have, for example, nothing in common with NIO APIs.
- Asynchronous APIs allow more efficient implementations but do so in the wrong way, that is by adopting a convoluted programming model rather than providing execution contexts more efficient than threads while keeping the extremely useful “blocking” abstraction. On the other hand asynchronous APIs can be turned very easily into both efficient and expressive fiber-blocking APIs.
The “status quo” and the way forward
Many organisations with substantial structure have already invested a lot in servlet-based technology, and changing direction for them is a relevant cost that needs to be weighted against concrete benefits.
Some of them are happy with it and are not affected by existing shortcomings. As for others it is possible, and hopefully it will happen, that future servlet specifications will address them but the servlet API is a big and complex one; it also it needs to retain some level of backward compatibility, so it is probably going to take some time for specification reviews to be released, let alone for servlet containers to implement them correctly, efficiently and reliably.
Of course alternatives to servlets exist, such as Ring, and some organisations can decide that the cost of moving to different APIs pays out for them in order to gain productivity and to allow building more maintainable code assets; this cost can often be lower for new implementations rather than for porting existing ones.
If the most felt shortcoming about servlet-based API in your organisation is either that of efficiency or that of the asynchronous programming model, a very viable and low cost alternative exists in Comsat: it will allow you to still use straightforward blocking abstractions and the familiar servlet API (as well as many other popular and standard ones in the web and DB areas) but with the level of efficiency provided by fibers.
Reference: | On Servlets and Async Servlets from our JCG partner Fabio Tudone at the Parallel Universe blog. |