Zero turn-around in Java?
November 21st, 2006 by Jevgeni KabanovZero turn-around is one of the selling points of PHP, Ruby, Python and other interpreted languages. Turns out it also works out-of-the-box with Aranea and here we’ll describe how we did it, what are the limitations and show off the compositional nature of Aranea. If you just want to get it to work see the reference manual entry on the topic.
Zero turn-around usually refers to the way changes made to the code are immediately visible in most interpreted languages. Indeed, since there is no compilation and almost no deployment, the time from making a change to seeing it in the browser is a fast as pressing Ctrl + S, Alt + Tab and F5. The situation in the JEE world is different — although compilation is relatively fast, together with deployment, cache reloads and framework initialization it might easily take up to a minute to see a change propagate in a large application.
The problem is that JVM spec clearly states that we can’t just reload code of a single class after it has been loaded into the JVM. HotSpot JVM offers some way by allowing to replace the code of the methods, but so far it forbids to anyhow alter structure and signatures, so it’s only of limited use. What we can do is to load classes using a particular (possibly our own) classloader, and then let it to be garbage collected among with all loaded classes. Then next time an instance is created the class will be loaded anew.
However even this won’t give us the desired result, as any existing instance of a class will still hold on to its previous definition (with the old classloader) and now we might actually have two conflicting class definitions in two different classloaders which can lead to all sorts of trouble. So we also need to somehow reload the object state in a new classloader.
Turns out that we can archive this with serialization — we just need to modify the ObjectInputStream to take a classloader parameter and load the instances using its classes. Thus what we have to do is:
- Load the classes using our classloader.
- Serialize the state of the instances
- Drop the old classloader and create a new one
- Deserialize state of the instances in the new classloader
Of course if some objects cannot be serialized (e.g do not implement Serializable or contain non-serializable fields) the whole scheme fails, since we cannot preserve their state.
Before we get to the actual Aranea stuff let’s try to implement the reloading procedure, that takes an object and reloads it using a fresh classloader. We know it should work through serialization, so it should look something like this:
...
private Serializable reload(Serializable child)
throws Exception {
return deepCopy(newClassLoader(), child);
}
...
deepCopy should just serialize and deserialize the object. There is one catch though — it should also resolve the classes using our own classloader, so we have to make a subclass of ObjectInputStream:
...
private static class ReloadingObjectInputStream
extends ObjectInputStream {
private ClassLoader cl;
public ReloadingObjectInputStream(
ClassLoader cl,
InputStream in)
throws IOException {
super(in);
this.cl = cl;
}
/*
* This method is used to resolve the classes
* when creating object instances.
*/
protected Class resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {
String name = desc.getName();
return cl.loadClass(name);
}
}
...
The deepCopy() method itself is straightforward and we could have used Apache SerializationUtils methods if it weren’t for the custom ObjectInputStream:
...
private Serializable deepCopy(
ClassLoader cl,
Serializable original) throws Exception {
//Serialize to a byte array
ByteArrayOutputStream baos =
new ByteArrayOutputStream(512);
ObjectOutputStream out =
new ObjectOutputStream(baos);
try {
out.writeObject(original);
}
finally {
out.close();
}
byte[] serialized = baos.toByteArray();
//Deserialize to an instance
ByteArrayInputStream bais =
new ByteArrayInputStream(serialized);
ReloadingObjectInputStream in =
new ReloadingObjectInputStream(cl, bais);
Object obj = in.readObject();
return (Serializable) obj;
}
...
We could have used just a usual classloader (most common is URLClassLoader) and just point its classpath to the “/WEB-INF/classes”. However there is also a trouble that the same classes are in the classpath of the web application classloader. The Java specification instructs classloaders to try loading classes with the parent classloader first, however in our case we would be able to reload those classes, so we need to invert this preference by delegating to parent only if we cannot find the class in the classpath:
...
private static class ReloadingClassloader
extends URLClassLoader {
public ReloadingClassloader(
URL[] urls,
ClassLoader parent) {
super(urls, parent);
}
public Class loadClass(String name)
throws ClassNotFoundException {
//If already loaded just return
Class c = findLoadedClass(name);
if (c != null)
return c;
//First try own classpath
//then delegate to parent
try {
return findClass(name);
}
catch (ClassNotFoundException e) {
return super.loadClass(name);
}
}
}
...
We create the classloader by putting “/WEB-INF/classes” into the classpath. getEnvironment().getEntry() is just a way in Aranea to look up services, in this case we need a ServletContext:
...
private ClassLoader newClassLoader()
throws MalformedURLException {
//Get the ServletContext
ServletContext sctx =
(ServletContext) getEnvironment().getEntry(
ServletContext.class);
//Return a classloader for "/WEB-INF/classes"
return new ReloadingClassloader(
new URL[] {sctx.getResource("/WEB-INF/classes")},
getClass().getClassLoader());
}
...
Now we have most of the machinery in place and before we move on to Aranea filter implementation we can spare a thought on the limitations of the solution. Obviously it will only work on serializable classes. What’s more, to get actual gain from it all classes should have serialVersionUID set to some fixed number (e.g. “0″). This should be done so that small changes in the method and class signatures wouldn’t cause a deserialization failure. Even then removing a class (even an inner or anonymous class) will cause serialization to fail as well as changing the field types or order. However it is still much more than HotSwap would allow (at least at the moment).
Now we can go on with implementing the Aranea filter that will do the work. There are several reasons why this solutions suits Aranea so well:
- Aranea is originally assembled from independent components by containment. Therefore one filter can define the classloader to load its children and do the rest of the tricks.
- All application widgets in Aranea are serializable and loaded by the parent classloader (most of the filters are configured by Spring and loaded by its classloader).
- Aranea components can only access their parents through the environment, which can be taken away at any given moment. Moreover parents have references only to their direct children, and can communicate with the indirect children only through messages.
Thus we can create a filter and put it in appropriate place just above the application widgets and we will be able to seamlessly reload all application widgets code.
At the moment we will assume that creating a new classloader and serializing/deserializing does not visibly affect response time in development, so we will just reload all classes before every request. Thus we do not need to check exactly which classes have changed, since any possible changes will be reloaded.
Let’s start by creating the filter service itself. It will have to take the child class name as a string, since we will need to load it reflectively in a freshly created classloader.
public class StandardClassReloadingFilterWidget
extends BaseApplicationWidget {
private String childClassName;
private RelocatableWidget child;
public void setChildClass(String childClass) {
this.childClassName = childClass;
}
protected void init() throws Exception {
//Create the classloader and use it
//to load the child class
ClassLoader cl = newClassLoader();
Class childClass = cl.loadClass(childClassName);
//Create an instance of child class and
//attach it to the filter
child =
new RelocatableDecorator(
(Widget) childClass.newInstance());
addWidget("c", child)'
}
...
Here we create the child widget reflectively in the classloader and add it under a name “c”. The only interesting part is that we also wrap it into a RelocatableDecorator, which we will return to later. Now the next method is called before every request and will do the actual reloading:
...
protected void update(InputData input)
throws Exception {
try {
//Remove all references to parents and
//reload the child classes
child._getRelocatable().overrideEnvironment(null);
child = (RelocatableWidget) reload(child);
}
catch (ClassNotFoundException e) {
log.error("Failed to reload widget classes", e);
}
finally {
//Restore the references to parents
child._getRelocatable()
.overrideEnvironment(getEnvironment());
}
//Reattach the child new instance to the filter
_getComposite().attach("c", child);
}
...
Here the point of wrapping the child into the RelocatableDecorator comes out — as all references from children to parents in Aranea have to go through the environment, by removing the environment from the child we also remove all possible links up allowing to serialize children only.
Now the filter is complete (except for the trivial render() method which we leave as an exercise for the reader) and can be tested in e.g. main example or any other Aranea application by adding a similar configuration entry:
<bean id="araneaApplicationStart" singleton="false" class="org.araneaframework.framework.filter.StandardClassReloadingFilterWidget"> <property name="childClass" value="org.araneaframework.example.main.web.DevelWidget"/> </bean>
Instead of the usual:
<bean id="araneaApplicationStart" class="org.araneaframework.example.main.web.LoginWidget" singleton="false"/>
You will most probably want to define a different root widget class (in this case DevelWidget), which will start the usual LoginWidget in a StandardFlowContainerWidget. Otherwise you will lose the reloading filter on login.
So, on the plus side we managed to build a useful feature that will save countless developer hours by adding just one filter in a correct place. We also worked through a nice enough example of how to add framework features to Aranea (there definitely are a couple of gotchas here, but they are few and described in the reference manual). The filter can be used to reload widgets and all their helper classes, which makes up basically all of the Aranea application web layer. Since JSPs and most other templating engines can also reload their code easily we get to see all of the changes to the web almost instantly.
On the minus side we are reloading all of the classes in the web layer and serializing/deserializing all of the state on every request, which may become costly for a large application. We could somewhat remedy it by checking that classes have changed before reloading them. Also this trick won’t work for the business layer, since it is usually not serializable and can be referenced from many different places. There are some possible ways around it by they need much more effort and deserve a separate post in the future
November 21st, 2006 at 6:13 am
I would have thought that whatever persistence layer you are using would be more appropriate than serialisation/deserialisation (sorry for the English English spellings). E.g., synchronise your objects with your DB, drop the connection, drop the classloader, load the new classloader, load the connected objects from the DB again.
Saying that, I’m a JSE user, not JEE, so I already have a small turn-around time, and probably don’t know enough of the issues involved with JEE..
Cheers.
November 22nd, 2006 at 9:24 am
Is zero turn-around really a big deal?…
Is zero turn around really that important for Java’s core user base? I just read the zero turn around blog entry over at the Aranea Blog and while this is a good question I’m not convinced this is such a big deal.
Let’s face it, the…
November 22nd, 2006 at 10:35 am
[...] Aranea Blog News, comments and rants by the Aranea framework team. « Zero turn-around in Java? [...]