Running on Java 24-ea+24-2960 (Preview)
Home of The JavaSpecialists' Newsletter

022Classloaders Revisited: "Hotdeploy"

Author: Dr. Christoph G. JungDate: 2001-06-07Java Version: 1.3Category: Language
 

Abstract: In this newsletter, Dr Christoph Jung amazes us by showing how we can build a minimalist application server by playing around with class loaders.

 

Welcome to the 22nd issue of The Java(tm) Specialists' Newsletter, sponsored by infor business solutions AG, an ERP company based in Germany. Many thanks go to Christoph Jung for again making an excellent contribution to The Java(tm) Specialists' Newsletter by taking the time to write about some of the advanced things he's busy with. This is the longest newsletter so far, and also the most in-depth. Don't start expecting the same quality when I write ;-), in fact, I might skip next week's letter to give you some time to digest this one.

As always, I would like to ask you to forward this newsletter to anybody you know who might be interested in Java. Please also remember that if you are a beginner in Java, some of these newsletters will go over your head. Don't give up!

javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.

Classloaders Revisited: "Hotdeploy"

A few weeks ago, I was in the middle of an ICQ chat with our admired Dr. Kabutz when he suddenly uttered a definite "hi, hi" which took me by surprise as it was different to the ubiquitous "emoticons" or smileys that I am used to. Nevertheless, I was immediately able to catch the thousands of denotations that this very special textual laughter was transporting. In this case, the semantics can be very well subsumed by the German term "verschmitzt" which is difficult to translate to English but would probably be a combination of "roguish", "twisty", "experienced" and "savvy". Similarly, "ha, ha" (a very open reaction to a joke) and "ho, ho" (kind of patronizing Santa Claus stance) are able to express much more context than any smiley construction I know of, at a minimum overhead of bandwidth. Measurements using WinZip have shown verbose laughter statistically being only 5% more expensive than its certainly poorer smiley counterpart.

And now ... I hope you kept a copy of newsletter 18 where Heinz illuminated us with the insight that class identity in the virtual machine is quite independent of even fully-qualified class names. The magical constructs that he used for his investigations were the java.lang.ClassLoader and suitable derivatives, such as the java.net.URLCLassloader. These are, amongst other innovations of the Java(tm) 2 Runtime, indeed a fascinating subject for experimentation.

Classes and ClassLoaders

Perhaps you found that having several classes called "A" in your java.exe was not very useful. On the contrary, it probably required quite an effort to keep the various sources and class files of that newsletter in separate directories. However, I would like to demonstrate how this rather theoretical possibility has significant practical value when it comes to building real programs that are able to simultaneously host multiple Java applications. The most well known examples of such programs are found in the J2EE(tm) application servers.

For this purpose, let us briefly revisit what the head of Maximum Solutions Ltd., by the way, one of the best Java consultancies I know of ;-), has demonstrated to our surprised eyes and what may be already apparent from the terminology: A java.lang.ClassLoader is an object that is used to locate external resources (such as *.class file placed in a directory or zipped into a jar-file) and to import the contained byte code for constructing java.lang.Class instances at runtime.

Each java.lang.Class in the JVM is hence associated with its so-called "defining classloader" that has imported it and that can be obtained using the Class.getClassLoader() method. For example, regard the following classes stock.Warehouse (interface to a mono-product warehouse exposing a reservation method) and sales.Order (remotely connects/links to a warehouse to satisfy the order in quantities of size "bunch").

// stock/Warehouse.java
package stock;
/** Sample remote interface that is linked by sales.Order. */
public interface Warehouse extends java.rmi.Remote {
  /** Reserve items in this warehouse */
  void reserve(int quantity) throws java.rmi.RemoteException;
}

