JVM自定义类加载器-读取与JAVA种名不同的文件名-并解析加载类

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类。。。