HomeDigital EditionSys-Con RadioSearch Java Cd
Advanced Java AWT Book Reviews/Excerpts Client Server Corba Editorials Embedded Java Enterprise Java IDE's Industry Watch Integration Interviews Java Applet Java & Databases Java & Web Services Java Fundamentals Java Native Interface Java Servlets Java Beans J2ME Libraries .NET Object Orientation Observations/IMHO Product Reviews Scalability & Performance Security Server Side Source Code Straight Talking Swing Threads Using Java with others Wireless XML
 

A Practical Solution for the Deployment of Java Server Pages, by Alexis Grandemange

Sometimes it's worthwhile to go back and visit your former projects. It certainly was for me - using presentation as a commodity to be deployed according to network configuration is the concept that resulted from my visit.

The original assignment was to reduce the operating costs of a large banking agency network. How could this be achieved with a network of 23,000 personal computers scattered over 2,000 sites connected by a frame relay with a guaranteed bandwidth of 32Kb? We selected an intranet solution that radically reduced PC client/server applications to a single local program, the browser. To reduce bandwidth needs we deployed one Web server per site to perform only three tasks - handle presentation, maintain reference data, and invoke central system applications. This intranet design saved money by reducing the number of machines to operate from 23,000 to 2,000 by allowing them to be operated with a browser.

We can implement this concept to reduce the load of central farms with Java and J2EE because a fast local loop isn't always available. My goal, however, is to show specifically how we can do it better. The aforementioned approach didn't support a Web server's update on the fly and we had to contend with the central system's synchronization. The solution I present here addresses these issues by allowing Web servers to download their presentations the way browsers download applets. Figure 1 illustrates a possible organization. Let's summarize what we would need:

  1. Inexpensive Java servers able to host JSPs and servlets
  2. An API allowing them to invoke central system applications
  3. A simple way to download a presumably large number of Java servers from any number of central repositories
Figure 1
Figure  1:

The first two requirements can be fulfilled with off-the-shelf products, and the local Java server has to address only the following three requirements:

  • Generate presentation
  • Invoke central applications
  • Maintain reference data
