本来这篇博客是准备在前面一篇博客结合在一起,结果发现写完类加载机制博客有点长了。于是我就将其分开一起进行讲解了。
类加载器主要作用就是将类的字节码加载到虚拟机当中。那么关于类的加载过程相关的内容我就不想提了,因为在上一篇博客Java类加载机制中已经进行了分析,不懂的去看前面的内容。我们现在就直接进入正题讲解类加载器相关的东西。
#类加载器的层次结构以及双亲委派模型
由上图我们可以看到类加载器的大致分类了。其中这张图还展现了双亲委派模型,这个我们在后面再进行解释,这里我们先来对每个类加载器进行一下讲解。
启动类加载器(Bootstrap ClassLoader):这个类加载器将会负责加载JAVA_HOME/lib或者是-Xbootclasspath参数所指定的路径中的类库。特别需要注意的是启动类加载器只会加载JVM所能识别的类库,换句话就是即使你将名称不符的类库放在了lib中,也是不会加载的。
扩展类加载器(Extension ClassLoader):这个类加载器负责加载JAVA_HOME/lib/ext或者java.ext.dirs所指定的路径下的所有类库。
应用类加载器(Application ClassLoader):也被称为系统类加载器。这个类加载器负责加载用户类路径也就是classpath中的类库。
自定类加载器(User ClassLoader):这就是用户根据自己需求来进行编译的类加载器了。
这些类加载器全部是继承自ClassLoader的。对于HotSpot虚拟机来说,上面的三个系统的类加载器除了启动类加载器是用C++进行编写的,其它的都是用Java来进行编写的。
上面简单的介绍了各个类加载器。现在我们开始将一下双亲委派模型吧!
双亲委派模型就是当一个类加载器接收到了类加载请求的时候,这个类加载器不会立即去加载这个类,而是把这个请求委托给它的父类加载器(注意这里说的父类加载器并不是继承的关系,它们是一种包含的关系,也就是子类加载器中包含有父类加载器),每个层次的类加载器都是这样进行操作的。知道传给启动类加载器为止。然后,从启动类加载器开始,进行类的加载(也就是从其搜索范围内进行类的搜索),如果能进行加载就进行加载,如果不行的话就向下委托给它的子类加载器。
我们咋一看这样的方式感觉多此一举呀!为什么不等尝试在子类加载器中加载后,如果不能加载的时候再委托给父类呢?答案在这里。通过双亲委派模型的话,这会让一个类具有相应的优先级。比如说:如果我们自己写一个Java.lang包,并在其中写一个Object类。那么我们在进行Object对象的使用的使用,通过双亲委派模型我们会得到JAVA_HOME/lib下的Object类而不是这个Obect类。这东西可以自己去试一试。
每个类加载器都拥有自己的类名称空间
一个类在虚拟机中的独立性都是通过类加载器以及类本身的包名和类名来进行唯一确定的。每个类加载器都有自己独立的命名空间。其实也就是说同一个Class文件,通过不同的类加载器进行加载,这两个类也是不相同的。
下面是一个通过重写loadClass方法(一般自定义ClassLoader的时候,我们不推荐重写loadClass方法,而是推荐重写findClass方法,这里是为了需要才这么做的,理由稍后解释)来破坏下面要提到的双亲委派模型进行自定义类加载器加载类的代码:
public class Main {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
//自定义类加载器
ClassLoader classLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream inputStream = getClass().getResourceAsStream(fileName);
//为什么要加上这一句,那是因为在下面的defineClass方法中会通过这个类加载器继续加载其父类一直到加载完Object类为止,比如说像Object类的加载就不能通过这个类加载器进行加载,所以需要交给父加载器。
if (inputStream == null){
return super.loadClass(name);
}
byte[] b = new byte[inputStream.available()];
inputStream.read(b);
return defineClass(name,b,0,b.length);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
};
//通过自定义类加载器加载该Main类
Class cl = classLoader.loadClass("Main");
Object obj = cl.getDeclaredConstructor().newInstance();
System.out.println(obj.getClass());
System.out.println(obj.getClass().getClassLoader());
System.out.println(obj instanceof Main);
//通过系统的应用类加载器加载该Main类
Main main = new Main();
System.out.println(main.getClass());
System.out.println(main.getClass().getClassLoader());
System.out.println(main instanceof Main);
}
}
下面是执行结果:
我们可以看到自定义ClassLoader加载这个这个类用instanceof来进行判断是false,而用系统默认的AppClassLoader来进行加载时true。这说明了类加载器不同即使时同一个类也是不相同的。还有就是instanceof进行对象类型检验的时候是检验是不是通过正确的双亲委派进行的类加载所得的类的检查。
自定义类加载器
上面简单的介绍了类加载器,现在我们就进行今天的重点。
自定义类加载器的操作:
- 自定义一个继承自ClassLoader的类
- 然后覆盖父类的findClass方法。前面我们说到不赞成覆盖父类的loadClass方法,为什么呢?那是因为loadClass方法写的是双亲委派模型的代码。如果我们去覆盖了这个方法,就会破坏双亲委派模型。而findClass的覆盖并不会破坏因为这个方法是一个空实现。而findClass方法的执行只有在该类未被加载,并且父类加载器无法加载的时候才会进行执行。
我们实现findClass方法需要做到下面两点:
- 加载来自本地文件系统或者其它来源的类的字节码
- 调用ClassLoader的defineClass方法,向虚拟机提供字节码。
下面我通过自定义类加载器来实现一个简单的字节码加密以及解密操作(并且通过这个进行一个双亲委派模型的理解):
首先我们看代码:(这里对每个类的主要功能进行了分析,具体的理解还请读者,自己去细看)
- Caesar.java:这是实现加密操作的一个类
- CryptoClassLoader.java:这是实现字节码解密操作的一个ClassLoader
- CaesarTest.java:这是我们的测试用类
- Main.java:这是我们的主类
//Caesar.java
public class Caesar {
public static void main(String[] args) {
try (FileInputStream in = new FileInputStream(args[0]);
FileOutputStream out = new FileOutputStream(args[1])) {
int key = Integer.parseInt(args[2]);
int ch;
while ((ch = in.read()) != -1) {
byte c = (byte) (ch + key);
out.write(c);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
//CryptoClassLoader.java
public class CryptoClassLoader extends ClassLoader {
private int key;
public CryptoClassLoader(int key) {
this.key = key;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classBytes = null;
classBytes = loadClassBytes(name);
Class<?> cl = defineClass(name, classBytes, 0, classBytes.length);
if (cl == null) throw new ClassNotFoundException();
return cl;
}
private byte[] loadClassBytes(String name) {
String cname = name.replace('.', '/') + ".caesar";
byte[] bytes = null;
try {
bytes = Files.readAllBytes(Paths.get(cname));
for (int i = 0;i < bytes.length;i++){
bytes[i] = (byte) (bytes[i] - key);
}
} catch (IOException e) {
e.printStackTrace();
}
return bytes;
}
}
//CaesarTest.java
public class CaesarTest {
public static void main(String[] args) {
System.out.println("CaesarTest");
}
}
//Main.java
public class Main {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
ClassLoader loader = new CryptoClassLoader(3);
Class<?> cl = loader.loadClass("CaesarTest");
System.out.println(cl.getClassLoader());
Method method = cl.getMethod("main", String[].class);
method.invoke(null, (Object) new String[]{});
}
}
下面我们来进行测试一下:
- 首先,我们先通过
javac
命令编译出CaesarTest类的字节码、Caesar类、Main类的字节码。由于Main中引用了CryptoClassLoader类,所以编译后会自动生成其字节码。
//也就是依次执行下面的命令
javac CaesarTest.java
javac Caesar.java
javac Main.java
- 然后,我们在通过执行Caesar对CaesarTest.class文件进行加密,这里我们将加密后的字节码命名为CaesarTest.caesar。因为如果也命名为后缀为.class的文件的话,加密后的字节码在我们自定义ClassLoader的父类加载器的加载中会出问题的。
//这是这一步的执行命令。注意:第一个是我们要进行加密的字节码,第二个是加密后的文件,第三个是加密密码
java Caesar CaesarTest.class CaesarTest.caesar 3
- 之后,我们执行Main类。
//这一步的命令
java Main
然而,我们看看执行的结果:
emmm…不对呀!!!我们加载的类加载器应该是我们的自定义类加载器呀!为什么是AppClassLoader呢?仔细想想看,我们上面的类加载器是重写其findClass方法的。这个并不会去破坏类双亲委派模型。也就是说通过这个模型最终加载的是CaesarTest.class文件,而不是在我们自定义类加载器中去进行解密加载的CaesarTest.caesar文件。既然是CaesarTest.class文件惹得祸,那么我们就把这个文件删掉,然后在去执行Main方法,这是满足上面我们讲的findClass方法执行的两个条件了这是就会进行解密并加载CaesarTest.caesar文件的操作了。
然后我们删掉再执行Main类,就是这样了:
- 最后,我们来试一下密码错误的情况。我们在Main类中将密码从3改为4再试一下:
//这次执行的命令
javac Main.java
java Main
没错,密码不对,这次报了一个ClassFormatError。也就是我们的字节码格式有误,毕竟解密出错了嘛!
总结
今天我们对ClassLoader相关的东西进行了讲解。然后再通过一个自定义ClassLoader来加载加密后的字节码的操作来实战了一下。对于类加载器,我们主要要知道类加载器的各个层次以及理解双亲委派模型。
参考
《Java核心技术 卷二》
《深入理解Java虚拟机》
转载请注明链接