11.3. Servlet CollaborationSometimes servlets have to cooperate, usually by sharing some information. We call communication of this sort servlet collaboration. Collaborating servlets can pass the shared information directly from one servlet to another through method invocations, as shown earlier. This approach requires each servlet to know the other servlets with which it is collaborating--an unnecessary burden. There are several better techniques. 11.3.1. Collaboration Through the System Properties ListOne simple way for servlets to share information is by using Java's system-wide Properties list, found in the java.lang.System class. This Properties list holds the standard system properties, such as java.version and path.separator, but it can also hold application-specific properties. Servlets can use the properties list to hold the information they need to share. A servlet can add (or change) a property by calling: System.getProperties().put("key", "value"); That servlet, or another servlet running in the same JVM, can later get the value of the property by calling: String value = System.getProperty("key"); The property can be removed by calling: System.getProperties().remove("key"); It's best if the key for a property includes a prefix that contains the name of the servlet's package and the name of the collaboration group. For example, "com.oreilly.servlet.ShoppingCart". The Properties class is intended to be String based, meaning that each key and value is supposed to be a String. This limitation, though, isn't commonly enforced and can (although it's quite a hack) be ignored by servlets that want to store and retrieve non-String objects. Such servlets can take advantage of the fact that the Properties class extends the Hashtable class, so the Properties list can (quite rudely) be treated as a Hashtable when storing keys and values. For example, a servlet can add or change a property object by calling: System.getProperties().put(keyObject, valueObject); // hack It can retrieve the property object by calling: SomeObject valueObject = (SomeObject)System.getProperties().get(keyObject); It can remove the property object by calling: System.getProperties().remove(keyObject); This misuse of the Properties list causes the getProperty(), list() and save() methods of the Properties class to throw ClassCastException objects when they naturally--but erroneously--assume each key and value to be a String. For this reason, if there's any chance these methods might be called, you should instead use one of the techniques for servlet collaboration we describe later in the chapter. Also, remember the class files for keyObject and valueObject should be found in the server's classpath, not in the default servlet directory where they would be loaded, and perhaps reloaded, by the special servlet class loaders. There are three more caveats to using the system Properties list for servlet collaboration: the information isn't naturally persistent between server restarts, the information can be viewed (and modified or deleted) by other classes executing in the servlet's JVM, and some servlet security managers may not grant servlets access to the system property list. 11.3.1.1. Using properties to sell burritosDespite the stern warnings, servlet collaboration through the system-wide Properties list works well for servlets that are sharing insensitive, noncritical, easily replaceable information. As a fun example, imagine a set of servlets that sell burritos and share a "special of the day." An administrative servlet could set the special of the day using the following code: System.getProperties().put("com.LaCostena.special.burrito", "Pollo Adobado"); System.getProperties().put("com.LaCostena.special.day", new Date()); Thereafter, every other servlet on the server can access the special and display it with code like this: String burrito = System.getProperty("com.LaCostena.special.burrito"); Date day = (Date)System.getProperties().get("com.LaCostena.special.day"); DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT); String today = df.format(day); out.println("Our burrito special today (" + today + ") is: " + burrito); 11.3.1.2. Faster image chainingServlets performing image effects in a servlet chain can boost their speed dramatically by using the system Properties list to pass their images. In Chapter 6, "Sending Multimedia Content", we saw the standard method by which the servlets in a chain pass images from link to link. The first servlet takes an Image object, encodes it to a stream of bytes, and passes the bytes to the next servlet. The receiving servlet decodes the bytes back into the original Image object. The technique works fine, but it can be prohibitively slow for large images. An alternative solution is for the first servlet to save the Image object itself in the system-wide Properties list, then pass on a small unique key by which the next servlet in the chain can locate the Image. In a sense, the old approach works by shoving an entire elephant through the small portal between servlets. The new approach works by passing just the elephant's leash. Example 11-5 demonstrates exactly how a servlet passes on a key to an Image object saved in the system Properties list. Example 11-5. Passing an Image through the Properties listimport java.awt.*; import java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class ChainImageSource extends HttpServlet { int keynum = 0; // used to create a unique key public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { // Get an Image String imageFile = req.getRealPath("/system/images/serverduke.gif"); Image image = Toolkit.getDefaultToolkit().getImage(imageFile); // Create a unique key under which to store the image String key = "com.oreilly.servlet.ChainImageSource." + keynum++; // Store the image in the system Properties list using that key System.getProperties().put(key, image); // Tell the next servlet to expect an image key res.setContentType("java-internal/image-key"); PrintWriter out = res.getWriter(); // Send the key out.println(key); } } Notice how the servlet generates its unique key. It prefixes the key with the string "com.oreilly.servlet.ChainImageSource", something no other servlet is likely to use. Then it appends a different integer value for each image. Also notice how this servlet uses the custom content type "java-internal/image-key" to indicate that it's passing on an image key. Example 11-6 shows the other half of this servlet chain--a servlet that uses the key to fetch the original Image object. Example 11-6. Fetching an image passed through the Properties listimport java.awt.*; import java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class ChainImageSink extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { // See what content type we're receiving String contentType = req.getContentType(); Image image = null; // An "image/*" content type means to expect the image as an encoded // byte stream if (contentType != null && contentType.startsWith("image")) { // Receive the image bytes as shown in Chapter 6, "Sending Multimedia Content" } // A "java-internal/image-key" content type means to expect a key else if ("java-internal/image-key".equals(contentType)) { // Read the first line of content to get the key String key = req.getReader().readLine(); // Retrieve the Image stored under that key image = (Image)System.getProperties().get(key); // Always remove the Image, to avoid a memory leak System.getProperties().remove(key); } // Other content types cannot be handled else { throw new ServletException("Incoming content type must be " + "\"image/*\" or \"java-internal/image-key\""); } // Proceed to use the image as appropriate... res.setContentType("text/plain"); PrintWriter out = res.getWriter(); out.println("Received the image: " + image); } } The most important thing to notice with this example is that the receiving servlet bears the responsibility of removing the Image from the system Properties list to avoid a potentially large memory leak. This leash-passing technique works only when the source servlet can be absolutely sure its key is being sent to another servlet, not to a dumbfounded user who expected an image. This can be ensured if every chain has as its final servlet a special servlet whose sole purpose is to accept an image key and emit that Image's encoded byte stream. 11.3.2. Collaboration Through a Shared ObjectAnother way for servlets to share information is through a shared object. A shared object can hold the pool of shared information and make it available to each servlet as needed. In a sense, the system Properties list is a special case example of a shared object. By generalizing the technique into sharing any sort of object, however, a servlet is able to use whatever shared object best solves its particular problem. Often the shared object incorporates a fair amount of business logic or rules for manipulating the object's data. This business logic protects the shared object's actual data by making it available only through well-defined methods. It can enforce data integrity, trigger events to handle certain conditions, and basically abstract lots of little data manipulations into a single method invocation. This capability isn't available with the Properties list. There's one thing to watch out for when collaborating through a shared object: the garbage collector. It can reclaim the shared object if at any time the object isn't referenced by a loaded servlet. To keep the garbage collector at bay, it's wise for every servlet using a shared object to save a reference to the object. 11.3.2.1. Using a shared class to sell burritosFor an example of servlet collaboration through a shared object, let's look at how several servlets selling burritos can maintain a shared inventory of burrito ingredients. First, we need a shared burrito inventory class. This class is responsible for maintaining the ingredient count and making the count available through its public methods. An example burrito inventory class is shown in Example 11-7. You'll notice that this class is a singleton (a class that has just one instance). This makes it easy for every servlet sharing the class to maintain a reference to the same instance. Example 11-7. A shared burrito inventory classpublic class BurritoInventory { // Protect the constructor, so no other class can call it private BurritoInventory() { } // Create the only instance, save it to a private static variable private static BurritoInventory instance = new BurritoInventory(); // Make the static instance publicly available public static BurritoInventory getInstance() { return instance; } // How many "servings" of each item do we have? private int cheese = 0; private int rice = 0; private int beans = 0; private int chicken = 0; // Add to the inventory public void addCheese(int added) { cheese += added; } public void addRice(int added) { rice += added; } public void addBeans(int added) { beans += added; } public void addChicken(int added) { chicken += added; } // Called when it's time to make a burrito. // Returns true if there are enough ingredients to make the burrito, // false if not. Decrements the ingredient count when there are enough. synchronized public boolean makeBurrito() { // Burritos require one serving of each item if (cheese > 0 && rice > 0 && beans > 0 && chicken > 0) { cheese--; rice--; beans--; chicken--; return true; // can make the burrito } else { // Could order more ingredients return false; // cannot make the burrito } } } BurritoInventory maintains an inventory count for four burrito ingredients: cheese, rice, beans, and chicken. It holds the counts with private instance variables. For serious production use, information like these counts should probably be kept in an external database. Each ingredient's inventory count can be increased through the addCheese(), addRice(), addBeans(), and addChicken() methods. These methods might be called from a servlet accessed by the ingredient preparers throughout the day. All the counts are decreased together in the makeBurrito() method. This method checks that there are enough ingredients to make a full burrito. If there are, it decrements the ingredient count and returns true. If there aren't, it returns false (and, in an improved version, may choose to order more ingredients). The makeBurrito() method might be called by a servlet selling burritos over the Web, and perhaps also by a servlet communicating with the check-out cash register. Remember, the class file for BurritoInventory should be placed somewhere in the server's classpath (such as in server_root/classes), just like all the other non-servlet class files. Example 11-8 shows how a servlet adds ingredients to the inventory. Example 11-8. Adding ingredients to the shared inventoryimport java.io.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; public class BurritoInventoryProducer extends HttpServlet { // Get (and keep!) a reference to the shared BurritoInventory instance BurritoInventory inventory = BurritoInventory.getInstance(); public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.setContentType("text/html"); PrintWriter out = res.getWriter(); out.println("<HTML>"); out.println("<HEAD><TITLE>Burrito Inventory Producer</TITLE></HEAD>"); // Produce random amounts of each item Random random = new Random(); int cheese = Math.abs(random.nextInt() % 10); int rice = Math.abs(random.nextInt() % 10); int beans = Math.abs(random.nextInt() % 10); int chicken = Math.abs(random.nextInt() % 10); // Add the items to the inventory inventory.addCheese(cheese); inventory.addRice(rice); inventory.addBeans(beans); inventory.addChicken(chicken); // Print the production results out.println("<BODY>"); out.println("<H1>Added ingredients:</H1>"); out.println("<PRE>"); out.println("cheese: " + cheese); out.println("rice: " + rice); out.println("beans: " + beans); out.println("chicken: " + chicken); out.println("</PRE>"); out.println("</BODY></HTML>"); } } Every time this servlet is accessed, it produces a random amount of each ingredient (somewhere from zero to nine servings) and adds it to the inventory. Then this servlet prints the results of its work, as you can see in Figure 11-4. Figure 11-4. The output from BurritoInventoryProducerThe most important thing to note about this servlet is that it always keeps its reference to the shared BurritoInventory instance, preventing the garbage collector from reclaiming the instance as long as this servlet is loaded. Example 11-9 shows how a servlet can consume the ingredients from the inventory. Example 11-9. Consuming ingredients from the shared inventoryimport java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class BurritoInventoryConsumer extends HttpServlet { // Get (and keep!) a reference to the shared BurritoInventory instance private BurritoInventory inventory = BurritoInventory.getInstance(); public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.setContentType("text/html"); PrintWriter out = res.getWriter(); out.println("<HTML>"); out.println("<HEAD><TITLE>Burrito Inventory Consumer</TITLE></HEAD>"); out.println("<BODY><BIG>"); if (inventory.makeBurrito()) { out.println("Your burrito will be ready in a few minutes."); } else { out.println("We're low on ingredients.<BR>"); out.println("Looks like you're gonna starve."); } out.println("</BIG></BODY></HTML>"); } } This servlet calls the makeBurrito() method, telling the inventory it wants to make a burrito. This servlet doesn't have to (and, in fact, isn't allowed to) decrement the counts itself. This servlet saves its own reference to the BurritoInventory instance, making sure that even if BurritoInventoryProducer is unloaded, the BurritoInventory instance is still referenced and therefore protected from the garbage collector. 11.3.2.2. Using a servlet as the shared objectWe should mention that it's possible for a servlet to act as the shared object. Using a shared servlet has the added advantage that the servlet can maintain its state using its init() and destroy() methods to load and save its state. Plus, a shared servlet can print its current state each time it's accessed. Example 11-10 shows the BurritoInventory class rewritten to be a servlet. Example 11-10. The shared burrito inventory class, rewriten as a servletimport java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class BurritoInventoryServlet extends HttpServlet { // How many "servings" of each item do we have? private int cheese = 0; private int rice = 0; private int beans = 0; private int chicken = 0; // Add to the inventory as more servings are prepared. public void addCheese(int added) { cheese += added; } public void addRice(int added) { rice += added; } public void addBeans(int added) { beans += added; } public void addChicken(int added) { chicken += added; } // Called when it's time to make a burrito. // Returns true if there are enough ingredients to make the burrito, // false if not. Decrements the ingredient count when there are enough. synchronized public boolean makeBurrito() { // Burritos require one serving of each item if (cheese > 0 && rice > 0 && beans > 0 && chicken > 0) { cheese--; rice--; beans--; chicken--; return true; // can make the burrito } else { // Could order more ingredients return false; // cannot make the burrito } } // Display the current inventory count. public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.setContentType("text/html"); PrintWriter out = res.getWriter(); out.println("<HTML><HEAD><TITLE>Current Ingredients</TITLE></HEAD>"); out.println("<BODY>"); out.println("<TABLE BORDER=1>"); out.println("<TR><TH COLSPAN=2>Current ingredients:</TH></TR>"); out.println("<TR><TD>Cheese:</TD><TD>" + cheese + "</TD></TR>"); out.println("<TR><TD>Rice:</TD><TD>" + rice + "</TD></TR>"); out.println("<TR><TD>Beans:</TD><TD>" + beans + "</TD></TR>"); out.println("<TR><TD>Chicken:</TD><TD>" + chicken + "</TD></TR>"); out.println("</TABLE>"); out.println("</BODY></HTML>"); } // Load the stored inventory count public void init(ServletConfig config) throws ServletException { super.init(config); loadState(); } public void loadState() { // Try to load the counts FileInputStream file = null; try { file = new FileInputStream("BurritoInventoryServlet.state"); DataInputStream in = new DataInputStream(file); cheese = in.readInt(); rice = in.readInt(); beans = in.readInt(); chicken = in.readInt(); file.close(); return; } catch (IOException ignored) { // Problem during read } finally { try { if (file != null) file.close(); } catch (IOException ignored) { } } } public void destroy() { saveState(); } public void saveState() { // Try to save the counts FileOutputStream file = null; try { file = new FileOutputStream("BurritoInventoryServlet.state"); DataOutputStream out = new DataOutputStream(file); out.writeInt(cheese); out.writeInt(rice); out.writeInt(beans); out.writeInt(chicken); return; } catch (IOException ignored) { // Problem during write } finally { try { if (file != null) file.close(); } catch (IOException ignored) { } } } } BurritoInventoryServlet is no longer a singleton: it's now a normal HTTP servlet. It defines an init() method that loads its state and a destroy() method that saves its state. It also defines a doGet() method that displays its state, as shown in Figure 11-5. Figure 11-5. The output from BurritoInventoryServlet, showing its stateRemember that, even as a servlet, the BurritoInventoryServlet.class file should remain in the server's standard classpath to keep it from being reloaded. The BurritoInventoryProducer and BurritoInventoryConsumer classes can get a reference to the BurritoInventoryServlet using the technique discussed earlier in this chapter for servlet reuse: // Get the inventory servlet instance if we haven't before if (inventory == null) { inventory = (BurritoInventoryServlet)ServletUtils.getServlet( "BurritoInventoryServlet", req, getServletContext()); // If the load was unsuccessful, throw an exception if (inventory == null) { throw new ServletException( "Could not locate BurritoInventoryServlet"); } } Instead of calling BurritoInventory.getInstance(), the producer and consumer classes can ask the server for the BurritoInventoryServlet instance. 11.3.3. Collaboration Through InheritancePerhaps the easiest technique for servlet collaboration is through inheritance. Each servlet interested in collaborating can extend the same class and inherit the same shared information. This simplifies the code for the collaborating servlets and limits access to the shared information to the proper subclasses. The common superclass can hold a reference to the shared information, or it can hold the shared information itself. 11.3.3.1. Inheriting a shared referenceA common superclass can hold any number of references to shared business objects that are easily made available to its subclasses. Example 11-11 shows such a superclass, usable for our burrito inventory example. Example 11-11. A superclass holding a reference to shared informationimport javax.servlet.*; import javax.servlet.http.*; public class BurritoInventorySuperclass extends HttpServlet { protected static BurritoInventory inventory = new BurritoInventory(); } This BurritoInventorySuperclass creates a new BurritoInventory instance. BurritoInventoryProducer and BurritoInventoryConsumer can then subclass BurritoInventorySuperclass and inherit a reference to this instance. The code for the revised BurritoInventoryConsumer is shown in Example 11-12 to clarify. Example 11-12. Using an inherited business objectimport java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class BurritoInventoryConsumer extends BurritoInventorySuperclass { public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.setContentType("text/html"); PrintWriter out = res.getWriter(); out.println("<HTML>"); out.println("<HEAD><TITLE>Burrito Inventory Consumer</TITLE></HEAD>"); out.println("<BODY><BIG>"); if (inventory.makeBurrito()) { out.println("Your burrito will be ready in 3 minutes."); } else { out.println("We're low on ingredients.<BR>"); out.println("Looks like you're gonna starve."); } out.println("</BIG></BODY></HTML>"); } } The BurritoInventory class doesn't have to be a singleton anymore. The subclasses naturally inherit the same instance. Again, the class file for BurritoInventorySuperclass should be put in the server's classpath to keep it from being reloaded. 11.3.3.2. Inheriting the shared informationIn addition to holding shared references, a common superclass can hold shared information itself and optionally make it available through inherited business logic methods. Example 11-13 shows BurritoInventorySuperclass rewritten using this technique. It's essentially an alternate form of BurritoInventoryServlet. Example 11-13. A superclass holding its own shared informationpublic class BurritoInventorySuperclass extends HttpServlet { // How many "servings" of each item do we have? private static int cheese = 0; private static int rice = 0; private static int beans = 0; private static int chicken = 0; // Add to the inventory as more servings are prepared. protected static void addCheese(int added) { cheese += added; } protected static void addRice(int added) { rice += added; } protected static void addBeans(int added) { beans += added; } protected static void addChicken(int added) { chicken += added; } // Called when it's time to make a burrito. // Returns true if there are enough ingredients to make the burrito, // false if not. Decrements the ingredient count when there are enough. synchronized static protected boolean makeBurrito() { // ...etc... } // ...The rest matches BurritoInventoryServlet... There are only two differences between this servlet superclass and BurritoInventoryServlet. First, all the variables and methods are now static. This guarantees that there's just one inventory kept for all the subclasses. Second, all the methods are now protected. This makes them available only to subclasses. By inheriting from a superclass that contains the shared information, BurritoInventoryProducer and BurritoInventoryConsumer can call the inventory methods directly. For example, BurritoInventoryProducer can add items to the inventory with this code: // Add the items to the inventory addCheese(cheese); addRice(rice); addBeans(beans); addChicken(chicken); BurritoInventoryConsumer can consume the ingredients with this code: if (makeBurrito()) Copyright © 2001 O'Reilly & Associates. All rights reserved. |
|