一、简介
通常情况下,我们都是直接使用系统类加载器,但是有些时候,由于某种特殊需求,我们也需要自定义类加载器。比如,应用程序是根据网络来传输字节码文件信息, 为了保证在网络传输过程中字节码文件的安全,通常都会进行加密,这样我们在加载类的时候,就需要进行解密,这种需求使用系统提供的类加载器是实现不了的,这就需要我们自己定义加密解密类加载器。自定义类加载器一般都是继承ClassLoader类。
二、自定义类加载器流程
【a】继承java.lang.ClassLoader类
【b】首先检查请求的类型是否已经被这个类加载器加载到命名空间中,如果已经装载则直接返回
【c】委托类加载请求给父类加载器,如果父类加载器能够完成加载工作,则返回父类加载器加载的Class实例
【d】调用本类的findClass()方法,试图获取对应的字节码,如果获取的到,则调用defineClass()导入类型到方法区,如果获取不到,返回异常给loadClass()方法,loadClass()转抛异常,终止加载过程。
- 注意:被两个类加载器加载的同一个类,JVM并不认为是相同的类。
三、自定义类加载器示例
下面我们自定义一个文件系统类加载器,实现传入类的全限定名,然后根据IO流读取.class字节码信息。
首先在d:java下面新建一个Test.java:
package com.wsh;
public class Test {
public static void main(String[] args) {
System.out.println("test");
}
}
接着使用命令行工具编译Test.java,生成Test.class字节码文件:
这样在d:/java/com/wsh/路径下生成了Test.class字节码文件:
/**
* @Description: 自定义文件系统类加载器
* @author: weishihuai
* @Date: 2019/1/16 16:32
*/
public class FileSystemClassLoader extends ClassLoader {
/**
* 根目录路径
*/
private String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//1. 查找该类加载器是否已经装载这个类,如果已经装载,则直接返回该Class对象
Class<?> loadedClass = findLoadedClass(name);
if (null == loadedClass) {
//如果未装载,依据双亲委托机制,寻找父类加载器进行加载
ClassLoader parent = this.getParent();
try {
loadedClass = parent.loadClass(name);
} catch (Exception e) {
// e.printStackTrace();
}
//如果父类加载器加载成功,则返回父类加载器加载的Class对象
if (null != loadedClass) {
return loadedClass;
} else {
//文件流读取返回字节数组
byte[] classData = getClassData(name);
//如果自己都加载失败的话直接抛出ClassNotFoundException异常
if (null == classData) {
throw new ClassNotFoundException();
} else {
//使用defineClass()加载类
loadedClass = defineClass(name, classData, 0, classData.length);
}
}
} else {
return loadedClass;
}
return loadedClass;
}
/**
* 根据路径名称获取.class字节数组信息
*
* @param name 路径
* @return
*/
private byte[] getClassData(String name) { //com.wsh.Test d:/java/com/wsh/Test.class
StringBuilder path = new StringBuilder(rootDir).append(File.separator).append(name.replace(".", File.separator)).append(".class");
ByteArrayOutputStream byteArrayOutputStream = null;
InputStream inputStream = null;
try {
inputStream = new FileInputStream(path.toString());
byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, len);
}
return byteArrayOutputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != byteArrayOutputStream) {
try {
byteArrayOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != inputStream) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
}
测试类:
public class TestFileSystemClassLoader {
public static void main(String[] args) {
FileSystemClassLoader fileSystemClassLoader = new FileSystemClassLoader("d:/java");
try {
//加载d:/java/com/wsh/Test.class字节码信息
Class<?> loaderClass = fileSystemClassLoader.loadClass("com.wsh.Test");
//class com.wsh.Test
System.out.println(loaderClass);
//com.wsh.jvm.classloader.FileSystemClassLoader@677327b6
System.out.println(loaderClass.getClassLoader());
Object object = loaderClass.newInstance();
//com.wsh.jvm.classloader.FileSystemClassLoader@677327b6
System.out.println(object.getClass().getClassLoader());
Class<?> loaderClass2 = fileSystemClassLoader.loadClass("com.wsh.jvm.classloader.Test");
//由于双亲委托机制,在classpath下的类默认都会由AppClassLoader类加载器加载
//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(loaderClass2.getClassLoader());
Class<?> loaderClass3 = fileSystemClassLoader.loadClass("java.lang.String");
//因为Java核心类库是由引导类加载器BootStrapClassLoader进行加载,所以返回为null
//null
System.out.println(loaderClass3.getClassLoader());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
}
}
- 注意点:因为类Test 本身可以被 AppClassLoader 类加载,因此我们不能把 Test.class 放在类路径下。否则,由于双亲委托机制的存在,会导致该类由AppClassLoader 加载,而不会通过我们自定义类加载器来加载。
四、自定义类加载器示例二
通过上面的示例,对自定义类加载器的流程已有初步的了解,接下来,我们再通过一个示例【自定义加密解密类加载器】加深对自定义类加载器的理解。
因为.class字节码文件是二进制文件,所以简单起见,实现对字节码取反的方式进行加密,然后使用自定义解密类加载器加载该类。
关于取反,我们可以通过异或的方式进行 xxxx ^ 0xff ,通过下图了解取反怎么取:
【第一步】编写加密方法:
/**
* 加密方法
*
* @param src 源文件路径
* @param dest 目标文件路径
*/
public static void encryptClass(File src, File dest) {
InputStream fileInputStream = null;
OutputStream fileOutputStream = null;
try {
fileInputStream = new FileInputStream(src);
fileOutputStream = new FileOutputStream(dest);
//接收长度
int len;
while ((len = fileInputStream.read()) != -1) {
//通过异或操作对读取的输入流进行取反
fileOutputStream.write(len ^ 0xff);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != fileOutputStream) {
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != fileInputStream) {
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
【第二步】对.class二进制字节码文件进行取反加密:
public class Test{
public static void main(String[] args) {
encryptClass(new File("d:/java/com/wsh/Test.class"), new File("d:/java/temp/com/wsh/Test.class"));
}
}
执行完之后,在temp/com/wsh目录下就可以看到加密之后的字节码文件,下面我们就需要自定义解密类加载器去读取这个字节码文件:
【第三步】自定义解密类加载器
/**
* @Description: 解密类加载器
* @Author: weishihuai
* @Date: 2019/1/16 21:36
*/
public class DecipherClassLoader extends ClassLoader {
/**
* 根目录路径
*/
private String rootDir;
public DecipherClassLoader(String rootDir) {
this.rootDir = rootDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//1. 查找该类加载器是否已经装载这个类,如果已经装载,则直接返回该Class对象
Class<?> loadedClass = findLoadedClass(name);
if (null == loadedClass) {
//如果未装载,依据双亲委托机制,寻找父类加载器进行加载
ClassLoader parent = this.getParent();
//这里加try-catch的原因是怕父类加载器加载失败之后,报错就不会执行下面的代码.
try {
loadedClass = parent.loadClass(name);
} catch (Exception e) {
// e.printStackTrace();
}
//如果父类加载器加载成功,则返回父类加载器加载的Class对象
if (null != loadedClass) {
return loadedClass;
} else {
//文件流读取返回字节数组
byte[] classData = getClassData(name);
//如果自己都加载失败的话直接抛出ClassNotFoundException异常
if (null == classData) {
throw new ClassNotFoundException();
} else {
//使用defineClass()加载类
loadedClass = defineClass(name, classData, 0, classData.length);
}
}
} else {
return loadedClass;
}
return loadedClass;
}
/**
* 根据路径名称获取.class字节数组信息
*
* @param name 路径
* @return
*/
private byte[] getClassData(String name) {
StringBuilder path = new StringBuilder(rootDir).append(File.separator).append(name.replace(".", File.separator)).append(".class");
ByteArrayOutputStream byteArrayOutputStream = null;
InputStream inputStream = null;
byte[] data = null;
try {
inputStream = new FileInputStream(path.toString());
byteArrayOutputStream = new ByteArrayOutputStream();
int len;
while ((len = inputStream.read()) != -1) {
//对加密后的二进制文件再次取反就是解密
byteArrayOutputStream.write(len ^ 0xff);
}
data = byteArrayOutputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != byteArrayOutputStream) {
try {
byteArrayOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != inputStream) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return data;
}
}
【第四步】测试解密类加载器
如果我们使用之前的FileSystemClassLoader来加载这个加密后的在字节码文件的话,会直接报错:
public class TestDecrptClassLoader {
public static void main(String[] args) {
FileSystemClassLoader fileSystemClassLoader = new FileSystemClassLoader("d:/java/temp");
try {
Class<?> clazz = fileSystemClassLoader.loadClass("com.wsh.Test");
System.out.println(clazz);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
下面我们使用DecipherClassLoader来加载该类:
public class TestDecrptClassLoader {
public static void main(String[] args) {
DecipherClassLoader decipherClassLoader = new DecipherClassLoader("d:/java/temp");
try {
Class<?> clazz2 = decipherClassLoader.loadClass("com.wsh.Test");
System.out.println(clazz2);
System.out.println(clazz2.getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
由上图可见,已经成功通过自定义的解密类加载器加载了Test类。
五、总结
通过两个自定义类加载器,对自定义类加载器的流程有了进一步的认识,通常情况下,我们使用系统默认的类加载器即能满足大部分的需求,对于一些特殊的需求,那么我们可以通过自定义类加载器来满足特定的需求。本文是笔者对自定义类加载器的一些见解和认识,仅供大家学习参考,不对之处,希望大家多多指点。