JVM自定义类加载器-读取与JAVA种名不同的文件名-并解析加载类
本文的目的,是通过解剖和修改JVM的类加载器,来详细分析JVM的类加载机制。
其实任何一个JVM的类加载器不过是做了如下的工作:
1. 确定JAVA类文件的位置。
2. 读取类文件内容,将类文件内容读取成二进制字节流。
3. 解析并加载类内容。
4. 最后,将类的“标识”返回给要使用这个类的代码中。
那下面我们就来做一个比较“另类”的试验:
在JAVA规范中,public类名必须与类所在的文件名相同。
但本文将尝试在一个名为“T1.bin”的文件中,加载一个名为“Test1”的类,以此来创造一种与JVM标准类加载器不同的类加载器。
先讲讲我的实现思路:
1. 先编写一个类,此类将被其它类来调用,这个类的类名和文件名在测试时将不会相同。这个类的类名为“Test1”,而文件名为“T1.bin”。
2. 编写自己的类加载器,此类加载器不是从寻常的".class"文件中来加载一个类,而是从代码中写死的“D:/Users/T1.bin”文件中来加载一个类。
3. 编写业务逻辑类,此类将利用反射原理,调用类加载器,读取T1.bin文件,构造第一步中建立的类的对象,并调用这个对象的方法。
首先在Eclipse中新建一个Java工程,名为“Test1”,在我的硬盘上,此工程的存储地址为“D:\Users\haojian.XINAO\workspace\Test1”。
在此工程的src目录下,新建一个Java类,名为“Test1”,内容如下:
/** * @author haojian * */ public class Test1 { // 此类的此方法,将被调用来测试 public void testMethod() { System.out.print("---test---"); // 标识1 } }
则打开"D:\Users\haojian.XINAO\workspace\Test1"目录后,在src目录下将有Test1.java文件,在bin目录下将有“Test1.class”文件。
将Test1.class文件复制到D:\Users目录下,修改文件名为“T1.bin”。此时此T1.bin文件在逻辑上已与Eclipse中的Test1工程没有任何关系。
编写读取此T1.bin文件的类加载器:
在Test1目录下新建包,名为"cld",在此包下新建类,内容如下:
/** * */ package cld; import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.WritableByteChannel; /** * @author haojian * */ public class MyClassLoader1 extends ClassLoader { public MyClassLoader1(ClassLoader parent, String baseDir) { super(parent); } protected Class findClass(String name) throws ClassNotFoundException { byte[] bytes = loadClassBytes(name); Class theClass = defineClass(name, bytes, 0, bytes.length); if (theClass == null) throw new ClassFormatError(); return theClass; } private byte[] loadClassBytes(String className) throws ClassNotFoundException { try { String classFile = getClassFile(); FileInputStream fis = new FileInputStream(classFile); FileChannel fileC = fis.getChannel(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); WritableByteChannel outC = Channels.newChannel(baos); ByteBuffer buffer = ByteBuffer.allocateDirect(1024); while (true) { int i = fileC.read(buffer); if (i == 0 || i == -1) { break; } buffer.flip(); outC.write(buffer); buffer.clear(); } fis.close(); return baos.toByteArray(); } catch (IOException fnfe) { throw new ClassNotFoundException(className); } } private String getClassFile() { return "D:/Users/T1.bin"; } }
MyClassLoader1类实现了自定义的类加载器, getClassFile() 方法的作用,是确定要读取的文件名,loadClassBytes()方法则将文件中的内容读取成二进制数组。findClass()则可以看做是MyClassLoader1类与它的父类ClassLoader类之间的接口,后面将要讲到。
此类将从D:/Users/T1.bin文件中读取Java类内容,并加载到JVM中去。
下面将编写业务流程代码,去“T1.bin”文件中读取并加载“Test1”类。并调用Test1类的“testMethod”方法。
/** * */ package cld; import java.lang.reflect.Method; /** * @author haojian * */ public class TestClass { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub // 创建自己写的类加载器的对象 MyClassLoader1 myClassLoader1 = new MyClassLoader1(TestClass.class.getClassLoader()); // 设定要加载的类名称 String className = "Test1"; Class loadedClassTest1 = null; try { // 实际干活,去“T1.bin”文件中加载"Test1"类 loadedClassTest1 = myClassLoader1.loadClass(className); // 标识2 } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } // 下面是利用反射来实际调用Test1类的“testMethod”方法 Object oTest1 = null; try { oTest1 = loadedClassTest1.newInstance(); Method testMethodCall = null; testMethodCall = loadedClassTest1.getMethod("testMethod"); testMethodCall.invoke(oTest1); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
以下为控制台输出结果:
---test---
可以看到,程序中确实实际调用了“标识1”位置的语句。
也许有人会问,MyClassLoader1这个类为什么会到D:/Users/T1.bin文件中去加载"Test1"类呢?在TestClass的main方法里,并没有调用getClassFile()方法啊?
让我们解析一下MyClassLoader1这个类的执行过程,大家就清楚了。。。
让我们先看标识2的代码,这里调用了MyClassLoader1这个类的loadClass(className)方法,这个方法做了什么工作呢?
我们查看MyClassLoader1类,发现没有loadClass方法,那么这个方法就应该是由它的父类来实现的。则我们继续打开ClassLoader这个类的loadClass方法:
public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // First, check if the class has already been loaded Class c = findLoadedClass(name); if (c == null) { try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClass0(name); } } catch (ClassNotFoundException e) { // If still not found, then invoke findClass in order // to find the class. c = findClass(name); } } if (resolve) { resolveClass(c); } return c; }
此方法的执行过程如下:
1. 首先查看JVM中是否已经加载过这个类。
2. 如没有加载过这个类,则首先用此类的父类来加载参数类名中指向的类。
3. 如仍然不能加载类,则调用findClass方法。
因为我们已经确定这个类是新加载过的,以前没有加载,所以1、2两步必然不能正确加载此类,程序必然会执行到第3步。
则我们继续追踪findClass()此类的代码。。。
protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
其实在ClassLoader类里,对findClass()是没有任何实现的,需要它的子类来实现。这样,执行权就顺利的交给了MyClassLoader1的findClass()方法,而通过findClass()->loadClassBytes()->getClassFile()就能顺利执行到getClassFile()方法了。
其实我这篇文章里面讲到的,都只是对类文件的读取,并没有涉及到最核心的“将二进制字节流解析成内存中的JAVA类结构”,将二进制解析成内存结构是由“defineClass()”方法来完成的,以后会慢慢讲到的。
其实按照我文章里的这种思路,大家可以尝试在数据库中、网站文件中、FTP中等地方来加载一个JAVA类。。。