Servlet Persistence

 < Free Open Study > 



So far, we have focused entirely on how servlets read from persistent resources, such as relational databases and directory services. In most cases, the same rules apply when you need to write to a resource. For example, with JDBC the only difference between a write operation and a read operation lies in the semantics of the SQL statement in question. The actual connection and statement objects are obtained in the same manner.

However, there is one feature of the servlet specification that is of special interest in relation to the process of storing data in persistent storage - the ability to listen for session and context events such as when the servlet container is initialized or a particular session gets destroyed. This, of course, comes in addition to the ability of servlets to handle shutdown through their destroy() methods.

Using these features, we have the ability to persist session or servlet state in the case of a server shutdown. This can be useful in many scenarios, including when:

  • The servlet stores a complex data object in memory that requires a lot of resources to construct (for example, an XML file matched against fields in a database). To avoid recreating this object every time the servlet container is restarted, we can have it serialized to a file at server shutdown, and read when the container goes back up.

  • A servlet or a filter collects information on a specific session and stores it in a persistent data store, such as a relational database. Instead of performing an insert for each entry collected, we can store the entries in-memory and have them flushed to the data store when the session terminates.

start sidebar

A filter is a web service components that add functionality to the request and response processing of a web application. We'll learn more about them in Chapter 7.

end sidebar

In this section, we will look at two examples:

  • How we can serialize a servlet object to a file

  • How to perform a batch insert into a relational database when a session terminates

Persisting the Servlet State

Through the built-in methods of the Servlet API, it is straightforward to detect when the servlet container is about to shut down. We might implement a ServletContextListener and use its contextDestroyed() method to persist selected servlets or data structures. When working directly with servlets, however, it's probably easier just to extend the destroy() method of the Servlet class, as that method will be invoked before the servlet in question is terminated.

We have a number of techniques available to use to persistently store the state of a servlet. We could write each field of the servlet to a relational database table, but that would probably incur too much overhead, both when writing and reading. Better yet, we can make use of serialization - the conversion of an object to a byte representation. This representation can be stored in a file and used later on to restore the object.

start sidebar

All Java class that implement the java.io.Serializable or java.io.Externalizable interfaces can be stored and retrieved in a serialized form.

end sidebar

The Serializable interface contains no methods that we need to implement. Simply by declaring that a given class implements Serializable, we can ensure that objects of that class can be written to a serialized form. If we implement Externalizable, the class itself is responsible for the external format of its contents (the logic of which we must implement), which is why Externalizable is more rarely used.

To work with serializable objects, we make use of two classes: java.io.ObjectInputStream and java.io.ObjectOutputStream. These classes are used to read and write objects from a persistent store. They contain methods for working with different data types, such as readString(), writeInt(), and readObject().

A Sample Data Servlet Framework

To further illustrate the process of working with serialized objects in the servlet container, we'll demonstrate a sample servlet framework. Our servlet, when initialized, obtains the name of the file it uses to persist its state when the server shuts down. In our example, the servlet stores a hash table instance that we assume contains some mission-critical data objects that would be expensive (in terms of memory or the CPU process) to initially construct. We're not interested in the actual details of these data objects or the data source they are obtained from - our only concern is that it is better in terms of performance to store these objects in a serialized form between lifecycles of the servlet.

When our servlet is initialized, it determines whether its state has been previously stored in a serialized form in a local file. It does so by calling the private readSerialize() method, which returns true if a serialized object was successfully read. If not, the servlet proceeds to construct a new data structure from the external resource associated. It does so by calling the readResource() method.

