Saturday, June 19, 2010

How to add new nonCacheable resource by inheriting Tomcat's DefaultServlet

Do you have a web resource in your application which is dynamically generated/modified? And do you use JavaScript/Ajax (i.e. you do not have the resource mapped to a servlet in your application web.xml) to read that resource? If both answers are yes, then you may have seen that the request for the resource does not return the updated file always. The reason is DefaultServlet class of Tomcat which implements a caching technique for resources. If there is a HTTP request to access the resource before 5 sec of the last modification time, DefaultServlet will send the file from its cache (older content).


DefaultServlet is a servet provided by Tomcat distribution (and JBoss too). As the name suggests, it is the default choice. If your web application does not map a web resource to some servlet, DefaultServlet serves that resource. Only exception is JSP pages. Check out DefaultServlet Javadoc and source code. The servlet mapping is defined in Tomcat web.xml which is normally present at $CATALINA_HOME/conf/web.xml. In JBoss 5.1.0 GA, we find this at $JBoss_HOME/server/default/deployers/jbossweb.deployer/web.xml. I have shown the part of web.xml below-

<servlet>
        <servlet-name>default</servlet-name>
        <servlet-class>
          org.apache.catalina.servlets.DefaultServlet
        </servlet-class>
        <init-param>
            <param-name>debug</param-name>
            <param-value>0</param-value>
        </init-param>
        <init-param>
            <param-name>listings</param-name>
            <param-value>true</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

...
    <servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

The solution is simple. You need to extend the DefaultServlet and ProxyDirContext class of Tomcat and override the nonCacheable array. Below is the example code.


/** 
 * AppServlet extends DefaultServlet of Tomcat
 */
package com.myapp;

import java.io.File; 

import javax.naming.NamingException; 
import javax.servlet.ServletException; 

import org.apache.catalina.servlets.DefaultServlet; 
import org.apache.naming.resources.FileDirContext;

public class AppServlet extends DefaultServlet 
{ 
   private static final String APP_WEB_ROOT = "/"; 

   public AppServlet() 
   { 
      super(); 
   } 

   /** 
    * {@inheritDoc} 
    */ 
   @Override 
   public void init() throws ServletException 
   { 
      super.init(); 

      final FileDirContext appWebDirContext = new FileDirContext(); 
      
      final String realPath = getServletContext().getRealPath( APP_WEB_ROOT ); 
      final File realPathFile = new File( realPath ); 
      appWebDirContext.setDocBase( realPathFile.getAbsolutePath() ); // Get the absolute path of the application's real path.

      try 
      { 
         this.resources = new MyProxyDirContext( this.resources 
               .getEnvironment(), appWebDirContext ); 
      } 
      catch (NamingException e) 
      { 
         throw new IllegalStateException(); 
      } 
   } 

}


Override the ProxyDirContext class.



/**
 * MyProxyDirContext extends ProxyDirContext of Tomcat
 * Here override the nonCacheable array
 */
package com.myapp;

import java.util.Hashtable; 
import org.apache.log4j.Logger;
import javax.naming.directory.DirContext; 
import org.apache.naming.resources.CacheEntry; 
import org.apache.naming.resources.ProxyDirContext; 

public class MyProxyDirContext extends ProxyDirContext 
{ 
   private static final String[] NON_CACHEABLE_FOLDERS = 
   { "/WEB-INF/lib/", "/WEB-INF/classes/", "/xml/" }; 

   public MyProxyDirContext( final Hashtable env, 
         final DirContext dirContext ) 
   { 
      super( env, dirContext ); 
      nonCacheable = NON_CACHEABLE_FOLDERS; 
   }
}

Now my web application structure looks like this-


Notice the xml directory which contains app.xml (assume this as dynamic resource). My JSP will load the app.xml using JavaScript (add the function as onload item). As the /xml/ URL pattern is added as nonCacheable, Tomcat will read the resource from file system (not from cache). So, we will always get the updated file content. Here is the script code -



function loadXml()
{
   var file = location.protocol + "//" + location.host + location.port +  "/xml/app.xml";
   var xmldoc = new ActiveXObject("Microsoft.XMLDOM");
   xmldoc.async = false;
   xmldoc.load(file);
   // print some data from the xml file
   alert(xmldoc.getElementsByTagName("MY")[0].firstChild.nodeValue);
}


Finally the web.xml for this app -




<servlet>
        <servlet-name>AppServlet</servlet-name>
        <servlet-class>
          com.myapp.AppServlet
        </servlet-class>
        <init-param>
            <param-name>debug</param-name>
            <param-value>0</param-value>
        </init-param>
        <init-param>
            <param-name>listings</param-name>
            <param-value>false</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>AppServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>



This approach suffers a performance hit. Do not use this technique for static files.

No comments:

Post a Comment