欢迎阅读本专题的其他博客:
在大多数情况下,系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下,您还是需要为应用开发出自己的类加载器。比如您的应用通过网络来传输Java类的字节代码,为了保证安全性,这些字节代码经过了加密处理。这个时候您就需要自己的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在Java虚拟机中运行的类来。下面将通过两个具体的实例来说明类加载器的开发。
要创建自定义的类加载器只需要扩展java.lang.ClassLoader类就可以,然后覆盖它的findClass(Stringname)方法即可。该方法根据参数指定的类的名字,去找对应的class文件。然后返回class对应的对象。下面我们就根据我们自定义的类加载器的源码来具体详解一下这个自定义的步骤:
自定义的类加载器:
package com.bzu.csh.test;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
/**
*
* @author 曹胜欢
* @version 1.0
*
*/
public class MyClassLoader extends ClassLoader {
private String name; // 类加载器的名字
private String path = "d:\\"; // 加载类的路径
private final String fileType = ".class"; // class文件的扩展名
public MyClassLoader(String name) {
super(); // 让系统类加载器成为该类加载器的父加载器
this.name = name;
}
public MyClassLoader(ClassLoader parent, String name) {
super(parent); // 显式指定该类加载器的父加载器
this.name = name;
}
@Override
public String toString() {
return this.name;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
/**
* @param 类文件的名字
* @return 类文件中类的class对象
*
* 在这里我们并不需要去显示的调用这里的findclass方法,在上篇文章中,我们通过查看
* loadclass的源码可以发现,她是在loadclass中被调用的,所以这里我们只需重写这个方法,
* 让它根据我们的想法去查找类文件就ok,他会自动被调用
*
*
* defineClass()将一个 byte 数组转换为 Class 类的实例。必须分析 Class,然后才能使用它
* 参数:
* name - 所需要的类的二进制名称,如果不知道此名称,则该参数为 null
* b - 组成类数据的字节。off 与 off+len-1 之间的字节应该具有《Java Virtual Machine Specification》定义的有效类文件的格式。
* off - 类数据的 b 中的起始偏移量
* len - 类数据的长度
*/
@Override
public Class> findClass(String name) throws ClassNotFoundException {
byte[] data = this.loadClassData(name);//获得类文件的字节数组
return this.defineClass(name, data, 0, data.length);//
}
/**
*
* @param 类文件的名字
* @return 类文件的 字节数组
* 通过类文件的名字获得类文件的字节数组,其实主要就是用
* 输入输出流实现。
*/
private byte[] loadClassData(String name) {
InputStream is = null;
byte[] data = null;
ByteArrayOutputStream baos = null;
try {
this.name = this.name.replace(".", "\\");
is = new FileInputStream(new File(path + name + fileType));
baos = new ByteArrayOutputStream();
int ch = 0;
while (-1 != (ch = is.read())) {
baos.write(ch);
}
data = baos.toByteArray();
} catch (Exception ex) {
ex.printStackTrace();
} finally {
try {
is.close();
baos.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
return data;
}
我想上面的注释中已经足够让大家明白这个自定义类加载器的原理了。在这我在重复的从上到下的再说一遍,加深一下大家的理解。首先在构造方法中,我们可以通过构造方法给类加载器起一个名字,也可以显示的指定他的父累加器器,如果没有显示的指出父类加载器的话他默认的就是系统类加载器。由于我们继承了ClassLoader类,所以它自动继承了父类的loadclass方法。我们以前看过loadclass的源码知道,它调用了findclass方法去查找类文件。所以在这里我们重写了ClassLoader的findclass方法。在这个方法中首先调用loadClassData方法,通过类文件的名字获得类文件的字节数组,其实主要就是用输入输出流实现。然后调用defineClass()将一个字节数组转换为Class类的实例。有时候我们手动生成的二进制码的class文件被加密了。所以在我们在利用我们自定义的类加载器的时候还要写一个解密的方法进行解密,这里我们就不实现了。
我们实现了自定义类加载器,下一步我们来看一下我们怎么来应用我们这个自定义的类加载器:
public static void main(String[] args) throws Exception {
//创建一个loader1类加载器,设置他的加载路径为d:\\serverlib\\,设置默认父加载器为系统类加载器
MyClassLoader loader1 = new MyClassLoader("loader1");
loader1.setPath("d:\\myapp\\serverlib\\");
//创建一个loader2类加载器,设置他的加载路径为d:\\clientlib\\,并设置父加载器为loader1
MyClassLoader loader2 = new MyClassLoader(loader1, "loader2");
loader2.setPath("d:\\myapp\\clientlib\\");
//创建一个loader3类加载器,设置他的加载路径为d:\\otherlib\\,并设置父加载器为根类加载器
MyClassLoader loader3 = new MyClassLoader(null, "loader3");
loader3.setPath("d:\\myapp\\otherlib\\");
test(loader2);
System.out.println("----------");
test(loader3);
}
public static void test(ClassLoader loader) throws Exception {
Class clazz = loader.loadClass("com.bzu.csh.test.Sample");
Object object = clazz.newInstance();
}
类加载器结构图
(PS:突然发现WPS自带的画图工具也挺好用,虽然有点难看,哈哈)
当执行这段代码的时候。首先让loader2去加载Sample类文件,当然我们在执行这段代码的前提时在各个默认加载器中已经有我们Sample的class文件。Loader2首先让父加载器是loader1去加载,然后loader1会让系统类加载器去加载,系统类加载器会让扩展类加载器加载,扩展类加载器会让根类加载器加载,由于系统类加载器,扩展类加载器,根类加载器的默认路径中都没有我们要的sample类,所以loader2的默认路径有sample这个类,也就是说loader2会去加载这个sample类。当执行test(loader3)的时候,由于loader3的默认父加载器是根类加载器,并且根类加载前默认路径没有对应的sample.class文件,所以,直接的loader3类加载器就去加载这个类。
最后要说明的一点是,自定义类加载不光只能从我们本地加载到class文件,我们也可以加载网络,即基本的场景是:Java字节代码(.class)文件存放在服务器上,客户端通过网络的方式获取字节代码并执行。当有版本更新的时候,只需要替换掉服务器上保存的文件即可。通过类加载器可以比较简单的实现这种需求。其实他的实现和本地差不多,基本上就是geclassdata方法改变了一些。下面我们来具体看一下:
private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
URL url = new URL(path);
InputStream ins = url.openStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
在通过网络加载了某个版本的类之后,一般有两种做法来使用它。第一种做法是使用Java反射API。另外一种做法是使用接口。需要注意的是,并不能直接在客户端代码中引用从服务器上下载的类,因为我们写的类加载器被加载所用的类加载器和我们加载的网络类不是同一个类加载器,所以客户端代码的类加载器找不到这些类。使用Java反射API可以直接调用Java类的方法。而使用接口的做法则是把接口的类放在客户端中,从服务器上加载实现此接口的不同版本的类。在客户端通过相同的接口来使用这些实现类。
不同类加载器的命名空间关系:
1.同一个命名空间内的类是相互可见的。
2.子加载器的命名空间包含所有父加载器的命名空间。因此子加载器加载的类能看见父加载器加载的类。例如系统类加载器加载的类能看见根类加载器加载的类。
3.由父加载器加载的类不能看见子加载器加载的类。
4.如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见。
5.当两个不同命名空间内的类相互不可见时,可以采用Java的反射机制来访问实例的属性和方法。