Inside Paulo Abrantes' head
[ start | index | login or register ]
start > 2007-07-23 > 1

Java Programming: Hot Deploy

Created by pabrantes. Last edited by pabrantes, one year and 77 days ago. Viewed 6,283 times. #5
[diff] [history] [edit] [rdf]
labels
attachments
flowchart.png (47438)
model.png (39332)
pabrantes-HotDeploy-POC.tar.gz (4420)

Java Programming: Hot Deploy

This post is the second part of >>Java Programming: First steps with ClassLoaders. In the first part I explained what were class loaders, how they worked and why such part of the JVM might need customization.

A possible use for customized class loaders is hot deploy. This feature is the one I'm currently most interested and it's the one I'll be writing about.
I'll explain the motivation, the concept, the implementation and finally the integration. I'll also provide the source code of a simple proof of concept of everything that will be described in this post.

Like I said before hot deploy is the name given to the ability of loading new versions of a class in runtime, such feature is quite popular among java containers like tomcat or jboss.

The motivation behind such feature is simple, availability. Nowadays, with all kind of services online and everyone using them, availability is even more critical than in the beginning of the internet. After all, now there are many companies doing business on the internet and downtime for them means less profit.
Switching classes in runtime avoids server's downtime, hence, better availability. That's why, in my opinion, most of the Java containers support hot deploy.

Although hot deploy seems a good feature, it's not natively supported by the Java Virtual Machine (JVM) because, like I've said in the previous post, once a given class is defined by a ClassLoader it cannot be redefined. That's where a custom class loader enters. The idea is simple, exploit the JVM behaviour.

Classes are not only identified by it's package name and class name, but also by the class loader instance that defined the class. Hence, using a new instance when needed will allow the developer to load the new class version into the JVM. It might sound simple, but the process itself has implications regarding how to deal the new class casting. Other problems related with the use of class loaders are:

  • Memory usage. Various definitions of classes will be loaded into memory, since new class loaders instances keep defining new versions of classes.
  • The possible scenario of old instances and new instances co-existing, this is specially problematic if object serialization is being used.
These are the kind of trade-offs that should be thought through when thinking in implementing hot deploy within an application.

Implementation

I think by now it's clear that each time a new version needs to be loaded, a new instance of a class loader as to be created, that's what creates the problem of class casting. There are at least three ways of solving such problem:

  1. Using reflection: Treating every object loaded by the custom class loader as a java.lang.Object and using Java Reflection API to invoke methods. This, in my opinion, isn't the best solution.

  2. Using the same base class in hierarchy: This implies that a base class could never be hot deployed, since it could never be redefined and that all the class loaders instances would return the same definition (based on delegation).

  3. Using the same interface for each class hierarchy: This approach is similar to the one previously suggested, interfaces are delegated to base class loaders so are always the same and each class that supports hot deploy will have to implement one of those interfaces.
After presenting these solutions, it's once again visible that delegation plays an important role in the class loading system.

Another problem that should be solved - although not necessary - is making the new class loaders creation process transparent. The solution I suggest is encapsulation, instead of having only a single class loader class, actually there are two, but one of them - let's call it RuntimeClassLoader - won't be visible to the developer that is using the actual custom class loader.

The class loading cycle for the custom class loader can be described by the following flowchart:

flowchart

The previous flowchart shows two interesting things:

  • The class loader contains a delegation list. The idea is to let the developer decide which classes are loaded only once and which are reloaded.
  • The class loader contains a class cache. If a class isn't in the delegation list, but it's still up to date reloading it would just be an useless overhead.
The cache is nothing more than a simple HashMap of class names related with an object that contains the actual Class object and the file system's modification timestamp for the class file (which in my implementation I called ClassHolder).

Show HotDeployClassLoader source code
package net.pabrantes.classLoaders;

import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map;

