The road to reusable Servlet components
- By Attila Szegedi
- December 1, 2001
Web applications conforming to the Servlet specification make good components: If you follow the specification right, at the end you can package Web applications into a .war file and then deploy them. There is no componentization problem at Web application granularity. However, the pieces that make up a Web app often need better componentization.
My company recently had two projects running in parallel that each needed a Web interface for user administration. The interface consisted of several servlets, JSP pages, and static content (icons, online help, etc.). We wanted to make the whole user administration module as self-contained and reusable as possible. Ideally, we would have liked to drop a single .jar file into the WEB-INF/lib directory.
Early on we realized we needed multiple, independently developed components dropped into the same Web application. Each component also needed to co-exist in the same servlet contexts and sessions without causing side effects to other components.
Several other issues also surfaced, and we found satisfying solutions to most of them. Using those projects as an example, I will provide some general tips that should make life easier when developing servlet-technology Web applications.
These tips will help you to build components for Web applications that are pretty much self-contained. The components will consist of only two files: a JAR file with compiled servlet, JSP and accompanying classes, as well as static resources; and an XML entity file. Integrating such components is as easy as dropping the jar into the WEB-INF/lib directory, and including the XML entity in the WEB-INF/web.xml.
Tip 1: Encapsulate Servlet-Context and Session State In a Single Class
An all-too-common and bad programming practice is cramming the ServletContext and HttpSession objects with too many "attributes." For example, you could have a "username" attribute in the session (with java.lang.String as expected type) and a "locale" attribute (with java.util.Locale as expected type). Whenever you needed the user's preferred locale, you would obtain the session object, and extract the locale from it with a call:
Locale locale =
(Locale)httpSession.getAttribute("locale");
However, this approach has several problems:
- You give up the compiler's strong type-checking safety by having to perform an explicit cast of the return value on each getAttribute call.
- You can easily mistype the string literal "locale." This will cause an unexpected NullPointerException (or better yet, a ClassCastException) at runtime, rather than its being caught at compile time.
- You risk a conflict with another component that uses the too-common attribute name "locale."
- Most importantly (at least from the component perspective), you break your component's encapsulation, as any other component in the same session can access, modify, and remove your attributes.
What should you do instead? I suggest you gather all the objects you would store as session attributes into a single object, and then gather all the objects you would store as servlet context attributes into another single object. The class definition for such an object might look like the code in
Listing 1.
This is a fully encapsulated session for our component. To obtain locale information, you would now call the following:
Locale locale =
Session.getInstance(httpSession).getLocale();
This approach has all the advantages you would expect from a good component:
- It is accessed through an on-demand per-session static initialization method, getInstance(). This method hides all of the complexity encountered when looking up the session object among various HTTP session attributes, creating the object if it is not found and binding it as a session attribute. As the class itself, this method is package-private, so your component's inner state is hidden from outside the package.
- The getLocale() method is guaranteed to return Locale object, no explicit casts are necessary. The safety of the compiler's strong type-checking has been reintroduced to produce clearer code and to catch several bugs at development time.
- There is no string literal involved, so all typos are caught at compile time.
- You can be pretty sure that no other component will register a session attribute named "com.foo.myapp. Session"; thus, you have done your best to avoid any potential name conflicts in the HTTP session attribute namespace.
- The class and its important methods are package-private, so other components cannot corrupt the state of the Web application or the session. While there's really nothing we could do to prevent a malicious peer component from removing our Session object from the HttpSession attributes, the effects are essentially the same as having the whole HttpSession invalidated. Fortunately, that is an expected state (as sessions do expire), and we must prepare our component for it anyway.
The session state class can be further enhanced with two other approaches. First, provide a per-component session invalidator method (as depicted in
Listing 1). Then centralize session post-mortem cleanup by implementing
SessionBindingListener on
Session class. Because of the problems associated with unreliable scheduling, implement
SessionBindingListener for cleanup rather than finalization.
Tip 2: Embed Static Resources
Your component can have several resources named "static," which are URLs whose response is a constant stream of bytes. Built-in icons, help pages, etc. are the most likely candidates. The naïve approach for serving these resources is to dump them into the Webapp directory so they are served as any other static content. However, this approach hardly resembles componentization: Each resource is a separate file resulting in a multitude of files to manage, and the files become mixed with other static content of the application with no clear separation.
I recommend a design that integrates the static resources with other elements of your component; pack them into the component's jar file. However, you must provide access to them somehow. The simplest approach is to develop a servlet that relays between Class.getResourceAsStream() (the natural way to access resources in the jar file as a stream) and the HTTP response output stream. Listing 2 is a bareboned ResourceServlet that should give you an idea of what I am talking about.
It must be noted that the embedded resources approach has an obvious drawback: Response throughput is lower compared to the possibility of having the servlet container (or even the embedding Web server, as in Apache Web Server/Apache Tomcat scenario) serve static content directly. So this is not a "use mindlessly" rule: you must carefully weigh the advantages and disadvantages. In the long run, the effect of reduced static content throughput can be minimized by using browser-side caching, presuming static resources don't change frequently (fortunately, they usually don't). This is why the servlet should implement the getLastModified() method.
By doing this, the servlet container can generate Last-Modified response headers, as well as honor If-Modified-Since request headers. These headers are at the heart of client-side caching: When a browser downloads an URL, it remembers its Last-Modified value as well as its contents. The next time the browser fetches the URL, it passes the remembered Last-Modified value in the If-Modified-Since header. The servlet container will then compare the value to that returned by the servlet's getLastModified() method. If the value returned is not more recent than the header value, content will not be returned. Instead, an HTTP response code signaling that the resource has not been modified will be returned. In Listing 2, the servlet code assumes resources were not modified after the servlet class was loaded. To modify the resources, assign the value of the currentTimeMillis() call to assign value to a static field. This is a naïve approach, meant to be illustrative. A more serious approach could involve placing the build-time timestamp into a bundled property file, which the class would read when it is loaded and use as the value for getLastModified().
Of course, this servlet could be further improved in several ways, for example:
- It could cache recently used resources through SoftReferences.
- It could have a resource name extension to ContentType mapping (i.e., ".htm" = "text/html"). However, most browsers are very good at guessing content type from a URL or analyzing several bytes of the response, so even if this feature is not in place it won't hurt much.
- It could read custom response header values from accompanying property resources (i.e., logo.gif.properties file in the jar could contain response headers for logo.gif). Again, this is a relatively specialized need.
Tip 3: Compile Your JSPs
While I advise people not to use the JSP technology in favor of cleaner designs of template engines and XSLT, I realize many people still resort to it. For example, when I had the task of integrating my co-workers' subprojects into a bigger project, I found that several people had used JSP. I was then forced to use the only possible solution for integrating these projects—JSP componentization. A side note: If you don't understand what the problem is with using JSP technology, I recommend you read Jason Hunter's excellent two-part article "The Problems with JSP"
1 and "You Make the Decision."
2
What's the issue with JSP componentization, you might say. Technically, there is no problem. A JSP is just a HttpServlet, and should be treated as such. It should be bundled into your component's .jar file as a compiled class. A JSP file is usually translated into Java source code and then compiled into bytecode. A tool called a JSP compiler performs the JSP-to-Java step, and the resulting Java source file (which is just an intermediate file) is then compiled to bytecode using an ordinary Java compiler. Many JSP containers do this two-step compilation behind the scenes, when the JSP page is requested for the first time. While this makes it possible for on-the-fly code updating, a potentially time-consuming compilation step is incurred at runtime. Therefore, we should perform JSP compilation when all of the other classes are compiled.
This brings us to the issue of building the project. At this point, I must become a bit tool-specific. I personally use Ant to build my projects and Tomcat as servlet container while developing applications, so I will be specific to them. Listing 3 presents a fragment of an Ant build file that incorporates JSP compilation into a normal make process (the important lines are highlighted in red).
First, an uptodate task checks to see if there is a JSP file whose timestamp is more recent than the JAR file; if a more recent timestamp does not exist, the JSP compiler is not invoked, thereby improving build process performance. This is achieved using the unless attribute of the jspc target in conjunction with the aforementioned uptodate task.
The important part of the jspc target is the invocation of Tomcat's JSP compiler, a class named org.apache.jasper.JspC. The classpath for invocation contains all of the dependencies the JSP compiler needs. Two particularly interesting options of the JspC invocation are -p, which declares that the JSP classes be defined in the specified package; and -webinc, which generates an XML fragment for defining the resulting servlets in the web.xml file (more on this later).
The jspc target will generate Java source files for JSP files in the ${basedir}/jsp directory and place them into ${basedir}/jspc. The javac invocation in the compile target (which is dependent on jspc) has two src arguments: one declaring the source directory of the projects "ordinary" source files, and one declaring where the JspC-generated Java files were placed. This allows the JSP servlets to be compiled with the other source files.
You might note there is a replace task in the jspc target. Its only purpose is to fix a JspC bug; when run on a Win32 platform, it generates URL paths with back slashes instead of forward slashes.
Another benefit from precompiling JSP is that it leaves compilation control in the user's hands. You can turn on code optimizations in the Java compiler, or even obfuscate the code before shipping it.
However, there is one point you should consider: precompiled JSPs will gain runtime dependency on the JSP compiler's runtime. If you use Tomcat's JSP compiler, for example, you will end up with compiled JSPs that are tied to Tomcat's JSP runtime, namely the org.apache.jasper.runtime package. If you're lucky, you can just bundle this package with your app (with Tomcat's license model you can). If you're not lucky, you cannot use precompiled JSPs.
Tip 4: Provide XML Entities for web.xml
At the beginning of this article, I said it would be ideal if we could end up with a single .jar file being the component—you just drop it into the WEB-INF/lib and it works. Well, we can't—we need one more file.
To integrate a component into a Web application, you must at least register its servlets (including JSPs) in the Web application's web.xml file. You should therefore provide a fragment—a "parsed entity," in XML terminology—of that XML file relevant to your component. If you have an all-JSP component and use the JSP compiler provided with Tomcat, you're in luck; Tomcat's JSP compiler can generate the appropriate entity file automatically as part of the JSP-to-Java translation process using the -webinc switch. An appropriate entity file will typically look like the code found in Listing 4.
The code in Listing 4 is repeated for every servlet. As you can see, Tomcat's JSP compiler will derive a URL for every JSP from its class name. While this mapping might not look too elegant, it has the same advantage packaging has—avoidance of name conflicts. If you look into the future and plan to integrate multiple components into a single Web application, you don't want to cope with two components being mapped to the /delete.jsp URL.
As a side note, JSP files usually have lowercase first letters in their name. As a result, the generated Java classes will also follow this naming, which restrains from the conventional Java class naming.
To include this fragment in the Web application's web.xml file, the code should look something like Listing 5. (Note that I had to break lines in DTD public id and URL to accommodate the column width, but in reality you wouldn't do this as it is illegal in this context.) It is assumed that the file org.foo.myapp.jsp.ent is located in the same directory as the web.xml file itself.
Conclusion
By using the techniques described, we were able to assemble servlets, JSP pages, and static Web resources (such as GIF icons) into true, easily deployable, fully encapsulated, and reusable components for Web applications. The resulting components consist of only two files and are easily deployable.
I have no doubt that by following these guidelines any team or person developing Web applications can increase their productivity, as well as the maintainability of their code base.
References
- Hunter, J. "The Problems with JSP," http://www.servlets.com/soapbox/problems-jsp.html.
- Stevens, Jon S. "You Make the Decision," http://jakarta.apache.org/velocity/ymtd/ymtd.html.
- Jakarta Tomcat, http://jakarta.apache.org/tomcat/.