Java HotSwap Ⅳ-Reloading Java Classes Series 1-Objects, Classes and ClassLoaders

Java HotSwap Ⅳ-Reloading Java Classes Series 1-Objects, Classes and ClassLoaders

引自-http://zeroturnaround.com/rebellabs/reloading-objects-classes-classloaders/
        -来自JRebel的研发公司zeroturnaround
________________________________________________________________
Reloading Java Classes 101: Objects, Classes and ClassLoaders

Welcome to Turnaround article series from ZeroTurnaround.

In this article we will review how to reload a Java class using a dynamic classloader. To get there we’ll see how objects, classes and classloaders are tied to each other and the process required to make changes. We begin with a bird’s eye view of the problem, explains the reloading process, and then proceed to a specific example to illustrate typical problems and solutions. Other articles in the series include:

  • RJC101: Objects, Classes and ClassLoaders
  • RJC201: How do Classloader leaks happen?
  • RJC301: Classloaders in Web Development — Tomcat, GlassFish, OSGi, Tapestry 5 and so on
  • RJC401: HotSwap and JRebel — Behind the Scenes
  • RJC501: How Much Does Turnaround Cost?

A Bird’s Eye View

The first thing to understand when talking about reloading Java code is the relation between classes and objects. All Java code is associated with methods contained in classes. Simplified, you can think of a class as a collection of methods, that receive “this” as the first argument. The class with all its methods is loaded into memory and receives a unique identity. In the Java API this identity is represented by an instance of java.lang.Class that you can access using theMyObject.class expression.

Every object created gets a reference to this identity accessible through the Object.getClass()method. When a method is called on an object, the JVM consults the class reference and calls the method of that particular class. That is, when you call mo.method() (where mo is an instance of MyObject), then the JVM will call mo.getClass().getDeclaredMethod("method").invoke(mo) (this is not what the JVM actually does, but the result is the same).


Every Class object is in turn associated with its classloader (MyObject.class.getClassLoader()). The main role of the class loader is to define a class scope — where the class is visible and where it isn’t. This scoping allows for classes with the same name to exist as long as they are loaded in different classloaders. It also allows loading a newer version of the class in a different classloader.


The main problem with code reloading in Java is that although you can load a new version of a class, it will get a completely different identity and the existing objects will keep referring the previous version of the class. So when a method is called on those objects it will execute the old version of the method.

Let’s assume that we load a new version of the MyObject class. Let’s refer to the old version asMyObject_1 and to the new one as MyObject_2. Let’s also assume that MyObject.method() returns “1” in MyObject_1 and “2” in MyObject_2. Now if mo2 is an instance of MyObject_2:

  • mo.getClass() != mo2.getClass()
  • mo.getClass().getDeclaredMethod("method").invoke(mo)
    != mo2.getClass().getDeclaredMethod("method").invoke(mo2)
  • mo.getClass().getDeclaredMethod("method").invoke(mo2) throws a ClassCastException, because the Class identities of mo and mo2 do no match.

This means that any useful solution must create a new instance of mo2 that is an exact copy ofmo and replace all references to mo with it. To understand how hard it is, remember the last time you had to change your phone number. It’s easy enough to change the number itself, but then you have to make sure that everyone you know will use the new number, which is quite a hassle. It’s just as difficult with objects (in fact, it’s actually impossible, unless you control the object creation yourself), and we’re talking about many objects that you must update at the same time.

Down and Dirty

Let’s see how this would look in code. Remember, what we’re trying to do here is load a newer version of a class, in a different classloader. We’ll use an Example class that looks like this:

public   class  Example  implements  IExample {
  
private   int  counter;
  
public  String message() {
    
return   " Version 1 " ;
  }
  
public   int  plusPlus() {
    
return  counter ++ ;
  }
  
public   int  counter() {
    
return  counter;
  }
}

We’ll use a main() method that will loop infinitely and print out the information from the Exampleclass. We’ll also need two instances of the Example class: example1 that is created once in the beginning and example2 that is recreated on every roll of the loop:

public   class  Main {
  
private   static  IExample example1;
  
private   static  IExample example2;

  
public   static   void  main(String[] args)  {
    example1  =  ExampleFactory.newInstance();

    
while  ( true ) {
      example2  =  ExampleFactory.newInstance();

      System.out.println( " 1)  "   +
        example1.message()  +   "  =  "   +  example1.plusPlus());
      System.out.println( " 2)  "   +
        example2.message()  +   "  =  "   +  example2.plusPlus());
      System.out.println();

      Thread.currentThread().sleep( 3000 );
    }
  }
}

IExample is an interface with all the methods from Example. This is necessary because we’ll be loading Example in an isolated classloader, so Main cannot use it directly (otherwise we’d get aClassCastException).