public class MyClassLoader extends ClassLoader {

private Map<String,ClassHolder> classTimeStamps; private List<String> classesToDelegate;

public MyClassLoader(ClassLoader classLoader) { super(classLoader);

classTimeStamps = new HashMap<String, ClassHolder>(); classesToDelegate = new ArrayList<String>(); }

public void addClassToDelegateList(String name) { classesToDelegate.add(name); }

protected boolean isClassInDelagationList(String name) { return classesToDelegate.contains(name); }

@Override protected Class<?> findClass(String name) throws ClassNotFoundException { if(!classTimeStamps.containsKey(name) || oldTimeStamp(name)) { System.out.println(name + " needs loading"); classTimeStamps.remove(name); return new RuntimeClassLoader(this).loadClass(name); } else { System.out.println(name + " is still up to date"); return classTimeStamps.get(name).getClazz(); } }

private boolean oldTimeStamp(String name) { File file = new File(name.replace(".","/") + ".class"); return file.lastModified() > classTimeStamps.get(name).getTimeStamp(); }

@Override public Class<?> loadClass(String name) throws ClassNotFoundException { return (isClassInDelagationList(name)) ? getParent().loadClass(name) : findClass(name); }

private class ClassHolder { private Class clazz; private long timeStamp;

public ClassHolder(Class clazz, long timeStamp) { this.clazz = clazz; this.timeStamp = timeStamp; }

public Class getClazz() { return clazz; }

public long getTimeStamp() { return timeStamp; }

}

private class RuntimeClassLoader extends ClassLoader { public RuntimeClassLoader(ClassLoader classLoader) {

super(classLoader); }

@Override protected Class<?> findClass(String name) throws ClassNotFoundException { return loadClass(name); }

public Class<?> loadClass(String name) throws ClassNotFoundException { if (isClassInDelagationList(name)) { return getParent().loadClass(name); } byte[] classBytes = null; try { classBytes = getBytes(name.replace(".", "/") + ".class"); } catch (IOException e) { return findSystemClass(name); }

Class clazz = defineClass(name, classBytes, 0, classBytes.length); File file = new File(name.replace(".","/") + ".class"); classTimeStamps.put(name,new ClassHolder(clazz,file.lastModified())); return clazz; }

private byte[] getBytes(String filename) throws IOException {

File file = new File(filename); long len = file.length(); byte raw[] = new byte[(int) len]; FileInputStream fin = new FileInputStream(file); int r = fin.read(raw); if (r != len) throw new IOException("Can't read all, " + r + " != " + len); fin.close(); return raw; } }

}

Integration

In order to test the class loader a simple application was developed. It's a terminal application that has a command line and is implemented using the>>command design pattern.
The objective is simple, create the application in a way that the commands are hot deployable.

The development is quite simple:

  1. Creation of a simple application based on the Command Design Pattern;
  2. Creation of a file system monitor that can identify changes in the file system;
  3. Integration of both sub-applications listed in 1 and 2 and the class loader.
To explain the integration process I'll use a top down approach because I think it will be simpler to understand. Below is the UML for the complete system.

model

The CoreEngine class is the application core and it uses two things:

  • The HotDeployClassLoader to be able to load new commands or modifications of existing ones in runtime.
  • The FileSystemWatcher to be notified when it commands have changed.
The FileSystemWatcher is a process that runs in an independent thread and monitors a given file system directory, in this case it will be monitoring the commands directory.
The main idea is to create a snapshot of certain file types, in this case class files (jars could also be monitored). By snapshot I mean a file representation containing it's name and modification timestamp.

In given intervals the FileSystemWatcher will compare it's internal snapshot against the actual directory state and if it doesn't match it notifies all INotifyClient's that are registered. In this case it's our CoreEngine that is registered at the FileSystemWatcher.

Show FileSystemWatcher source code
package net.pabrantes.fsWatcher;

import java.io.File; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Set; import java.util.TreeSet;

public class FileSystemWatcher implements Runnable {

private static Comparator<FileRepresentation> comparator = new Comparator<FileRepresentation>() {

public int compare(FileRepresentation fr0, FileRepresentation fr1) { return fr0.getName().compareTo(fr1.getName()); }

};

protected File directory; protected Set<FileRepresentation> representations; protected List<INotifyClient> clients;

private FileRepresentation[] cachedArray = null; private long sleepTime = 10000;

public FileSystemWatcher(String directoryName) { File directoryToWatch = new File(directoryName); if (!directoryToWatch.isDirectory()) { throw new RuntimeException("It needs to be a directory"); } directory = directoryToWatch; representations = getCurrentRepresentation(); clients = new ArrayList<INotifyClient>(); }

public void setSleepTime(long sleepTime) { this.sleepTime = sleepTime; }

public long getSleepTime() { return this.sleepTime; }

public void registerClient(INotifyClient client) { clients.add(client); }

public Set<FileRepresentation> getCurrentRepresentation() { Set<FileRepresentation> representations = new TreeSet<FileRepresentation>( comparator);

for (File file : directory.listFiles(new JavaRunnableFilenameFilter())) { representations.add(new FileRepresentation(file.getName(), file .lastModified())); } return representations; }

private FileRepresentation[] getCachedArray() { if (cachedArray == null) { cachedArray = new FileRepresentation[representations.size()]; representations.toArray(cachedArray); } return cachedArray; }

public void notifyListeners() { for(INotifyClient client : clients) { client.notifyModification(); } }

public void modificationTrigger(Set<FileRepresentation> newData) { System.out.println("Detected modification"); representations = newData; cachedArray = null; notifyListeners(); }

public void run() {

for (;;) { Set<FileRepresentation> currentRepresentation = getCurrentRepresentation(); if (currentRepresentation.size() != representations.size()) { modificationTrigger(currentRepresentation); }

FileRepresentation[] inCache = getCachedArray(); FileRepresentation[] current = new FileRepresentation[currentRepresentation.size()]; currentRepresentation.toArray(current);

for (int i = 0; i < current.length; i++) { if (!current[i].equals(inCache[i])) { modificationTrigger(currentRepresentation); } }

try { Thread.sleep(getSleepTime()); } catch (InterruptedException e) { return; } }

} }

