java 的类加载机制背景知识:
《Java 类加载器( ClassLoader)浅析》
《Three principles of Classloader operation(Classloader操作三原则)》
以上博文中所提及的java类加载机制,都是java1.2及以后的版本,而在最早的java1.1中,是没有parent-child模式的。
这里,将分别对java1.1 和 java1.2及以后的 类加载版本进行展示。
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.*; /* A CompilingClassLoader compiles your Java source on-the-fly. It checks for nonexistent .class files, or .class files that are older than their corresponding source code. */ public class CompilingClassLoader extends ClassLoader { // Given a filename, read the entirety of that file from disk // and return it as a byte array. private byte[] getBytes( String filename ) throws IOException { // Find out the length of the file File file = new File( filename ); long len = file.length(); // Create an array that's just the right size for the file's // contents byte raw[] = new byte[(int)len]; // Open the file FileInputStream fin = new FileInputStream( file ); // Read all of it into the array; if we don't get all, // then it's an error. int r = fin.read( raw ); if (r != len) throw new IOException( "Can't read all, "+r+" != "+len ); // Don't forget to close the file! fin.close(); // And finally return the file contents as an array return raw; } // Spawn a process to compile the java source code file // specified in the 'javaFile' parameter. Return a true if // the compilation worked, false otherwise. private boolean compile( String javaFile ) throws IOException { // Let the user know what's going on System.out.println( "CCL: Compiling "+javaFile+"..." ); // Start up the compiler Process p = Runtime.getRuntime().exec( "javac "+javaFile ); // Wait for it to finish running try { p.waitFor(); } catch( InterruptedException ie ) { System.out.println( ie ); } // Check the return code, in case of a compilation error int ret = p.exitValue(); // Tell whether the compilation worked return ret==0; } // The heart of the ClassLoader -- automatically compile // source as necessary when looking for class files public Class loadClass( String name, boolean resolve ) throws ClassNotFoundException { // Our goal is to get a Class object Class clas = null; // First, see if we've already dealt with this one clas = findLoadedClass( name ); //System.out.println( "findLoadedClass: "+clas ); // Create a pathname from the class name // E.g. java.lang.Object => java/lang/Object String fileStub = name.replace( '.', '/' ); // Build objects pointing to the source code (.java) and object // code (.class) String javaFilename = fileStub+".java"; String classFilename = fileStub+".class"; File javaFile = new File( javaFilename ); File classFile = new File( classFilename ); //System.out.println( "j "+javaFile.lastModified()+" c "+ // classFile.lastModified() ); // First, see if we want to try compiling. We do if (a) there // is source code, and either (b0) there is no object code, // or (b1) there is object code, but it's older than the source if (javaFile.exists() && (!classFile.exists() || javaFile.lastModified() > classFile.lastModified())) { try { // Try to compile it. If this doesn't work, then // we must declare failure. (It's not good enough to use // and already-existing, but out-of-date, classfile) if (!compile( javaFilename ) || !classFile.exists()) { throw new ClassNotFoundException( "Compile failed: "+javaFilename ); } } catch( IOException ie ) { // Another place where we might come to if we fail // to compile throw new ClassNotFoundException( ie.toString() ); } } // Let's try to load up the raw bytes, assuming they were // properly compiled, or didn't need to be compiled try { // read the bytes byte raw[] = getBytes( classFilename ); // try to turn them into a class clas = defineClass( name, raw, 0, raw.length ); } catch( IOException ie ) { // This is not a failure! If we reach here, it might // mean that we are dealing with a class in a library, // such as java.lang.Object } //System.out.println( "defineClass: "+clas ); // Maybe the class is in a library -- try loading // the normal way if (clas==null) { clas = findSystemClass( name ); } //System.out.println( "findSystemClass: "+clas ); // Resolve the class, if any, but only if the "resolve" // flag is set to true if (resolve && clas != null) resolveClass( clas ); // If we still don't have a class, it's an error if (clas == null) throw new ClassNotFoundException( name ); // Otherwise, return the class return clas; } }
CCLRun.java
import java.lang.reflect.*; /* CCLRun executes a Java program by loading it through a CompilingClassLoader. */ public class CCLRun { static public void main( String args[] ) throws Exception { // The first argument is the Java program (class) the user // wants to run String progClass = args[0]; // And the arguments to that program are just // arguments 1..n, so separate those out into // their own array String progArgs[] = new String[args.length-1]; System.arraycopy( args, 1, progArgs, 0, progArgs.length ); // Create a CompilingClassLoader CompilingClassLoader ccl = new CompilingClassLoader(); // Load the main class through our CCL Class clas = ccl.loadClass( progClass ); // Use reflection to call its main() method, and to // pass the arguments in. // Get a class representing the type of the main method's argument Class mainArgType[] = { (new String[0]).getClass() }; // Find the standard main method in the class Method main = clas.getMethod( "main", mainArgType ); // Create a list containing the arguments -- in this case, // an array of strings Object argsArray[] = { progArgs }; // Call the method main.invoke( null, argsArray ); } }
Foo.java
public class Foo { static public void main( String args[] ) throws Exception { System.out.println( "foo! "+args[0]+" "+args[1] ); } }
执行如下:
% java CCLRun Foo arg1 arg2 CCL: Compiling Foo.java... foo! arg1 arg2
具体参见资料
这是一篇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.
A.loadClass() | (not-found?) (by findLoadedClass) | B.loadClass() | (not found?) (by findLoadedClass) | systemclassloader.loadClass (Bs parent, also can be | called classpath classloader) | (not found?) (by findLoadedClass) | bootstrap classloader.loadClass (the bootstrap classloader, | (this has no parent) | (not found?) | systemclassloader.findClass (on system classloader, | will try to "find" class in "classpath") | (not found?) ClassNotFoundException | B.findClass | (not found?) ClassNotFoundException | A.findClass | (not found?) | ClassNotFoundException
注意,对于extensions class loader ,它的parent加载器是null,因为bootstrap加载器是本地实现的,并非java实现,于是,如何从extension 加载器向上回溯呢?答案如下:
try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader }这是
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; import java.io.*; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; import biaobiaoqi.asm.*; public class ASMClassLoader extends ClassLoader { String basePath ; /**reference to System Classloader as the parent class loader * @param path <br> the path of .class files will be loaded */ public ASMClassLoader(String path){ basePath = path ; } /** * reference to parent as it's parent classloader * @param path * @param parent */ public ASMClassLoader(String path , ClassLoader parent){ super(parent); basePath = path ; } @Override public Class findClass(String name) throws ClassNotFoundException{ System.out.println("findClass"); byte[] raw; try { raw = getBytesFromBasePath( name ); } catch (IOException e) { e.printStackTrace(); throw new ClassNotFoundException(); } byte[] transformed = instrumentBtyeCode(raw); /* try{ FileOutputStream file = new FileOutputStream( "/home/biaobiaoqi/" +name.replace( '.', '/' )+".class"); file.write( transformed); file.close(); } catch (IOException e) { e.printStackTrace(); } */ if (transformed == null){ throw new ClassNotFoundException(); } return defineClass(name, transformed, 0, transformed.length ); } private byte[] getBytesFromBasePath( String className ) throws IOException ,ClassNotFoundException{ String fileStub = className.replace( '.', '/' ); String classFileName = basePath +fileStub+".class"; File file = new File( classFileName ); 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; } private byte[] instrumentBtyeCode(byte[] raw){ ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); ASMClassAdapter mca = new ASMClassAdapter(cw); ClassReader cr = new ClassReader(raw); cr.accept(mca, 0); return cw.toByteArray(); } @Override public Class loadClass( String name, boolean resolve ) throws ClassNotFoundException { System.out.println("loadClass_resolve"); return super.loadClass(name ,resolve); } @Override public Class loadClass( String name ) throws ClassNotFoundException { System.out.println("loadClass"); return super.loadClass(name ); } // The heart of the ClassLoader -- automatically compile /* // source as necessary when looking for class files public Class loadClass( String name, boolean resolve ) throws ClassNotFoundException { Class clas = null; clas = findLoadedClass( name ); String fileStub = name.replace( '.', '/' ); //TODO setting paths to the bin directory in the eclipse project String classFilename = "./bin/"+fileStub+".class"; File classFile = new File( classFilename ); if (classFile.exists() && !name.endsWith("Trace")){ //instrument the codes except for Trace.class try { byte[] raw = getBytesFromBasePath( name ); byte[] transformed = instrumentBtyeCode(raw); //for test try{ FileOutputStream file = new FileOutputStream(classFilename); file.write( cw.toByteArray()); file.close(); } catch(FileNotFoundException ex) { System.out.println("FileNotFoundException : " + ex); } clas = defineClass( name, transformed, 0, transformed.length ); } catch( IOException ie ) { } } if (clas==null) { clas = findSystemClass( name ); } if (resolve && clas != null) resolveClass( clas ); if (clas == null) throw new ClassNotFoundException( name ); return clas; }*/ }