1、背景
最近在做项目的过程中,由于系统需要提供一个对外接口,使系统使用者可以以脚本的形式提交自己的代码,每个用户可以在系统规范的约束下编写脚本,由系统去执行用户的代码,实现了热部署。什么叫热部署呢?简单来说就是把代码当成U盘或者外设一样即插即用,每个用户可以维护自己的解决方案(也就是一段脚本,一个单独的类),在更新修改解决方案的过程中而不需要重新编译启动整个系统。我们采用的方案就是GroovyClassLoader,我主要讲一讲自己对ClassLoader的理解和使用。
2、类加载与类加载器
类加载:
类加载的过程就是将Class文件中描述的各种信息加载到虚拟机中,供程序后期运行和使用的。
类加载的生命周期主要分为五个步骤:
1、加载:
通过一个类的全限定名来获取描述此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法去的运行时数据结构
在内存中生成一个代表这个类的java.lang.Class 对象,作为方法区的各种数据类型的入口
2、验证
为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害到自身的安全。包括文件格式验证,元数据验证,字节码验证,符号引用验证。
3、准备
为变量分配内存,设置类变量的初始值。
4、解析
将常量池中的符号应用替代为直接引用。
5、初始化
是类加载生命周期的最后一个过程,执行类中定义的java程序代码
该过程如图所示:
类加载器:
在前面的类加载过程中,大部分动作都是完全由虚拟机主导和控制的。而类加载器使得用户可以在加载的过程中参与进来,结合前面的内容,类加载器就是将“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到java虚拟机外部来实现。将主动权交给程序猿。
类加载器和这个类本身确定了其在java虚拟机中的唯一性,每一个类加载器都有一个独立的类命名空间,也就意味着,如果比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那么这两个类就注定不相同。
java的类加载器:
1、Bootstrap Class Loader:负责加载JAVA_HOME/lib目录下或-Xbootclasspath指定目录的jar包;
2、Extention Class Loader:加载JAVA_HOME/lib/ext目录下的或-Djava.ext.dirs指定目录下的jar包。
3、System Class Loader:加载classpath或者-Djava.class.path指定目录下的类或jar包。
ClassLoader各司其职,加载在不同路径下的class文件,值得注意的是,类加载采用的是双亲委托的设计模式,即传入一个类限定名,逐层向上到Bootstrap Class Loader中查找,如果找到即返回,若没有找到,则在Extention Class Loader中找,若还没有找到则在System Class Loader下找,即classpath中,如果还没有找到,则调用findClass(name)方法,执行用户自己的类加载逻辑(可能在其他的地方)
ClassLoader中的几个重要的方法:
1、loadClass(String name, boolean resolve):加载类的方法,在jdk1.2以前需要重写该方法实现用户自己的逻辑,1.2以后为了向下兼容,仍然可以重写该方法,但是建议用户将自己的加载逻辑实现在findName(name)中。这样系统先向上寻找能否加载到该类,如果加在不到,将调用用户自定义的findName函数加载对象.
/**
* @param name 类名字
* @param resolve 是否解析,如果只是想知道该class是否存在可以设置该参数为false
* @return 返回一个class泛型
* @throws ClassNotFoundException
*/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
/**
* getClassLoadingLock(name)
* 为类的加载操作返回一个锁对象。为了向后兼容,这个方法这样实现:如果当前的classloader对象注册了并行能力,
* 方法返回一个与指定的名字className相关联的特定对象,否则,直接返回当前的ClassLoader对象。
*/
synchronized (getClassLoadingLock(name)) {
// 首先查看class是否已经被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//如果父加载器不为空,则委托给父加载器去加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
/**
* 如果父加载器为空,说明父加载器已经是Bootstrap ClassLoader了,则直接使用根加载器加载,也就是使用虚拟机加
* 载器加载
*/
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//如果以上的加载器在自己的路径上面都没有加载到,则调用findClass(name)调用用户自定义的加载器
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
//根据resolve参数决定是否解析该类
if (resolve) {
resolveClass(c);
}
return c;
}
}
2、ClassLoader getParent() :可以返回委托的父类加载器。在你自定义加载器找不到相应类的时候,可以调用此方法,不过在ClassLoader的默认实现中,ClassLoader先判断父类加载器是否可以加载,然后再调用用户自定义的findClass方法。
3、 resolveClass():若resolve参数为true的时候,我们需要调用该函数,resolve我们的classLoader。
4、ClassLoader getSystemClassLoader():提供了一个直接访问系统classloader的方法。
3、废话少说上代码!
下面我将以一个例子来阐述如何使用ClassLoader,自定义的ClassLoader将加载被加密的类,而且这个类存储的路径不在ClassPath中,也不可以被Bootstrap Class Loader和Extention Class Loader加载,在实际应用中,可以是网络中传递过来的加密字节流,抑或着是实现脚本的热部署操作。
package com.siyu;
import java.io.*;
public class ClassLoaderTest extends ClassLoader {
//自定义加载器加载该路径下面的文件
private String directory;
public ClassLoaderTest(String directory) {
this.directory = directory;
}
/**
* 重写findClass,用户可以做以下的事情
* 1.可以加载boot、ext、system加载器所加载不了的路径下的文件
* 2.可以解密加密后的class文件
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//解密密钥
byte key = (byte) 1;
//加密文件的路径
String fileName = directory + name + ".class";
File file = new File(fileName);
byte[] decryptedByte = readFromFile(file);
//解密为原始的class文件
for (int i = 0; i < decryptedByte.length; i++) {
decryptedByte[i] = (byte) (decryptedByte[i] ^ key);
}
//defineClass实现了链接阶段的验证等
return defineClass(null, decryptedByte, 0, decryptedByte.length);
}
private byte[] readFromFile(File fileName) {
try {
byte[] bytes = null;
FileInputStream fin = new FileInputStream(fileName);
int i;
if ((i = fin.read()) != -1) {
//初始化数组大小和文件大小一样
bytes = new byte[fin.available()];
fin.read(bytes);
}
return bytes;
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
private byte[] encrypt(byte[] bytes) {
byte key = (byte) 1;
//依次加密的代码
for (int i = 0; i < bytes.length; i++) {
bytes[i] = (byte) (bytes[i] ^ key); //利用异或加密
}
return bytes;
}
public void encryptFile(String fileName, String directory) {
try {
String name = fileName.substring(fileName.lastIndexOf("\\") + 1, fileName.length() - 6);
//加密文件的路径
String destFileName = directory + "encryted" + name + ".class";
//如果加密文件不存在则创建加密文件
File f = new File(destFileName);
if (f == null) {
f.createNewFile();
}
//加密
byte[] encryptedByte = encrypt(readFromFile(new File(fileName)));
FileOutputStream fos = new FileOutputStream(destFileName);
//把加密后的字节写入到加密文件中
fos.write(encryptedByte);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
//设置加密路径
ClassLoaderTest classLoaderTest=new ClassLoaderTest("C:\\EncryptedClass\\");
//将test.class加密后存储到EncryptedClass目录下
classLoaderTest.encryptFile("C:\\Users\\jasonchu.zsy\\IdeaProjects\\BoKeTest\\out\\production\\BoKeTest\\com\\siyu\\test.class"
,"C:\\EncryptedClass\\");
try {
Class<?> t=classLoaderTest.loadClass("encrytedtest");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}