ClassLoader编程实践

java1.1中的实现


java1.1的类加载机制相对单一,而用户自定义加载器的重写却比较复杂。。

主要需要重写ClassLoader中一个方法:Class loadClass(String name)。

Class loadClass(String name):loadClass(String name)(或者loadClass(String name , boolean resolve))这个方法是加载的核心,他将根据类名(全名,比如java.lang.String)获得对应类的二进制数据,然后通过Class defineClass(byte[] b)将二进制数据加载到JVM的方法区,defineClass返回对应类的Class 实例,并根据可选的参数resolve决定是否需要现在解析这个类,之后将这个Class实例作为 loadClass方法的返回值。

其中,若是没有办法实现加载和defineClass,即无法通过本加载器直接加载类的情况,则使用Class findSystemClass(String name)委派系统的加载器查找,如果能找到则加载,否则,抛出ClassNotFoundException。可见,这个方法必须放在 loadClass方法中自定义加载的代码之后。

重写loadClass方法还是相对复杂的,需要考虑到各种加载情况。


以下的例子实现了这一过程。

它是一个能直接由.java源文件实现类加载的 加载器:CompilingClassLoader。

就像之前说到的,只需要重写loadClass()方法,用户自定义的加载实现逻辑都在这个方法中。至于代码中的其中的

其他的方法,比如defineClass(String name),findSystemClass(String name)都是由loadClass调用的。

CompilingClassLoader类中还包含了执行对去字节码、编译java文件的方法,都为loadClass()方法所用。

在defineClass()或findSystemClass()执行结束前,可能返回的异常挺多的:NoClassDefFoundError ,ClassNotFoundError , ClassFormatError , UnsupportedClassVersionError , LinkageError , ClassCircularityError , IncompatibleClassChangeError . 详细信息请大家查阅java doc。


