JVM中除了根加载器之外其他加载器都是ClassLoader的子类实例, 可以通过扩展ClassLoader的子类,通过重写方法来实现自定义的类加载器。
ClassLoader中有两个关键的方法如下,
- loadClass(...), 系统调用这个方法来加载指定类的Class对象
在这个方法中,一般需要做四件事,先后顺序如下,
- findLoadedClass(..)看是否已经加载类——加载器的缓存机制
- 在父加载器上调用loadClass(..), 父加载器为null,则使用根加载器加载——加载器的父类委托机制
- 调用findClass方法查找类
- findClass(...),根据名称查找类
ClassLoader中还有一个核心方法 Class defineClass(...),它负责将class文件读入字节数组,并把它转为Class对象。
所以如果需要自定义加载器,只需要重写findClass(...)方法就行了。因为loadClass不仅用到了缓存机制和父类委托机制,还直接调用了findClass类。
下面是一个简单例子来实现自定义加载器, 这个程序通过重写findClass实现自定义类的加载机制,让它在加载之前先编译源文件,这样就实现运行java文件之前先编译它, 从而可以通过这个程序直接执行java源文件。
1 import java.io.File; 2 import java.io.FileInputStream; 3 import java.io.IOException; 4 import java.lang.reflect.InvocationTargetException; 5 import java.lang.reflect.Method; 6 7 public class MyClassLoader extends ClassLoader{ 8 // 读取一个文件的内容 9 private byte[] getBytes(String filename) throws IOException { 10 File file = new File(filename); 11 long len = file.length(); 12 byte[] raw = new byte[(int)len]; 13 try { 14 FileInputStream fin = new FileInputStream(file); 15 //一次读取class文件的全部二进制数据 16 int r = fin.read(raw); 17 if (r != len) { 18 throw new IOException("无法读取全部文件: "+r+" != "+raw); 19 } 20 return raw; 21 } catch (IOException e) { 22 throw e; 23 } 24 } 25 26 //定义编译指定java文件的方法 27 private boolean compile(String javaFile) throws IOException{ 28 System.out.println("MyClassLoader: " + "正在编译 " + javaFile); 29 Process p = Runtime.getRuntime().exec("javac " + javaFile); 30 try { 31 //其他线程都等这个线程完成 32 p.waitFor(); 33 } catch (InterruptedException e) { 34 System.out.println(e); 35 } 36 //获取javac线程的退出值 37 int ret = p.exitValue(); 38 return ret == 0; 39 } 40 41 //重写ClassLoader的findClass方法 42 protected Class<?> findClass(String name) throws ClassNotFoundException { 43 Class clazz = null; 44 // 将包路径中的点(.)替换成斜杠(/) 45 String fileStub = name.replace(".", "/"); 46 String javaFilename = fileStub + ".java"; 47 String classFilename = fileStub + ".class"; 48 File javaFile = new File(javaFilename); 49 File classFile = new File(classFilename); 50 //当指定java源文件存在,且class文件不存在,或者java源文件的修改时间比class文件的修改文件更晚时,重新编译 51 if (javaFile.exists() && (!classFile.exists() 52 || javaFile.lastModified() > classFile.lastModified())) { 53 try { 54 //如果编译失败,或者该class不存在 55 if (!compile(javaFilename) || !classFile.exists()) { 56 throw new ClassNotFoundException("ClassNotFoundException " + javaFilename); 57 } 58 } catch (IOException e) { 59 e.printStackTrace(); 60 } 61 } 62 63 //如果class文件存在,将其转换为Class对象 64 if (classFile.exists()) { 65 try { 66 //将class文件的二进制数据读入数组 67 byte[] raw = getBytes(classFilename); 68 //调用ClassLoader的defineClass方法将class文件转换成Class对象 69 clazz = defineClass(name, raw, 0, raw.length); 70 } catch (IOException e) { 71 e.printStackTrace(); 72 } 73 } 74 75 // 如果clazz为null, 表明失败, 则抛出异常 76 if (clazz == null) { 77 throw new ClassNotFoundException(name); 78 } 79 80 return clazz; 81 } 82 83 public static void main(String[] args) throws Exception { 84 // 如果运行该程序时没有参数,即没有目标类 85 86 if (args.length < 1) { 87 88 System.out.println("缺少目标类,请安如下格式运行Java源文件"); 89 System.out.println("java MyClassLoader ClassName"); 90 } 91 92 //第一个参数是要运行的类 93 String progClass = args[0]; 94 95 //剩下的参数将做为目标运行时的参数 96 //将这些参数复制到一个新的数组中 97 String[] progArgs = new String[args.length - 1]; 98 System.arraycopy(args, 1, progArgs, 0 , progArgs.length); 99 MyClassLoader mcl = new MyClassLoader(); 100 //加载需要运行的类 101 Class<?> clazz = mcl.loadClass(progClass); 102 //获取需要运行类的主方法 103 Method main = clazz.getMethod("main", (new String[0]).getClass()); 104 Object argsArray[] = {progArgs}; 105 main.invoke(null, argsArray); 106 } 107 }
下面写一个测试用的类,
1 public class Test{ 2 public static void main(String[] args) throws Exception { 3 System.out.println("this is Test"); 4 } 5 }
使用 java MyClassLoader Test 执行程序
自定义类加载器的意义
自定义加载类,还能实现以下重要功能,
- 执行代码前自动验证代码签名
- 根据用户提供的密码解密代码, 从而实现代码混淆器来防止代码被反编译*.class文件
- 根据需求动态地加载类
- 根据需求把其他数据以字节码的形式加载到应用中