Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

JRebel Unloaded

DZone's Guide to

JRebel Unloaded

Investigate class reloading in this extremely interesting article about trying to recreate JRebel's renowned class reloading features.

· Java Zone
Free Resource

Try Okta to add social login, MFA, and OpenID Connect support to your Java app in minutes. Create a free developer account today and never build auth again.

Welcome to the second installment the Discotek.ca series on byte code engineering. The first article, an overview of byte code engineering, can be found here.

JRebel is indisputably the industry leading class reloading software. It is a useful product has earned its reputation by helping to expedite Java development for many organizations. How this product works is a mystery to most. I’d like to explain how I think it works and provide a basic prototype (with source code).

Since the adoption of application servers to insulate business logic from generic plumbing logic, developers have been suffering through the time consuming process of building and redeploying before testing server side code changes. The larger the application, the longer the build/redeploy cycle tends to be. For a developer who tests frequently, the time spent building and redeploying can consume a significant part of a work day. The actual cost to a project can be equated to the number of developers * salary/per hour * number of hours spent building and redeploying. This figure does not have to be just the cost of doing business.

Some time ago when I was exploring instrumentation, I wrote a product called Feenix, which I thought would help people overcome the same class reloading as JRebel, but that didn’t happen. The product still exists on my web site, but I doubt anyone actually uses it. For now, I keep it there as a painful reminder of my failure, which should inspire me to build a better one. I didn’t understand why my product failed until Anton Arhipov, a JRebel author, provided some insightful criticism:

Feenix can do as much as the Java Instrumentation API allows it to do. Which basically means it doesn't really add value on top of standard HotSwap of the JVM. 

There are several products that provide a mechanism to modify class functionality in a running JVM, but they are not all created equal. Probably the most well known is Java’s built-in hotswap, which IDE’s like Eclipse take advantage of in debug mode. Others, like Feenix, take advantage of Java’s built-in instrumentation API. Due to limitations of the JVM, most of these attempts fall short. Specifically, the JVM limits the types of changes allowed to a loaded class. For instance, the JVM will not allow you to change the class schema. This means you cannot change the number of fields or methods or their signatures. You also cannot change the inheritance hierarchy. They also cannot alter the behavior of existing objects. Unfortunately, this dramatically diminishes the utility of these products.

Enter JRebel. JRebel appears to be the most functional and praised class reloading product in the marketplace. It has very few shortcomings and appears to be extremely well supported. JRebel is a commercial product and is likely to be prohibitively expensive to most developers who pay for tools out of their own pocket. The JRebel supporters have published some articles discussing how they have solved various class reloading problems, but as they are a commercial product they naturally do not discuss implementation in detail. Knowing the details may lead to an alternative open source product. If there is enough interest, I’ll integrate the JRebel style class reloading into Feenix and open source it.

Creating a class reloading mechanism (CRM) must solve several problems:

  1. The CRM must be aware of the where the new versions of classes are located. These classes may be on a local disk or in a remote location. They may be bundled in a jar, war, or ear.
  2. While not technically class loading, the CRM should also support the reloading of non-class resources like images or html files.
  3. The CRM should ensure that when a classloader loads a class for the first time, it loads the latest version. Despite a class being already loaded by a classloader, the CRM should ensure new instances of a class will use the functionality of the latest version of a class.
  4. The CRM should ensure that the functionality of existing objects should use the functionality of the latest version of its class.
  5. While class reloading is clearly the core functionality required by any CRM, there are common frameworks used in many applications whose re-configuration would require a build/redeploy cycle. These changes ought to be less frequent than code changes, but there is still value in providing reload functionality of this kind.

The fourth problem above dwarfs the others in terms of complexity, but also usefulness. It is less expensive for application servers to reuse pooled objects rather than always create new instances. Unless a CRM can make pooled instances aware of class changes, it will serve very little purpose. The JRebel developers claim to do “class versioning” to solve these problems, but leave much room for interpretation of the implementation. We know that class loaders may only load a class once. The exception to this rule is instrumentation, but we know this isn’t how JRebel has solved this problem (mainly because they are open about it, but also) because instrumentation will not allow the class schema to be changed. Another approach to CRM design is commonly known as “throw-away classloaders”, which uses a new class loader to load each new version of a class. This design has many drawbacks, but above all cannot solve the problem of introducing new functionality to existing objects.

