Java安全之 ClassLoader类加载器
Java安全之 ClassLoader类加载器
0x00 前言
前面这里抛出一个问题,Java到底是什么类型的编程语言?是编译型?还是解释型?在这个问题是其实一直都都有疑惑,如果说是解释型语言的话,那么为什么需要编译呢?如果说是编译型语言的话,那么在编译完成后,需要JVM去解析才能运行呢?其实两种说法都对,也不对。下面来看看java的一个执行流程就知道是怎么回事了。
0x01 类加载机制
Java中的源码.java
后缀文件会在运行前被编译成.class
后缀文件,Java类初始化的时候会调用java.lang.ClassLoader
加载字节码,.class
文件中保存着Java代码经转换后的虚拟机指令,当需要使用某个类时,虚拟机将会加载它的.class
文件,并创建对应的class对象,将class文件加载到虚拟机的内存。
在其中其实包含了比较多的内容,下面来看看他的详细执行流程。
具体的实现分为三大步骤:
第一步 加载:
类加载指的是将class文件读入内存,并为之创建一个java.lang.Class对象,即程序中使用任何类时,系统都会为之建立一个java.lang.Class对象,系统中所有的类都是java.lang.Class的实例。
类的加载由类加载器完成,JVM提供的类加载器叫做系统类加载器,此外还可以通过继承ClassLoader基类来自定义类加载器。
第二步 连接:
连接阶段负责把类的二进制数据合并到JRE中
其又可分为如下三个阶段:
验证:确保加载的类信息符合JVM规范,无安全方面的问题。
准备:为类的静态Field分配内存,并设置初始值。
解析:将类的二进制数据中的符号引用替换成直接引用。
第三步 初始化:
类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化
双亲委托机制
当一个类加载器查找class和resource时,是通过“委托模式”进行的,它首先会判断这个class是不是已经加载成功,如果没有加载的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了,直接返回,如果没有找到,则一级一级返回,最后是由自身去查找这些对象;这种机制就叫做双亲委托。
其他的方式也可以去加载类的二进制数据:
1.从本地文件系统加载class文件。
2.从JAR包中加载class文件,如JAR包的数据库启驱动类。
3.通过网络加载class文件。
4.把一个Java源文件动态编译并执行加载。
0x02 ClassLoader类加载器
前面提到过编译成class字节码后的文件,会使用类加载器加载字节码。也就是说在java中所有的类都会通过加载器进行加载才能运行。在JVM类加载器中最顶层的是Bootstrap ClassLoader(引导类加载器)
、Extension ClassLoader(扩展类加载器)
、App ClassLoader(系统类加载器)
,AppClassLoader
是默认的类加载器,如果类加载时我们不指定类加载器的情况下,默认会使用AppClassLoader
加载类。
ClassLoader类 核心方法:
1.loadClass(String className),根据名字加载一个类。
2.defineClass(String name, byte[] b, int off, int len),将一个字节流定义为一个类。
3.findClass(String name),查找一个类。
4.findLoadedClass(String name),在已加载的类中,查找一个类。
5.resolveClass(链接指定的Java类)
0x03 自定义Classloader加载class文件
在ClassLoader中有四个很重要实用的方法loadClass()、findLoadedClass()、findClass()、defineClass(),可以用来创建属于自己的类的加载方式;比如我们需要动态加载一些东西,或者从C盘某个特定的文件夹加载一个class文件,又或者从网络上下载class主内容然后再进行加载等。分三步搞定:
1、编写一个类继承ClassLoader抽象类;
2、重写findClass()方法;
3、在findClass()方法中调用defineClass()方法即可实现自定义ClassLoader;
下面来编写一个test类
package com.test;
public class test {
public String method(){
return "hello,world";
}
}
编写完成后使用javac进行编译为class字节码文件
javac .\test.java
会发现多出一个class字节码文件,那么我们需要再对其转换为byte类型,方便后面使用类加载器进行加载执行。
import sun.misc.IOUtils;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
public class ulits {
public static void main(String[] args) throws IOException {
InputStream fis = new FileInputStream("test.class");
byte[] bytes = IOUtils.readFully(fis, -1, false);
System.out.println(Arrays.toString(bytes));
}
}
自定义加载器类:
package com.test;
import java.lang.reflect.Method;
public class classloadertest extends ClassLoader{
private static String testclassname= "com.test.test";
//转换byte后的字节码
private static byte[] classbytes= new byte[]{-54, -2, -70, -66, 0, 0, 0, 52, 0, 29, 10, 0, 6, 0, 15, 9, 0, 16, 0, 17, 8, 0, 18, 10, 0, 19, 0, 20, 7, 0, 21, 7, 0, 22, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 4, 109, 97, 105, 110, 1, 0, 22, 40, 91, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 86, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, 105, 108, 101, 1, 0, 9, 116, 101, 115, 116, 46, 106, 97, 118, 97, 12, 0, 7, 0, 8, 7, 0, 23, 12, 0, 24, 0, 25, 1, 0, 17, -23, -114, -75, -47, -122, -18, -108, -111, -23, -114, -76, -26, -124, -84, -27, -89, -101, 7, 0, 26, 12, 0, 27, 0, 28, 1, 0, 13, 99, 111, 109, 47, 116, 101, 115, 116, 47, 116, 101, 115, 116, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 121, 115, 116, 101, 109, 1, 0, 3, 111, 117, 116, 1, 0, 21, 76, 106, 97, 118, 97, 47, 105, 111, 47, 80, 114, 105, 110, 116, 83, 116, 114, 101, 97, 109, 59, 1, 0, 19, 106, 97, 118, 97, 47, 105, 111, 47, 80, 114, 105, 110, 116, 83, 116, 114, 101, 97, 109, 1, 0, 7, 112, 114, 105, 110, 116, 108, 110, 1, 0, 21, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 86, 0, 33, 0, 5, 0, 6, 0, 0, 0, 0, 0, 2, 0, 1, 0, 7, 0, 8, 0, 1, 0, 9, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 10, 0, 0, 0, 6, 0, 1, 0, 0, 0, 3, 0, 9, 0, 11, 0, 12, 0, 1, 0, 9, 0, 0, 0, 37, 0, 2, 0, 1, 0, 0, 0, 9, -78, 0, 2, 18, 3, -74, 0, 4, -79, 0, 0, 0, 1, 0, 10, 0, 0, 0, 10, 0, 2, 0, 0, 0, 5, 0, 8, 0, 6, 0, 1, 0, 13, 0, 0, 0, 2, 0, 14};
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//只处理com.test.test类
if (name.equals(testclassname)) {
//将一个字节流定义为一个类。
return defineClass(testclassname, classbytes, 0, classbytes.length);
}
return super.findClass(name);
}
public static void main(String[] args) throws Exception {
//创建加载器
classloadertest classloadertest = new classloadertest();
//使用我们自定义的类去加载testclassname
Class aClass = classloadertest.loadClass(testclassname);
//反射创建test类对象
Object o = aClass.newInstance();
//反射获取method方法
Method method = o.getClass().getMethod("method");
//反射去调用执行method方法
String str = (String) method.invoke(o);
System.out.println(str);
}
}
可以看到我们class中的method方法执行了。
总结
1、java文件编译成class字节码文件
2、转换字节码文件为byte类型,目的方便存储,加载执行
3、自定义一个类加载器类,自定义处理流程(findClass),实现对象的调用。过程为创建对象,加载class,反射创建自定义class的对象,反射获取需要执行的方法,执行方法。
参考文章
https://javasec.org/javase/ClassLoader/
https://blog.csdn.net/javazejian/article/details/73413292
https://blog.csdn.net/CNAHYZ/article/details/82219210
https://blog.csdn.net/briblue/article/details/54973413
https://blog.csdn.net/xyang81/article/details/7292380
0x04 结尾
对于这篇文章,对于没了解过类加载器的来说还是比较吃力的,比如我。在文中有些地方写的也比较模糊,所以贴了几个不错的文章在上面,作为参考。