Session Management Using the Servlet API

 < Free Open Study > 



In the previous section we discussed three examples, each of which demonstrates how to track user sessions programmatically, using URL rewriting, cookies, and hidden form fields respectively. Each of these techniques required some unique string to be exchanged between the client and the server, so that the server could recognize the client. However, as we can infer from these examples, there are complications with all three approaches:

  • Creating a unique ID programmatically is not reliable, particularly in a multi-threaded environment. For instance, what prevents two users from getting the same random number? This issue gets more complicated when you consider clustered environments where the ID is required to be unique across several JVMs (we'll discuss clustering later in this chapter). We need a more robust and reliable approach for this.

  • Programmatically retrieving an ID (either from the request parameters, or from cookies) is an additional task that each of your servlets must implement, which lowers the maintainability of the code and makes it more prone to errors.

  • This approach requires additional coding if we need to manage state with each session.

As far as servlet programming is concerned, these additional tasks make servlet programming more complex. The Java Servlet API, however, has provisions that eliminate the need to implement these tasks. In this section, we will look at the session handling interfaces of the Java Servlet API. The Servlet API provides the following facilities for managing sessions:

  • Management of session lifecycle, including session creation and termination

  • Management of session state

The Java Servlet API includes the following interfaces and classes for session management in the javax.servlet.http package:

Interface

Description

HttpSession

Provides an abstraction of a session

HttpSessionListener

Handles events associated with session creation and termination (lifecycle events)

HttpSessionBindingListener

Handles events associated with binding and unbinding state for sessions

HttpSessionActivationListener

Handles events associated with session activation and passivation (these processes will be explained later)

HttpSessionEvent

Encapsulates lifecycle-specific session events

HttpSessionBindingEvent

Encapsulates session binding/unbinding events

Let's begin our discussion of these interfaces by taking a closer look at the HttpSession interface.

The HttpSession Interface

The HttpSession interface provides core session-management functionality. This interface abstracts a session. The container creates a session when a client first visits a server. Conceptually an HttpSession object is an object that lives for the duration of a client session, and is associated with the request objects.

J2EE web containers use three mechanisms to establish sessions:

  • Cookies

    By default, most containers rely on cookies to establish sessions.

  • URL rewriting

    Web containers support URL rewriting to accommodate clients that do not accept or support cookies. However, in order for web applications to work correctly with URL rewriting, we need to take an extra coding step by calling the encodeURL() method with each URL, and using the return value in the content (instead of the original URL).

  • SSL based sessions

    The Secure Socket Layer (SSL) protocol is used for secure HTTP (HTTPS).

This interface also acts as a placeholder for storing data associated with the current session. Using this functionality, you can place client-specific objects in the session. This interface provides java.util.Map-like functionality to put and get objects. The java.util.Map interface can store objects, with each object linked to a name (which itself can be another object). You can put an object into the map with a name, and retrieve the object using the same name. The HttpSession interface provides similar methods to manage state for each session. In this interface, each object (called an attribute) you add to the session should have a unique name (as a string). Servlets used during the course of a session may share these objects.

Let's now look at the methods provided by this interface. The first question is, how do we get an instance of this interface? The HttpServletRequest interface has the following methods to get instances of HttpSession:

     public HttpSession getSession() 

This method returns the session already associated with this request. However, if there is no session currently associated the request, supplying true to the following variant creates a new Session object and returns it:

     public HttpSession getSession(boolean create) 

If the argument is false, and there is no session currently associated with this request, this method returns null.

The following method returns a named attribute from the session:

     public Object getAttribute(String name) 

The getAttributeNames() method returns an Enumeration of names of all attributes placed into a session:

     public java.util.Enumeration getAttributeNames() 

The web container creates a session when a client first accesses the container. The getCreationTime() method returns the time (in milliseconds since midnight January 1, 1970 GMT) at which the session was created. As we shall see later, you can also recreate sessions programmatically:

     public long getCreationTime() 

The getId() method returns a unique identifier assigned to this session. This unique identifier is similar to the token we created in previous examples. Web containers use elaborate algorithms to create such identifiers so that the identifiers are unique even under concurrent requests:

     public String getId() 

The getLastAccessedTime() method returns the time the client last sent a request associated with this session. The time is expressed as milliseconds since midnight January 1, 1970 GMT:

     public long getLastAccessedTime() 

The following method returns the time the client last sent a request associated with this session. Again, the time is expressed as milliseconds since midnight January 1, 1970 GMT:

     public int getMaxInactiveInterval() 

The getServletContext() method, unsurprisingly, returns the ServletContext associated with the application that this session belongs to:

     public ServletContext getServletContext() 

The invalidate() method invalidates this session and removes any attributes bound to it:

     public void invalidate() 

The isNew() method returns true if the client does not yet know about this session or if the client chooses not to join the session:

     public boolean isNew() 

The removeAttribute() method removes the named attribute from the session:

     public void removeAttribute(String name) 

The following method adds the named attribute to the session. If the named attribute already exists in the session, the method replaces the old attribute value with the new value:

     public void setAttribute(String name, Object value) 

This method sets the maximum allowed time (in seconds) between two consecutive client requests to participate in a session. After expiry of this interval, the container invalidates the session. If a client request arrives after this interval, the request results in a new session:

     public void setMaxInactiveInterval (int interval) 

Let's now study the programming aspects of the session management API via a comprehensive example.

Implementing Session Management

The purpose of this example is to build a web application that allows users to create notes and store them on the server. Each note has a title and associated text, both stored in a database. This example will allow the user to:

  • View the list of notes previously created (using the user's e-mail address to retrieve the list of notes)

  • Create new notes

  • Edit existing notes

  • Change identity and create notes as a different user (identified by another e-mail address)

The following figure shows the flow of events and how we plan to implement the above functionality:

click to expand

In this figure, the normal arrows indicate the actions by the user, and the dotted arrows indicate the container responses. There are three components in this application:

  • index.html: This is a welcome page that collects the user's e-mail address.

  • ListNotesServlet: This servlet is responsible for obtaining the list of e-mails from the session. If the session is new, this servlet gets the list of e-mails from the database. This servlet then displays a page containing the list of notes (if any, with hyperlinks), a link to add a note, and another link to change the user. The arrows from this servlet show the possible user actions.

  • EditNoteServlet: This servlet performs two tasks. If the user clicks to edit a note, this servlet retrieves the note from the database and displays it in a form. If the user clicks to add a new note instead, this servlet displays an empty form. When the user submits this form, this servlet stores the note in the database, updates the list of notes in the session, and then forwards the request to the ListNotesServlet. The ListNotesServlet then displays the list of notes.

Therefore, this example relies on the session management API for the following tasks:

  • To track a user entering notes

  • To manage a list of notes

  • To invalidate current sessions and create new sessions

We'll start by creating the database schema.

Database Schema

We will use the MySQL RDBMS for managing the notes persistently. This RDBMS is available to download for free from http://www.mysql.com. To use this with your webapps, you will also need to download the JDBC driver mm.mysql-2.0.8-bin.jar. The database schema is simple - each note is associated with a note_id and an email address. The actual note data consists of a note_title, note, and the last_modified time.

Use the following SQL to create the database schema:

     CREATE DATABASE notepad;     USE notepad;     CREATE TABLE NOTES (       note_id INT AUTO_INCREMENT PRIMARY KEY,       email CHAR(50) NOT NULL,       note_title CHAR(80) NOT NULL,       note LONG VARCHAR,       last_modified TIMESTAMP NOT NULL     ); 

This SQL creates a table called NOTES with the note_id as the primary key. We specify this column as an AUTO_INCREMENT key such that each insert will automatically increment the number. In the above SQL script, we also create two indexes on the email and note_id columns. These are the columns we use to query for NOTES' data.

You should note that this schema is not exhaustive enough for use in a general production environment. At the end of this example, we will consider possible enhancements that could make this example more robust and usable.

Welcome Page

Let us now create the welcome page (index.html) for the notepad web application:

     <html>       <head>         <title>NotePad</title>         <link rel="stylesheet" type="text/css" href="style/global.css" />       </head>       <body>         <h3>NotePad</h3>         <form action="/notepad/list" method="GET">          <p>            Enter your email address:            <input type="text" name="email" />          </p>          <p>            <input type="submit" name="submit" value="Enter NotePad">          </p>         </form>       </body>     </html> 

This HTML displays a form for entering the e-mail address of the user. When the user enters the e-mail address, the form sends a GET request to the /notepad/list servlet.

NotePadServlet

This servlet is abstract, and does not directly handle HTTP requests. The purpose of this servlet is to provide the functionality to obtain database connections. You should note that in a production environment, we would probably want to connect to our database through JNDI, but to keep things simple, for this example we will connect to the database directly:

     import java.sql.*;     import javax.servlet.http.HttpServlet;     public abstract class NotePadServlet extends HttpServlet { 

The getConnection() method returns a database connection:

       protected Connection getConnection() throws NamingException, SQLException {         Connection connection = null;         try {           Class.forName("org.gjt.mm.mysql.Driver").newInstance();           connection = DriverManager.getConnection("jdbc:mysql://localhost/notepad");         } catch(ClassNotFoundException cnfe) {           throw new SQLException("Unable to load the driver: " + cnfe.getMessage());         }         return connection;       }     } 

ListNotesServlet

Let us now create the servlet to retrieve the notes and display them. This servlet extends from NotePadServlet:

     package sessions;     import java.util.*;     import java.io.*;     import java.sql.*;     import javax.naming.NamingException;     import javax.servlet.ServletException;     import javax.servlet.http.*;     public class ListNotesServlet extends NotePadServlet {       protected void doGet(HttpServletRequest request, HttpServletResponse response)                                               throws ServletException, IOException { 

This servlet retrieves the parameter email from the incoming request, and uses it to get a list of notes for that user. Note that the form in the welcome page contains an e-mail text field:

         String email = request.getParameter("email"); 

The servlet then gets the current user session by calling the getSession() method on the Request object. When the user visits this notepad application for the first time, the container automatically creates a new session for the user. The getSession() method returns the same session to the servlet. When called for the first time, the session will be empty; it does not contain any attributes since we've not yet added any:

         HttpSession session = request.getSession(); 

This servlet then checks to see if there is an attribute called email in the session. As you will see shortly, this web application offers functionality to change the identity of the user. Say the user revisits the index.html page and enters a new e-mail address to see notes stored under a different e-mail. In this case, the session would contain the old email attribute. The following code checks for this, and if the email is different from that retrieved from the request, it invalidates the current session, and obtains a new session. This step also ensures that a user with a given e-mail address cannot view the notes of another user with a different e-mail address. As we shall discuss later, we could also implement more robust security measures for this servlet:

         String currentEmail = (String) session.getAttribute("email");         if(currentEmail != null) {           if(!currentEmail.equals(email)) {             session.invalidate();             session = request.getSession();           }         } 

The servlet then puts the e-mail address in the session and checks if the list of notes is already available in the session. In this example, the list of notes is stored in a java.util.Map object. This object contains the note_id as the key, and the note_title as the value. The actual note text is retrieved from the database only on demand. This step ensures that we store only the essential information in the HttpSession object, for performance reasons. As we store more information in the session, the underlying JVM will have to allocate more and more memory, and as the number of concurrent users increases, it may limit the available memory for any processing. It is always a good practice to limit the number and size of attributes in the session. We should also remember to remove an attribute from the session if it is no longer required for this session:

         session.setAttribute("email", email);         Map noteList = (Map) session.getAttribute("noteList");         Connection connection = null;         PreparedStatement statement = null;         if(noteList == null) { 

When the user invokes this servlet for the first time, the noteList will be empty. The servlet then proceeds to retrieve the notes (only note_id and note_title) from the database, and fill this map, as shown below:

           try {             String sql =               "SELECT note_id, note_title FROM NOTES WHERE email = ?";             connection = getConnection();             statement = connection.prepareStatement(sql);             statement.setString(1, email);             ResultSet rs = statement.executeQuery();             noteList = new HashMap();             while(rs.next()) {               noteList.put(new Integer(rs.getInt(1)), rs.getString(2));             }           } catch(SQLException sqle) {             throw new ServletException("SQL Exception", sqle);           } finally {             try {               if(statement != null) {                 statement.close();               }             } catch(SQLException ignore) {}             try {               if(connection != null) {                 connection.close();               }             } catch(SQLException ignore) {}           } 

This code snippet obtains a connection and executes a SQL SELECT statement using the email. It then initializes the noteList map and adds each note_title found into the noteList map using the note_id (converted to an Integer object) as the key. It then adds the noteList to the session under the name noteList as shown below:

           session.setAttribute("noteList", noteList);         } 

The rest of the doGet() method prepares a response. The response includes the list of note titles. Each note is associated with a hyperlink to edit the note. The response also includes a link to add a new note, and another link to change the user. The former link takes the user to the EditNoteServlet, while the latter link takes the user back to the welcome page:

         response.setContentType("text/html");         PrintWriter writer = response.getWriter();         writer.println("<html><head>");         writer.println("<title>NotePad</title>");         writer.println(           "<link rel=\"stylesheet\" type=\"text/css\" href=\"style/global.css\" />");         writer.println("</head><body>");         writer.println("<h3>Notes</h3>");         if(noteList.size() == 0) {           writer.println("<p>You do not have any notes.</p>");         } else {           writer.println("<p>Click on the note to edit.</p><ul>");           Iterator iterator = noteList.keySet().iterator();           while(iterator.hasNext()) {             Integer noteId = (Integer) iterator.next();             String noteTitle = (String) noteList.get(noteId);             writer.println("<li><a href='/notepad/edit?notebackground-color:d9d9d9">noteId.toString() + "'>" + noteTitle + "</a></li>");           }           writer.println("</ul>");         }         writer.println("<p><a href='/notepad/edit'>Add a New Note</a></p>");         writer.println("<p><a href='/notepad/'>Change User</a></p>");         writer.println("</body></html>");         writer.close();       } 

Finally, this servlet also implements a doPost() method. As we shall see later, this method is required for the EditNoteServlet to forward to this servlet after saving a note in the database:

       protected void doPost(HttpServletRequest request, HttpServletResponse response)         throws ServletException, IOException {         doGet(request, response);       }     } 

EditNoteServlet

This servlet has two parts - a doGet() method to display a form with the note title and note text (empty for new notes), and a doPost() method to store/update the note in the database. The servlet also extends from the NotePadServlet. This servlet is rather long, as it implements two core features. First, given the note_id, it retrieves a note from the database and displays it in a form, and second it stores the note into the database. Let us now study the implementation more closely:

     package sessions;     import java.util.Map;     import java.io.*;     import java.sql.*;     import javax.naming.NamingException;     import javax.servlet.*;     import javax.servlet.http.*;     public class EditNoteServlet extends NotePadServlet { 

The doGet() method of this servlet is responsible for retrieving a note from the database. The same method is also used to display an empty form to enter a new note:

       protected void doGet(HttpServletRequest request, HttpServletResponse response)         throws ServletException, IOException { 

This method first checks whether there is a noteId in the request. If you examine the ListNotesServlet, the link to add a new note does not include a noteId in the URL, but the links to existing notes include the noteId as a query parameter. This parameter helps the EditNoteServlet to determine if the request is for a new note or for an existing note:

         String noteId = (String) request.getParameter("noteId");         String note = "";         String title = "";         boolean isEdit = false;         Connection connection = null;         PreparedStatement statement = null;         if(noteId != null) {           try { 

If there is no noteId in the request, the servlet generates an HTML form. Otherwise, this servlet retrieves the note from the database:

             String sql = "SELECT note_title, note FROM NOTES WHERE note_id = ?";             connection = getConnection();             statement = connection.prepareStatement(sql);             statement.setInt(1, Integer.parseInt(noteId));             ResultSet rs = statement.executeQuery();             rs.next();             title = rs.getString(1);             note = rs.getString(2);             isEdit = true;           } catch(SQLException sqle) {             throw new ServletException("SQL Exception", sqle);           } finally {             try {               if(statement != null) {                 statement.close();               }             } catch(SQLException ignore) {}             try {               if(connection != null) {                 connection.close();               }             } catch(SQLException ignore) {}           }         } 

The above code retrieves the note title and the note text using the note_id. It then stores the results in the local variables title and note respectively.

The following code generates the form containing the note. In the case of a new note, the variables title and note will be empty:

         response.setContentType("text/html");         PrintWriter writer = response.getWriter();         writer.println("<html><head>");         writer.println("<title>NotePad</title>");         writer.println(           "<link rel=\"stylesheet\" type=\"text/css\" href=\"style/global.css\" />");         writer.println("</head><body>");         writer.println("<h3>Notes</h3>");         writer.println("<h1>Add/Edit a Note</h1>");         if(isEdit) {           writer.println("<form action='/notepad/edit?notebackground-color:d9d9d9">"' method='POST'>");         } else {           writer.println("<form action='/notepad/edit' method='POST'>");         }         writer.println("<p>Title: <input type='text' name='title' size='40' value='" +                        title + "'></p>");         writer.println("<p><textarea name='note' cols='50' rows='15'>");         writer.println(note);         writer.println("</textarea></p>");         writer.println("<p><input type='Submit' name='submit'                        value='Save Note'></p>");         writer.println("</form></body></html>");         writer.close();       }       protected void doPost(HttpServletRequest request, HttpServletResponse response)         throws ServletException, IOException { 

The form makes a POST request for saving the note. When the user enters the note and clicks on the Save button, the doPost() method of this servlet gets invoked. The responsibility of the doPost() method is to store the note into the database. Before saving the note, this method retrieves all the necessary information from the Request and Session objects:

         String noteId = (String) request.getParameter("noteId");         HttpSession session = request.getSession();         String email = (String) session.getAttribute("email");         Map noteList = (Map) session.getAttribute("noteList");         String title = request.getParameter("title");         String note = request.getParameter("note");         Connection connection = null;         PreparedStatement statement = null;         try { 

This servlet uses the noteId parameter in the request to determine if this is a new note or an existing note. If the note is new, it inserts the note in the database:

           if(noteId == null) {             String sql = "INSERT INTO NOTES (email, note_title, note, last_modified)"                          + "VALUES(?, ?, ?, ?)";             connection = getConnection();             statement = connection.prepareStatement(sql);             statement.setString(1, email);             statement.setString(2, title);             statement.setString(3, note);             statement.setTimestamp(4, new Timestamp(System.currentTimeMillis()));             statement.executeUpdate();             // Retrieve the automatically inserted NOTE_ID             sql = "SELECT LAST_INSERT_ID()";             statement = connection.prepareStatement(sql);             ResultSet rs = statement.executeQuery(sql);             int id = 0;             while(rs.next ()) {               id = rs.getInt(1);             }             noteList.put(new Integer(id), title);           } else { 

The note_id is retrieved from the database after executing the INSERT statement. The note_id column is a column automatically incremented by the database whenever a new note is inserted. We therefore must retrieve the inserted value from the database.

If the note already exists, the doPost() method simply updates it:

             String sql = "UPDATE NOTES SET note_title = ?, note = ?, " +                          "last_modified = ? WHERE note_id = ?";             connection = getConnection();             statement = connection.prepareStatement(sql);             statement.setString(1, title);             statement.setString(2, note);             statement.setTimestamp(3, new Timestamp(System.currentTimeMillis()));             statement.setInt(4, Integer.parseInt(noteId));             statement.executeUpdate();             noteList.put(new Integer(noteId), title);           }         } catch(SQLException sqle) {           throw new ServletException("SQL Exception", sqle);         } finally {           try {             if(statement != null) {               statement.close();             }           } catch(SQLException ignore) {}           try {             if(connection != null) {               connection.close();             }           } catch(SQLException ignore) {}         } 

Whether we created or updated a note, this method puts the note_id and note_title in the noteList map, so that the ListNotesServlet can display the updated list of notes. After inserting/updating the note, the servlet forwards the request to the ListNotesServlet to display the updated list of notes:

         RequestDispatcher rd = request.getRequestDispatcher("/list?email=" + email);         rd.forward(request, response);       }     } 

Since the call to the forward() method is performed via the doPost() method, the ListNotesServlet should implement the doPost() method. This is why we implemented the doPost() method in the ListNotesServlet.

This completes the coding required for this web application. The next tasks are to write a deployment descriptor, and configure Tomcat to deploy the servlets.

Deployment Descriptor

Here's the deployment descriptor for our notepad application:

     <?xml version="1.0"?>     <!DOCTYPE web-app         PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"         "http://java.sun.com/dtd/web-app_2_3.dtd">     <web-app>       <servlet>         <servlet-name>listNotes</servlet-name>         <servlet-class>ListNotesServlet</servlet-class>       </servlet>       <servlet>         <servlet-name>editNote</servlet-name>         <servlet-class>EditNoteServlet</servlet-class>       </servlet>       <servlet-mapping>         <servlet-name>listNotes</servlet-name>         <url-pattern>/list/*</url-pattern>       </servlet-mapping>       <servlet-mapping>         <servlet-name>editNote</servlet-name>         <url-pattern>/edit/*</url-pattern>       </servlet-mapping>     </web-app> 

Compiling and Running the Example

The final step is to compile and run the example. You should create a web application called notepad, with following structure:

     notepad/             WEB-INF/                     classes/                             sessions                     src/                         sessions                     lib/ 

Place the welcome page in the top level of the web application, and class source files in the src/sessions directory. Add the web.xml file into the top level of the WEB-INF directory as usual. Next, place the mm.mysql-2.0.8-bin.jar file in the lib directory. Then compile the classes and move the class files into the classes/sessions directory.

Restart Tomcat, and navigate to http://localhost:8080/notepad. If you do this, you should see the welcome page:

click to expand

Enter an e-mail address and press the Enter NotePad button. This invokes the ListNotesServlet, retrieves notes (if any), and displays the following page:

click to expand

Since this is the first time the user entered this web application, there are no notes for this user, and hence the page contains only links to add a new note and to change user. Click on the Add a New Note link. This causes the EditNoteServlet to display a blank form so that we can enter a new note:

click to expand

Enter any note with a title and some text, and press the Save Note button. This invokes the doPost() method of the EditNoteServlet, that stores the note in the database and updates the noteList with the new noteId and note title. This servlet then forwards the user to the ListNotesServlet that displays the updated note list:

click to expand

You can continue to add more notes. You may instead change your mind, and enter this application with a new e-mail address. Click on the Change User link to do so, which will then invalidate your current session, obtain a new session, and then display the index.html page to allow you enter a new e-mail address.

NotePad with URL Rewriting

To test if the above NotePad functions without cookies, let's disable cookies in our browser. Check the browser help to get directions to disable all cookies, and restart the browser. As you try entering the web site and continuing to edit a previously created note, you will encounter a NullPointerException displayed in the browser.

What causes this exception? If you look at the source of EditNoteServlet, you will find that the exception was caused due to the variable nodeList being null. The servlet obtains this variable from the session. However, since the browser does not now accept or return cookies, the web container is unable to create and maintain a session. In this case, every time a servlet calls the getSession() method on the Request object, the container returns a new HttpSession object. In our case, the ListNotesServlet creates the list of notes and puts it in the session. However, since the EditNoteServlet is getting a new Session object, there is no way for the EditNoteServlet to retrieve the list of notes.

The Java Servlet API supports URL rewriting to deal with such cases. To enable this, we need to change the two servlets as highlighted below. First, here are the changes we need to make to ListNotesServlet:

     ...           } else {             writer.println("<p>Click on the note to edit.</p><ul>");             Iterator iterator = noteList.keySet().iterator();             while(iterator.hasNext()) {             Integer noteId = (Integer) iterator.next();             String noteTitle = (String) noteList.get(noteId);             // Rewrite the URL to the note             String url = response.encodeURL("/notepad/edit?notebackground-color:d9d9d9">noteId.toString());             writer.println("<li><a href='" + url + "'>" +                            noteTitle + "</a></li>");           }           writer.println("</ul>");         }         // Also rewrite the URL for adding a new note         String url = response.encodeURL("/notepad/edit");         writer.println("<p><a href='" + url + "'>Add a New Note</a></p>");         writer.println("<p><a href='/notepad/'>Change User</a></p>");         writer.println("</body></html>");         writer.close();       }     ...     } 

As you can see, there are two changes. In each of these changes we call the encodeURL() method on the request to "encode" the URL that is being generated. The purpose of this call is to rewrite the URL to include an ID corresponding to the session. Recall from the first example in this chapter that we programmatically rewrote URLs to include randomly generated IDs. The encodeURL() method does a similar task by including a unique ID associated with the current session in the link.

In the original servlet, the URLs it generated were of the form /notepad/edit?noteId=x where x is the note_id. After our modifications, the call to the encodeURL() method appends a token similar to the one we discussed in the first example in this chapter. This method returns a string similar to:

     /notepad/edit;jsessionid=C61461E2A200C573A102317140948083?noteId=x 

Note the extra parameter in this URL. The jsessionid is the token the web container uses instead of cookies. The long string following the jsessionid is a session identifier. Note that jsessionid is not an ordinary query parameter; query parameters follow the URL with a question mark, while the jsessionid is separated by a colon. This way, the container ensures that the session identifier is not confused with the query parameters.

Now make similar changes to the EditNoteServlet:

     ...         response.setContentType("text/html");         PrintWriter writer = response.getWriter();         writer.println("<html><body>");         writer.println("<h1>Add/Edit a Note</h1>");         if(isEdit) {           // Encode the URL to edit the note           String url = response.encodeURL("/notepad/edit?notebackground-color:d9d9d9">writer.println("<form action='" + url + "' method='POST'>");         } else {           // Encode the URL to add the note           String url = response.encodeURL("/notepad/edit");           writer.println("<form action='" + url + "' method='POST'>");         }         writer.println("<p>Title: <input type='text' name='title' size='40' value='" +                        title + "'></p>");         writer.println("<p><textarea name='note' cols='50' rows='15'>");         writer.println(note);         writer.println("</textarea></p>");         writer.println(                      "<p><input type='Submit' name='submit' value='Save Note'></p>");         writer.println("</form></body></html>");         writer.close();       }       protected void doPost(HttpServletRequest request,                             HttpServletResponse response)                             throws ServletException, IOException {     ...         RequestDispatcher rd =           request.getRequestDispatcher(response.encodeURL("/list?email=" + email));         rd.forward(request, response);       }     } 

When you have made these changes, recompile your servlets, restart Tomcat, and test the application again. This time, you will find the application that the application functions properly, as it did when cookies were enabled.

It is always a good practice to encode all dynamically generated URLs as shown in the above servlets. Although most browsers support cookies, and most users don't disable cookies in their browsers, there are Internet-enabled devices (certain cell phones and hand-held devices) that do not recognize cookies.

As we discussed above, URL rewriting does not completely solve the problem of session handling in the absence of cookies, since you cannot encode URLs in static web pages. As you build web-enabled applications, you should always be sure to consider the impact of a lack of cookie support, and try to design the application appropriately.

Listening For Session Lifecycle Events

As the container creates sessions, and as you add, remove, or change attributes in sessions, the web container instantiates event objects so that web applications can be notified of these events. In this section, we will look at how we can make applications receive notifications upon changes to Session objects.

Except for the HttpSessionBindingEvent, all other events that we discuss in this section are new in version 2.3 of the Servlet specification. The following figure shows the session lifecycle with its associated event objects:

click to expand

This figure shows the lifecycle of a session from creation to destruction. In-between, servlets may add, remove or replace attributes within the session. During its lifecycle a session may be passivated and activated zero or more times. In this figure, the rectangles on the right are the listeners that handle various events. In addition to the events shown above, as attributes are added/replaced/removed the container fires attribute-related events as shown below:

click to expand

We will discuss these events and the associated listeners in more detail in this section.

Listening For Session Creation and Termination

As we discussed in the previous section, the container creates a session when the user accesses the server for the first time. The session is subsequently accessible to servlets in the application. But how long should the container keep the Session object alive? This question is important since no user is likely to use the application indefinitely. In order to conserve system resources, we need to instruct the container to terminate a session after some meaningful interval. You should consider the following while building and deploying a web application:

  • Session Lifetime

    Based on the nature of the application, you should identify a suitable lifetime for a session. You can specify this interval in the deployment descriptor of the web application. For instance, the following entry to the deployment descriptor of the notepad application sets the session lifetime for all users to 60 minutes. Add this element after <servlet-mapping> elements and before the <resource-ref> element. At the end of 60 minutes, the container automatically terminates the session.

     <session-config>   <session-timeout>60</session-timeout> </session-config> 

  • Session Inactivity

    In addition to the above, you can also instruct the container to terminate a session based on how long the session has been inactive. For instance, the user might leave the browser after using the notepad application for 30 minutes. You may want the container to wait for 15 more minutes before allowing the container to terminate the session. In order to do so, you should call the setMaxInactiveInterval() method on the Session object. To call this method, you should specify the interval in seconds.

The Java Servlet API includes an interface javax.servlet.http.HttpSessionListener that is notified when a session is created or destroyed. Let's take a closer look at it.

The HttpSessionListener Interface

This interface specifies methods to deal with session creation and session destruction.

     public void sessionCreated(HttpSessionEvent event) 

The container invokes the above method when a session is created. The event is specified via the javax.servlet.http.HttpSessionEvent class. As we shall see below, this class encapsulates the Session object.

     public void sessionDestroyed(HttpSessionEvent event) 

The container invokes the sessionDestroyed() method when a session is destroyed.

Let us now look at the HttpSessionEvent class. The HttpSessionEvent class has only one method:

     public HttpSession getSession() 

This method returns the Session object associated with this event.

You can implement the HttpSessionListener interface, and deploy your implementation, via the web application deployment descriptor:

       <listener>         <listener-class>YourListenerClassHere</listener-class>       </listener> 

This element should be added before the <servlet> elements. When you specify a class implementing the HttpSessionListener interface, the container invokes the sessionCreated() method whenever a new session is created. The container similarly calls the sessionDestroyed() method whenever a session is destroyed.

What useful logic can you add into the implementation of these methods? Well, when the sessionCreated() method is called, your class gets access to the Session object via the HttpSessionEvent object passed. Since this session is newly created, it will not yet have any attributes. This precludes you from implementing any logic that relies on the state of a session. The same applies to the sessionDestroyed() event. This event is called after all the attributes have been removed from the session. Depending on your application, you may, however, implement any application logic that does not rely on any session data.

Note that you can specify more than one listener class by adding more <listener> elements to the deployment descriptor.

Listening For Session Activation and Passivation

Apart from creation and destruction, a container may generate objects when two other events occur - passivation and activation. Let's see what these events are and how they may occur.

There are two ways a container may trigger these events. In a realistic production environment, you may have one or more web applications deployed on a container with several hundred (or more) users accessing the container. Of all the users, however, only a few may be actively sending requests, while the other users are temporarily not accessing the system and there is still time left for terminating such sessions. In such cases, in order to conserve memory, the container may decide to copy all of the Session objects to a persistent storage area (such as a database or a file system) and remove the sessions from memory. This is not the same as terminating the sessions. The sessions are valid, but are not there in memory. This process is called passivation.

The container may load the session data when the user sends a request again (before the session timeout interval and before the session inactive interval) from the persistent area. This process is called activation. The process of activation and passivation are not deployment-time controlled - these events happen based on container implementation and the active load on the server.

There is another way that the container uses session passivation and activation. As we will discuss shortly, you can configure a set of web containers as a cluster, with all of the container instances sharing the load between them. In this case, from time to time, the container may decide to shift the load of one session to another container instance. For this purpose, the container passivates the session on one container instance, sends the contents over the network to the other container instance, and activates the session there.

Both activation and passivation require that the attributes held in the session are serializable, otherwise the container can't passivate the session. Since your application may add non-serializable attributes (such as objects related to network/file I/O) into a session, the Java Servlet API specifies an event listener that can be notified during activation and passivation. During these events, the event listener may close the network/file resources before passivation, and may recreate the same resources during activation. The javax.servlet.http.HttpSessionActivationListener interface serves this purpose.

The HttpSessionActivationListener Interface

The HttpSessionActivationListener interface specifies methods to deal with session passivation and session activation:

     public void sessionDidActivate(HttpSessionEvent event) 

The container invokes the sessionDidActivate() method when a session is activated. The event is specified via the HttpSessionEvent class:

     public void sessionWillPassivate(HttpSessionEvent event) 

The container invokes the method above before passivating a session. You can implement this interface and deploy the implementation via the <listener> element in the deployment descriptor, as mentioned previously.

Listening For Attribute-Specific Events

The Java Servlet API includes another interface to deal with attributes. The javax.servlet.http.HttpSessionAttributeListener interface deals with the following events:

  • Servlet adds an attribute to a session

  • Servlet removes an attribute from a session

  • Servlet replaces an attribute in a session with another attribute of the same name

The HttpSessionAttributeListener Interface

This interface specifies several methods.

     public void attributeAdded(HttpSessionBindingEvent event) 

The container calls the above event when an attribute is added to the session. This method receives an HttpSessionBindingEvent object; we will discuss this object in the next section.

     public void attributeRemoved (HttpSessionBindingEvent event) 

The container calls the attributeRemoved() method when an attribute is removed from the session:

     public void attributeReplaced (HttpSessionBindingEvent event) 

The attributeReplaced() method is called by the container when an attribute is replaced in the session. This happens when you call the setAttribute() method using the same attribute name.

All of these methods receive an instance of the javax.servlet.http.HttpSessionBindingEvent class. This class encapsulates the name of the attribute, the value of the attribute and the associated Session object. Let's take a closer look at the methods of this class.

The HttpSessionBindingEvent Class

This class contains the following getter methods:

     public String getName()     public Object getValue() 

These methods return the name and value respectively of the attribute being added, replaced, or removed:

     public HttpSession getSession() 

The above method returns the associated Session object.

This class has two constructors. However, you should note that the container creates instances of these objects. As before, you can deploy this class via the <listener> element in the deployment descriptor.

The HttpSessionBindingListener Interface

There is another approach to handling attribute-related events, via the javax.servlet.http.HttpSessionBindingListener interface. Unlike the other event handling interfaces discussed above, this interface may be implemented by the attributes themselves and an external listener is not necessary. As an attribute is being added to a session, or removed from a session, this container invokes methods on this interface so that the attribute being added (known as attribute binding) or removed (known as attribute unbinding) to the session can handle this event. Let's take a look at the methods of the interface:

     public void valueBound (HttpSessionBindingEvent event) 

The container calls the valueBound() method when the attribute is added to the session.

     public void valueUnbound (HttpSessionBindingEvent event) 

Conversely, the container calls the valueUnbound() method when the attribute is removed from the session. As discussed above, any attribute may implement this interface to get notified by the container.

NotePad Revisited: Logging

Let us now modify the notepad application we created earlier to illustrate how to listen for some of the events discussed above. Our modified notepad application will:

  • Log session information when a new session is created or when an existing session is destroyed

  • Log the e-mail of the user when the user enters and leaves the notepad application

Session Logging Class

The following class provides this functionality. This class implements the HttpSessionListener interface and logs data to the web container log:

     package sessions;     import javax.servlet.http.HttpSession;     import javax.servlet.http.HttpSessionListener;     import javax.servlet.http.HttpSessionEvent;     public class SessionLogger implements HttpSessionListener { 

The sessionCreated() method is called after the session is created:

       public void sessionCreated(HttpSessionEvent event) {         HttpSession session = event.getSession();         session.getServletContext().log("Session with ID " + session.getId()                                                            + " created.");       } 

The sessionDestroyed() method is called after the session is destroyed:

       public void sessionDestroyed(HttpSessionEvent event) {         HttpSession session = event.getSession();         session.getServletContext().log("Session with ID " + session.getId()                                                            + " destroyed.");       }     } 

In both methods we use the getSession() method to retrieve the session.

This class logs the events to the container log. Compile the above class and save the class in notepad/WEB-INF/classes/sessions.

User E-mail Logging Class

Let's now add a handler to handle session attribute related events. The purpose of it is to log each user e-mail address as a user enters and leaves the notepad web application. The following class provides this functionality:

     package sessions;     import javax.servlet.ServletContext;     import javax.servlet.http.*; 

The class implements HttpSessionAttributeListener to handle events associated with binding/unbinding attributes to the session:

     public class NotePadLogger implements HttpSessionAttributeListener { 

The attributeAdded() method is called when an attribute is added to the session:

       public void attributeAdded(HttpSessionBindingEvent event){         String name = event.getName();         if(name.equals("email")) {           String email = (String) event.getValue();           log(event, "User " + email + " started.");         }       } 

The attributeRemoved() method is very similar to the previous method but is called when an attribute is deleted from the session instead:

       public void attributeRemoved(HttpSessionBindingEvent event) {         String name = event.getName();         if(name.equals("email")) {           String email = (String) event.getValue();           log(event, "User " + email + " removed.");         }       } 

The following method is called when an attribute is replaced from the session:

       public void attributeReplaced(HttpSessionBindingEvent event) {         String name = event.getName();         if(name.equals("email")) {           String email = (String) event.getValue();           log(event, "User " + email + " exited. ");         }       } 

For logging purposes we are only interested in the email attribute. Each of the above methods check if the attribute is email, and if so, logs the event using the ServletContext object.

The final method in the class logs a message to the container log:

       private void log(HttpSessionBindingEvent event, String string) {         HttpSession session = event.getSession();         ServletContext context = session.getServletContext();         context.log(string);       }     } 

Compile these class and save the class in notepad/WEB-INF/classes/sessions.

Redeploying the NotePad Application

Add the new source code to src/sessions, compile the classes and save them in notepad/WEB-INF/classes/sessions.

In order to deploy these two classes correctly, add the following elements to web.xml as shown below:

       <listener>         <listener-class>sessions.SessionLogger</listener-class>       </listener>       <listener>         <listener-class>sessions.NotePadLogger</listener-class>       </listener> 

Add this element before all <servlet> elements.

Restart Tomcat, and use the notepad application. Then check the log file of Tomcat for event log messages. The log file will be located under the %CATALINA_HOME%/logs directory, and the file name will be of the form localhost_log.yyyy-mm-dd.txt where yyyy-mm-dd is the current date. In this file, you will find log messages of the form shown in the screenshot below:

click to expand

As you can see, the log shows the session creation and destruction events, and the attribute add and remove events.

Can we not use HttpSessionListener in place of the HttpSessionAttributeListener in the above example? Although the HttpSessionListener gets invoked when a session is created, and again when the session is destroyed, we cannot access session attributes during these calls. For instance, we add the email attribute after the session has been created. Similarly, the container deletes all of the session attributes before invoking the HttpSessionListener.

Advanced Session Handling

What if we considered deploying the above notepad web application for public use on an Internet web site, and expected thousands of users to concurrently use this application? We would have a few issues to consider:

  • How do we make sure that the application/server doesn't fail to respond to such heavy load?

  • How do we make sure that a user does not lose his/her data if a server/OS crash occurs?

  • How do make sure that our application code is suitable for deployment under such conditions?

There are many aspects of application design that can influence these issues, and there are a few aspects related to session handling. In this section we are going to take a look at these.

Session Handling Within Clusters

Before discussing how to handle sessions for such scenarios, let us consider how such applications are often deployed. Most web servers are deployed in clusters. A cluster is a group of servers meant to share the incoming load of HTTP requests. In the most commonly-used clustering configuration, a clustered web container is configured behind a web server such as Apache, iPlanet or IIS. Here the web server acts as a proxy for the web container cluster; all that the client sees is the web server. Note that it is also common to find more than one web server installed in a cluster. In the above configuration, the web server uses a plug-in to delegate incoming HTTP requests to a web server within the cluster. A web server plug-in is a piece of software installed on the web server that sends requests to the web container processes in the container. Note that it is also possible to configure clusters without using web servers, although such configurations are rare.

You should note that Tomcat does not support clustering, because it is only meant to be the reference implementation of the Java Servlet API.

Clustering introduces certain complexities that may affect your application:

  • In a cluster, more than one container instance may handle the requests for a given session. This implies that the container should make the Session object available to all such instances, otherwise the user may lose the session data, or the session data may be inconsistent. This process is called session replication.

  • The container may swap a session from one container instance to another instance. This process requires the container to passivate and activate the session.

These issues involve copying Session objects from one machine to another machine, which implies further complexities.

Making Session Attributes Serializable

First, since we need to copy Session objects across machines, attributes stored in a session should be serializable. If any attribute is not serializable, you should provide an implementation of the HttpSessionActivationListener interface such that the implementation may remove the non-serializable attributes from the session during passivation, and recreate them during activation.

When to Replicate Sessions

The second consideration is: what triggers session replication? Is there a chance that certain attributes do not get replicated? For example, consider the list of notes stored in the notepad web application. To modify this list, you can retrieve it from the session, and add or remove notes from it without the web container being aware of such modifications. Since the container does not manage session attributes, it has no way to figure out that such changes have been made. There are few questions to be considered in this case:

  • Should the container replicate such an attribute immediately after it is set (via the setAttribute() method), or wait until the request is processed?

  • What happens if your servlet modifies the attribute after adding it to the session?

  • What happens if you retrieve an attribute, modify it, but don't set the attribute back in the session?

These situations arise because you can modify a session attribute via its reference. The Java Servlet specification is not clear about these situations.

Answers to the above questions depend on the specific container you are using. Most containers replicate the session attributes at the end of a request. However, to avoid replication of unchanged attributes, the container may expect you to call setAttribute() to set the attribute again. You should refer to the documentation of your web container for more information about this issue.

Minimizing the Data Stored in Sessions

There is another important consideration about sessions. Since both replication and passivation involve network traffic and possibly storage, you should always make sure that the number and the size of attributes in the session are as small as possible. Even in stand-alone deployments, session attributes consume memory, and the fewer attributes your applications maintain in sessions the better. In effect, you should try to build your applications as stateless as possible. For instance, when we built our notepad application, although we could have put the note content in the session (along with the ID and the title), we did not do so because it would have increased the size of the session. Since we have the noteId in the session (via the noteList), we can retrieve any note whenever required. This helps to minimize the size of the session. Another way to avoid wasting resources is to remove attributes once they are no longer required to be in the session.

Fail-Over Considerations

In addition to these considerations, we should consider the fail-over abilities that some containers support. Fail-over allows another instance in the cluster to share the load of an instance in the event of a system failure. However, such a fail-over is still susceptible to failures that happen while processing a request. When an instance crashes while processing a request, you may lose all the changes made to the session before the crash. In the case of mission-critical data, it is usually better to store the data immediately in a database instead of storing it in the session for long periods. Always remember - a session is a temporary area in the working memory, and it is not a data storage facility.



 < Free Open Study > 



Professional Java Servlets 2.3
Professional Java Servlets 2.3
ISBN: 186100561X
EAN: 2147483647
Year: 2006
Pages: 130

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net