// sales/Order.java
package sales;
import java.rmi.*;
import stock.Warehouse;
/** Class that links in another jar */
public class Order {
  /** the stock.Warehouse which hosts our items */
  private final Warehouse wHouse;
  /** how much do we need? */
  private final int amount;
  /** quantize reservations */
  private static int bunch=5;
  /** constructs a new order */
  public Order(String itemName, int quantity)
      throws java.net.MalformedURLException, NotBoundException,
        RemoteException {
    // look into the rmi registry to locate the warehouse
    wHouse=(Warehouse)Naming.lookup("//localhost/"+itemName);
    this.amount=quantity;
  }
  /** method that delegates reservation to warehouse */
  public void reserve() throws RemoteException {
    for(int count = 0; count < amount; count += bunch)
      wHouse.reserve(bunch);
  }
}

Although sophisticated names and nifty logic should suggest an increasing level of practicability compared to Heinz's previous excursions, please remember that this is a technical newsletter and not a business logic course. Hence, instead of flaming about the obvious flaws and, at the same time, selling the attached files as ready-made ERP-competitor to our infor:COM, compile the sources into two separate directories "classes/stock" and "classes/sales":

javac -d classes\stock\ stock\*.java
javac -classpath classes\stock\ -d classes\sales\ sales\*.java
// server/Loader.java
package server;
import java.net.*;
import java.io.*;
public class Loader {
  /** Demonstration of some classloading issues */
  public static void main(String[] args) throws Exception {
    // construct a tiny classloader hierarchy
    ClassLoader stockLoader = new URLClassLoader(
      new URL[] {getStockLocation()});
    ClassLoader salesLoader = new URLClassLoader(
      new URL[] {getSalesLocation()}, stockLoader);
    // load order class
    Class orderC = salesLoader.loadClass("sales.Order");
    System.out.println(orderC + " loaded from " + salesLoader +
      "; defined by " + orderC.getClassLoader());
    // load warehouse class
    Class wHouseC = salesLoader.loadClass("stock.Warehouse");
    System.out.println(wHouseC + " loaded from " + salesLoader +
      "; defined by " + wHouseC.getClassLoader());
    // analyse class links
    System.out.println("loading and linking same " +
      wHouseC.equals(orderC.getDeclaredField("wHouse").getType()));
    System.exit(0);
  }
  /** where the stock classes can be found */
  protected static URL getStockLocation() throws IOException {
    return new File("classes/stock/").toURL();
  }
  /** where the sales classes can be found */
  protected static URL getSalesLocation() throws IOException {
    return new File("classes/sales/").toURL();
  }
}

Whenever the virtual machine (VM) gets hold of your just produced sales.Order - maybe using the loadClass("sales.Order") statement in the preceding server.Loader code - it will also automatically ask the defining classloader of sales.Order to additionally load the linked class into memory:

salesLoader.loadClass("stock.Warehouse").

Since we have placed the byte code of stock.Warehouse into the classes\stock directory that is not under the hood of salesLoader, salesLoader will delegate the call to its parent stockLoader and, upon successful resolution, the respective class-representations in the VM are linked together. These issues will become apparent if you run Loader from the "classes\server" folder (using the attached "lazy developer" policy file which you should never use in production environments, you have been warned!).

javac -d classes\server\ server\*.java
java -Djava.security.policy=server.policy -classpath
  classes\server server.Loader
// server.policy
grant {
  // Allow everything for now
  permission java.security.AllPermission;
};

The output will most likely look similarly to this:

class sales.Order loaded from java.net.URLClassLoader@253498;
   defined by java.net.URLClassLoader@253498
interface stock.Warehouse defined by java.net.URLClassLoader@209f4e
loading versus linking: true

Note that java.exe operates a bit lazily since salesLoader.loadClass("stock.Warehouse") and hence stockLoader.loadClass("stock.Warehouse") will not happen until you begin to inspect or instantiate the imported Order class! In our Loader code, this is implicitly triggered by the getDeclaredField("wHouse") call. This way, you will not end up loading every class file that is under control by any classloader into memory when you were just asking for Order. You will really appreciate this feature if you have experienced the class inflation phenomenon typical in an OO project ;-)