代码如下:
CompilingClassLoader.java
import java.io.*;  
  1.   
  2. /* 
  3.  
  4. A CompilingClassLoader compiles your Java source on-the-fly.  It 
  5. checks for nonexistent .class files, or .class files that are older 
  6. than their corresponding source code. 
  7.  
  8. */  
  9.   
  10. public class CompilingClassLoader extends ClassLoader  
  11. {  
  12.   // Given a filename, read the entirety of that file from disk   
  13.   // and return it as a byte array.   
  14.   private byte[] getBytes( String filename ) throws IOException {  
  15.     // Find out the length of the file   
  16.     File file = new File( filename );  
  17.     long len = file.length();  
  18.   
  19.     // Create an array that's just the right size for the file's   
  20.     // contents   
  21.     byte raw[] = new byte[(int)len];  
  22.   
  23.     // Open the file   
  24.     FileInputStream fin = new FileInputStream( file );  
  25.   
  26.     // Read all of it into the array; if we don't get all,   
  27.     // then it's an error.   
  28.     int r = fin.read( raw );  
  29.     if (r != len)  
  30.       throw new IOException( "Can't read all, "+r+" != "+len );  
  31.   
  32.     // Don't forget to close the file!   
  33.     fin.close();  
  34.   
  35.     // And finally return the file contents as an array   
  36.     return raw;  
  37.   }  
  38.   
  39.   // Spawn a process to compile the java source code file   
  40.   // specified in the 'javaFile' parameter.  Return a true if   
  41.   // the compilation worked, false otherwise.   
  42.   private boolean compile( String javaFile ) throws IOException {  
  43.     // Let the user know what's going on   
  44.     System.out.println( "CCL: Compiling "+javaFile+"..." );  
  45.   
  46.     // Start up the compiler   
  47.     Process p = Runtime.getRuntime().exec( "javac "+javaFile );  
  48.   
  49.     // Wait for it to finish running   
  50.     try {  
  51.       p.waitFor();  
  52.     } catch( InterruptedException ie ) { System.out.println( ie ); }  
  53.   
  54.     // Check the return code, in case of a compilation error   
  55.     int ret = p.exitValue();  
  56.   
  57.     // Tell whether the compilation worked   
  58.     return ret==0;  
  59.   }  
  60.   
  61.   // The heart of the ClassLoader -- automatically compile   
  62.   // source as necessary when looking for class files   
  63.   public Class loadClass( String name, boolean resolve )  
  64.       throws ClassNotFoundException {  
  65.   
  66.     // Our goal is to get a Class object   
  67.     Class clas = null;  
  68.   
  69.     // First, see if we've already dealt with this one   
  70.     clas = findLoadedClass( name );  
  71.   
  72.     //System.out.println( "findLoadedClass: "+clas );   
  73.   
  74.     // Create a pathname from the class name   
  75.     // E.g. java.lang.Object => java/lang/Object   
  76.     String fileStub = name.replace( '.''/' );  
  77.   
  78.     // Build objects pointing to the source code (.java) and object   
  79.     // code (.class)   
  80.     String javaFilename = fileStub+".java";  
  81.     String classFilename = fileStub+".class";  
  82.   
  83.     File javaFile = new File( javaFilename );  
  84.     File classFile = new File( classFilename );  
  85.   
  86.     //System.out.println( "j "+javaFile.lastModified()+" c "+   
  87.     //  classFile.lastModified() );   
  88.   
  89.     // First, see if we want to try compiling.  We do if (a) there   
  90.     // is source code, and either (b0) there is no object code,   
  91.     // or (b1) there is object code, but it's older than the source   
  92.     if (javaFile.exists() &&  
  93.          (!classFile.exists() ||  
  94.           javaFile.lastModified() > classFile.lastModified())) {  
  95.   
  96.       try {  
  97.         // Try to compile it.  If this doesn't work, then   
  98.         // we must declare failure.  (It's not good enough to use   
  99.         // and already-existing, but out-of-date, classfile)   
  100.         if (!compile( javaFilename ) || !classFile.exists()) {  
  101.           throw new ClassNotFoundException( "Compile failed: "+javaFilename );  
  102.         }  
  103.       } catch( IOException ie ) {  
  104.   
  105.         // Another place where we might come to if we fail   
  106.         // to compile   
  107.         throw new ClassNotFoundException( ie.toString() );  
  108.       }  
  109.     }  
  110.   
  111.     // Let's try to load up the raw bytes, assuming they were   
  112.     // properly compiled, or didn't need to be compiled   
  113.     try {  
  114.   
  115.       // read the bytes   
  116.       byte raw[] = getBytes( classFilename );  
  117.   
  118.       // try to turn them into a class   
  119.       clas = defineClass( name, raw, 0, raw.length );  
  120.     } catch( IOException ie ) {  
  121.       // This is not a failure!  If we reach here, it might   
  122.       // mean that we are dealing with a class in a library,   
  123.       // such as java.lang.Object   
  124.     }  
  125.   
  126.     //System.out.println( "defineClass: "+clas );   
  127.   
  128.     // Maybe the class is in a library -- try loading   
  129.     // the normal way   
  130.     if (clas==null) {  
  131.       clas = findSystemClass( name );  
  132.     }  
  133.   
  134.     //System.out.println( "findSystemClass: "+clas );   
  135.   
  136.     // Resolve the class, if any, but only if the "resolve"   
  137.     // flag is set to true   
  138.     if (resolve && clas != null)  
  139.       resolveClass( clas );  
  140.   
  141.     // If we still don't have a class, it's an error   
  142.     if (clas == null)  
  143.       throw new ClassNotFoundException( name );  
  144.   
  145.     // Otherwise, return the class   
  146.     return clas;  
  147.   }  
  1. <span style="font-size: 16px; ">import java.lang.reflect.*;  
  2.   
  3. /* 
  4.  
  5. CCLRun executes a Java program by loading it through a 
  6. CompilingClassLoader. 
  7.  
  8. */  
  9.   
  10. public class CCLRun  
  11. {  
  12.   static public void main( String args[] ) throws Exception {  
  13.   
  14.     // The first argument is the Java program (class) the user   
  15.     // wants to run   
  16.     String progClass = args[0];  
  17.   
  18.     // And the arguments to that program are just   
  19.     // arguments 1..n, so separate those out into   
  20.     // their own array   
  21.     String progArgs[] = new String[args.length-1];  
  22.     System.arraycopy( args, 1, progArgs, 0, progArgs.length );  
  23.   
  24.     // Create a CompilingClassLoader   
  25.     CompilingClassLoader ccl = new CompilingClassLoader();  
  26.   
  27.     // Load the main class through our CCL   
  28.     Class clas = ccl.loadClass( progClass );  
  29.   
  30.     // Use reflection to call its main() method, and to   
  31.     // pass the arguments in.   
  32.   
  33.     // Get a class representing the type of the main method's argument   
  34.     Class mainArgType[] = { (new String[0]).getClass() };  
  35.   
  36.     // Find the standard main method in the class   
  37.     Method main = clas.getMethod( "main", mainArgType );  
  38.   
  39.     // Create a list containing the arguments -- in this case,   
  40.     // an array of strings   
  41.     Object argsArray[] = { progArgs };  
  42.   
  43.     // Call the method   
  44.     main.invoke( null, argsArray );  
  45.   }  
  46. }
Foo.java
<span style="font-size: 16px; ">public class Foo  
  1. {  
  2.   static public void main( String args[] ) throws Exception {  
  3.     System.out.println( "foo! "+args[0]+" "+args[1] );  
  4.   }  
  5. }  
  1. java CCLRun Foo arg1 arg2  
  2. CCL: Compiling Foo.java...  
  3. foo! arg1 arg2  
具体参见资料

来自IBM DeveloperWorks : Understanding the Java ClassLoader 

这是一篇2001年的文章,是早年java1.1的实现方式。如今的java已经改变了 很多,变得更加人性化,多功能化,鲁棒性也更强了。


java1.2以后的实现


具体参见资料

改版以后,ClassLoader实现了parent-child模型,更好的控制安全性方面的问题。

为了延续parent-first的模式,通常在继承ClassLoader时不用重写loadClass()方法,而是重写findClass()方法。
findClass() : 方法中需要定义如何获取类的字节码,并使用defineClass()完成加载(仅仅加载,没有resolve等步骤)。
对应的有一个findLoadedClass() ,这个方法用来实现对查找当前加载器是否有加载某类。由于findClass是从原来的loadClass方法中抽离的代码,重写也简单很多,省去了开发者对流程控制的很多顾虑。

loadClass():如果使用parent-first的加载模型,loadClass()方法是不用重写的。它通过向父亲加载器 迭代实现了parent-first的委托关系。每次加载一个类时,先调findLoadedClass(),如果没有找到,则调用父亲加载器的loadClass(),如果找到了就返回Class实例,没有找到则父亲加载器会产生一个ClassNotFoundException,捕捉到这个Exception后,加载器会自己调用findClass()尝试实现对类的加载。如果依然没有成功加载,则产生一个ClassNotFoundException.

控制流程如图:这里显示了一个类未能成功加载所要经历的流程。(来自参考资料http://stackoverflow.com/questions/3544614/how-is-the-control-flow-to-findclass-of
      A.loadClass()  
  1.            |  
  2.        (not-found?) (by findLoadedClass)  
  3.            |  
  4.       B.loadClass()  
  5.            |  
  6.        (not found?) (by findLoadedClass)  
  7.            |  
  8. systemclassloader.loadClass  (Bs parent, also can be   
  9.            |                  called classpath classloader)  
  10.            |  
  11.        (not found?) (by findLoadedClass)  
  12.            |  
  13. ootstrap classloader.loadClass (the bootstrap classloader,   
  14.            |                   (this has no parent)  
  15.            |  
  16.        (not found?)  
  17.            |  
  18. systemclassloader.findClass  (on system classloader,   
  19.            |                   will try to "find" class in "classpath")  
  20.            |  
  21.        (not found?) ClassNotFoundException  
  22.            |  
  23.        B.findClass  
  24.            |  
  25.        (not found?) ClassNotFoundException  
  26.            |  
  27.        A.findClass  
  28.            |  
  29.         (not found?)  
  30.            |  
  31.    ClassNotFoundException  



注意,对于extensions class loader ,它的parent加载器是null,因为bootstrap加载器是本地实现的,并非java实现,于是,如何从extension 加载器向上回溯呢?答案如下:
try {  
  1.     if (parent != null) {  
  2.        c = parent.loadClass(name, false);  
  3.     } else {  
  4.        c = findBootstrapClassOrNull(name);  
  5.     }  
  6. catch (ClassNotFoundException e) {  
  7.    // ClassNotFoundException thrown if class not found   
  8.      // from the non-null parent class loader   
  9. }  
这是 ClassLoader的源代码,对于 parent 为null的情况,会直接调用findBootstrapClassOrNull方法尝试用bootstrap加载器加载 。通过源代码,能够很好的理解这里的parent-child 模型了。

另注意 对于基于parent-child模型的类加载器实现,都需要定义一个 以parent类加载器作为参数的构造函数,以指定父加载器。如果直接调用没有参数的构造函数,则默认制定的是systemclassloader作为parent。

以下为编程实例
下面的例子是我用来实现动态分析java类关系的加载器代码。具体方法是:调用ASM api , 在加载器中加载类时,修改.class文件中的字节码,加入相应语句,让对象在创建或执行相应指令时,在trace文件中记录自己的行为。不过实现的方式不是重点,重点是使用了自定义的类加载器的实现!
在编码的过程中,我遇到的一个错误是,将需要使用自定义加载器加载的类文件直接放在了eclipse工程中的bin目录下。而这个目录是可以通过appclassloader即systemclassloader找到路径并加载的。根据parent-first的实现,这些类直接被 systemclassloader加载了,也就绕过了自定义加载器的处理机制。修改过路径以后没有出现相应问题了。

ASMClassLoader.java
package biaobiaoqi.classLoader;  
  1. import java.io.*;  
  2.   
  3. import org.objectweb.asm.ClassReader;  
  4. import org.objectweb.asm.ClassWriter;  
  5.   
  6. import biaobiaoqi.asm.*;  
  7.   
  8.   
  9. public class ASMClassLoader extends ClassLoader  
  10. {  
  11.     String basePath ;  
  12.       
  13.     /**reference to System Classloader as the parent class loader 
  14.      * @param path <br> the path of .class files will be loaded 
  15.      */  
  16.     public ASMClassLoader(String path){  
  17.         basePath = path ;  
  18.     }  
  19.    
  20.     /** 
  21.      * reference to parent as it's parent classloader 
  22.      * @param path 
  23.      * @param parent 
  24.      */  
  25.     public ASMClassLoader(String path , ClassLoader parent){  
  26.         super(parent);  
  27.         basePath = path ;  
  28.     }  
  29.   
  30.     @Override  
  31.     public Class findClass(String name) throws ClassNotFoundException{  
  32.         System.out.println("findClass");  
  33.         byte[] raw;  
  34.         try {  
  35.             raw = getBytesFromBasePath( name );  
  36.         } catch (IOException e) {  
  37.             e.printStackTrace();  
  38.             throw new ClassNotFoundException();  
  39.         }  
  40.       
  41.         byte[] transformed = instrumentBtyeCode(raw);  
  42.         /* 
  43.         try{ 
  44.             FileOutputStream file = new FileOutputStream( "/home/biaobiaoqi/" +name.replace( '.', '/' )+".class"); 
  45.             file.write( transformed); 
  46.             file.close(); 
  47.         } 
  48.         catch (IOException e) { 
  49.             e.printStackTrace(); 
  50.         } 
  51.         */  
  52.         if (transformed == null){  
  53.             throw new ClassNotFoundException();  
  54.         }  
  55.         
  56.           
  57.         return defineClass(name, transformed, 0, transformed.length );  
  58.     }  
  59.     
  60.     private byte[] getBytesFromBasePath( String className ) throws IOException ,ClassNotFoundException{  
  61.         String fileStub = className.replace( '.''/' );  
  62.         String classFileName = basePath +fileStub+".class";  
  63.         File file = new File( classFileName );  
  64.           
  65.         long len = file.length();  
  66.         byte raw[] = new byte[(int)len];  
  67.         FileInputStream fin = new FileInputStream( file );  
  68.           
  69.         int r = fin.read( raw );  
  70.         if (r != len)  
  71.             throw new IOException( "Can't read all, "+r+" != "+len );  
  72.           
  73.         fin.close();  
  74.         return raw;  
  75.     }  
  76.       
  77.     private byte[] instrumentBtyeCode(byte[] raw){  
  78.         ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);  
  79.         ASMClassAdapter mca = new ASMClassAdapter(cw);   
  80.         ClassReader cr = new ClassReader(raw);  
  81.         cr.accept(mca, 0);  
  82.         return cw.toByteArray();  
  83.     }     
  84.       
  85.     @Override  
  86.     public Class loadClass( String name, boolean resolve )  
  87.         throws ClassNotFoundException {  
  88.         System.out.println("loadClass_resolve");  
  89.           return super.loadClass(name ,resolve);  
  90.     }  
  91.       
  92.       
  93.     @Override  
  94.     public Class loadClass( String name )  
  95.         throws ClassNotFoundException {  
  96.         System.out.println("loadClass");  
  97.           return super.loadClass(name );  
  98.     }  
  99.       
  100. }  

温馨提示:不要把需要用自定义加载器加载的类文件直接放在system classloader能达到的路径下,否则,parent-first会帮你加载好那个类。特别是在用eclipse等IDE开发的时候。。这个bug让我花了很长时间。。

你可能感兴趣的:(ClassLoader)