To introduce new functionality to existing objects, their execution must be forwarded to a method which contains the new functionality. As a class loader may load a given class only once, the new functionality must be hosted in a class with a new unique name. However, a class cannot know the name of it’s successor at compile- or run-time. We can use instrumentation to modify a class as it is loaded, but we won’t know the names of its successors until the CRM detects new compiled classes and makes them available to the JVM. Two mechanisms could be used to forward execution to its successor: reflection or an interface. Reflection can inspect a class’ methods and invoke the method with the matching name and signature. Reflection is known to be slow and is not suitable to be applied to every method invocation. Alternatively, an interface could be created which defines a method to allow invocation of any method in the successor class generically. Such a method might have the following name and signature:

public Object invoke(int methodId, Object invoker, Object args[]);

If the newer version of a given class implements this interface, execution can forwarded to the appropriate method. The methodId parameter is used to determine the method. The invoker parameter provides access to the state (fields) of the original object, and the args parameter provides the new method with access to the original method’s arguments.

A working solution has many more moving parts than the above outline. It also introduces two additional problems to solve. Each call to a reloaded object’s method will produce an extra unexpected frame on the stack, which may be confusing to developers. Any use of reflection on reloaded classes may not behave properly (given the class name has changed and an invoke method has been added, the inheritance hierachy doesn’t exist, etc). Identifying such problems is important as well as providing working solutions. Solving all of the above problems in one article will probably lead to heavy eyelids. Instead, let’s focus on a rudimentary implementation of the class forwarding functionality. We can always revisit the other issues in another article if there is interest.

This article will cover the following functional parts of a class reloading mechanism:

  1. A central component for discovering and managing class versions
  2. Generate a successor class and the interface to reference it
  3. Modify an application class to forward method calls to its successors
  4. Modify java.lang.ClassLoader to install the above functionality

Before diving into the details, I’d like to warn you that I have re-written this article twice. Despite my keen interest in byte code engineering, even I was boring myself to tears writing explanations of the ASM code. Consequently, this third and hopefully final draft will contain much less ASM code than the others. It will focus more on how class reloading works, but you can always refer to the source code in theResources section to see the implementation details.

Class Reloading Mechanism Design

The Class Version Manager (AKA ClassManager) is going to have several jobs:

  • Load a configuration which specifies the name-space of classes to reload and where to find them
  • Determine if a class version is out-dated
  • Provide the byte code for:
    • the new versions of a given class
    • the generic invokable interface class
    • the interface implementation class (which contains the new functionality)

If I discuss all of the above in detail, this article will be longer than War and Peace. Instead, I’ll gloss over the details that are not directly related to byte code engineering. For detailed information
on the configuration, you can look in ca.discotek.feenix.Configuraton and the static initializer of ca.discotek.feenix.ClassManager. Here is a sample configuration file:

<feenix-configuration project-name="example">
    <classpath>
        <entry>C:/eclipse/workspace/my-project/bin</entry>

        <!-- alternatively, you can use jar, war, and ear files -->
        <entry>C:/eclipse/workspace/my-project/dist/example.jar</entry>
        <entry>C:/eclipse/workspace/my-project/dist/example.war</entry>
        <entry>C:/eclipse/workspace/my-project/dist/example.ear</entry>

        <!--  Use the exclude tag to exclude namespaces. It uses a Java regular expression. -->
        <exclude>ca\.discotek\.feenix2\.example\.Example</exclude>
    </classpath>
</feenix-configuration>

To specify the location of the configuration file, use the feenix-config system property to specify the fully qualified path.

To determine if a class is outdated, we’ll use the following code found in ca.discotek.feenix.ClassManager:

static Map<String, Long> classTimestampMap = new HashMap<String, Long>();

static boolean isOutDated(String className, long timestamp) {
    Long l = classTimestampMap.get(className);
    if (l == null) {
        classTimestampMap.put(className, timestamp);
        return false;
    }
    else {
        classTimestampMap.put(className, timestamp);
        return timestamp > l;
    }
}

The caller passes in the name of the class and the timestamp of class they wish to test.

The last task of the Class Manager is to provide class byte code, but let’s first revisit exactly how classes will be reloaded. One important step is overriding the JVM’s java.lang.ClassLoader class such that it can instrument application classes as they are loaded. Each application class will have the following functionality inserted into the start of each method: if a new class version exists, forward execution to the corresponding method in an instance of that new class. Let’s look closer with a simple example of an application class:

class Printer {
    public void printMessage(String message) {
        System.out.println(message);
    }
}

The above class would be instrumented by our special java.lang.ClassLoader to look something like this: 

class Printer {

    Printer_interface printerInterface = null;