Hot-Deploy

As an educated audience you did know all this before of course. This loading & linking is certainly not restricted to our Order and Warehouse, but is used to intern every class into the VM. It even does this with the main class and the Java(tm) 2 runtime representations that the main class depends upon. The big difference to our example is, however, that you usually specify a single classpath/ classloader ("classes\server\") and a single main class ("server.Loader") at startup of java.exe, hence a single java "application".

With our freshly acquired knowledge, we have revealed the opportunity to dynamically start/shutdown applications at runtime which is often called "deploying": java.exe starts with a minimal setup, e.g., the below server.ApplicationServer, and is then equipped with additional application specifications on-the-fly. An application specification that is processed by the server's deploy(...) and start(...) methods consists of the path to the byte-code, the name of the main class, and the set of initial arguments.

// server/ApplicationServer.java
package server;
import java.net.*;
import java.util.*;
import java.io.*;
/** A tiny "application server" */
public class ApplicationServer extends Loader {
  /** this map stores application urls->active-threads */
  protected final Map applications = new HashMap();
  /** the deploy method interns an application
   * @param url jar where the application is packed
   * @param main fully-qualified name of Main class
   * @param args arguments to main method
   * @param parent parent classloader
   * @throws ClassNotFoundException if application not found
   * @return the classloader that has been generated for that app
   */
  public synchronized ClassLoader deploy(final URL url,
      String main, final String[] args, ClassLoader parent)
      throws ClassNotFoundException {

    System.out.println("Deploying "+url);

    // is this a redeploy?
    if(applications.containsKey(url)) // yes: tear it down first
      undeploy(url);

    // generate a new classloader
    final ClassLoader loader = constructClassLoader(
      new URL[] {url}, parent);
    // load the mainclass
    final Class mainClass = loader.loadClass(main);

    // construct a new "main" thread
    Thread newThread = new Thread() {
      public void run() {
        try{
          // run the main method with the given args
          Class[] params = {String[].class}; // args types
          mainClass.getMethod("main", params).invoke(
            mainClass, new Object[]{args});
          // keep application alive until teared down
          synchronized(this) { this.wait(); }
        } catch(java.lang.reflect.InvocationTargetException e) {
          e.getTargetException().printStackTrace();
        } catch(Exception e) {
        } finally {
          // we lock the appServer
          synchronized(ApplicationServer.this) {
            try{
              // in case that any application error occurs
              // (or the application is to be undeployed)
              System.out.println("Stopping " + url);
              // remove entry if still there
              applications.values().remove(this);
              // call cleanup method
              mainClass.getMethod("stop", new Class[0]).invoke(
                mainClass, new Object[0]);
            } catch(Exception _e) {}
          } // synchronized(appServer.this)
        } // finally
      }}; // method run(); class Thread()

      // set the thread context
      newThread.setContextClassLoader(loader);
      // register application x thread
      applications.put(url, newThread);
      // return classloader
      return loader;
  } // method deploy()

  /** starts an application that has already been deployed */
  public synchronized void start(URL url) {
    System.out.println("Starting " + url);
    ((Thread)applications.get(url)).start();
  }
  /** Undeploys a running application. Never, I repeat, NEVER, do
   *  this using Thread.stop() but use the various options that
   *  are proposed by your JDK documentation to gracefully notify
   *  a thread of shutdown.
   * @param url url where the application is packed
   * @throws Exception if the app could not be teared down
   */
  public synchronized void undeploy(URL url) {
    // uh uh. bastard. But for Heinz newsletter, its ok ;-)
    ((Thread) applications.get(url)).stop(new Exception("stop"));
  }
  /** class loader factory method */
  protected ClassLoader constructClassLoader(URL[] urls,
      ClassLoader parent) {
    return new URLClassLoader(urls,parent);
  }

  /** example usage of the appServer */
  public static void main(String[] args) throws Exception {
    BufferedReader stdin = new BufferedReader(
      new InputStreamReader(System.in));
    ApplicationServer appServer = new ApplicationServer();
    ClassLoader stockLoader = appServer.deploy(
      getStockLocation(), "stock.Main",
      new String[] {"screwdriver", "stock.WarehouseImpl",
        "screwdriver", "200"},
      null);
    appServer.start(getStockLocation());
    stdin.readLine();
    appServer.deploy(getSalesLocation(),
      "sales.Main",
      new String[] {"screwdriver","50"},
      stockLoader);
    appServer.start(getSalesLocation());
    stdin.readLine();
    appServer.deploy(appServer.getSalesLocation(),
      "sales.Main",
      new String[] {"screwdriver","80"},
      stockLoader);
    appServer.start(appServer.getSalesLocation());
    stdin.readLine();
    appServer.undeploy(appServer.getSalesLocation());
    appServer.undeploy(appServer.getStockLocation());
    System.exit(0);
  }
}

As example applications that are to be deployed by the presented server logic, we sketch below an exemplary batch sales. Main that constructs and processes a set of sales.Order. And we introduce an exemplary stock application stock. Main that exports remote warehouse services such as implemented by stock.WarehouseImpl.

// sales/Main.java
package sales;
/** An example batch application */
public class Main  {
  /** starts the batch */
  public static void main(String[] args) throws Exception {
    // analyse command-line
    for(int count=0; count<args.length; count++)
      // construct order and reserve
      new Order(args[count++],
        new Integer(args[count]).intValue()).reserve();
  }
}

// stock/WarehouseImpl.java
package stock;
import java.rmi.server.UnicastRemoteObject;
/** Example implementation of a remote service */
public class WarehouseImpl extends UnicastRemoteObject
    implements Warehouse {
  /** number of stored items */
  protected int quantity;
  /** constructs warehouse */
  public WarehouseImpl(String itemName, int quantity)
      throws java.rmi.RemoteException {
    this.quantity=quantity;
  }
  /** reserves items*/
  public void reserve(int amount) {
      System.out.println(this + " is about to reserve " +
        amount + " items.");
      if(quantity < amount) empty(amount - quantity);
      quantity -= amount;
  }
  /** what to do if the warehouse is empty */
  protected void empty(int underLoad)  {
      throw new IllegalArgumentException("warehouse empty");
  }
}

// stock/Main.java
package stock;
import java.rmi.*;
import java.util.*;
/**
 * Example service-publishing application
 */
public class Main {
  /** the services provided by this application */
  protected static Collection services = new Vector();

  /** create and export services */
  public static void main(String[] args) throws Exception {
    System.setSecurityManager(new RMISecurityManager());
    for(int count=0; count<args.length; count++) {
      services.add(args[count]);
      // use context classloader to resolve class names
      Naming.rebind("//localhost/"+args[count++],
        (Remote)Thread.currentThread().getContextClassLoader().
          loadClass(args[count++]).getConstructor(
            new Class[] {String.class,int.class}).newInstance(
            new Object[] {args[count++],
              new Integer(args[count])}));
    }
  }
  /** tearDown services means unPublish */
  public static void stop() {
    Iterator allServices=services.iterator();
    while(allServices.hasNext()) {
      try{
        Naming.unbind("//localhost/"+allServices.next());
      } catch(Exception e) {} 
    }
  }
}

If you follow the steps below, you should see these two applications interoperate via a dynamically created classloader hierarchy that is quite similar to the previous example (the RMI-codebase property is required to enable dynamic classloading through rmiregistry; it's value does not matter).

javac -d classes\stock\ stock\*.java
rmic -d classes\stock\ stock.WarehouseImpl
javac -classpath classes\stock\ -d classes\sales\ sales\*.java
javac -d classes\server\ server\*.java
start rmiregistry
pause // until rmiregistry is up
java -Djava.security.policy=server.policy
  -Djava.rmi.server.codebase=infor -classpath classes\server
  server.ApplicationServer

Pressing <Enter> for the first time outputs:

Deploying file:/J:/misc/deployer/classes/stock/
Starting file:/J:/misc/deployer/classes/stock/

Deploying file:/J:/misc/deployer/classes/sales/
Starting file:/J:/misc/deployer/classes/sales/

stock.WarehouseImpl[RemoteStub [ref: [endpoint:[192.168.202.184:2053]
(local),objID:[0]]]] is about to reserve 5 items.
stock.WarehouseImpl[RemoteStub [ref: [endpoint:[192.168.202.184:2053]
(local),objID:[0]]]] is about to reserve 5 items.
stock.WarehouseImpl[RemoteStub [ref: [endpoint:[192.168.202.184:2053]
(local),objID:[0]]]] is about to reserve 5 items.
stock.WarehouseImpl[RemoteStub [ref: [endpoint:[192.168.202.184:2053]
(local),objID:[0]]]] is about to reserve 5 items.
stock.WarehouseImpl[RemoteStub [ref: [endpoint:[192.168.202.184:2053]
(local),objID:[0]]]] is about to reserve 5 items.
stock.WarehouseImpl[RemoteStub [ref: [endpoint:[192.168.202.184:2053]
(local),objID:[0]]]] is about to reserve 5 items.
... etc.

We can now experience how flexible our application server has become. After the first sales batch has been executed, please set the static "bunch" variable in sales.Order to "10". Recompile the class and press <Enter> for the second time just to see the changed byte-code running in place of the outdated sales representations. That we could realise this behaviour without having to cycle java.exe, even without affecting the referred stock logic in memory, is called "hot-redeploy" - a very useful feature when it comes to incrementally debug server-side logic or to update customer sites on the fly.

Deploying file:/J:/misc/deployer/classes/sales/
Stopping file:/J:/misc/deployer/classes/sales/
Starting file:/J:/misc/deployer/classes/sales/

stock.WarehouseImpl[RemoteStub [ref:
[endpoint:[192.168.202.184:2053](local),objID:[0]]]] is
about to reserve 10 items.
 stock.WarehouseImpl[RemoteStub
[ref: [endpoint:[192.168.202.184:2053](local),objID:[0]]]]
is about to reserve 10 items.
 stock.WarehouseImpl[RemoteStub
[ref: [endpoint:[192.168.202.184:2053](local),objID:[0]]]]
is about to reserve 10 items.
 stock.WarehouseImpl[RemoteStub
[ref: [endpoint:[192.168.202.184:2053](local),objID:[0]]]]
is about to reserve 10 items.
 stock.WarehouseImpl[RemoteStub
[ref: [endpoint:[192.168.202.184:2053](local),objID:[0]]]]
is about to reserve 10 items.
 stock.WarehouseImpl[RemoteStub
[ref: [endpoint:[192.168.202.184:2053](local),objID:[0]]]]
is about to reserve 10 items.
 stock.WarehouseImpl[RemoteStub
[ref: [endpoint:[192.168.202.184:2053](local),objID:[0]]]]
is about to reserve 10 items.
 stock.WarehouseImpl[RemoteStub
[ref: [endpoint:[192.168.202.184:2053](local),objID:[0]]]]
is about to reserve 10 items.

Warning: The deprecated Thread.stop() method used in the unDeploy() implementation has allowed me to write concise "newsletter code". Never, I repeat, NEVER use Thread.stop() because it is inherently unsafe: In our example, there is indeed the small opportunity that handling of an application error (an InvocationTargetException reported through reflecting the main method) has not yet obtained the lock to the ApplicationServer.this instance and a simultaneous unDeploy(...) will implant a shutdown exception on top of the cleanup logic. Please, consult your JDK documentation for safe ways to shutdown your threads!

A second side note: With JDK/JRE 1.3, the apparent relationship between threads and classloaders has been made explicit by associating any thread with a so-called "context classloader" (Thread.setContextClassLoader(ClassLoader loader); Thread.getContextClassLoader()). They are however still unrelated according to 99% of the JDK code which still uses Class.forName(String name) - an awful static method that delegates to the "calling class" defining classloader. The correct way of loading classes in the light of our new knowledge should be always (see ApplicationServer.deploy() and stock.Main.main()):

Thread.currentThread().getContextClassLoader().loadClass(name)

Alternative Classloading Delegation

There is an unwritten law for middleware engineers that no matter how sophisticated your framework (and with exception of the Thread.stop(), we can be really proud of our ApplicationServer, couldn't we?), after two month of releasing the system basis, the application developers will cooperatively abuse it in ways that you have never imagined before.

Be sure that this st**pid stock team will have (as pressed by the product managers and ... sic! ... customers with ... sicut! ... wishes) compiled another, undoubtfully useful, but technically devastating warehouse implementation that is able to delegate unsatisfied reservation calls to other (vendor) warehouses:

// DelegatingWarehouseImpl.java
package stock;
import sales.Order;
/** Example service that links against sales.Order and introduces
 *  a mutual dependency */
public class DelegatingWarehouseImpl extends WarehouseImpl {
  /** name of the item at our vendor */
  protected String vendorItem;
  /** construct a new delegating warehouse */
  public DelegatingWarehouseImpl(String vendorItem, int quantity)
      throws java.rmi.RemoteException {
    super(vendorItem, quantity);
    this.vendorItem = vendorItem;
  }
  /** Overrides the underCapacity reaction to order at vendor */
  protected void empty(int underLoad) {
    try {
      new Order(vendorItem, underLoad).reserve();
      quantity += underLoad;
    } catch(Exception ex) {
      throw new IllegalArgumentException(
        "Could not place order " + ex);
    }
  }
}
  javac -classpath classes\sales\;classes\stock\
    -d classes\stock\ stock\DelegatingWarehouseImpl.java

Guess what ... they will phone you and tell you that there is an ugly NoClassDefFoundError: sales.Order thrown when trying to deploy and run the freshly compiled stock (with a voice indicating that this could not be much of a problem for these Java cracks in the tech department, knowhaddimean, nudge, nudge?).

Boom! Your whole nice server-side architecture collapses as SUN does not allow you to have the salesLoader (hosting DelegatingWarehouseImpl and the Warehouse that is needed by Order) both as parent AND child of the stockLoader (hosting Order that is needed by DelegatingWarehouseImpl). They would not call them children and parent, otherwise, would they?

Your options are now (leaving the Java(tm) platform is not an option, stupid!):

Yelling: "This is not the right project structure! We must extract a more abstract masterdata. Order that is deployed in a separate application and that is used by both sales and stock." But, with a tree-like application structure that follows the SUN classloader hierarchy, this cannot be done ad infinitum. You will most likely end up having most classes in masterdata and under the responsibility of a single person (which in any middle-sized company, sits on the same floor as the system basis programmer, hence this would be not a good idea).

Crying: "Forget classloader delegation! We deploy everything into a single URLClassloader by exposing the protected addURL(URL url) method." But then, you loose the hot-deploy feature, again! In order to exchange a tiny class in a tiny module in the server, you will have to tear down and restart the whole logic. Now try to sell your customer these large extra coffee breaks at maintenance time!

Thinking: You sit down silently and release a new server. SmartApplicationServer which implements classloader delegation a bit differently to SUN without your colleagues even noticing except that they will no more detect any NoClassDefFoundErrors when implementing the next of their great ideas while you are having that large extra coffee break, a Cohiba Siglo V and the new Linux Magazine:

// SmartApplicationServer.java
package server;
import java.io.*;
import java.net.*;
import java.util.*;

/** An "application server" that copes with mutual application
 *  dependencies */
public class SmartApplicationServer extends ApplicationServer {
  /** A classloader extension that is able to delegate to other
   *  application´s classloaders */
  protected class SmartClassLoader extends URLClassLoader {
    /** mirrors parent constructor */
    public SmartClassLoader(URL[] urls, ClassLoader parent) {
      super(urls,parent);
    }

    /** dispatch "normal" loadClass method to another name */
    protected Class loadClassNormal(String name, boolean resolve)
        throws ClassNotFoundException {
      return super.loadClass(name,resolve);
    }

    /** override "normal" loadClass method in order to delegate */
    protected Class loadClass(String name, boolean resolve)
        throws ClassNotFoundException {
      try{
        // first we try it traditionally
        return loadClassNormal(name,resolve);
      } catch(ClassNotFoundException e) {
        // if this doesnt help, we ask the other application
        // threads for help
        synchronized(SmartApplicationServer.this) {
          Iterator allThreads = applications.values().iterator();
          while(allThreads.hasNext()) {
            SmartClassLoader nextLoader = (SmartClassLoader)
              ((Thread)allThreads.next()).getContextClassLoader();
            if(nextLoader!=null && !nextLoader.equals(this)) {
              try{
                // try the context class loader of next thread
                return nextLoader.loadClassNormal(name,resolve);
              } catch(ClassNotFoundException _e) {
              }
            }
          }
          // they could not help us, hence we throw an exception
          throw new ClassNotFoundException(
            "class could not be found amongst applications.");
        } // synchronized(SmartApplicationServer.this)
      }  // catch
    } // method loadClass()
  } // class

  /** override parents factory method to construct dedicated
   *  classloaders */
  public ClassLoader constructClassLoader(URL[] urls,
      ClassLoader parent) {
    return new SmartClassLoader(urls,parent);
  }

  /** example of the improved application server */
  public static void main(String[] args) throws Exception {
    BufferedReader stdin = new BufferedReader(
      new InputStreamReader(System.in));
    ApplicationServer appServer=new SmartApplicationServer();
    appServer.deploy(getStockLocation(), "stock.Main",
      new String[] {"screwdriver", "stock.DelegatingWarehouseImpl",
      "vendorScrewer", "20", "vendorScrewer", "stock.WarehouseImpl",
      "vendorScrewer","200"},null);
    appServer.deploy(getSalesLocation(), "sales.Main",
      new String[] {"screwdriver","50"}, null);
    appServer.start(getStockLocation());
    stdin.readLine();
    appServer.start(getSalesLocation());
    stdin.readLine();
    appServer.undeploy(getSalesLocation());
    appServer.undeploy(getStockLocation());
    System.exit(0);
  }
}
javac -d classes\server\ server\*.java
java -Djava.security.policy=server.policy
  -Djava.rmi.server.codebase=infor -classpath classes\server
  server.SmartApplicationServer

Let me elaborate a bit on the steps that we need to take to realise this third, comfortably sounding scenario. The key ingredient is the introduction of a SmartApplication.SmartClassLoader inner class derived from java.net.URLClassLoader that is intimately coupled to the SmartApplicationServer, more specifically, to its "applications" map that stores the running deployments.

In java.lang.ClassLoader, loadClass(String name) is fixedly and simply implemented to call loadClass(name,false) whose task is to not only to locate and intern the class, but also to immediately resolve linked classes if the second boolean parameter is set to true. We can now easily implement a different delegation semantics by overriding the second method:

First we try to call the "standard" classloading semantics which we have re-exposed as loadClassNormal(String name, boolean resolve). If this succeeds we return the found class (at the minimum "overhead" of a try/catch block).

Else (in the above example in which we have removed any classloading hierarchy, this happens both if sales.Order tries to link stock.Warehouse as well as if stock.WarehouseImpl tries to link sales.Order) we catch the ClassNotFoundException and iterate through all other deployed applications to also try loadClassNormal(...) through their main thread's context classloader until the right resource has been found.

Only if the class could not be found in all deployed applications, we throw the final ClassNotFoundException.

By these "minimally invasive" extensions, we finally produce the desired output:

Deploying file:/J:/misc/deployer/classes/stock/
Deploying file:/J:/misc/deployer/classes/sales/
Starting file:/J:/misc/deployer/classes/stock/

Starting file:/J:/misc/deployer/classes/sales/

stock.DelegatingWarehouseImpl[RemoteStub [ref:
[endpoint:[192.168.202.184:2269](local),objID:[0]]]]
is about to reserve 10 items.
stock.DelegatingWarehouseImpl[RemoteStub [ref:
[endpoint:[192.168.202.184:2269](local),objID:[0]]]] is about
to reserve 10 items.
 stock.DelegatingWarehouseImpl[RemoteStub
[ref: [endpoint:[192.168.202.184:2269](local),objID:[0]]]] is
about to reserve 10 items.
 stock.WarehouseImpl[RemoteStub [ref:
[endpoint:[192.168.202.184:2269](local),objID:[1]]]] is about
to reserve 10 items.
 stock.DelegatingWarehouseImpl[RemoteStub
[ref: [endpoint:[192.168.202.184:2269](local),objID:[0]]]] is
about to reserve 10 items.
 stock.WarehouseImpl[RemoteStub [ref:
[endpoint:[192.168.202.184:2269](local),objID:[1]]]] is about
to reserve 10 items.
 stock.DelegatingWarehouseImpl[RemoteStub
[ref: [endpoint:[192.168.202.184:2269](local),objID:[0]]]]
is about to reserve 10 items.
 stock.WarehouseImpl[RemoteStub
[ref: [endpoint:[192.168.202.184:2269](local),objID:[1]]]]
is about to reserve 10 items.

Stopping file:/J:/misc/deployer/classes/sales/
Stopping file:/J:/misc/deployer/classes/stock/

Ideally, this has been just the beginning of a wonderful exploration into the world of "how can I tweak these suboptimal JDK-classes to fit my practical needs". For example, you will most likely ask how the hell an application programmer or even a customer deployer should be able to manage all those dependencies between applications (usually: jar-files, such as stock.jar and sales.jar) once your system has evolved to a particular size. The solution to this is quite straightforward: Jar dependencies are usually encoded in the Manifest.mf class-path entry and that is where your application server can obtain them in order to identify deployment needs and deployment order.

Similarly, the loadClass calls delegated from one application to another (in a full-blown implementation, also calls to getResource(...) must be delegated) introduce runtime dependencies: If you cycle the byte code of the "defining application" you must also cycle the dependent byte-code of the consuming application. I leave this (together with caching of class locations) as an exercise for you.

Alternatively, if you are a bit lazy, nevertheless curious, i.e., your are a system programmer, you can have a look at the latest org.jboss.deployment.scope.* sources of the JBoss Open Source application server (https://www.jboss.org) whose ingenious mastermind, Marc Fleury, has been the originator of the presented classloading semantics. Besides clustering support, the goal for the next major release of Jboss (3.0 - Project Rabbit Hole) is to have a deployer that can manage multiple scopes, i.e., "virtual applications" such as we have today constructed out of stock.* and sales.*

He, he!


Thanks for taking the time to read this newsletter, please also take the time to give us feedback, we always appreciate it.

Heinz

 

Comments

We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)

When you load these comments, you'll be connected to Disqus. Privacy Statement.

Related Articles

Browse the Newsletter Archive

About the Author

Heinz Kabutz Java Conference Speaker

Java Champion, author of the Javaspecialists Newsletter, conference speaking regular... About Heinz

Superpack '23

Superpack '23 Our entire Java Specialists Training in one huge bundle more...

Free Java Book

Dynamic Proxies in Java Book
Java Training

We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.

Java Consulting

We can help make your Java application run faster and trouble-shoot concurrency and performance bugs...