|
|
|
|
Java Servlet ProgrammingBy Jason Hunter with William Crawford1st Edition November 1998 1-56592-391-X, Order Number: 391X 528 pages, $39.95 |
Sample Chapter 3: The Servlet Life Cycle
In this chapter:
The Servlet Alternative
Servlet Reloading
Init and Destroy
Single-Thread Model
Background Processing
Last Modified Times
The servlet life cycle is one of the most exciting features of servlets. This life cycle is a powerful hybrid of the life cycles used in CGI programming and lower-level NSAPI and ISAPI programming, as discussed in Chapter 1, Introduction.
The Servlet Alternative
The servlet life cycle allows servlet engines to address both the performance and resource problems of CGI and the security concerns of low-level server API programming. A servlet engine may execute all its servlets in a single Java virtual machine (JVM). Because they are in the same JVM, servlets can efficiently share data with each other, yet they are prevented by the Java language from accessing one another's private data. Servlets may also be allowed to persist between requests as object instances, taking up far less memory than full-fledged processes.
Before we proceed too far, you should know that the servlet life cycle is highly flexible. Servers have significant leeway in how they choose to support servlets. The only hard and fast rule is that a servlet engine must conform to the following life cycle contract:
- Create and initialize the servlet.
- Handle zero or more service calls from clients.
- Destroy the servlet and then garbage collect it.
It's perfectly legal for a servlet to be loaded, created, and instantiated in its own JVM, only to be destroyed and garbage collected without handling any client requests or after handling just one request. Any servlet engine that makes this a habit, however, probably won't last long on the open market. In this chapter we describe the most common and most sensible life cycle implementations for HTTP servlets.
A Single Java Virtual Machine
Most servlet engines want to execute all servlets in a single JVM. Where that JVM itself executes can differ depending on the server, though. With a server written in Java, such as the Java Web Server, the server itself can execute inside a JVM right alongside its servlets.
With a single-process, multithreaded web server written in another language, the JVM can often be embedded inside the server process. Having the JVM be part of the server process maximizes performance because a servlet becomes, in a sense, just another low-level server API extension. Such a server can invoke a servlet with a lightweight context switch and can provide information about requests through direct method invocations.
A multiprocess web server (which runs several processes to handle requests) doesn't really have the choice to embed a JVM directly in its process because there is no one process. This kind of server usually runs an external JVM that its processes can share. With this approach, each servlet access involves a heavyweight context switch reminiscent of FastCGI. All the servlets, however, still share the same external process.
Fortunately, from the perspective of the servlet (and thus from your perspective, as a servlet author), the server's implementation doesn't really matter because the server always behaves the same way.
Instance Persistence
We said above that servlets persist between requests as object instances. In other words, at the time the code for a servlet is loaded, the server creates a single class instance. That single instance handles every request made of the servlet. This improves performance in three ways:
- It keeps the memory footprint small.
- It eliminates the object creation overhead that would otherwise be necessary to create a new servlet object. A servlet can be already loaded in a virtual machine when a request comes in, letting it begin executing right away.
- It enables persistence. A servlet can have already loaded anything it's likely to need during the handling of a request. For example, a database connection can be opened once and used repeatedly thereafter. It can even be used by a group of servlets. Another example is a shopping cart servlet that loads in memory the price list along with information about its recently connected clients. Yet another servlet may choose to cache entire pages of output to save time if it receives the same request again.
Not only do servlets persist between requests, but so do any threads created by servlets. This perhaps isn't useful for the run-of-the-mill servlet, but it opens up some interesting possibilities. Consider the situation where one background thread performs some calculation while other threads display the latest results. It's quite similar to an animation applet where one thread changes the picture and another one paints the display.
A Simple Counter
To demonstrate the servlet life cycle, we'll begin with a simple example. Example 3-1 shows a servlet that counts and displays the number of times it has been accessed. For simplicity's sake, it outputs plain text.
Example 3-1. A simple counter
import java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class SimpleCounter extends HttpServlet { int count = 0; public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.setContentType("text/plain"); PrintWriter out = res.getWriter(); count++; out.println("Since loading, this servlet has been accessed " + count + " times."); } }The code is simple--it just prints and increments the instance variable named
count--but it shows the power of persistence. When the server loads this servlet, the server creates a single instance to handle every request made of the servlet. That's why this code can be so simple. The same instance variables exist between invocations and for all invocations.A Simple Synchronized Counter
From the servlet-developer's perspective, each client is another thread that calls the servlet via the
service(),doGet(), ordoPost()methods, as shown in Figure 3-1. [1]Figure 3-1. Many threads, one servlet instance
![]()
If your servlets only read from the request, write to the response, and save information in local variables (that is, variables declared within a method), you needn't worry about the interaction among these threads. Once any information is saved in nonlocal variables (that is, variables declared within a class but outside any specific method), however, you must be aware that each of these client threads has the ability to manipulate a servlet's nonlocal variables. Without precautions, this may result in data corruption and inconsistencies. For example, the
SimpleCounterservlet makes a false assumption that the counter incrementation and output occur atomically (immediately after one another, uninterrupted). It's possible that if two requests are made toSimpleCounteraround the same time, each will print the same value forcount. How? Imagine that one thread increments the count and just afterward, before the first thread prints the count, the second thread also increments the count. Each thread will print the same count value, after effectively increasing its value by 2.[2] The order of execution goes something like thiscount++ // Thread 1 count++ // Thread 2 out.println // Thread 1 out.println // Thread 2Now, in this case, the inconsistency is obviously not a problem, but many other servlets have more serious opportunities for errors. To prevent these types of problems and the inconsistencies that come with them, we can add one or more synchronized blocks to the code. Anything inside a synchronized block or a synchronized method is guaranteed not to be executed concurrently by another thread. Before any thread begins to execute synchronized code, it must obtain a monitor (lock) on a specified class. If another thread already has that monitor--because it is already executing the same synchronized block or some other block with the same monitor--the first thread must wait. All this is handled by the language itself, so it's very easy to use. Synchronization, however, should be used only when necessary. On some platforms, it requires a fair amount of overhead to obtain the monitor each time a synchronized block is entered. More importantly, during the time one thread is executing synchronized code, the other threads may be blocked waiting for the monitor to be released.
For
SimpleCounter, we have four options to deal with this potential problem. First, we could add the keywordsynchronizedto thedoGet()signature:public synchronized void doGet(HttpServletRequest req, HttpServletResponse res)This guarantees consistency by synchronizing the entire method, using the servlet class as the monitor. In general, though, this is not the right approach because it means the servlet can handle only one GET request at a time.
Our second option is to synchronize just the two lines we want to execute atomically:
PrintWriter out = res.getWriter(); synchronized(this) { count++; out.println("Since loading, this servlet has been accessed " + count + " times."); }This approach works better because it limits the amount of time this servlet spends in its synchronized block, while accomplishing the same goal of a consistent count. Of course, for this simple example, it isn't much different than the first option.
Our third option is to create a synchronized block that performs all the work that needs to be done serially, then use the results outside the synchronized block. For our counter servlet, we can increment the count in a synchronized block, save the incremented value to a local variable (a variable declared inside a method), then print the value of the local variable outside the synchronized block:
PrintWriter out = res.getWriter(); int local_count; synchronized(this) { local_count = ++count; } out.println("Since loading, this servlet has been accessed " + local_count + " times.");This change shrinks the synchronized block to be as small as possible, while still maintaining a consistent count.
Our last option is to decide that we are willing to suffer the consequences of ignoring synchronization issues. Sometimes the consequences are quite acceptable. For this example, ignoring synchronization means that some clients may receive a count that's a bit off. Not a big deal, really. If this servlet were supposed to return unique numbers, however, it would be a different story.
Although it's not possible with this example, an option that exists for other servlets is to change instance variables into local variables. Local variables are not available to other threads and thus don't need to be carefully protected from corruption. At the same time, however, local variables are not persistent between requests, so we can't use them to store the persistent state of our counter.
A Holistic Counter
Now, the "one instance per servlet" model is a bit of a gloss-over. The truth is that each registered name for a servlet (but not each alias) is associated with one instance of the servlet. The name used to access the servlet determines which instance handles the request. This makes sense because the impression to the client should be that differently named servlets operate independently. The separate instances are also a requirement for servlets that accept initialization parameters, as discussed later in this chapter.
Our
SimpleCounterexample uses thecountinstance variable to track the number of times it has been accessed. If, instead, it needed to track the count for all instances (and thus all registered aliases), it can in some cases use a class, or static, variable. These variables are shared across all instances of a class. Example 3-2 demonstrates with a servlet that counts three things: the times it has been accessed, the number of instances created by the server (one per name), and the total times all of them have been accessed.
Example 3-2. A more holistic counter
import java.io.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; public class HolisticCounter extends HttpServlet { static int classCount = 0; // shared by all instances int count = 0; // separate for each servlet static Hashtable instances = new Hashtable(); // also shared public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.setContentType("text/plain"); PrintWriter out = res.getWriter(); count++; out.println("Since loading, this servlet instance has been accessed " + count + " times."); // Keep track of the instance count by putting a reference to this // instance in a Hashtable. Duplicate entries are ignored. // The size() method returns the number of unique instances stored. instances.put(this, this); out.println("There are currently " + instances.size() + " instances."); classCount++; out.println("Across all instances, this servlet class has been " + "accessed " + classCount + " times."); } }This
HolisticCountertracks its own access count with thecountinstance variable, the shared count with theclassCountclass variable, and the number of instances with theinstanceshashtable (another shared resource that must be a class variable). Sample output is shown in Figure 3-2.Figure 3-2. Output from HolisticCounter
![]()
Servlet Reloading
If you tried using these counters for yourself, you may have noticed that any time you recompiled one, its count automatically began again at 1. Trust us--it's not a bug, it's a feature. Most servers automatically reload a servlet after its class file (under the default servlet directory, such as server_root/servlets) changes. It's an on-the-fly upgrade procedure that greatly speeds up the development-test cycle--and allows for long server uptimes.
Servlet reloading may appear to be a simple feature, but it's actually quite a trick--and requires quite a hack.
ClassLoaderobjects are designed to load a class just once. To get around this limitation and load servlets again and again, servers use custom class loaders that load servlets from the default servlets directory. This explains why the servlet classes are found in server_root/servlets, even though that directory doesn't appear in the server's classpath.When a server dispatches a request to a servlet, it first checks if the servlet's class file has changed on disk. If it has changed, the server abandons the class loader used to load the old version and creates a new instance of the custom class loader to load the new version. Old servlet versions can stay in memory indefinitely (and, in fact, other classes can still hold references to the old servlet instances, causing odd side effects, as explained in Chapter 11, Interservlet Communication), but the old versions are not used to handle any more requests.
Servlet reloading is not performed for classes found in the server's classpath (such as server_root/classes) because those classes are loaded by the core, primordial class loader. These classes are loaded once and retained in memory even when their class files change.
It's generally best to put servlet support classes (such as the utility classes in
com.oreilly.servlet) somewhere in the server's classpath (such as server_root/classes) where they don't get reloaded. The reason is that support classes are not nicely reloaded like servlets. A support class, placed in the default servlets directory and accessed by a servlet, is loaded by the same class loader instance that loaded the servlet. It doesn't get its own class loader instance. Consequently, if the support class is recompiled but the servlet referring to it isn't, nothing happens. The server checks only the timestamp on servlet class files.[3]A frequently used trick to improve performance is to place servlets in the default servlet directory during development and move them to the server's classpath for deployment. Having them out of the default directory eliminates the needless timestamp comparison for each request.
Init and Destroy
Just like applets, servlets can define
init()anddestroy()methods. A servlet'sinit(ServletConfig)method is called by the server immediately after the server constructs the servlet's instance. Depending on the server and its configuration, this can be at any of these times:
- When the server starts
- When the servlet is first requested, just before the
service()method is invoked- At the request of the server administrator
In any case,
init()is guaranteed to be called before the servlet handles its first request.The
init()method is typically used to perform servlet initialization--creating or loading objects that are used by the servlet in the handling of its requests. Why not use a constructor instead? Well, in JDK 1.0 (for which servlets were originally written), constructors for dynamically loaded Java classes (such as servlets) couldn't accept arguments. So, in order to provide a new servlet any information about itself and its environment, a server had to call a servlet'sinit()method and pass along an object that implements theServletConfiginterface. Also, Java doesn't allow interfaces to declare constructors. This means that thejavax.servlet.Servletinterface cannot declare a constructor that accepts aServletConfigparameter. It has to declare another method, likeinit(). It's still possible, of course, for you to define constructors for your servlets, but in the constructor you don't have access to theServletConfigobject or the ability to throw aServletException.This
ServletConfigobject supplies a servlet with information about its initialization (init) parameters. These parameters are given to the servlet itself and are not associated with any single request. They can specify initial values, such as where a counter should begin counting, or default values, perhaps a template to use when not specified by the request. In the Java Web Server, init parameters for a servlet are usually set during the registration process. See Figure 3-3.Figure 3-3. Setting init parameters in the Java Web Server
![]()
Other servers set init parameters in different ways. Sometimes it involves editing a configuration file. One creative technique you can use with the Java Web Server, but currently by no other servers, is to treat servlets as JavaBeans. Such servlets can be loaded from serialized files or have their init properties set automatically by the server at load time using introspection. See the Java Web Server documentation for more information.
The
ServletConfigobject also holds a reference to aServletContextobject that a servlet may use to investigate its environment. See Chapter 4 Retrieving Information, for a full discussion of this ability.The server calls a servlet's
destroy()method when the servlet is about to be unloaded. In thedestroy()method, a servlet should free any resources it has acquired that will not be garbage collected. Thedestroy()method also gives a servlet a chance to write out its unsaved cached information or any persistent information that should be read during the next call toinit().A Counter with Init
Init parameters can be used for anything. In general, they specify initial values or default values for servlet variables, or they tell a servlet how to customize its behavior in some way. Example 3-3 extends our
SimpleCounterexample to read an init parameter (namedinitial) that stores the initial value for our counter.
Example 3-3. A counter that reads init parameters
import java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class InitCounter extends HttpServlet { int count; public void init(ServletConfig config) throws ServletException { super.init(config); String initial = config.getInitParameter("initial"); try { count = Integer.parseInt(initial); } catch (NumberFormatException e) { count = 0; } } public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.setContentType("text/plain"); PrintWriter out = res.getWriter(); count++; out.println("Since loading (and with a possible initialization"); out.println("parameter figured in), this servlet has been accessed"); out.println(count + " times."); } }The
init()method accepts an object that implements theServletConfiginterface. It uses the config object'sgetInitParameter()method to get the value for the init parameter namedinitial. This method takes the name of the parameter as aStringand returns the value as aString. There is no way to get the value as any other type. This servlet therefore converts theStringvalue to anintor, if there's a problem, defaults to a value of0.Take special note that the first thing the
init()method does is callsuper.init(config). Every servlet's init() method must do this!Why must the
init()method callsuper.init(config)? The reason is that a servlet is passed itsServletConfiginstance in itsinit()method, but not in any other method. This could cause a problem for a servlet that needs to access its config object outside ofinit(). Callingsuper.init(config)solves this problem by invoking theinit()method ofGenericServlet, which saves a reference to the config object for future use.So, how does a servlet make use of this saved reference? By invoking methods on itself. The
GenericServletclass itself implements theServletConfiginterface, using the saved config object in the implementation. In other words, after the call tosuper.init(config), a servlet can invoke its owngetInitParameter()method. That means we could replace the following call:String initial = config.getInitParameter("initial");with:
String initial = getInitParameter("initial");This second style works even outside of the
init()method. Just remember, without the call tosuper.init(config)in theinit()method, any call to theGenericServlet's implementation ofgetInitParameter()or any otherServletConfigmethods will throw aNullPointerException. So, let us say it again: every servlet's init() method should call super.init(config) as its first action. The only reason not to is if the servlet directly implements thejavax.servlet.Servletinterface, where there is nosuper.init().A Counter with Init and Destroy
Up until now, the counter examples have demonstrated how servlet state persists between accesses. This solves only part of the problem. Every time the server is shut down or the servlet is reloaded, the count begins again. What we really want is persistence across loads--a counter that doesn't have to start over.
The
init()anddestroy()pair can accomplish this. Example 3-4 further extends theInitCounterexample, giving the servlet the ability to save its state indestroy()and load the state again ininit(). To keep things simple, assume this servlet is not registered and is accessed only as http://server:port/servlet/InitDestroyCounter. If it were registered under different names, it would have to save a separate state for each name.
Example 3-4. A fully persistent counter
import java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class InitDestroyCounter extends HttpServlet { int count; public void init(ServletConfig config) throws ServletException { // Always call super.init(config) first (servlet mantra #1) super.init(config); // Try to load the initial count from our saved persistent state try { FileReader fileReader = new FileReader("InitDestroyCounter.initial"); BufferedReader bufferedReader = new BufferedReader(fileReader); String initial = bufferedReader.readLine(); count = Integer.parseInt(initial); return; } catch (FileNotFoundException ignored) { } // no saved state catch (IOException ignored) { } // problem during read catch (NumberFormatException ignored) { } // corrupt saved state // No luck with the saved state, check for an init parameter String initial = getInitParameter("initial"); try { count = Integer.parseInt(initial); return; } catch (NumberFormatException ignored) { } // null or non-integer value // Default to an initial count of "0" count = 0; } public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.setContentType("text/plain"); PrintWriter out = res.getWriter(); count++; out.println("Since the beginning, this servlet has been accessed " + count + " times."); } public void destroy() { saveState(); } public void saveState() { // Try to save the accumulated count try { FileWriter fileWriter = new FileWriter("InitDestroyCounter.initial"); String initial = Integer.toString(count); fileWriter.write(initial, 0, initial.length()); fileWriter.close(); return; } catch (IOException e) { // problem during write // Log the exception. See . } } }Each time this servlet is about to be unloaded, it saves its state in a file named InitDestroyCounter.initial. In the absence of a supplied path, the file is saved in the server process' current directory, usually the server_root.[4] This file contains a single integer, saved as a string, that represents the latest count.
Each time the servlet is loaded, it tries to read the saved count from the file. If, for some reason, the read fails (as it does the first time the servlet runs because the file doesn't yet exist), the servlet checks if an init parameter specifies the starting count. If that too fails, it starts fresh with zero. You can never be too careful in
init()methods.Servlets can save their state in many different ways. Some may use a custom file format, as was done here. Others may save their state as serialized Java objects or put it into a database. Some may even perform journaling, a technique common to databases and tape backups, where the servlet's full state is saved infrequently while a journal file stores incremental updates as things change. Which method a servlet should use depends on the situation. In any case, you should always be watchful that the state being saved isn't undergoing any change in the background.
Right now you're probably asking yourself "What happens if the server crashes?" It's a good question. The answer is that the
destroy()method will not be called.[5] This doesn't cause a problem fordestroy()methods that only have to free resources; a rebooted server does that job just as well (if not better). But it does cause a problem for a servlet that needs to save its state in itsdestroy()method. For these servlets, the only guaranteed solution is to save state more often. A servlet may choose to save its state after handling each request, such as a "chess server" servlet should do, so that even if the server is restarted, the game can resume with the latest board position. Other servlets may need to save state only after some important value has changed--a "shopping cart" servlet needs to save its state only when a customer adds or removes an item from her cart. Last, for some servlets, it's fine to lose a bit of the recent state changes. These servlets can save state after some set number of requests. For example, in ourInitDestroyCounterexample, it should be satisfactory to save state every 10 accesses. To implement this, we can add the following line at the end ofdoGet():if (count % 10 == 0) saveState();Does this addition make you cringe? It should. Think about synchronization issues. We've opened up the possibility for data loss if
saveState()is executed by two threads at the same time and the possibility forsaveState()not to be called at all ifcountis incremented by several threads in a row before the check. Note that this possibility did not exist whensaveState()was called only from thedestroy()method: thedestroy()method is called just once per servlet instance. Now thatsaveState()is called in thedoGet()method, however, we need to reconsider. If by some chance this servlet is accessed so frequently that it has more than 10 concurrently executing threads, it's likely that two servlets (10 requests apart) will be insaveState()at the same time. This may result in a corrupted data file. It's also possible the two threads will incrementcountbefore either thread notices it was time to callsaveState(). The fix is easy: move thecountcheck into the synchronized block wherecountis incremented:int local_count; synchronized(this) { local_count = ++count; if (count % 10 == 0) saveState(); } out.println("Since loading, this servlet has been accessed " + local_count + " times.");The moral of the story is harder: always be vigilant to protect servlet code from multithreaded access problems.
Even though this series of counter examples demonstrates the servlet life cycle, the counters themselves aren't particularly useful because they count only the number of times they themselves have been accessed. You can find two truly useful counters--that count accesses to other pages--in the next chapter.
Single-Thread Model
Although it is standard to have one servlet instance per registered servlet name, it is possible for a servlet to elect instead to have a pool of instances created for each of its names, all sharing the duty of handling requests. Such servlets indicate this desire by implementing the
javax.servlet.SingleThreadModelinterface. This is an empty, tag interface that defines no methods or variables and serves only to flag the servlet as wanting the alternate life cycle.A server that loads a
SingleThreadModelservlet must guarantee, according to the Servlet API documentation, "that no two threads will execute concurrently the service method of that servlet." To accomplish this, each thread uses a free servlet instance from the pool, as shown in Figure 3-4. Thus, any servlet implementingSingleThreadModelcan be considered thread safe and isn't required to synchronize access to its instance variables.Figure 3-4. The Single Thread Model
![]()
Such a life cycle is pointless for a counter or other servlet application that requires central state maintenance. The life cycle can be useful, however, in avoiding synchronization while still performing efficient request handling.
For example, a servlet that connects to a database sometimes needs to perform several database commands atomically as part of a single transaction. Normally, this would require the servlet to synchronize around the database commands (letting it manage just one request at a time) or to manage a pool of database connections where it can "check out" and "check in" connections (letting it support multiple concurrent requests). By instead implementing
SingleThreadModeland having one "connection" instance variable per servlet, a servlet can easily handle concurrent requests by letting its server manage the servlet instance pool (which doubles as a connection pool). The skeleton code is shown in Example 3-5.
Example 3-5. Handling database connections using SingleThreadModel
import java.io.*; import java.sql.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; public class SingleThreadConnection extends HttpServlet implements SingleThreadModel { Connection con = null; // database connection, one per pooled servlet instance public void init(ServletConfig config) throws ServletException { super.init(config); // Establish the connection for this instance con = establishConnection(); con.setAutoCommit(false); } public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.setContentType("text/plain"); PrintWriter out = res.getWriter(); // Use the connection uniquely assigned to this instance Statement stmt = con.createStatement(); // Update the database any number of ways // Commit the transaction con.commit(); } public void destroy() { if (con != null) con.close(); } private Connection establishConnection() { // Not implemented. See . } }Background Processing
Servlets can do more than simply persist between accesses. They can also execute between accesses. Any thread started by a servlet can continue executing even after the response has been sent. This ability proves most useful for long-running tasks whose incremental results should be made available to multiple clients. A background thread started in
init()performs continuous work while request-handling threads display the current status withdoGet(). It's a similar technique to that used in animation applets, where one thread changes the picture and another paints the display.Example 3-6 shows a servlet that searches for prime numbers above one quadrillion. It starts with such a large number to make the calculation slow enough to adequately demonstrate caching effects--something we need for the next section. The algorithm it uses couldn't be simpler: it selects odd-numbered candidates and attempts to divide them by every odd integer between 3 and their square root. If none of the integers evenly divides the candidate, it is declared prime.
Example 3-6. On the hunt for primes
import java.io.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; public class PrimeSearcher extends HttpServlet implements Runnable { long lastprime = 0; // last prime found Date lastprimeModified = new Date(); // when it was found Thread searcher; // background search thread public void init(ServletConfig config) throws ServletException { super.init(config); // always! searcher = new Thread(this); searcher.setPriority(Thread.MIN_PRIORITY); // be a good citizen searcher.start(); } public void run() { // QTTTBBBMMMTTTOOO long candidate = 1000000000000001L; // one quadrillion and one // Begin loop searching for primes while (true) { // search forever if (isPrime(candidate)) { lastprime = candidate; // new prime lastprimeModified = new Date(); // new "prime time" } candidate += 2; // evens aren't prime // Between candidates take a 0.2 second break. // Another way to be a good citizen with system resources. try { searcher.sleep(200); } catch (InterruptedException ignored) { } } } private static boolean isPrime(long candidate) { // Try dividing the number by all odd numbers between 3 and its sqrt double sqrt = Math.sqrt(candidate); for (long i = 3; i <= sqrt; i += 2) { if (candidate % i == 0) return false; // found a factor } // Wasn't evenly divisible, so it's prime return true; } public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.setContentType("text/plain"); PrintWriter out = res.getWriter(); if (lastprime == 0) { out.println("Still searching for first prime..."); } else { out.println("The last prime discovered was " + lastprime); out.println(" at " + lastprimeModified); } } public void destroy() { searcher.stop(); } }The searcher thread begins its search in the
init()method. Its latest find is saved inlastprime, along with the time it was found in inlastprimeModified. Each time a client accesses the servlet, thedoGet()method reports the largest prime found so far and the time it was found. The searcher runs independently of client accesses; even if no one accesses the servlet it continues to find primes silently. If several clients access the servlet at the same time, they all see the same current status.Notice that the
destroy()method stops the searcher thread.[6] This is very important! If a servlet does not stop its background threads, they continue to run until the virtual machine exits. Even when a servlet is reloaded (either explicitly or because its class file changed), its threads won't be stopped. Instead, it's likely that the new servlet will create extra copies of the background threads. And, at least with the Java Web Server, even explicitly restarting the web server service doesn't stop background threads because the Java Web Server virtual machine continues its execution.Last Modified Times
By now, we're sure you've learned that servlets handle
GETrequests with thedoGet()method. And that's almost true. The full truth is that not every request really needs to invokedoGet(). For example, a web browser that repeatedly accessesPrimeSearchershould need to calldoGet()only after the searcher thread has found a new prime. Until that time, any call todoGet()just generates the same page the user has already seen, a page probably stored in the browser's cache. What's really needed is a way for a servlet to report when its output has changed. That's where thegetLastModified()method comes in.Most web servers, when they return a document, include as part of their response a
Last-Modifiedheader. An exampleLast-Modifiedheader value might be:Tue, 06-May-98 15:41:02 GMTThis header tells the client the time the page was last changed. That information alone is only marginally interesting, but it proves useful when a browser reloads a page.
Most web browsers, when they reload a page, include in their request an
If-Modified-Sinceheader. Its structure is identical to theLast-Modifiedheader:Tue, 06-May-98 15:41:02 GMTThis header tells the server the
Last-Modifiedtime of the page when it was last downloaded by the browser. The server can read this header and determine if the file has changed since the given time. If the file has changed, the server must send the newer content. If the file hasn't changed, the server can reply with a simple, short response that tells the browser the page has not changed and it is sufficient to redisplay the cached version of the document. For those familiar with the details of HTTP, this response is the 304 "Not Modified" status code.This technique works great for static pages: the server can use the file system to find out when any file was last modified. For dynamically generated content, though, such as that returned by servlets, the server needs some extra help. By itself, the best the server can do is play it safe and assume the content changes with every access, effectively eliminating the usefulness of the
Last-ModifiedandIf-Modified-Sinceheaders.The extra help a servlet can provide is implementing the
getLastModified()method. A servlet should implement this method to return the time it last changed its output. Servers call this method at two times. The first time the server calls it is when it returns a response, so that it can set the response'sLast-Modifiedheader. The second time occurs in handlingGETrequests that include theIf-Modified-Sinceheader (usually reloads), so it can intelligently determine how to respond. If the time returned bygetLastModified()is equal to or earlier than the time sent in theIf-Modified-Sinceheader, the server returns the "Not Modified" status code. Otherwise, the server callsdoGet()and returns the servlet's output.[7]Some servlets may find it difficult to determine their last modified time. For these situations, it's often best to use the "play it safe" default behavior. Many servlets, however, should have little or no problem. Consider a "bulletin board" servlet where people post carpool openings or the need for racquetball partners. It can record and return when the bulletin board's contents were last changed. Even if the same servlet manages several bulletin boards, it can return a different modified time depending on the board given in the parameters of the request. Here's a
getLastModified()method for ourPrimeSearcherexample that returns when the last prime was found.public long getLastModified(HttpServletRequest req) { return lastprimeModified.getTime() / 1000 * 1000; }Notice that this method returns a
longvalue that represents the time as a number of milliseconds since midnight, January 1, 1970, GMT. This is the same representation used internally by Java to store time values. Thus, the servlet uses thegetTime()method to retrievelastprimeModifiedas along.Before returning this time value, the servlet rounds it down to the nearest second by dividing by 1000 and then multiplying by 1000. All times returned by
getLastModified()should be rounded down like this. The reason is that theLast-ModifiedandIf-Modified-Sinceheaders are given to the nearest second. IfgetLastModified()returns the same time but with a higher resolution, it may erroneously appear to be a few milliseconds later than the time given byIf-Modified-Since. For example, let's assumePrimeSearcherfound a prime exactly 869127442359 milliseconds since the beginning of the Disco Decade. This fact is told to the browser, but only to the nearest second:Thu, 17-Jul-97 09:17:22 GMTNow let's assume that the user reloads the page and the browser tells the server, via the
If-Modified-Sinceheader, the time it believes its cached page was last modified:Thu, 17-Jul-97 09:17:22 GMTSome servers have been known to receive this time, convert it to exactly 869127442000 milliseconds, find that this time is 359 milliseconds earlier than the time returned by
getLastModified(), and falsely assume that the servlet's content has changed. This is why, to play it safe,getLastModified()should always round down to the nearest thousand milliseconds.The
HttpServletRequestobject is passed togetLastModified()in case the servlet needs to base its results on information specific to the particular request. The generic bulletin board servlet can make use of this to determine which board was being requested, for example.
1. Does it seem confusing how one servlet instance can handle multiple requests at the same time? If so, it's probably because when we picture an executing program we often see object instances performing the work, invoking each other's methods and so on. But, although this model works for simple cases, it's not how things actually work. In reality, all real work is done by threads. The object instances are nothing more than data structures manipulated by the threads. Therefore, if there are two threads running, it's entirely possible that both are using the same object at the same time.
2.Odd factoid: if
countwere a 64-bitlonginstead of a 32-bitint, it would be theoretically possible for the increment to be only half performed at the time it is interrupted by another thread. This is because Java uses a 32-bit wide stack.3. For the daredevils out there, here's a stunt you can try to force a support class reload. Put the support class in the servlet directory. Then convince the server it needs to reload the servlet that uses the support class (recompile it or use the Unix utility touch). The class loader that reloads the servlet should also load the new version of the support class.
4. The exact location of the current user directory can be found using
System.getProperty("user.dir").5. Unless you're so unlucky that your server crashes while in the
destroy()method. In that case, you may be left with a partially-written state file--garbage written on top of your previous state. To be perfectly safe, a servlet should save its state to a temporary file and then copy that file on top of the official state file in one command.6. Stopping threads using the
stop()method as shown here is deprecated in JDK 1.2 in favor of a safer flag-based system, where a thread must periodically examine a "flag" variable to determine when it should stop, at which point it can clean up and return from itsrun()method. See the JDK documentation for details. Example source code can be found in an article titled "Scott's Solutions: Programming with threads in Java 1.2", written by Scott Oaks for Java Report Online, found at http://www.sigs.com/jro/features/9711/oaks.html.7. A servlet can directly set its
Last-Modifiedheader insidedoGet(), using techniques discussed in Chapter 5 Sending HTML Information. However, by the time the header is set insidedoGet(), it's too late to decide whether or not to calldoGet().
© 2001, O'Reilly & Associates, Inc.