    static void check_update() {
        Printer_interface localPrinterInterface = ClassManager.getUpdate(ca.discotek.feenix.example.Printer.class);
        if (localPrinterInterface != null)
            printerInterface = localPrinterInterface;
    }

    public void printMessage(String message) {
        check_update();
        if (printerInterface != null) {
            printerInterface.invoke(0, this, new Object[]{message});
            return;
        }
        else {
            System.out.println(message);
        }
    }
}

The modified version of Print class has the following changes:

  • The Printer_interface printerInterface field was added.
  • The check_update method was added.
  • The printMessagemethod now has the logic:
    1. Check for a class update
    2. If an update exists, invoke the corresponding method in the new class.
    3. Otherwise, execute the original code

The check_update method calls ClassManager.getUpdate(…). This method will determine if an update is available and if so, generate a new implementation class:

public static Object getUpdate(Class type) {
    String dotClassName = type.getName();
    String slashClassName = dotClassName.replace('.', '/');

    File file = db.getFile(slashClassName + ".class");
    if (file != null && file.isFile()) {
        long lastModified = file.lastModified();
        if (isOutDated(dotClassName, lastModified)) {
            String newName = slashClassName + IMPLEMENTATION_SUFFIX + getNextVersion(slashClassName);
            byte bytes[] = getClassBytes(newName);
            try {
                Method method = ClassLoader.class.getDeclaredMethod("defineMyClass", new Class[]{String.class, byte[].class});
                Class newType = (Class) method.invoke(type.getClassLoader(), new Object[]{newName.replace('/', '.'), bytes});
                return newType.newInstance();
            }
            catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    return null;
}

Once getUpdate(…) has called ClassManager.getClassBytes(…) to retrieve the raw bytes representing the class, it will use reflection to call a defineMyClass method in java.lang.ClassLoaderdefineMyClass is a method we’ll add later when we generate a custom java.lang.ClassLoader class. To convert raw bytes to a java.lang.Class object, you need to have access to the defineClass methods in java.lang.ClassLoader, but they all are restricted to protected access. Hence, we add our own public method which will forward the call to a defineClass method. We need to access the method using reflection as it does exist at compile time.

The modified Printer class introduces the Printer_interface class and the ClassManager.getUpdate(…) method introduces the new version of the Printer class, Printer_impl_0, which implements the Printer_interface interface class. These classes will not exist on the application classpath as they are generated at run-time. We’ll override java.lang.ClassLoader‘s loadClass methods to call getUpdate(…) has calledClassManager.getClassBytes(…) to discover new versions of our application classes and generate the interface and implementation classes as needed. Here is the getUpdate(…) has called getClassBytes(…) method:

public static byte[] getClassBytes(String slashClassName) {
    if (isInterface(slashClassName))
        return InterfaceGenerator.generate(slashClassName, trimInterfaceSuffix(slashClassName));
    else if (isImplementation(slashClassName)) {
        String rootClassName = trimImplementationSuffix(slashClassName);
        File file = db.getFile(rootClassName.replace('.', '/') + ".class");
        if (file != null)
            return ImplementationGenerator.generate(slashClassName, file);
    }
    else {
        File file = db.getFile(slashClassName + ".class");
        if (file != null)
            return ModifyClassVisitor.generate(slashClassName, file);
    }

    return null;
}

There are a lot of implementation details that are not obvious from this method. The isInterface and isImplementation methods examine the class name suffix to make their determinations. If the class name suffix does not match the interface or implementation class known suffix formats, a request is for a regular class.

If the requested class is for the interface class that an implementation class implements, InterfaceGenerator.generate(…) is invoked to generate the interface class. Here is the generated interface’s invoke method for the Printer example:

public java.lang.Object __invoke__(int index, ca.discotek.feenix.example.gui.Printer__interface__, java.lang.Object[]) 

The ImplementationGenerator class is used to generate the class that implements the interface generated by InterfaceGenerator. This class is larger and more complicated than InterfaceGenerator. It does the following jobs:

  1. Generates the raw byte code for a class with a new namespace. The name will be the same as the original, but with a unique suffix appended.
  2. It copies all methods from the original class, but converts initializer methods to regular methods, with method name __init__ and static initializer names to __clinit__.
  3. For non-static methods, it adds a parameter of type <interface generated by InterfaceGenerator>.
  4. Changes non-static methods that operate on this to operate on the parameter added in the previous bullet.
  5. For constructors, it strips out calls to super.<init>. Regular methods cannot call instance initializers.

The InterfaceGenerator and ImplementationGenerator classes are useless without a way to modify application classes to take advantage of them. ModifyClassVisitor does this job. It adds the check_update method and modifies each method such that it will check for updated classes versions and forward execution to those if they exist. It also changes all fields to be public and non-final. This is necessary so they can be accessed by implementation classes. These attributes are most functional at compile time, but of course these changes may have an effect on applications that use reflection. Solving this problem will have to be put on the to-do list for now, but I suspect it is not all that difficult. The solution probably involves overriding the JRE’s classes reflection classes appropriately (BTW it can also solve problems arising from the use of reflection concerning the methods and fields we have added to application classes).

Let’s now discuss how to modify java.lang.ClassLoader. JRebel generates a bootstrap jar, which contains a new java.lang.ClassLoader class (among others) and supersedes the JRE’s java.lang.ClassLoader using the JVM’s -Xbootclasspath/p: parameter. We’ll also take this approach, but you should note you probably have to perform this task for every version of the target JVM you wish to run. There may be internal API changes between versions that would break compatibility if you used the generated ClassLoader class from JRE X with JRE Y.

To generate a new java.lang.ClassLoader, I have created three classes:

ClassLoaderGenerator does some basic tasks. It is the entry point into the program. It’s main method requires the path to the target JRE’s rt.jar file and the output directory. It pulls the raw bytes from the rt.jar’s java.lang.ClassLoader, it invokes ClassLoaderClassVisitor to produce the raw bytes of our modified java.lang.ClassLoader, and will then bundle the these bytes in a java/lang/ClassLoader.class entry of afeenix-classloader.jar file, which is the deposited to the specified output directory.

ClassLoaderClassVisitor uses ASM to make byte code modifications directly, but it also pulls raw byte code from ClassLoaderTargeted. Specifically, I wrote methods in ClassLoaderTargeted that I wanted to appear in the generated version of java.lang.ClassLoader. While I do enjoy writing byte code instructions directly with ASM, it can be really tedious, especially if you are continually making incremental changes as you develop. By writing the code in Java, this process becomes more like regular Java development (as opposed to byte code level development). This approach may cause some folks to say “But why not use the Asmifier” to generate the ASM code for you? This approach is probably half way between my approach and writing the ASM code from scratch, but running ASM and copying the generated code intoClassLoaderClassVisitor is fairly tedious work too.

Let’s take a look under the hood of ClassLoaderClassVisitor. The first job it will do will be to rename the defineClass and loadClass methods (we will add our own defineClass and loadClass methods later):

public MethodVisitor visitMethod(int access,
        String name,
        String desc,
        String signature,
        String[] exceptions) {

    MethodVisitor mv = super.visitMethod(access, METHOD_NAME_UTIL.processName(name), desc, signature, exceptions);
    if (name.equals(LOAD_CLASS_METHOD_NAME) && desc.equals("(Ljava/lang/String;)Ljava/lang/Class;"))
        return new InvokeMethodNameReplacerMethodVisitor(mv, methodNameUtil);
    else if (name.equals(DEFINE_CLASS_METHOD_NAME))
        return new InvokeMethodNameReplacerMethodVisitor(mv, methodNameUtil);
    else
        return mv;
}

The visitMethod method of line 7 is called for each method defined in java.lang.ClassLoader. The METHOD_NAME_UTIL is an object that is initialized to replace Strings match “defineClass” or “loadClass” with the same name, but with a “_feenix_” prefix. ClassLoader’s loadClass(String name) method calls loadClass(String name, boolean resolve) Lines 8-9 are used to update any method instructions in the new_feenix_loadClass(String name) method such that _feenix_loadClass(String name, boolean resolve) is called instead. Similarly, lines 10-11 ensure that the new _feenix_defineClass methods will always call other _feenix_defineClass methods and not the defineClass methods.

The other interesting part of ClassLoaderClassVisitor is the visitEnd method:

public void visitEnd() {
    try {
        InputStream is =
            Thread.currentThread().getContextClassLoader().getResourceAsStream(ClassLoaderTargeted.class.getName().replace('.', '/') + ".class");
        ClassReader cr = new ClassReader(is);
        ClassNode node = new UpdateMethodInvocationsClassNode();
        cr.accept(node, ClassReader.SKIP_FRAMES);

        Iterator<MethodNode> it = node.methods.listIterator();
        MethodNode method;
        String exceptions[];
        while (it.hasNext()) {
            method = it.next();
            if (method.name.equals(DEFINE_CLASS_METHOD_NAME) ||
                method.name.equals(LOAD_CLASS_METHOD_NAME) ||
                method.name.equals(DEFINE_MY_CLASS_METHOD_NAME)) {

                exceptions = method.exceptions == null ? null : method.exceptions.toArray(new String[method.exceptions.size()]);
                MethodVisitor mv = super.visitMethod(method.access, method.name, method.desc, method.signature, exceptions);
                method.accept(mv);
            }
        }
    }
    catch (Exception e) {
        throw new Error("Unable to create classloader.", e);
    }

    super.visitEnd();
}

This method reads all the methods defined in ClassLoaderTargeted and adds the methods we want (some are just there so that it will compile) to our java.lang.ClassLoader. The methods we want are all the defineClassloadClass, and defineMyClass methods. There is just one problem with them: some the method instructions in these classes will operate on ClassLoaderTargeted, not java.lang.ClassLoader, so we need to sweep through each method instruction and adjust it accordingly. You’ll notice in line 6 we use a UpdateMethodInvocationsClassNode object to read the ClassLoaderTargeted byte code. This class will update the method instructions as necessary.

Class Reloading in Action

To try out Feenix 2.0 (BTW I am calling it 2.0 to distinguish it from the original 1.0 version, but by no means should this be considered a fully functioning finalized distribution) for yourself, do the following:

  1. Download the Feenix 2.0 distribution and unpack the zip. Let’s say you put it in /projects/feenix-2.0.
  2. Let’s assume your target JVM is located at /java/jdk1.7.0. Run the following command to generate the feenix-classloader.jar file in the /projects/feenix-2.0 directory:
/java/jdk1.7.0/bin/java -jar /projects/feenix-2.0/discotek.feenix-2.0.jar /java/jdk1.7.0/jre/lib/rt.jar /projects/feenix-2.0 
  1. Download the example project into directory /projects/feenix-example and unpack into that directory.
  2. Create a project in your favourite IDE that you will use to edit the example project code.
  3. Configure the /projects/feenix-example/feenix.xml file to point to the directory that contains the project’s compiled classes. If you are Eclipse, you can probably skip this step as it already points to the project’s bin directory.
  4. Using your IDE, run ca.discotek.feenix.example.Example with the following JVM options:
 -Xbootclasspath/p:C:\projects\feenix-2.0\feenix-classloader.jar;C:\projects\feenix-2.0\discotek.feenix-2.0.jar -noverify -Dfeenix-config=C:\projects\feenix-example\cfg\feenix.xml
  1. A window will appear with three buttons. Click each button to generate some baseline text.
    1. Print from Existing Printer. Demonstrates how you can alter the functionality for an existing object.
    2. Print from New Printer. Demonstrates how you can alter the functionality for new objects.
    3. Print Static. Demonstrates how you can alter the functionality for a static method.
  2. Navigate to the ca.discotek.feenix.example.gui.Printer class and modify the text for the message field. Navigate to ca.discotek.feenix.example.gui.ExampleGui and modify the Printer.printStatic‘s String parameter. Save your changes to cause the IDE to compile the new classes.
  3. Click each button in the window again and observe your changes.

This concludes our investigation into class reloading. You should keep in mind that this demonstration is a proof of concept and may not work as expected with your own project code (it is not thoroughly tested). You should also keep in mind the following points:

  • I should mention that the -noverify JVM parameter is required in order to allow constructors to be reloaded.
  • The code to override java.lang.ClassLoader does not override defineTransformedClass.
  • There are still some outstanding issues (mainly related to reflection).
  • There is still a major problem with accessing fields or methods that only exist in new versions of a class.
  • Should consider using the synthetic modifier to any generated fields or methods.
  • Feenix uses a rebundled copy of ASM. It is rebundled with the ca.discotek.rebundled package prefix to avoid class clashes when an application requires ASM on the classpath for its own purposes.
  • Some of the Class Reloading Mechanism goals listed in the introduction were not addressed (does not reload non-class resources or framework configuration files).

Resources

Next Blog in the Series Teaser

I would be surprised if anyone who stays up with the latest Java news has not yet heard of Plumbr. Plumbr uses a java agent to identify memory leaks in your application. At the time of writing, Plumbr is “$139 per JVM per month”. OUCH! In my next byte code engineering blog, I’ll show you how you can identify memory leaks in your code for free using instrumentation and Phantom References.

If you enjoyed this article, you may wish to follow discotek on twitter.

- See more at: https://discotek.ca/blog/?p=230

Build and launch faster with Okta’s user management API. Register today for the free forever developer edition!

Topics:
jrebel ,java ,devops

Published at DZone with permission of Rob Kenworthy, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}