本篇文章主要介绍java类加载器,以及自己动手编写自己的简单的类加载器。
java虚拟机中可以安装多个类加载器,默认的类加载器有3个:BootStrap,ExtClassLoader,AppClassLoader。不同的 类记载负责加载不同位置的类。类加载器本身也是java类,因为类加载器本身也要被类加载器加载,显然必须要有第一个类加载器,这个类加载器不是java类,这个类加载器就是BootStrap。类加载器是以父子的树形关系进行组织的,在实例化每个类加载器时,需要为其指定一个父类加载器或者采用系统类加载器作为其父类加载器。下图列出了类加载器的关系以及其管辖范围:
下面写代码来测试下如上的关系:
ClassLoader loader = ClassLoaderDemo.class.getClassLoader();
while( loader != null){
System.out.println(loader.getClass().getName());
loader = loader.getParent();
}
运行上面的代码得出如下的结果:
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader
从上面的结果可以分析出:最顶层的类加载器获取到的是null,为什么是null呢?因为这个类加载器不是java类,而是用c写的。从而验证了如上的关系。
另外介绍一个类加载器的委任机制,比如要加载某个类时,子类加载器会询问父类加载器是加载了,直接追溯到在高层的类加载器,如果父类加载器都不能加载了,则发起源类加载器再去加载,再加载不到就会跑出ClassNotFoundException异常,而不会再去找子类加载器去加载。可以去做实验去验证这点,比如将当前类路径下面的某个类(这个类主要输出加载这个类的类加载器)打成jar放到jre/lib/ext目录下,运行查看输出结果。为什么会有类加载器的委任机制呢?主要是出于安全性的考虑,用户可以自定义类加载器,加入没有类加载器的委任机制就很难确保内存中只有某个类的一份字节码,假如委任给父类加载器处理,只有父类加载器加载过了或者能加载就交给父加载器。
当java虚拟机要加载一个类时,到底派谁去加载呢?
- 首先当前线程的类加载器去加载线程的第一个类;
- 如果A类引用了B类,Java虚拟机将使用加载A类的类加载器去加载B类
- 也可以直接使用ClassLoader.loadClass()方法来指定某个类加载器去加载某个类
java中是怎样加载类的呢?在java中引进类加载器,用来加载类的,相应的类为ClassLoader,此类为抽象类,自定义的类加载器需要继承此类,从api中,可以看到,类加载器加载类时使用的方法为ClassLoader,此方法有相应的去加载类的一些算法,自定义类加载器需要实现public Class findClass(String name)方法,参数为类名,返回一个Class对象,ClassLoader内部实现了一个defineClass方法,这个类根据你提供的类的字节数组来加载类,ClassLoader使用了模板方法设计模式。
下面给出自定义类加载器的代码,先通过MyClassLoader的main方法来对某个编译后的类进行加密,然后类加载器加载时再进行解密,加解密算法很简单,只用到了异或
package cn.zq.demo;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
public class MyClassLoader extends ClassLoader{
/**
* class文件的扩展名
*/
private static final String CLASS_EXT = ".class";
/**
* 类所在的路径.
*/
private String path;
public MyClassLoader(String path) {
super();
this.path = path;
}
/**
* 此方法由jvm调用
*/
public Class<?> findClass(String name) throws ClassNotFoundException {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
/**
* 读取class文件
* @param name 类的完整名称
* @return 二进制字节数组
*/
private byte[] loadClassData(String name) {
FileInputStream fis = null;
try {
name = name.substring(name.lastIndexOf(".") + 1);
fis = new FileInputStream(path + "\\" + name + CLASS_EXT);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
cypher(fis, baos);
return baos.toByteArray();
} catch (Exception e) {
throw new RuntimeException(e);
}finally{
if(fis != null){
try {
fis.close();
} catch (Exception e) {
throw new RuntimeException(e);
}finally{
fis = null;
}
}
}
}
private static void cypher(InputStream in, OutputStream out) throws Exception{
int b = -1;
while( ( b = in.read() ) != -1){
out.write(b ^ 0XFF);
}
}
public static void main(String[] args) throws Exception{
String srcPath = args[0]; //源class文件所在路径
String fileDir = args[1]; //加密后的class文件所在目录
FileInputStream fis = new FileInputStream(srcPath);
String filename = srcPath.substring(srcPath.lastIndexOf("\\") + 1);
String destPath = fileDir + "\\" + filename;
FileOutputStream fos = new FileOutputStream(destPath);
cypher(fis, fos);
fos.close();
fis.close();
}
}
package cn.zq.demo;
public class Data {
public String toString() {
return "My data!";
}
}
package cn.zq.demo;
public class ClassLoaderDemo {
public static void main(String[] args) throws Exception {
ClassLoader loader = ClassLoaderDemo.class.getClassLoader();
while( loader != null){
System.out.println(loader.getClass().getName());
loader = loader.getParent();
}
MyClassLoader myClassLoader = new MyClassLoader("classfile");
Object o = myClassLoader.loadClass("cn.zq.demo.Data").newInstance();
System.out.println(o.getClass().getName() + "->" + o);
}
}
运行上面的代码有可能出现的异常为:
java.lang.ClassFormatError: Incompatible magic value 889275713 in class file cn/zq/demo/Data
表示类文件是无效的,
运行上面的程序如果出现的结果是:cn.zq.demo.Data->My data!
那么恭喜,您的程序可以跑通了,你的类加载器成功运行了,最后给出项目文件的地址,大家需要可以自行下载,如果遇到什么问题可以及时的联系我,随时为您解答各种力所能及的问题。