『Java安全』ClassLoader类加载器_双亲委派_自定义类加载器_类加载隔离

前言

一切Java类都必须经过JVM加载才能运行,加载类的类就是类加载器(ClassLoader),它负责将class字节码转换成内存的class类。JVM 运行并不是一次性加载全部类,是按需加载,当用到未知类时才进行加载。

Java自带的三个类加载器

Java自带三个类加载器:

  1. BootstrapClassLoader:引导类加载器(根加载器
  2. ExtensionClassLoader:扩展类加载器
  3. AppClassLoader:系统类加载器(默认

类加载器的定义声明在%JAVA_HOME%/lib/rt.jar/sun/Launch.class,它是JVM的入口。

BootstrapClassLoader

BootstrapClassLoader 负责加载 JVM 运行时核心类,它负责索引环境变量%JAVA_HOME%/lib下的所有核心类jar包和class文件,最常见的就是rt.jarBootstrap ClassLoader是由C/C++编写的,是JVM的一部分,并不是一个Java类。也被称为根加载器。

由于是C/C++定义的因此没有其Java类,在Launch.class定义了根加载器的索引路径sun.boot.class.path
在这里插入图片描述
获取并输出:

System.out.println(System.getProperty("sun.boot.class.path"));

在这里插入图片描述

ExtensionClassLoader

ExtensionClassLoader负责加载扩展类,索引环境变量%JAVA_HOME%/lib/ext下的所有扩展类jar包和class文件,这些扩展包通常以javax开头。为了方便简写为ExtClassLoader。

Launch.class下面定义了它
在这里插入图片描述
内部通过java.ext.dirs定义了索引路径
在这里插入图片描述
输出:

System.out.println(System.getProperty("java.ext.dirs"));

在这里插入图片描述

AppClassLoader

AppClassLoader负责索引环境变量%CLASSPATH%下的 jar 包和class文件。我们自己通常都是由它来加载,此外如果不声明类加载器,它是默认的类加载器。

Launch.class同样也定义了索引路径:
在这里插入图片描述

可以看到本项目的out目录

System.out.println(System.getProperty("java.class.path"));

在这里插入图片描述

类加载器顺序

从运行开始,类加载器按照以下顺序加载类

  1. BootstrapClassLoader
  2. ExtClassLoader
  3. AppClassLoader

按需加载

程序在运行中由于是按需加载,加载到未知类时,由调用它的类的类加载器加载。也就是说:在我们编写的Java文件public class下的新类都是由AppClassLoader加载。

获取类加载器

每个对象里面都有一个classLoader属性记录了当前的类是由谁来加载的。使用静态方法Class.class.getClassLoader()可以获取某类的类加载器。

ClassLoader cl = Main.class.getClassLoader();

可以看到我们编写的Java确实是由AppClassLoader加载的
在这里插入图片描述

BootstrapClassLoader为空

打印String类的类加载器:
在这里插入图片描述
抛出空指针异常,这是由于String等基础类是由根加载器加载的,如果加载器为空则表示由根加载器加载。

父加载器不是加载器的父类

加载器的父类

在Launch.class看到:AppClassLoader和ExtClassLoader都继承自URLClassLoader
在这里插入图片描述
Java定义了URLClassLoader可以加载远程和本地的类,由于Ext和App只需要加载本地的,因此它们都是URLClassLoader的子类。这里是代码实现上的父子关系

同样URLClassLoader继承SecureClassLoader,SecureClassLoader最终继承自ClassLoader,以下是类加载器的继承关系:
在这里插入图片描述

另外:我们可以使用URLClassLoader加载远程的jar来实现远程的类方法调用以验证漏洞。

父加载器

每个加载器都有一个父加载器,使用方法ClassLoader.getParent()获取父加载器
在这里插入图片描述
可以发现:AppClassLoader的父加载器是ExtClassLoader,然后父加载器是Null(根加载器),以下是类加载器的功能关系:
在这里插入图片描述
在Launch.class中定义了App的父加载器是Ext
在这里插入图片描述

BootstrapClassLoader为空的原因

Ext的父加载器并没有显式的定义,Ext的声明也是隐式的:
在这里插入图片描述
然后调用父类URLClassLoader的构造方法,注意第二个传参是Null
在这里插入图片描述
URLClassLoader又将这个空的parent继续super向上传递
在这里插入图片描述
然后一直到ClassLoader的构造方法,调用this.parent=parent,因此是Ext的父加载器是Null
在这里插入图片描述

类加载流程

ClassLoader.class规定了类加载的流程,入口在public Class<?> loadClass(String name)
在这里插入图片描述

  1. 首先检测该类是否被加载,如果被加载直接返回该类
  2. 如果未被加载,检测是否声明了父加载器,如果声明类父加载器则使用父加载器加载它
  3. 如果未被加载,也未声明父加载器,再次尝试用根加载器加载它
  4. 如果根加载器也无法加载,再使用该加载器自身加载

双亲委派

根据以上步骤,首先委托会依照App-Ext-Boot向上传递,逐级检查是否被加载;如果未被加载再依照Boot-Ext-App向下查找,直至找到或抛出错误。

以上方法被称为双亲委派,三个 ClassLoader之间形成了级联的父子关系,每个 ClassLoader都很懒,尽量把工作交给父加载器做,父加载器干不了了自己才会干,最后会递归到根加载器。

优点

  • 避免类的重复加载, 确保一个类的全局唯一性
  • 保护程序安全, 防止核心 API 被随意篡改

缺点

  • 父加载器无法访问子加载器所加载的类

自定义类加载器

因为自带的类加载器只能从指定的环境变量加载,而非环境变量路径的加载就需要自定义类加载器来实现。

自定义类加载器的条件

编写自定义类加载器需要继承ClassLoader类,需要涉及以下两个重要的方法:

  1. findClass()自定义加载器自己的加载方法,如果父加载器都无法加载时调用,需要重写,该方法的目的是获取class字节码
  2. defineClass(),在findClass()内调用,用于组装class对象

另外,自定义类加载器不要轻易覆盖loadClass()方法,否则可能会导致自定义加载器无法加载核心类库。在使用自定义加载器时,要明确好它的父加载器是谁,将父加载器通过子类的构造器传入,如果缺省,父加载器是App。

自定义类加载器示例

HelloWorld.class

编写一个HelloWorld.java,编译为class文件等待导入

// HelloWorld.java
public class HelloWorld {
    public void hello(){
        System.out.println("Hello World!");
    }
}
javac HelloWorld.class

然后将它移动到其他目录下,作为一个临时lib,这里我选择的是./resource
在这里插入图片描述

CustomizedHelloWorldClassLoader.java

写一个最简单的自定义类加载器

// CustomizedHelloWorldClassLoader.java
package Test;

import java.io.File;
import java.io.FileInputStream;
import java.io.ByteArrayOutputStream;

public class CustomizedHelloWorldClassLoader extends ClassLoader{
	// 重写方法
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 待读取的class文件名
        String fileName = "HelloWorld.class";
		// 存放class的路径
        String customizedPath = "./resource";
        File file = new File(customizedPath, fileName);

        try {
        	// 按字节读入class文件
            FileInputStream fis = new FileInputStream(file);
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            int length;
            try {
                while ((length = fis.read()) != -1){
                    bos.write(length);
                }
            }catch (Exception e){
                e.printStackTrace();
            }
            // 转换成array
            byte[] data = bos.toByteArray();

            fis.close();
            bos.close();

			// 组装class
            return defineClass(name, data, 0 ,data.length);

        } catch (Exception e){
            e.printStackTrace();
        }

        return super.findClass(name);
    }
}

CustomizedClassLoaderTest.java

写一个测试的main方法

// CustomizedClassLoaderTest.java
package Test;

import java.lang.reflect.Method;

public class CustomizedClassLoaderTest {
    public static void main(String[] args) {
    	// 实例化加载器
        CustomizedHelloWorldClassLoader helloWorldClassLoader = new CustomizedHelloWorldClassLoader();
        try {
        	// 调用类加载器加载类
            Class cls = helloWorldClassLoader.loadClass("Test.HelloWorld");

            if (cls != null){
                try {
                	// 使用反射实例化类并调用类方法
                    Object obj = cls.newInstance();
                    Method method = cls.getMethod("HelloWorld");
                    method.invoke(obj);
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

测试

运行CustomizedClassLoaderTest.java,成功调用
在这里插入图片描述

自定义类加载器的用途

  • 调用自己编译的类绕过检测
  • (弱)加解密Java字节码

类加载隔离——解决钻石依赖问题

不同的类加载器可以加载相同的类(非继承关系),同级跨类加载器调用方法时必须使用反射。也就是说被不同类加载器加载的名称一样的类实际上是不同的类。,那么如果要加载不同版本的class就可以使用两个加载器了。

利用上面的自定义类加载器,复制一份重命名为CustomizedHelloWorldClassLoaderMimic.java,然后运行代码

// ClassLoaderIsolation.java
package Test;

public class ClassLoaderIsolation {
    public static void main(String[] args) throws Exception{
        CustomizedHelloWorldClassLoader helloWorldClassLoader = new CustomizedHelloWorldClassLoader();
        CustomizedHelloWorldClassLoaderMimic helloWorldClassLoaderMimic = new CustomizedHelloWorldClassLoaderMimic();

        Class cls1 = helloWorldClassLoader.loadClass("HelloWorld");
        Class cls2 = helloWorldClassLoaderMimic.loadClass("HelloWorld");

        System.out.println(helloWorldClassLoader);
        System.out.println(helloWorldClassLoaderMimic);
        System.out.println(cls1==cls2);

    }
}

运行结果显示:加载器不相等,加载的类也不相等
在这里插入图片描述

引用

欢迎在评论区留言,欢迎关注我的CSDN @Ho1aAs

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值