本文的目的,是通过解剖和修改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类。。。