Fortunately for us servlet developers, it's not always necessary for a servlet to manage its own sessions using the techniques we have just discussed. The Servlet API provides several methods and classes specifically designed to handle session tracking on behalf of servlets. In other words, servlets have built in session tracking.[2]
[2] Yes, we do feel a little like the third grade teacher who taught you all the steps of long division, only to reveal later how you could use a calculator to do the same thing. But we believe, as your teacher probably did, that you better understand the concepts after first learning the traditional approach.
The Session Tracking API, as we call the portion of the Servlet API devoted to session tracking, should be supported in any web server that supports servlets. The level of support, however, depends on the server. The minimal implementation provided by the servlet classes in JSDK 2.0 manages sessions through the use of persistent cookies. A server can build on this base to provide additional features and capabilities. For example, the Java Web Server has the ability to revert to using URL rewriting when cookies fail, and it allows session objects to be written to the server's disk as memory fills up or when the server shuts down. (The items you place in the session need to implement the Serializable interface to take advantage of this option.) See your server's documentation for details pertaining to your server. The rest of this section describe the lowest-common-denominator functionality provided by Version 2.0 of the Servlet API.
Session tracking is wonderfully elegant. Every user of a site is associated with a javax.servlet.http.HttpSession object that servlets can use to store or retrieve information about that user. You can save any set of arbitrary Java objects in a session object. For example, a user's session object provides a convenient location for a servlet to store the user's shopping cart contents or, as you'll see in Chapter 9, "Database Connectivity", the user's database connection.
A servlet uses its request object's getSession() method to retrieve the current HttpSession object:
public HttpSession HttpServletRequest.getSession(boolean create)
This method returns the current session associated with the user making the request. If the user has no current valid session, this method creates one if create is true or returns null if create is false. To ensure the session is properly maintained, this method must be called at least once before any output is written to the response.
You can add data to an HttpSession object with the putValue() method:
public void HttpSession.putValue(String name, Object value)
This method binds the specified object value under the specified name. Any existing binding with the same name is replaced. To retrieve an object from a session, use getValue():
public Object HttpSession.getValue(String name)
This methods returns the object bound under the specified name or null if there is no binding. You can also get the names of all of the objects bound to a session with getValueNames():
public String[] HttpSession.getValueNames()
This method returns an array that contains the names of all objects bound to this session or an empty (zero length) array if there are no bindings. Finally, you can remove an object from a session with removeValue():
public void HttpSession.removeValue(String name)
This method removes the object bound to the specified name or does nothing if there is no binding. Each of these methods can throw a java.lang.IllegalStateException if the session being accessed is invalid (we'll discuss invalid sessions in an upcoming section).
Example 7-4 shows a simple servlet that uses session tracking to count the number of times a client has accessed it, as shown in Figure 7-2. The servlet also displays all the bindings for the current session, just because it can.
import java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class SessionTracker extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.setContentType("text/html"); PrintWriter out = res.getWriter(); // Get the current session object, create one if necessary HttpSession session = req.getSession(true); // Increment the hit count for this page. The value is saved // in this client's session under the name "tracker.count". Integer count = (Integer)session.getValue("tracker.count"); if (count == null) count = new Integer(1); else count = new Integer(count.intValue() + 1); session.putValue("tracker.count", count); out.println("<HTML><HEAD><TITLE>SessionTracker</TITLE></HEAD>"); out.println("<BODY><H1>Session Tracking Demo</H1>"); // Display the hit count for this page out.println("You've visited this page " + count + ((count.intValue() == 1) ? " time." : " times.")); out.println("<P>"); out.println("<H2>Here is your session data:</H2>"); String[] names = session.getValueNames(); for (int i = 0; i < names.length; i++) { out.println(names[i] + ": " + session.getValue(names[i]) + "<BR>"); } out.println("</BODY></HTML>"); } }
This servlet first gets the HttpSession object associated with the current client. By passing true to getSession(), it asks for a session to be created if necessary. The servlet then gets the Integer object bound to the name "tracker.count". If there is no such object, the servlet starts a new count. Otherwise, it replaces the Integer with a new Integer whose value has been incremented by one. Finally, the servlet displays the current count and all the current name/value pairs in the session.
Sessions do not last forever. A session either expires automatically, after a set time of inactivity (for the Java Web Server the default is 30 minutes), or manually, when it is explicitly invalidated by a servlet. When a session expires (or is invalidated), the HttpSession object and the data values it contains are removed from the system.
Beware that any information saved in a user's session object is lost when the session is invalidated. If you need to retain information beyond that time, you should keep it in an external location (such as a database) and store a handle to the external data in the session object (or your own persistant cookie).
There are several methods involved in managing the session life cycle:
This method returns whether the session is new. A session is considered new if it has been created by the server but the client has not yet acknowledged joining the session. For example, if a server supports only cookie-based sessions and a client has completely disabled the use of cookies, calls to the getSession() method of HttpServletRequest always return new sessions.
This method causes the session to be immediately invalidated. All objects stored in the session are unbound.
This method returns the time at which the session was created, as a long value that represents the number of milliseconds since the epoch (midnight, January 1, 1970, GMT).
This method returns the time at which the client last sent a request associated with this session, as a long value that represents the number of milliseconds since the epoch.
Each of these methods can throw a java.lang.IllegalStateException if the session being accessed is invalid.
To demonstrate these methods, Example 7-5 shows a servlet that manually invalidates a session if it is more than a day old or has been inactive for more than an hour.
import java.io.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; public class ManualInvalidate extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.setContentType("text/html"); PrintWriter out = res.getWriter(); // Get the current session object, create one if necessary HttpSession session = req.getSession(true); // Invalidate the session if it's more than a day old or has been // inactive for more than an hour. if (!session.isNew()) { // skip new sessions Date dayAgo = new Date(System.currentTimeMillis() - 24*60*60*1000); Date hourAgo = new Date(System.currentTimeMillis() - 60*60*1000); Date created = new Date(session.getCreationTime()); Date accessed = new Date(session.getLastAccessedTime()); if (created.before(dayAgo) || accessed.before(hourAgo)) { session.invalidate(); session = req.getSession(true); // get a new session } } // Continue processing... } }
So, how does a web server implement session tracking? When a user first accesses the site, that user is assigned a new HttpSession object and a unique session ID. The session ID identifies the user and is used to match the user with the HttpSession object in subsequent requests. Behind the scenes, the session ID is usually saved on the client in a cookie or sent as part of a rewritten URL. Other implementations, such as using SSL (Secure Sockets Layer) sessions, are also possible.
A servlet can discover a session's ID with the getId() method:
public String HttpSession.getId()
This method returns the unique String identifier assigned to this session. For example, a Java Web Server ID might be something like HT04D1QAAAAABQDGPM5QAAA. The method throws an IllegalState-Exception if the session is invalid.
All valid sessions are grouped together in a HttpSessionContext object. Theoretically, a server may have multiple session contexts, although in practice most have just one. A reference to the server's HttpSessionContext is available via any session object's getSessionContext() method:
public HttpSessionContext HttpSession.getSessionContext()
This method returns the context in which the session is bound. It throws an IllegalStateException if the session is invalid.
Once you have an HttpSessionContext, it's possible to use it to examine all the currently valid sessions with the following two methods:
public Enumeration HttpSessionContext.getIds() public HttpSession HttpSessionContext.getSession(String sessionId)
The getIds() method returns an Enumeration that contains the session IDs for all the currently valid sessions in this context or an empty Enumeration if there are no valid sessions. getSession() returns the session associated with the given session ID. The session IDs returned by getIds() should be held as a server secret because any client with knowledge of another client's session ID can, with a forged cookie or URL, join the second client's session.
Example 7-6 demonstrates the use of these methods with a servlet that manually invalidates all the sessions on the server that are more than a day old or have been inactive more than an hour.
import java.io.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; public class ManualInvalidateScan extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.setContentType("text/plain"); PrintWriter out = res.getWriter(); // Get the current session object, create one if necessary HttpSession dummySession = req.getSession(true); // Use the session to get the session context HttpSessionContext context = dummySession.getSessionContext(); // Use the session context to get a list of session IDs Enumeration ids = context.getIds(); // Iterate over the session IDs checking for stale sessions while (ids.hasMoreElements()) { String id = (String)ids.nextElement(); out.println("Checking " + id + "..."); HttpSession session = context.getSession(id); // Invalidate the session if it's more than a day old or has been // inactive for more than an hour. Date dayAgo = new Date(System.currentTimeMillis() - 24*60*60*1000); Date hourAgo = new Date(System.currentTimeMillis() - 60*60*1000); Date created = new Date(session.getCreationTime()); Date accessed = new Date(session.getLastAccessedTime()); if (created.before(dayAgo)) { out.println("More than a day old, invalidated!"); session.invalidate(); } else if (accessed.before(hourAgo)) { out.println("More than an hour inactive, invalidated!"); session.invalidate(); } else { out.println("Still valid."); } out.println(); } } }
A servlet that manually invalidates sessions according to arbitrary rules is useful on servers with limited session expiration capabilities.
Every server that supports servlets should implement at least cookie-based session tracking, where the session ID is saved on the client in a persistent cookie. Many web servers also support session tracking based on URL rewriting, as a fallback for browsers that don't accept cookies. This requires additional help from servlets.
For a servlet to support session tracking via URL rewriting, it has to rewrite every local URL before sending it to the client. The Servlet API provides two methods to perform this encoding:
This method encodes (rewrites) the specified URL to include the session ID and returns the new URL, or, if encoding is not needed or not supported, it leaves the URL unchanged. The rules used to decide when and how to encode a URL are server-specific. All URLs emitted by a servlet should be run through this method.
This method encodes (rewrites) the specified URL to include the session ID and returns the new URL, or, if encoding is not needed or not supported, it leaves the URL unchanged. The rules used to decide when and how to encode a URL are server-specific. This method may use different rules than encodeUrl(). All URLs passed to the sendRedirect() method of HttpServletResponse should be run through this method.
Note that encodeUrl() and encodeRedirectedUrl() employ a different capitalization scheme than getRequestURL() and getRequestURI(). The following code snippet shows a servlet writing a link to itself that is encoded to contain the current session ID:
out.println("Click <A HREF=\"" + res.encodeUrl(req.getRequestURI()) + "\">here</A>"); out.println("to reload this page.");
On servers that don't support URL rewriting or have URL rewriting turned off, the resulting URL remains unchanged. Now here's a code snippet that shows a servlet redirecting the user to a URL encoded to contain the session ID:
res.sendRedirect(res.encodeRedirectUrl("/servlet/NewServlet"));
On servers that don't support URL rewriting or have URL rewriting turned off, the resulting URL remains unchanged.
A servlet can detect whether the session ID used to identify the current HttpSession object came from a cookie or from an encoded URL using the isRequestedSessionIdFromCookie() and isRequestedSessionIdFromUrl() methods:
public boolean HttpServletRequest.isRequestedSessionIdFromCookie() public boolean HttpServletRequest.isRequestedSessionIdFromUrl()
Determining if the session ID came from another source, such as an SSL session, is not currently possible.
A requested session ID may not match the ID of the session returned by the getSession() method, such as when the session ID is invalid. A servlet can determine whether a requested session ID is valid using isRequestedSession- IdValid() :
public boolean HttpServletRequest.isRequestedSessionIdValid()
The SessionSnoop servlet shown in Example 7-7 uses most of the methods discussed thus far in the chapter to snoop information about the current session and other sessions on the server. Figure 7-3 shows a sample of its output.
import java.io.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; public class SessionSnoop extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.setContentType("text/html"); PrintWriter out = res.getWriter(); // Get the current session object, create one if necessary HttpSession session = req.getSession(true); // Increment the hit count for this page. The value is saved // in this client's session under the name "snoop.count". Integer count = (Integer)session.getValue("snoop.count"); if (count == null) count = new Integer(1); else count = new Integer(count.intValue() + 1); session.putValue("snoop.count", count); out.println("<HTML><HEAD><TITLE>SessionSnoop</TITLE></HEAD>"); out.println("<BODY><H1>Session Snoop</H1>"); // Display the hit count for this page out.println("You've visited this page " + count + ((count.intValue() == 1) ? " time." : " times.")); out.println("<P>"); out.println("<H3>Here is your saved session data:</H3>"); String[] names = session.getValueNames(); for (int i = 0; i < names.length; i++) { out.println(names[i] + ": " + session.getValue(names[i]) + "<BR>"); } out.println("<H3>Here are some vital stats on your session:</H3>"); out.println("Session id: " + session.getId() + "<BR>"); out.println("New session: " + session.isNew() + "<BR>"); out.println("Creation time: " + session.getCreationTime()); out.println("<I>(" + new Date(session.getCreationTime()) + ")</I><BR>"); out.println("Last access time: " + session.getLastAccessedTime()); out.println("<I>(" + new Date(session.getLastAccessedTime()) + ")</I><BR>"); out.println("Requested session ID from cookie: " + req.isRequestedSessionIdFromCookie() + "<BR>"); out.println("Requested session ID from URL: " + req.isRequestedSessionIdFromUrl() + "<BR>"); out.println("Requested session ID valid: " + req.isRequestedSessionIdValid() + "<BR>"); out.println("<H3>Here are all the current session IDs"); out.println("and the times they've hit this page:</H3>"); HttpSessionContext context = session.getSessionContext(); Enumeration ids = context.getIds(); while (ids.hasMoreElements()) { String id = (String)ids.nextElement(); out.println(id + ": "); HttpSession foreignSession = context.getSession(id); Integer foreignCount = (Integer)foreignSession.getValue("snoop.count"); if (foreignCount == null) out.println(0); else out.println(foreignCount.toString()); out.println("<BR>"); } out.println("<H3>Test URL Rewriting</H3>"); out.println("Click <A HREF=\"" + res.encodeUrl(req.getRequestURI()) + "\">here</A>"); out.println("to test that session tracking works via URL"); out.println("rewriting even when cookies aren't supported."); out.println("</BODY></HTML>"); } }
This servlet begins with the same code as the SessionTracker servlet shown in Example 7-4. Then it continues on to display the current session's ID, whether it is a new session, the session's creation time, and the session's last access time. Next the servlet displays whether the requested session ID (if there is one) came from a cookie or a URL and whether the requested ID is valid. Then the servlet iterates over all the currently valid session IDs, displaying the number of times they have visited this page. Finally, the servlet prints an encoded URL that can be used to reload this page to test that URL rewriting works even when cookies aren't supported.
Note that installing this servlet is a security risk, as it exposes the server's session IDs--these may be used by unscrupulous clients to join other clients' sessions. The SessionServlet that is installed by default with the Java Web Server 1.1.x has similar behavior.
Some objects may wish to perform an action when they are bound or unbound from a session. For example, a database connection may begin a transaction when bound to a session and end the transaction when unbound. Any object that implements the javax.servlet.http.HttpSessionBindingListener interface is notified when it is bound or unbound from a session. The interface declares two methods, valueBound() and valueUnbound(), that must be implemented:
public void HttpSessionBindingListener.valueBound( HttpSessionBindingEvent event) public void HttpSessionBindingListener.valueUnbound( HttpSessionBindingEvent event)
The valueBound() method is called when the listener is bound into a session, and valueUnbound() is called when the listener is unbound from a session.
The javax.servlet.http.HttpSessionBindingEvent argument provides access to the name under which the object is being bound (or unbound) with the getName() method:
public String HttpSessionBindingEvent.getName()
The HttpSessionBindingEvent object also provides access to the HttpSession object to which the listener is being bound (or unbound) with getSession() :
public HttpSession HttpSessionBindingEvent.getSession()
Example 7-8 demonstrates the use of HttpSessionBindingListener and HttpSessionBindingEvent with a listener that logs when it is bound and unbound from a session.
import java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class SessionBindings extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.setContentType("text/plain"); PrintWriter out = res.getWriter(); // Get the current session object, create one if necessary HttpSession session = req.getSession(true); // Add a CustomBindingListener session.putValue("bindings.listener", new CustomBindingListener(getServletContext())); out.println("This page intentionally left blank"); } } class CustomBindingListener implements HttpSessionBindingListener { // Save a ServletContext to be used for its log() method ServletContext context; public CustomBindingListener(ServletContext context) { this.context = context; } public void valueBound(HttpSessionBindingEvent event) { context.log("BOUND as " + event.getName() + " to " + event.getSession().getId()); } public void valueUnbound(HttpSessionBindingEvent event) { context.log("UNBOUND as " + event.getName() + " from " + event.getSession().getId()); } }
Each time a CustomBindingListener object is bound to a session, its valueBound() method is called and the event is logged. Each time it is unbound from a session, its valueUnbound() method is called so that event too is logged. We can observe the sequence of events by looking at the server's event log.
Let's assume that this servlet is called once, reloaded 30 seconds later, and not called again for at least a half hour. The event log would look something like this:
[Tue Jan 27 01:46:48 PST 1998] BOUND as bindings.listener to INWBUJIAAAAAHQDGPM5QAAA [Tue Jan 27 01:47:18 PST 1998] UNBOUND as bindings.listener from INWBUJIAAAAAHQDGPM5QAAA [Tue Jan 27 01:47:18 PST 1998] BOUND as bindings.listener to INWBUJIAAAAAHQDGPM5QAAA [Tue Jan 27 02:17:18 PST 1998] UNBOUND as bindings.listener from INWBUJIAAAAAHQDGPM5QAAA
The first entry occurs during the first page request, when the listener is bound to the new session. The second and third entries occur during the reload, as the listener is unbound and rebound during the same putValue() call. The fourth entry occurs a half hour later, when the session expires and is invalidated.
Let's end this chapter with a look at how remarkably simple our shopping cart viewer servlet becomes when we use session tracking. Example 7-9 shows the viewer saving each of the cart's items in the user's session under the name "cart.items".
import java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class ShoppingCartViewerSession extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.setContentType("text/html"); PrintWriter out = res.getWriter(); // Get the current session object, create one if necessary. HttpSession session = req.getSession(true); // Cart items are maintained in the session object. String[] items = (String[])session.getValue("cart.items"); out.println("<HTML><HEAD><TITLE>SessionTracker</TITLE></HEAD>"); out.println("<BODY><H1>Session Tracking Demo</H1>"); // Print the current cart items. out.println("You currently have the following items in your cart:<BR>"); if (items == null) { out.println("<B>None</B>"); } else { out.println("<UL>"); for (int i = 0; i < items.length; i++) { out.println("<LI>" + items[i]); } out.println("</UL>"); } // Ask if they want to add more items or check out. out.println("<FORM ACTION=\"/servlet/ShoppingCart\" METHOD=POST>"); out.println("Would you like to<BR>"); out.println("<INPUT TYPE=submit VALUE=\" Add More Items \">"); out.println("<INPUT TYPE=submit VALUE=\" Check Out \">"); out.println("</FORM>"); // Offer a help page. Encode it as necessary. out.println("For help, click <A HREF=\"" + res.encodeUrl("/servlet/Help?topic=ShoppingCartViewer") + "\">here</A>"); out.println("</BODY></HTML>"); } }
Copyright © 2001 O'Reilly & Associates. All rights reserved.