Our servlet also contains some statements for writing to the standard output stream. The purpose of these is to illustrate the logic of the servlet and verify that it really works as expected:

    package persistence.servlet;    import java.io.*;    import java.util.Hashtable;    import javax.servlet.*;    import javax.servlet.http.*;    public class DataServlet extends HttpServlet implements Serializable {      private static Hashtable data;      private static ServletContext ctx;      private static String filename;      public void init() throws ServletException { 

Determine whether the servlet has been initialized:

        if (filename == null) {          System.out.println("Initializing the data servlet..."); 

Obtain the file name of the serialized file:

          filename = getServletConfig().getInitParameter("DataServletOutput");          ctx = getServletContext(); 

Determine whether a serialized file exists:

          if (!readSerialized()) {            readResource();          }        }      } 

The first time the servlet is initialized, or when a serialized servlet state is for some reason not available, the servlet calls the private readResource() method. We assume that this method would access the data source and put a collection of objects into the hash table:

      private void readResource() {        System.out.println("Reading from resource...");        data = new Hashtable();        // Read data from an external data source.      } 

The private writeSerialized() method serializes the local hash table instance and writes the resulting output stream to a file to the application server's file system. The destination file is specified by the developer at deployment time, through an initialization parameter in the application's deployment descriptor:

      private void writeSerialized() {        System.out.print("Attempting to write object...");        ObjectOutputStream out = null;        try {          out = new ObjectOutputStream(new FileOutputStream(filename));          out.writeObject(data);          out.flush();          System.out.println("success!");        } catch (Throwable t) {          System.out.println("failed!");          ctx.log("DataServlet", t);        } finally {          if (out != null) {            try {              out.close();            } catch (Throwable ignored) {}          }        }      } 

When the servlet is initialized, it attempts to locate and read its previous state from a serialized file in the file system, through the readSerialized() method. If such a file exists, and it contains a valid serialization of the hash table in question, this method returns true:

      private boolean readSerialized () {        System.out.print("Attempting to read object...");        File f = new File(filename);        ObjectInputStream in = null;        try {          if (f.exists ()) {            in = new ObjectInputStream(new FileInputStream(f));            data = (Hashtable) in.readObject();            System.out.println("success!");            return true;          }          System.out.println("file does not exist!");        } catch (Throwable t) {          System.out.println("failed!");          ctx.log("DataServlet", t);        } finally {          if (in != null) {            try {              in .close();            } catch (Throwable ignored) {}          } 

Delete the file after it has been used:

          if (f.exists()) {            try {              f.delete();            } catch (Throwable io) {              ctx.log("DataServlet", io);            }          }        }        return false;      } 

Finally, we implement the servlet's destroy() method, in which we call the writeSerialized() method. This will store the state of the servlet in a persistent storage, ready to be accessed the next time the servlet is initialized:

      public void destroy() {        System.out.println("Destroying...");        writeSerialized();        super.destroy();      }    } 

Deploying the Application

To deploy our sample servlet, we need to add the following servlet declaration to the application's deployment descriptor:

    <servlet>      <servlet-name>DataServlet</servlet-name>      <servlet-class>persistence.servlet.DataServlet</servlet-class>      <init-param>        <param-name>DataServletOutput</param-name>        <param-value>DataServlet.ser</param-value>      </init-param>      <load-on-startup>1</load-on-startup>    </servlet> 

We could easily alter the path to the file that should store the serialized form by changing the value of the <param-value> element. Files are located relative to the directory from which the server was started (probably %CATALINA_HOME%/bin).

Testing the Servlet

Once you've deployed the servlet, restart the server and monitor the output to the standard output device (that is, the command prompt from where you started the server) Once the server is started, you should observe the output:

click to expand

Next, stop the server. When you start again, you should get different output, or:

click to expand

Persisting the Session State

In the previous example, we illustrated how a servlet can persist its state by writing serialized objects to a file when the application terminates. Next, we'll focus on persisting the state of a user's session.

Session state persistence does not refer to the process of replicating session information between multiple servlet containers (which is a common procedure in clustered applications). Here, we're referring only to the task of storing session attributes in a persistent storage when the session in question is destroyed.

We will focus on the scenario in which an application gathers user information and stores attributes in each user's HttpSession object. Eventually, this information should be stored in persistent storage, such as a relational database. Before the HttpSessionListener interface was introduced to the Servlet API, we would had to write an entry to the data store in question each time we gained new data, which would incur a great deal of I/O and networking overhead. Now by implementing an appropriate session listener, we can gather information over the course of each session, and persist the whole collection before the session terminates. That way, we will increase performance by:

  • Reducing the number of times we prepare database statements

  • Reduce the number of times we make round trips to the server

  • Making use of the bulk-insert features of the JDBC drivers

This approach, however, comes with its own price. The more objects we store in a session, the greater the impact is that we have on the performance of the application server. Even though each object that we store may be small by itself, the eventual size could be unacceptably large if the application has to deal with a great many users. Fortunately, such problems can often be solved by scaling the hardware used. In a three-tier environment, the database tier is usually running on the most powerful machines that may be expensive to upgrade. Therefore, it is usually easier to upgrade the hardware at the middle tier, or even add more machines in a cluster.

In general, if the database is the bottleneck in your application, the design approach we present in this section may help to decrease its load. However, if the database is running well, the performance gain from this approach will probably not be worth the problems it may cause in the middle tier.

To provide a concrete example of the design we've discussed, we'll implement a sample application. For this application, we will develop a filter that monitors each page request on our website. It will store the pages requested by each user in a data object in each respective user's HttpSession object. We'll also implement a session listener that detects when sessions terminate and when new attributes are set for each session. Each time a session is destroyed, the session listener gathers the request statistics for that session, and flushes it into the database, where it can be worked with using analytical methods.

To start with, we need to set up the database in which we'll store the request information.

Creating the Database

To store requester information, we declare a specific table, REQUESTERS:

    USE persistence;    CREATE TABLE requesters (      requester_id INTEGER AUTO_INCREMENT PRIMARY KEY,      remote_host VARCHAR(255) NOT NULL,      user_agent VARCHAR(255) NOT NULL    ); 

To associate each requester with a unique ID, we automatically increment the primary key. To store the requests made by each user, we create a REQUESTS table:

    CREATE TABLE requests (      requester_id INTEGER,      hitdate DATE,      uri VARCHAR(255) NOT NULL    ); 

Implementing the Requester Class

Each instance of our Requester class represents a particular user on our website. When a user makes their first request, a new Requester instance is created and bound to the user's HttpSession instance. For each Requester, we store:

  • The name of the remote host

  • The name of the user agent

  • A list of pages this requester has requested

Each Requester object will hold only a little amount of data for each request being made and as the average number of requests per user is probably no more than ten, it would require a large number of concurrent users for this class to affect the overall performance of the system:

    package persistence.requester;    import java.util.*;    import javax.servlet.*;    import javax.servlet.http.*;    public class Requester {      public static final String KEY = "RequesterKey";      private String remoteHost;      private String userAgent;      private Vector requests;      public Requester(HttpServletRequest request) {        this.remoteHost = request.getRemoteHost();        this.userAgent = request.getHeader("User-Agent");        requests = new Vector();      }      public String getRemoteHost() {        return remoteHost;      }      public String getUserAgent() {        return userAgent;      }      public void addRequest(HttpServletRequest request) {        requests.add(request.getRequestURI());      }      public Enumeration getRequests() {        return requests.elements();      }    } 

Implementing the Request Filter

To register request information, we implement a filter that takes each request and determines whether it is made over HTTP. If the request is over HTTP, the filter determines whether the requester in question has already been associated with a Requester instance. If not, a new instance is made and bound to the session. Finally, the filter logs the request being made to the Requester object, via the addRequest() method of Requester:

    package persistence.requester;    import java.io.IOException;    import javax.servlet.*;    import javax.servlet.http.*;    public class RequestFilter implements Filter {      private String attribute = null;      private FilterConfig filterConfig = null;      public void destroy() {        this.attribute = null;        this.filterConfig = null;      }      public void doFilter(ServletRequest request, ServletResponse response,                           FilterChain chain)          throws IOException, ServletException {        try {          if (request instanceof HttpServletRequest) {            System.out.println("Filtering...");            HttpServletRequest ref = (HttpServletRequest) request;            HttpSession session = ref.getSession();            Requester req = (Requester) session.getAttribute(Requester.KEY);            if (req == null) {              req = new Requester(ref);              session.setAttribute(Requester.KEY, req);            }            req.addRequest(ref);          }        } catch (Throwable t) {          filterConfig.getServletContext().log("RequestFilter", t);        }        chain.doFilter(request, response);      }      public void init(FilterConfig filterConfig) throws ServletException {        this.filterConfig = filterConfig;      }    } 

Implementing the Session Listener

For the purpose of handling session shutdown, we define a new session listener class that implements three interfaces:

  • HttpSessionListener - so that we are notified when a session terminates.

  • ServletContextListener - so that we are notified when the servlet container is initialized.

  • HttpSessionAttributeListener - so that that we can keep track of sessions that have been associated with a Requester instance. Our session listener needs to be notified when sessions are associated with a Requester instance because it needs to store a local reference to each Requester. If, instead, it attempted to look up the Requester attribute in its sessionDestroyed() method (see below) an exception would be thrown as the session would already be marked for destruction (and so no attributes could be obtained from it).

Here is the implementation of SessionListener (some method bodies are empty, as although they are required for our application they must still be implemented):

    package persistence.requester;    import java.sql.*;    import java.util.*;    import javax.naming.InitialContext;    import javax.servlet.*;    import javax.servlet.http.*;    import javax.sql.DataSource;    public class SessionListener implements HttpSessionListener,                                            ServletContextListener,                                            HttpSessionAttributeListener {      private DataSource ds;      private ServletContext context;      private Hashtable sessions; 

Through its attributeAdded() method our SessionListener can determine whether the session in question has been filtered. If a Requester instance is found for that session, a reference to it is stored locally, using the session ID as a key:

      public void attributeAdded(HttpSessionBindingEvent event) {        if (event.getValue() instanceof Requester &&            event.getName().equals(Requester.KEY)) {          sessions.put(event.getSession().getId(), event.getValue());        }      }      public void attributeRemoved(HttpSessionBindingEvent event) {}      public void attributeReplaced(HttpSessionBindingEvent event) {} 

When the servlet context is initialized, the SessionListener is initialized:

      public void contextInitialized(ServletContextEvent event) {        context = event.getServletContext();        sessions = new Hashtable();      }      public void contextDestroyed(ServletContextEvent event) {        context = null;        sessions = null;      } 

The first time a new session is created, we get a DataSource from the JNDI tree. So, for this application to work, a DataSource must be properly configured and bound to JNDI when the server starts:

      public void sessionCreated(HttpSessionEvent event) {        if (ds == null) {          ServletContext context = event.getSession().getServletContext();          try {            InitialContext ctx = new InitialContext();            ds = (DataSource)ctx.lookup(context.getInitParameter("DataSource"));          } catch (Throwable t) {            log(t);          }        }      } 

Finally, when we detect that a session is being destroyed (because it has timed out), we determine whether that session has been associated with a Requester instance. If not, we do not proceed any further. If a Requester instance is found, we open a database connection and create a new requester entry in the database. When the new entry has been created, we insert a new row for each request into the REQUESTS database table:

      public void sessionDestroyed(HttpSessionEvent event) {        Connection conn = null;        PreparedStatement pstmt_requesters = null;        Statement stm = null;        PreparedStatement pstmt_requests = null;        String sessionID = event.getSession().getId();        Requester req = (Requester) sessions.get(sessionID); 

To create a new requester entry, we need to make two database calls: one to insert the new row and another to select the new requester ID (that is needed for the REQUESTS table):

        if (req != null) {          try {            System.out.println("Initialize database resources.");            conn = ds.getConnection();            pstmt_requesters = conn.prepareStatement("INSERT INTO requesters " +                                                     "(remote_host, user_agent)"                                                     + "VALUES (?,?)");            pstmt_requesters.setString(1, req.getRemoteHost());            pstmt_requesters.setString(2, req.getUserAgent());            pstmt_requesters.execute();            System.out.println("Get the ID of the last entry");            stm = conn.createStatement();            ResultSet rset = stm.executeQuery("SELECT requester_id FROM " +                                              "requesters ORDER BY " +                                              "requester_id DESC"); 

We need to get the value of the last ID used:

            rset.first();            int lastID = rset.getInt(1);            System.out.println("LAST ID: " + lastID);            pstmt_requests = conn.prepareStatement("INSERT into requests (" +                                           "requester_id, hitdate, uri) " +                                           "VALUES (?,?,?)");            for (Enumeration e = req.getRequests(); e.hasMoreElements();) {                System.out.println("request...");                pstmt_requests.setInt(1, lastID);                pstmt_requests.setDate(2, (new java.sql.Date((                                           new java.util.Date()).getTime())));                pstmt_requests.setString(3, (String) e.nextElement());                pstmt_requests.execute();            }          } catch (Throwable t) {            if (conn != null) {              try {                conn.rollback();              } catch (SQLException ignored) {}            }            log(t); 

Make sure all resources are shut down:

          } finally {            if (conn != null) {              try {                conn.close();              } catch (SQLException ignored) {}            }            if (pstmt_requesters != null) {              try {                pstmt.close();              } catch (SQLException ignored) {}            }            if (pstmt_requests != null) {              try {                cstmt.close();              } catch (SQLException ignored) {}            }          }        } 

Remove the Requester object:

        sessions.remove(sessionID);      } 

Finally, we declare a method for logging down exceptions that may occur.

        private void log(Throwable t) {            if (context != null) {                context.log("SessionListener", t);            } else {                t.printStackTrace(System.out);            }        }    } 

Deploying the Application

To deploy the request filter and session listener, add the following elements to the application's deployment descriptor:

    <filter>      <filter-name>Request Filter</filter-name>      <filter-class>persistence.requester.RequestFilter</filter-class>    </filter>    <filter-mapping>      <filter-name>Request Filter</filter-name>      <url-pattern>/*</url-pattern>    </filter-mapping>    <listener>      <listener-class>persistence.requester.SessionListener</listener-class>    </listener> 

To reduce the time we have to wait to see some results in the database, we can reduce the session-timeout value for our server (which is by default 30 minutes). We can change this in by setting the value of <session-timeout> to 1 minute in %CATALINA_HOME%/conf/web.xml:

    <web-app>      ...      <session-config>        <session-timeout>1</session-timeout>      </session-config>      ...    </web-app> 

When you have made your changes, restart Tomcat. Then navigate to a selection of pages, before allowing the session to terminate. Then we can query our database to discover the details of the requests that were made. First we query the requesters table:

click to expand

And then we query we requests table:

click to expand



 < Free Open Study > 



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

Similar book on Amazon

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