public   interface  IExample {
  String message();
  
int  plusPlus();
}

From this example, you might be surprised to see how easy it is to create a dynamic class loader. If we remove the exception handling it boils down to this:

public   class  ExampleFactory {
  
public   static  IExample newInstance() {
    URLClassLoader tmp  =
      
new  URLClassLoader( new  URL[] {getClassPath()}) {
        
public  Class loadClass(String name) {
          
if  ( " example.Example " .equals(name))
            
return  findClass(name);
          
return   super .loadClass(name);
        }
      };

    
return  (IExample)
      tmp.loadClass( " example.Example " ).newInstance();
  }
}

The method getClassPath() for the purposes of this example could return the hardcoded classpath. However, in the full source code (available in the Resources section below) you can see how we can use the ClassLoader.getResource() API to automate that.

Now let’s run Main.main and see the output after waiting for a few loop rolls:

1) Version 1 = 3 2) Version 1 = 0

As expected, while the counter in the first instance is updated, the second stays at “0”. If we change the Example.message() method to return “Version 2”. The output will change as follows:

1) Version 1 = 4 2) Version 2 = 0

As we can see, the first instance continues incrementing the counter, but uses the old version of the class to print out the version. The second instance class was updated, however all of the state is lost.

To remedy this, let’s try to reconstruct the state for the second instance. To do that we can just copy it from the previous iteration.

First we add a new copy() method to Example class (and corresponding interface method):

public  IExample copy(IExample example) {
  
if  (example  !=   null )
    counter  =  example.counter();
  
return   this ;
}

Next we update the line in the Main.main() method that creates the second instance:

example2  =  ExampleFactory.newInstance().copy(example2);

Now waiting for a few iterations yields:

1) Version 1 = 3 2) Version 1 = 3

And changing Example.message() method to return “Version 2” yields:

1) Version 1 = 4 2) Version 2 = 4

As you can see even though it’s possible for the end user to see that the second instance is updated and all its state is preserved, it involves managing that state by hand. Unfortunately, there is no way in the Java API to just update the class of an existing object or even reliably copy its state, so we will always have to resort to complicated workarounds.

In subsequent articles we’ll review how web containers, OSGi, Tapestry 5, Grails and others confront the problem of managing state when reloading classes, then we’ll dig into how HotSwap, Dynamic Languages, and the Instrumentation API work, and go behind the scenes with JRebel as well.     
___________________________________________________________________________________________
  笔记记录:
        1.可以存在两个相同名字的class,只要他们是由不同的classloader加载即可,即允许用一个不同的classloader加载一个新的class
        2.如果想达到hotswap的目的,新加载的class实例必须要复制旧的实例的状态并且替换所有旧的实例的引用。非常麻烦,就好比你自己更换电话号码很简单,但是要确保所有人都知道你换了新号码一样.
        3.如同上述的例子一样,必须手动的管理新旧实例的状态保存。没有任何的Java API帮助我们替换一个旧的实例的class或者可靠的复制它的状态,所以我们有时不得不求助于更加复杂的解决方案.
        4.可参考web containers,OSGI,Tapestry 5,Grails等是如何面对上述reloading class遇到的问题.
          5.注意接口IExample很重要.-->
                1.IExample为公有接口,由系统classloader加载.
                2.如何没有接口,则无法实现hotswap,因为不同的classloader加载的hotswap class均不同,会报ClassCastException.但如果面向接口编程,则没有问题,虽然是不同的classloader加载的,但是都是公用接口.
                3.如果没有接口,也可以考虑将Example作为公有接口类,但是后续hotswap的class必须继承该Example才可以满足需求._不过不建议使用继承,比较麻烦.
_______________________________________________________________________________
附本人测试代码:

package  com.mavsplus.example.java.rjc;

/**
 * 
 * 接口
 * 
 *  @author  landon
 *  @since  1.8.0_25
 
*/
public  interface  IExample {

     public  String message();

     public  int  plusPlus();

     public  int  counter();
    
     public  IExample copy(IExample example);
}


package  com.mavsplus.example.java.rjc;

import  java.net.URL;
import  java.net.URLClassLoader;

/**
 * 每次都new一个classloader来加载class并且实例化
 * 
 *  @author  landon
 *  @since  1.8.0_25
 
*/
public  class  ExampleFactory {