At least two Open Source products meet these needs: Tomcat ( http://jakarta.apache.org/downloads/binindex.html) and Resin (www.caucho.com/download/index.xtp).

EJBs, Remote Method Invocation (RMI), or Java Message Service (JMS) can be used as the API to connect to central systems.

The last point, presentation downloading, implies development. This requires more explanation and thought and is the core of the article. Presentation downloading relies on a Java class loader and leverages on JSPs and servlets specifications, which I'll present first.

Standard
The Java Servlet Specification v2.2 defines a Web application as a collection of HTML pages, servlets, and classes that exists as a structured hierarchy of directories. The root of this hierarchy is the document root that serves files such as images or HTML. If your Java server waits for HTTP requests on www.iamakishirofan.com and you defined your Web application as gunnm, your users will be able to invoke zalem.html, stored at the root with the URL www.iamakishirofan.com/gunnm/zalem.html.

A WEB-INF directory contains a web.xml file that describes, among miscellaneous things, servlet and JSP definitions, initialization parameters, mapping to URL, and security constraints. It can also contain a classes subdirectory in which classes, servlets, taglibs, JSP invoked beans, compiled JSP, and more are stored. A Web application should be packaged in a .war file - the JAR archive of the hierarchy.

This packaging is convenient as it gathers all related components in a single delivery. It has another important property: all servlets and JSPs of a .war are served the same ServletContext, which is different from the ServletContext of other packages. Servlets and JSPs can use this ServletContext to access .war data, such as resources and initialization parameters, or to store and retrieve application-wide attributes.

The servlet container loads and instantiates servlets. It initializes them before their first use by calling their init() method with an object that implements the ServletConfig interface. This provides access to servlet-specific data. When it decides to unload a servlet, the container invokes the servlet destroy() method and unreferences it. Each time the container has to route a request to a servlet, it invokes the servlet's service() method.

A compiled JSP is a servlet, even if it doesn't extend HttpServlet or GenericServlet as a normal servlet but as another class that's application server dependent. In the case of Resin, it's com.caucho.jsp.JavaPage and with Tomcat, org.apache.jasper.runtime.HttpJspBase. As you can see, compiled JSPs are no longer portable even if there are only minor differences. The specification requires a JSP to implement a standard HttpJspPage interface. A JSP indirectly handles container requests as depicted in Figure 2.

Figure 2
Figure  2:

A compiled JSP implements a _jspService() method and, optionally, a jspInit() and a jspDestroy() method. The specification implies that, for instance, when the container invokes Servlet.init(), jspInit() is invoked somewhere in the implementation of the JSP base class. I've provided the Tomcat implementation in Listing 1. All Java servers I tested have similar code.

Choices
Back to our requirement - the solution I want to implement involves four participant types:

  • Browsers: Submit HTTP requests
  • Java servers: Process presentation and download the JSPs and servlets from a repository
  • Repositories: Must be accessed with a URL. A suitable repositories list includes HTTP servers as depicted in Figure 3 and FTP servers
  • Java application servers: Process EJB requests
I need to implement a piece of code in the Java server that's able to seamlessly retrieve JSPs and servlets from a central point, cache them, and support remote update. The solution depicted in Figure 3 is just common sense: I define a special servlet, JSPservlet, and package it in a .war file to handle all requests targeting its Web application. This servlet is responsible for loading target JSPs and servlets and forwarding them requests. To minimize data transfers, I handle archives (.jar) files only and cache downloaded archives, not only in memory but also on disk to survive a scheduled shutdown or a crash.

Figure 3
Figure  3:

To simplify the development I don't handle JSP compilation. It doesn't mean the solution doesn't support JSPs, only that they have to be precompiled, not a real drawback. Compiling JSPs is the only safe way to ensure a JSP can compile, and I prefer to avoid downloading failing code. I also don't support single thread servlets that guarantee only one thread at a time will execute through a given servlet instance's service() method. The support of this feature would require instantiating a new target servlet when already created target servlets are processing a request. It would add complexity to the logic and have an adverse impact on scalability.

Listing 2 shows the deployment descriptor (web.xml) of the JSPservlet application and how to define that a JSPservlet must handle all requests targeting the application. You specify in <servlet-mapping> <url-pattern>/</url-pattern>, not <url-pattern>*</url-pattern> as you'd expect. Note that I use <init-param> to set every machine-dependent parameter. Deployers can then modify them to accommodate different installation and operating system requirements. cachePath is the directory in which downloaded JARs are locally stored, and remoteLocations indicates a property file in which remote locations are defined. For instance, if a JAR file named myjar must be downloaded from an HTTP server www.mysite.com, remoteLocations will contain an entry myjar=HTTP://www.iamakishirofan.com.

Implementation
Let's look at the class diagram in Figure 4. You see the aforementioned JSPservlet that relies on a JSPhandlers HashMap of JSPhandler. There's a JSPhandler instance per application that reads parameters and maintains ClassEntry objects, one per archive. ClassEntry maintains a target servlets cache and a JSPloader instance.

Figure 4
Figure  4:

JSPloader is the class loader itself and maintains a class cache. It's also responsible for saving locally downloaded archives.

We can now see how the solution works. The Java server calls JSPservlet service(). To know which servlet is requested, JSPservlet.service() uses the request object. It first finds the appropriate JSPhandler with getHandler(), passing the application name it retrieves using the request getContextPath(). Then it gets a reference on the target with JSPhandler.get(), passing the path to the target returned by the request getPathInfo(). Eventually it uses this reference to invoke the target object service() method. As you can see in Listing 3 that's all for JSPservlet.

Listing 4 shows the implementation of JSPhandler. Its constructor retrieves parameter values from web.xml using ServletConfig. getInitParameter() and restores remote location properties from their persisted state. I chose to use the first part of the path as the archive name and the remaining part as the class name. Given the URL www.iamakishirofan.com/gunnm/ gally/nano/machine, if the application server is configured with the JSPservlet application on gunnm, ContextPath will be gunnm, the archive will be gally, and the servlet path in the archive, nano/ machine.class. This may seem a bit rough compared to the Web application flexible mapping but it's simpler to administer and implement. So JSPhandler.get() parses the pathInfo string and uses the archive part to find the corresponding ClassEntry in classEntries HashMap. It creates a ClassEntry if the search fails and invokes its get() method.

Now we can look at the ClassEntry implementation in Listing 5. Its constructor creates a JSPloader. Its get() method first tries to get the target servlet from its instance cache, servletObjects. No matter how many times a servlet is invoked, a single object is used and reused. If the object doesn't exist yet, it uses JSPloader to retrieve its class, invokes Class.newInstance() to instantiate it, and Servlet.init() to initialize it. It's extremely close to the Java server's implementations.

Class Loader
Before diving into the last and most complicated piece of code, JSPloader in Listing 6, let's recap what a class loader is and what our class loader is supposed to do. A class loader is an object responsible for loading classes. Given the class name, it can generate or load its binary code. It inherits from ClassLoader, which provides methods you can override (loadClass is the most flexible method). ClassLoader also implements a service method, defineClass, that converts the binary code in the Java class and resolveClass that links it. JSPloader must load classes from JAR files located either in the cachePath or at a URL.

Back to our example: it retrieves the archive from the local cache in cachePath/gally.jar or downloads it from a URL, which is the value of a gally property persisted in remoteLocations. In addition, when JSPloader downloads an archive, it must save this archive in its local cache, cachePath/gally.jar.

I prefer loading classes in a JSPloader constructor to minimize disk and network access duration and numbers. Another advantage is that forced loading can be performed outside peak hours by an administration JSP. JSPloader will then deliver a better response time as classes are already in memory. I found the memory use - same order of magnitude as the size of a downloaded archive - wasn't a showstopper. Note that I link a class only when requested, and ClassEntry instantiates objects only once, when they're first requested.

The JSPloader constructor tries downloading the archive from the local cache with loadClassDataFS() and then from its remote location with loadClassDataURL(). Both methods build a JarInputStream from an input stream that loadClassDataFS() gets from a FileInputStream and loadClassDataURL gets from a URL.openStream(). Since the JarInputStream handling is the same, I implemented it in a parseStream method.

parseStream loops around JarInputStream.getNextJarEntry(), which reads the next JAR file entry and positions the stream at the beginning of the data. Once parseStream has a JAR entry, it gets its name with JarEntry.getName() and uses a BufferedInputStream to read it. Then it converts it to a class with ClassLoader.defineClass and stores it in a classes memory cache. When it has to locally store a remotely downloaded archive, it uses a JarOutputStream; each time it's read an entry it rewrites it using JarOutputStream. putNextEntry() and JarOutputStream.write().

loadClass is invoked with two arguments, the name of the class and a boolean, resolve, that indicates if the class must be linked. Here I use the passive mode on purpose. Who invokes loadClass()? It depends. When ClassEntry invokes loadClass with the class name only, no magic happens. ClassLoader implements a loadClass(name) method that invokes loadClass(name, false). But the loaded class is associated with a JSPloader instance, which becomes the current class loader. If the loaded class uses another class, the Java Virtual Machine (JVM) will invoke JSPloader.loadClass to load it. This is why JSPloader.loadClass delegates class loading for the classes it doesn't find in its classes cache to the system class loader and its parent through the loadForward method.

The JSPloader.loadClass also delegates in two other interesting cases. If the class name starts with "java.", ClassLoader refuses to create it for security reasons. So I don't even try. The other case is "javax.servlet.Servlet". ClassEntry casts the target object it creates in a servlet. As I said, every class is associated with a class loader instance. In fact the JVM maintains the uniqueness of class_name, class_loader_object and not of class_name alone. So a cast of an object of class A loaded by class_loader_object1 to the same class loaded by class_loader_object2 fails. Therefore I check javax.servlet.Servlet and don't risk loading it from the archive.

Considerations
The order of the search has an obvious security impact. I prefer trying the class's memory cache first for speed and flexibility: I really depend on the Java server JDK for Java. I can download anything else, including the EJB, JMS, or RMI library code, but it has a security impact. If you don't trust your remote location, it's safer searching locally first.

My code is reasonably close to JDK 1.1 code: just replace HashMap with Hashtable and JarInputStream with ZipInputStream to run it with JDK 1.1. If local caching and JDK 1.1 have no value for you, consider URLClassLoader as an alternative to JSPloader. However, it's not really optimized for server-side use and you'd probably prefer the compatible NetworkClassLoader of Harish Prabandham provided in Tomcat. Its design is similar to JSPloader but instead of caching defined classes, it caches class data.

In Part 2 I'll describe how to handle images, support Web applications without restriction, and require updates from a browser. In Part 3 I'll demonstrate how to host downloaded classes in a sandbox, such as applets.

Conclusion
Through the class loader comprehensive mechanism it's easy to write a tool that's able to download servlets and JSPs from a remote location. It's even relatively easy to make it portable, though Java servers are probably the most hostile environment since they use class loaders intensively.

The idea probably has value for corporate intranets and B2Bs. Assume company B wants to provide access to its Web application to company A, which maintains a Java server. It simply configures its server to automatically download the code from B to enjoy reduced communication bills and better response time. It's a win-win situation since B doesn't have to process presentation. Now suppose A has many partners. As each downloaded archive is processed by a different class loader instance, it can use the same class names without collision. If A uses a different Web application for each partner, the partners won't share the same context. And A partners don't even have to know about A's Java server host and operating system. However, its real potential may be elsewhere.

If we could define a standard describing how to require a download and from where - for instance with XML over HTTP - even ISPs could host pages. Presentation would become a commodity like routing or a name service.

Author Bio
Alexis Grandemange is an architect and system designer. A Java programmer since 1996 with a background in C++ and COM, his main interest is J2EE with a focus on design, optimization, and performance issues. [email protected]

	

Listing 1: Apache implementation of _jspService and jspInit

public final void init(ServletConfig config)
  throws ServletException {
  this.config = config;
  jspInit();
}
public final void destroy() {
  jspDestroy();
}
public final void service(
  HttpServletRequest request,
  HttpServletResponse response) 
  throws ServletException, IOException {
  _jspService(request, response);
}

Listing 2: JSPservlet web.xml

<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, -
Inc.//DTD Web Application 1.2//EN"
"http://java.sun.com/j2ee/dtds/web-app_2_2.dtd">

<web-app>
  <servlet>
    <servlet-name>JDJloader</servlet-name>
    <servlet-class>JDJloader.JSPservlet
    </servlet-class>
    <init-param>
      <param-name>cachePath</param-name> 
      <param-value>C:/temp</param-value>
      <description>local cache</description> 
    </init-param>
    <init-param>
      <param-name>remoteLocations</param-name> 
      <param-value>C:/temp/jdj.properties</param-value>
      <description>jar remote location</description> 
    </init-param>
  </servlet>
  <servlet-mapping>
    <servlet-name>JDJloader</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
</web-app>

Listing 3: JSPservlet code

public class JSPservlet extends HttpServlet {
  public static HashMap JSPhandlers = null;
  public static final synchronized JSPhandler
    getHandler(ServletConfig sc,
    String contextPath) throws ServletException {
    JSPhandler jh = null;
    if (JSPhandlers == null)
      JSPhandlers = new HashMap();
    else
      jh = (JSPhandler)JSPhandlers.get(
        contextPath);
    if (jh != null)
      return jh;
    jh = new JSPhandler(sc, contextPath);
    JSPhandlers.put(contextPath, jh);
    return jh;
  }
  public void service(HttpServletRequest request,
    HttpServletResponse response)
    throws ServletException, IOException {
    JSPhandler jh = getHandler(getServletConfig(),
      request.getContextPath());
    Servlet srv = jh.get(request.getPathInfo());
    srv.service(request, response);
  }
}

Listing 4: JSPhandler code

public class JSPhandler {
  String cachePath;
  HashMap classEntries = new HashMap();
  Properties remoteLocProp = new Properties();
  ServletConfig servletConfig;
  JSPhandler(ServletConfig sc,String contextPath){
    servletConfig = sc;
    cachePath = sc.getInitParameter("cachePath");
    if (cachePath == null)
      cachePath = "C:/temp";
    String remoteLocFile = sc.getInitParameter(
      "remoteLocations");
    if (remoteLocFile == null)
      remoteLocFile = cachePath + contextPath +
        ".properties";
    File f = new File(remoteLocFile);
    if ((f != null) && f.exists()) {
      try {
        remoteLocProp.load(new DataInputStream(
          new FileInputStream(f)));
      }
      catch(Exception e) {}
    }
  }
  final synchronized Servlet get(String pathInfo)
    throws ServletException {
    String fullName = pathInfo;
    if (pathInfo.startsWith("/"))
      fullName = pathInfo.substring(1);
    int idx = fullName.indexOf('/');
    String jarName = fullName.substring(0, idx);
    String classPath = fullName.substring(idx + 1);
    ClassEntry ce = null;
    if (classEntries.containsKey(jarName)) {
      ce = (ClassEntry)classEntries.get(jarName);
      return ce.get(classPath);
    }
    ce = new ClassEntry(this, jarName);
    classEntries.put(jarName, ce);
    return ce.get(classPath);
  }
}

Listing 5: ClassEntry code

class ClassEntry {
  JSPhandler handler;
  JSPloader jl;
  HashMap servletObjects;
  ClassEntry(JSPhandler jh, String jarName)
    throws ServletException {
    handler = jh;
    String jarURL = (String)
      jh.remoteLocProp.get(jarName);
    jl = new JSPloader(jh, jarName, jarURL);
    servletObjects = new HashMap();
  }
  final Servlet get(String classPath)
    throws ServletException {
    if (servletObjects.containsKey(classPath))
      return (Servlet)
        servletObjects.get(classPath);
    Servlet srv = null;
    try {
      Class jspClass = jl.loadClass(
        classPath.replace('/', '.'));
      srv = (Servlet)jspClass.newInstance();
      srv.init(handler.servletConfig);
      servletObjects.put(classPath, srv);
    }
    catch(Exception e) {
      throw new ServletException("ClassEntry.get("
        + classPath + ") " + e);
    }
    return srv;
  }
}

Listing 6: JSPloader code

public class JSPloader extends ClassLoader {
  JSPhandler handler;
  String jarURL;
  String jarName;
  HashMap classes = null;
  ClassLoader parent;
  public JSPloader(JSPhandler jh, String jarName,
    String jarURL)
    throws javax.servlet.ServletException {
    super();
    handler = jh;
    this.jarURL = jarURL;
    this.jarName = jarName;
    parent = getParent();
    if (!loadClassDataFS()) {
      if (!loadClassDataURL())
        throw new javax.servlet.ServletException(
        "JSPloader.JSPloader unable to load jar");
    }
  }
  private final boolean parseStream(
    JarInputStream jis, boolean toSave) {
    JarEntry je = null;
    boolean rc = true;
    try {
      JarOutputStream jos = null;
      if (toSave)
        jos = new JarOutputStream(
          new BufferedOutputStream(
          new FileOutputStream(handler.cachePath +
          "/" + jarName + ".jar")));
      while((je = jis.getNextJarEntry()) != null){
        String entryName = je.getName();
        if (entryName.endsWith(".class")) {
          if (toSave)
          jos.putNextEntry((JarEntry)je.clone());
          ByteArrayOutputStream baos =
            new ByteArrayOutputStream();
          BufferedInputStream bis =
            new BufferedInputStream(jis);
          int i;
          while((i = bis.read()) != -1)
            baos.write(i);
          if (classes == null)
            classes = new HashMap(100);
          byte[] buf = baos.toByteArray();
          String k = entryName.substring(0,
  entryName.lastIndexOf('.')).replace('/', '.');
          Class jarCl = defineClass(k, buf, 0,
            buf.length);
          classes.put(k, jarCl);
          if (toSave)
            jos.write(buf, 0, buf.length);
        }
        jis.closeEntry();
      }
      jis.close();
      if (toSave) {
        jos.closeEntry();
        jos.close();
      }
    }
    catch(Exception e) {
      rc = false;
    }
    return rc;
  }
  private final boolean loadClassDataFS() {
    String jarPath = handler.cachePath + "/" +
      jarName + ".jar";
    JarInputStream jis = null;
    try {
      jis = new JarInputStream(
        new FileInputStream(jarPath));
    }
    catch(Exception e) {
      return false;
    }
    return parseStream(jis, false);
  }
  private final boolean loadClassDataURL() {
    JarInputStream jis = null;
    try {
      URL url = new URL(jarURL + "/" + jarName
        + ".jar");
      InputStream is =
        url.openConnection().getInputStream();
      jis = new JarInputStream(is);
    }
    catch(Exception e) {
      return false;
    }
    return parseStream(jis, true);
  }
  private final Class loadForward(String name)
    throws ClassNotFoundException{
    try {
      return findSystemClass(name);
    }
    catch(ClassNotFoundException cnfe) {}

    try {
      return parent.loadClass(name);
    }
    catch(ClassNotFoundException cnfe) {
      throw cnfe;
    }
    catch(Exception e2) {
      throw new ClassNotFoundException(
        e2.toString());
    }
  }
  public synchronized Class loadClass(String name,
    boolean resolve)
    throws ClassNotFoundException {
    if (name.equals("javax.servlet.Servlet") ||
      name.startsWith("java."))
      return loadForward(name);
    if ((classes != null) &&
      (classes.containsKey(name))) {
      Class cl = (Class)classes.get(name);
      if (resolve)
        resolveClass(cl);
      return cl;
    }
    return loadForward(name);
  }
}



  
 

Download Complete Source Listings (Zip format - 1.12 MB)
 

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.
  E-mail: [email protected]

Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.