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 结尾

对于这篇文章,对于没了解过类加载器的来说还是比较吃力的,比如我。在文中有些地方写的也比较模糊,所以贴了几个不错的文章在上面,作为参考。

posted @ 2020-09-23 17:44  nice_0e3  阅读(1524)  评论(0编辑  收藏  举报