     public  static  IExample newInstance()  throws  Exception {
         //  必须用url形式,本地file需要:file:形式-如"file:\\E:\\github\\mavsplus-all\\mavsplus-examples\\src\\main\\resources\\rjc\\"
        
//  这里硬编码-该目录下只有编译后的Example.class_即只有Example.class用urlClassloader加载,其他均默认加载
        
//  重点:一个比较容易犯错的地方:是最后的路径少了"\",否则会报ClassNotFoundException
        URLClassLoader newClazzLoader  =  new  URLClassLoader( new  URL[] {  new  URL(
                 " file:\\E:\\github\\mavsplus-all\\mavsplus-examples\\src\\main\\resources\\rjc\\ " ) }) {

             //  注意:必须覆写loadClass方法-否则还会默认加载默认classpath下的Example(因为用Eclipse编写的Example类,默认targe目录也是在classpath下)
            @Override
             public  Class <?>  loadClass(String name)  throws  ClassNotFoundException {
                 if  (name.equals( " com.mavsplus.example.java.rjc.Example " )) {
                     return  findClass(name);
                }

                 return  super .loadClass(name);
            }
        };

         return  (IExample) newClazzLoader.loadClass( " com.mavsplus.example.java.rjc.Example " ).newInstance();
    }
}


package  com.mavsplus.example.java.rjc;

/**
 * 用来reload的class
 * 
 *  @author  landon
 *  @since  1.8.0_25
 
*/
public  class  Example  implements  IExample {

     private  int  counter;

    @Override
     public  String message() {
         //  return "Version 1";
         return  " Version 2 " ;
    }

    @Override
     public  int  plusPlus() {
         return  counter ++ ;
    }

    @Override
     public  int  counter() {
         return  counter;
    }

    @Override
     public  IExample copy(IExample example) {
         if  (example  !=  null ) {
            counter  =  example.counter();
        }

         return  this ;
    }
}


package  com.mavsplus.example.java.rjc;

import  java.util.concurrent.TimeUnit;

/**
 * <a href=
 * " http://zeroturnaround.com/rebellabs/reloading-objects-classes-classloaders/ "
 * >rjc series 1</>
 * 
 * 测试reloading java class
 * 
 * <pre>
 *   ___________________________________________
 *       1.默认输出
 *           1)Version 1 = 0
 *              2)Version 1 = 0
 *              
 *              1)Version 1 = 1
 *              2)Version 1 = 0
 *              
 *              1)Version 1 = 2
 *              2)Version 1 = 0
 *              
 *              1)Version 1 = 3
 *              2)Version 1 = 0
 *              
 *              1)Version 1 = 4
 *              2)Version 1 = 0
 *      --->即每次reload时,example2的状态一直是0.
 *  _____________________________________________
 *          2.修改Example.java,将message方法修改,返回Version 2,保存(可以直接在程序运行的时候进行修改,因为运行的程序是用的是另一个地方的Example的clazz)
 *                  ->将Example.class放到指定的加载位置
 *              1)Version 1 = 5
 *              2)Version 2 = 0
 *              
 *              1)Version 1 = 6
 *              2)Version 2 = 0
 *              
 *              1)Version 1 = 7
 *              2)Version 2 = 0
 *              
 *              1)Version 1 = 8
 *              2)Version 2 = 0
 *              
 *              1)Version 1 = 9
 *              2)Version 2 = 0
 *         从输出看_确实新加载了修改过的Example.class_因为message返回值变化了--->但是因为每次都是新加载并且实例化的,所以实例的状态还是0
 * ________________________________________________________________________________________
 *         3.继续修改Example.java,增加一个copy方法,用来复制旧的状态数据并且修改main(exampel2的赋值)_
 *         1)Version 1 = 7
 *         2)Version 1 = 7
 * 
 *         1)Version 1 = 8
 *         2)Version 1 = 8
 * 
 *         1)Version 1 = 9
 *         2)Version 2 = 9
 * 
 *         1)Version 1 = 10
 *         2)Version 2 = 10
 *         从输出看,每次reload example2后,旧的实例状态也保存了_纯手动维护这个实例状态
 * </pre>
 * 
 *  @author  landon
 *  @since  1.8.0_25
 
*/
public  class  RJCTest {

     private  static  IExample example1;
     private  static  IExample example2;

     public  static  void  main(String[] args)  throws  Exception {
        example1  =  ExampleFactory.newInstance();

         while  ( true ) {
             //  第一次运行的时候,example2为null->第二次后则会克隆旧的对象状态
            example2  =  ExampleFactory.newInstance().copy(example2);

            System.out.println( " 1) "  +  example1.message()  +  "  =  "  +  example1.plusPlus());
            System.out.println( " 2) "  +  example2.message()  +  "  =  "  +  example2.plusPlus());

            System.out.println();

            TimeUnit.SECONDS.sleep( 5 );
        }
    }
}

你可能感兴趣的:(Java HotSwap Ⅳ-Reloading Java Classes Series 1-Objects, Classes and ClassLoaders)