When the CoreEngine is notified by the FileSystemWatcher it reloads the commands.
On a regular Command Pattern is normal to see such piece of code:

public List<ICommand> loadCommands() { List<ICommand> commands = new ArrayList<ICommand>(); commands.add(new Command1()); commands.add(new Command2()); return commands; // When there's a new command add a line here }

But since in this particular case the loading of ICommand classes is done at runtime in a dynamic way (meaning that can be added new commands or remove existing), no implementations can be specified in the code.

So the previous code actually becomes:

public List<ICommand> loadCommands() { List<ICommand> commands = new ArrayList<ICommand>(); try { File directory = new File("commands"); for (String name : directory.list(new JavaRunnableFilenameFilter())) { Class commandToLoad = this.hotDeployLoader.loadClass("commands." + name.split("\\.")[0]); if (!commandToLoad.isInterface()) { newCommands.add((ICommand) commandToLoad.newInstance()); } } } catch (Exception e) { System.err.println("Something went wrong, skipping class update"); System.err.println(e.getMessage());

// return the old command list, so the application still // keeps commands. return this.commands; } return commands; }

The test if a given loaded class is or isn't an interface is being done because ICommand - the interface that is being used - is also in the same package. This test could be discarded if the interfaces would be put in another package.

The complete source code of this proof of concept is also >>available for download (no build file is provided). In order to test the application, the following steps have to be done:

  1. Code compilation
  2. Start CoreEngine
  3. Modify an existing command or create a new one putting it in the command directory
  4. Compilation of new and modified commands
Improvements

All applications can be improved. Even if it's only a simple example, there are still possible improvements. Here are some that I can recall at the moment.

  • Improve notification system in order to notify which files have been modified, created and added.
  • Improve modification detection algorithm.
  • Create the concept of context in CoreEngine, so commands would know the context are being ran and use it, for example, to implement the undo task.
  • Creation of a build file with regular build tasks and also hot deploy tasks.
Conclusions

Hot deploy isn't the holy grail, but it may be a useful feature in some cases. It has been shown that implementing hot deploy isn't that hard but there are certain details that cannot be overlooked, such as the class casting problem and the memory usage.

Hope it has been useful. Comments are more than welcome.

Icon-Comment m4ktub, one year and 77 days ago. Icon-Permalink

That's a very nice example. I just wanted to leave a URL to related work in the Sun's JVM Hot Swap mechanism.

>>http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4910812

This (more in the PDF available through there) shows that the JVM already supports some of the desired features and people are considering the inclusion of more.

(bah, can't think straight right now, so see! no bashing smiley )

Icon-Comment pabrantes, one year and 77 days ago. Icon-Permalink

Thanks for the link, during all this tests I disregarded the Hot Swap mechanism, I'll have to look more into it too.

(bah, can't think straight right now, so see! no bashing smiley)
m4ktub

You need vacations! smiley

Please login to www.pabrantes.net.
Who am I?
paulo-roca2My name is Paulo Abrantes AKA pabrantes and I'm a software developer. I'm currently employed at >>CIIST working as a Java developer in >>FenixEDU.

This blog is mostly about Java programming, domain driven design and snipsnap bliki developing. Everything written in this blog is my personal opinion and it may not reflect the opinions of my employer and co-workers.


Blog subscription
subscribe by rss subscribe by email

Links
>> Home
>> Paulo's Profile
>> Post History
>> Add to Technorati Favorites
>> Paulo's Photo Gallery
>> WishList
>> Posting without Login

Search Blog
Fellow Bloggers

Recent Posts

Java Programming: Bytecode Injection
Intermission: Sorry For Downtime
Software Developing: Studying The Bliki Domain Model
SnipSnap Developing: Trying to settle a roadmap
System Administration: Load Balancing with Apache
Blogging: Two years have passed
Software Developing: The SnipSnap Saga
Java Programming: Getting your code spicy with Groovy
Software Developing: Fluent Interfaces
Software Developing: Implementing a ShoutBox on SnipsSnip
Software Developing: SnipSnap, SnipIt and SnipSnip
Java Programming: Proxies and Access Control
Java Programming: Proxies and References
Java Programming: References' Package
YALM: Yet Another Layout Modification

For older posts, please refer to post-history for a complete Post History

Logged in Users: (0)
… and 6 Guests.
This is a modified version of snipsnap.org created by >>Paulo Abrantes