JAVA安全之类加载器

作者介绍

类加载器的介绍

我们写的代码都是.java结尾,然后通过javac编译成.class的字节码文件,这个字节码文件存放在本地

.class字节码保存着转换后的虚拟机指令,使用到对象的时候,会吧这个.class字节码就要加载到java虚拟机内存里面,这个过程就叫做类加载

在开发过程中,了解类加载是很有作用的,ClassLoader会把我们的class文件加载到jvm虚拟机里面,加载到jvm虚拟机里面就可以运行了,他不会吧把全部的class的全部东西加载到jvm虚拟机里面,他会去动态加载调用,想想也是的,一次性加载那么多jar包那么多class那内存不崩溃。本文的目的也是学习ClassLoader这种加载机制

java虚拟机有两个分区:

  • • 方法区:这个区存放着字节码的二进制数据

  • • 堆区:这个区会生成class对象,去引用方法区的节码的二进制数据

java内的自带的三个加载器

    1. Bootstrap ClassLoader(引导类加载器) 负责加载java 核心类库,例如 java.lang.xxx包中的类加载的文件夹xxx/lib,这个ClassLoader完全是jvm自己控制的,需要加载哪个类是最顶层的加载器,使用本地代码实现C和C++,没有直接的 Java 对象表示
    1. Extension ClassLoader(扩展类加载器) 加载扩展类库,通常是放置在 xxx/lib/ext 目录或通过系统属性由 Java 代码实现,是 ClassLoader 的子类
    1. **Application ClassLoader(应用类加载器)**面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类

**Bootstrap ClassLoader(引导类加载器)**查看加载了什么

可以使用sun.boot.class.path系统属性查看加载了什么东西,sun.boot.class.path 是一个系统属性,用于表示 Bootstrap ClassLoader(引导类加载器) 的类加载路径

代码

package 类加载.java内的自带的三个加载器;

publicclassBootstrapClassLoader引导类加载器{
    publicstaticvoidmain(String[] args){
        // 获取 Bootstrap ClassLoader 的加载路径
        StringbootClassPath=System.getProperty("sun.boot.class.path");
        System.out.println("Bootstrap ClassLoader加载的路径:\n"+ bootClassPath);
}
}

运行结果

Bootstrap ClassLoader加载的路径:
/home/zss/YingYong/jdk1.8.0_65/jre/lib/resources.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/rt.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/sunrsasign.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/jsse.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/jce.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/charsets.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/jfr.jar:/home/zss/YingYong/jdk1.8.0_65/jre/classes

**Extension ClassLoader(扩展类加载器)**查看加载了什么

java.ext.dirs 系统属性查看

package 类加载.java内的自带的三个加载器;

publicclassExtensionClassLoader扩展类加载器{
    publicstaticvoidmain(String[] args){
        System.out.println(System.getProperty("java.ext.dirs"));
    }
}

运行结果

/home/zss/YingYong/jdk1.8.0_65/jre/lib/ext:/usr/java/packages/lib/ext

**Application ClassLoader(应用类加载器)**查看加载了什么

java.ext.dirs 系统属性查看

package 类加载.java内的自带的三个加载器;

publicclassApplicationClassLoader应用类加载器{
    publicstaticvoidmain(String[] args){
        // 获取 AppClassLoader 的类路径
        System.out.println(System.getProperty("java.class.path"));
    }
}

运行结果

/home/zss/YingYong/jdk1.8.0_65/jre/lib/charsets.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/deploy.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/ext/cldrdata.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/ext/dnsns.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/ext/jaccess.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/ext/jfxrt.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/ext/localedata.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/ext/nashorn.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/ext/sunec.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/ext/sunjce_provider.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/ext/sunpkcs11.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/ext/zipfs.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/javaws.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/jce.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/jfr.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/jfxswt.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/jsse.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/management-agent.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/plugin.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/resources.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/rt.jar:/home/zss/笔记/代码/java/javaAll3/target/classes:/home/zss/YingYong/idea-IU-241.18034.62/lib/idea_rt.jar

加载器加载顺序

    1. 检查类是否已加载 每个类加载器在加载类之前,会先检查目标类是否已经被加载。如果已加载,则直接返回,不重复加载
    1. 双亲委派机制(下面详细说) 当某个类加载器收到加载类的请求时,按照以下顺序进行处理:

      1. 委托父加载器加载 首先将加载请求委托给父加载器(即上一层的加载器)
      1. 父加载器加载失败时,尝试自身加载 如果父加载器无法加载目标类,则当前加载器会尝试自行加载

我们先了解一下getParent()方法

每个 ClassLoader 都有一个父加载器 ,通过调用 getParent() 方法,我们可以获取到该类加载器的父加载器

代码演示加载

创建一个类abc的java类

package 类加载.java加载器加载顺序;

public class abc {
}

我们加载这个abc这个类看看他的加载器

package 类加载.java加载器加载顺序;

publicclassrun{
    publicstaticvoidmain(String[] args){
        // 获取 abc 类的类加载器(应用类加载器)
        ClassLoaderappClassLoader= abc.class.getClassLoader();
        System.out.println(appClassLoader);// 输出 abc 类的类加载器

        // 获取 abc 类的父加载器
        System.out.println(appClassLoader.getParent());// 输出父加载器(扩展类加载器)

        // 获取父加载器的父加载器
        System.out.println(appClassLoader.getParent().getParent());// 输出引导类加载器(为 null)
    }
}

运行结果

sun.misc.Launcher$AppClassLoader@14dad5dc
sun.misc.Launcher$ExtClassLoader@1540e19d
null

为什么没有显示Bootstrap ClassLoader因为由于引导类加载器(Bootstrap ClassLoader)是 JVM 内部实现的,他是c++编写的

双亲委派

ClassLoader 类使用委托模型来搜索类和资源,每个 ClassLoader 实例都有一个相关的父类加载器。需要查找类或资源时,ClassLoader 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器,原理详情下面的loadClass分析代码执行过程就是双亲委派机制

ClassLoader 类的方法

ClassLoader是加载类的核心类,在程序运行时,并不会一次性加载所有的 class 文件进入内存,而是通过 Java 的类加载机制(ClassLoader)进行动态加载,从而转换成 java.lang.Class 类的一个实例

方法名 作用 备注
loadClass(String name) 加载指定名称的类,遵循双亲委派模型。 常用方法,默认实现先调用 findClass
findClass(String name) 寻找类的定义,通常由自定义加载器重写。 默认抛出 ClassNotFoundException
defineClass(String name, byte[] b, int off, int len) 将二进制字节数组转换为 Java 类对象。 用于自定义类加载器加载字节码。
resolveClass(Class<?> c) 解析类及其依赖关系。 通常在加载类后手动调用以完成解析。
findLoadedClass(String name) 检查类是否已被加载,如果已加载则返回 Class 对象。 防止重复加载同一个类。
getParent() 获取当前类加载器的父加载器。 返回父类加载器;引导类加载器返回 null
getSystemClassLoader() 获取系统类加载器(即 AppClassLoader)。 用于加载应用程序类路径下的类。
getResource(String name) 查找指定名称的资源,返回资源的 URL。 可用于加载配置文件等资源。
getResourceAsStream(String name) 以输入流形式查找资源,便于读取资源内容。 配合流操作读取文件资源。
getResources(String name) 查找所有与指定名称匹配的资源,返回一个枚举的 URL 集合。 用于加载多个相同名称的资源。
clearAssertionStatus() 清除当前类加载器及其加载的类的断言状态。 重置断言状态。
setDefaultAssertionStatus(boolean enabled) 设置默认断言状态,适用于当前类加载器加载的所有类。 改变全局默认断言行为。
setClassAssertionStatus(String className, boolean enabled) 设置特定类的断言状态。 单独调整某个类的断言启用状态。
setPackageAssertionStatus(String packageName, boolean enabled) 设置特定包的断言状态。 对某个包中的所有类调整断言启用状态。

loadClass对象方法介绍

loadClassClassLoader 类中最核心的方法的其中一个,用于加载指定名称的类。它遵循 双亲委派模型

点进去

在点进去

里面的源代码是如下:

protected Class<?> loadClass(String name,boolean resolve)throwsClassNotFoundException{
    synchronized(getClassLoadingLock(name)){// 这段代码的作用是保证类加载过程的线程安全性
    Class<?> c = findLoadedClass(name);
    if(c ==null){
        longt0=System.nanoTime();//记录当前时间的纳秒级时间戳
        try{
            if(parent !=null){// parent是当前类加载器的父类加载器
            c = parent.loadClass(name,false);
        }else{
            c = findBootstrapClassOrNull(name);
        }
        }catch(ClassNotFoundException e){
        }

        if(c ==null){
            longt1=System.nanoTime();//记录当前时间的纳秒级时间戳
            c = findClass(name);

            sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);// 计算时间
            sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);// 计算时间
            sun.misc.PerfCounter.getFindClasses().increment();// 计算时间
        }
    }
    if(resolve){
        resolveClass(c);
    }
    return c;
    }
}

读一下源代码

1、第一行定义的这个loadClass,他接收两个参数

protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{

第一个参数是要加载的类的完全限定名称,第二个参数如果设置是true则调用 resolveClass(Class<?> c)

2、第三行,

Class<?> c = findLoadedClass(name); 

用于检查某个类是否已经被当前类加载器加载过,如果已经加载直接返回该类的 Class对象,如果未加载返回 null

3、第八行

parent.loadClass(name, false);

这个会拿他的父类去加载一遍这个类,看看有没有这个类,这个调用是基于父类委派模型,如果父加载器存在它会尝试加载该类

4、第十行

c = findBootstrapClassOrNull(name);

如果没有父类(parent)等于空就会调用上面的这行代码,findBootstrapClassOrNull是会去尝试加载类

5、第15行

if (c == null) {

如果类 c 还没有被加载

6、第17行

c = findClass(name);

用于加载指定名称的类,如果当前加载器无法加载该类可能会抛出ClassNotFoundException

findClass对象方法介绍

findClass(String name)

参数是一个类名

findClass方法用于在类加载器中查找并加载指定名称的类,他会根据类的名称查询加载类的字节码返回一个 Class对象,如果没有找到抛出ClassNotFoundException异常

defineClass对象方法介绍

defineClass(String name, byte[] b, int off, int len)

name:要定义的类的全限定名(包括包名)例如com.xxx.aaabbb

b:包含类字节码的字节数组,这个字节数组应该包含类的所有字节码(从 .class 文件读取出来的内容)

off:字节数组的起始偏移量,通常为 0,表示从字节数组的第一个字节开始读取

len:要读取的字节长度。如果传入的字节数组 b 是整个类的字节码,通常传入该字节数组的长度

defineClass方法是将原始字节码(二进制)转换成 class对象的核心方法,比如.class文件、网络

它允许将已经加载的类字节码转化为可以在jvm 中使用的class实例,如果字节码不符合 jvm 的格式规范会抛出异常

方法使用ClassLoader

loadClass使用

创建一个MyClass方法下面是代码

package 类加载.类加载.test_loadClass;

publicclassMyClass{
    privateStringmyField="Hello, World!";
        publicStringgetMyField(){
        return myField;
    }
    publicStringStudent(String name){
        System.out.println("Student: "+ name);
        return name;
    }
}

使用loadClass调用上面创建的类

package 类加载.类加载.test_loadClass;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

publicclasstest_loadClass{
    publicstaticvoidmain(String[] args)throwsClassNotFoundException,NoSuchFieldException,NoSuchMethodException,InvocationTargetException,InstantiationException,IllegalAccessException{
        // 获取当前类加载器
        ClassLoaderclassLoader= test_loadClass.class.getClassLoader();

        // 使用 loadClass 方法加载类
        Class<?> clazz = classLoader.loadClass("类加载.类加载.test_loadClass.MyClass");
        System.out.println("Loaded class: "+ clazz.getName());

        // 方法返回某个类的所有public方法
        Method[] a = clazz.getMethods();
        for(Method i:a){
            System.out.println(i);
        }
    }
}

运行结果

Loaded class:类加载.类加载.test_loadClass.MyClass
public java.lang.String类加载.类加载.test_loadClass.MyClass.getMyField()
public java.lang.String类加载.类加载.test_loadClass.MyClass.Student(java.lang.String)
publicfinalvoid java.lang.Object.wait(long,int)throws java.lang.InterruptedException
publicfinalnativevoid java.lang.Object.wait(long)throws java.lang.InterruptedException
publicfinalvoid java.lang.Object.wait()throws java.lang.InterruptedException
publicboolean java.lang.Object.equals(java.lang.Object)
public java.lang.String java.lang.Object.toString()
publicnativeint java.lang.Object.hashCode()
publicfinalnative java.lang.Class java.lang.Object.getClass()
publicfinalnativevoid java.lang.Object.notify()
publicfinalnativevoid java.lang.Object.notifyAll()

Class.forName

Class.forName()也可以对类进行加载,但是Class.forName()和ClassLoader是有区别的

抽象类ClassLoader中实现的方法loadClass,loadClass只是加载,不会解析更不会初始化所反射的类,常用于做懒加载,提高加载速度,使用的时候再通过.newInstance()真正去初始化类

看一下Class.forName()加载一个累

创建一个被加载的类

package 类加载.forName和ClassLoader;

public class MyClass {
    static {
        System.out.println("aaaaa");
    }

}

调用MyClass代码

package 类加载.forName和ClassLoader;

publicclasstest{
    publicstaticvoidmain(String[] args)throwsClassNotFoundException{
        // 动态加载 MyClass 类
        Class<?> clazz =Class.forName("类加载.forName和ClassLoader.MyClass");
    }
}

运行结果

aaaaa

自定义类加载

为什么要自定义类加载器,ClassLoader在默认情况下只会加载文件系统或 jar 包中加载类,这个限制很大,我们就可以自定义类加载器去加载网络或其他自定义位置类,可以类文件经过加密处理,然后接收然后解密后加载,增加应用安全性

自定义类加载的步骤

    1. 继承 ClassLoader 类 自定义类加载器需要继承 ClassLoader 并重写其方法,如 findClass
    1. 重写 findClass 方法 findClass 是类加载器的核心方法,用于加载类的字节码并将其定义为一个 Class 对象
    1. 通过 defineClass 方法定义类 使用 defineClass 将字节码数组转换为 Class 对象

代码演示:

1、创建了一个类加载器

package 类加载.自定义类加载;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;

publicclassMyClassLoaderextendsClassLoader{
    staticString URL="http://127.0.0.1:8081/";
    //自定义的类加载器,加载来自网络的字节码
    @Override
    protectedClass<?> findClass(String name)throwsClassNotFoundException{
        byte[] classData =null;
        try{
            classData = loadClassDataFromNetwork(name);
        }catch(IOException e){
        thrownewRuntimeException(e);
        }
        if(classData ==null){
            thrownewClassNotFoundException(name);
         }
            return defineClass(name, classData,0, classData.length);// 定义类
        }

    //模拟从网络加载类的字节码数据
    privatebyte[] loadClassDataFromNetwork(String className)throwsIOException{
        StringencodedClassName= className.replace('.','/')+".class";
        encodedClassName=java.net.URLEncoder.encode(encodedClassName,"UTF-8").replace("%2F","/");
        URLurl=newURL(URL + encodedClassName);//请求从远程服务器加载字节码
        InputStreaminputStream= url.openStream();
        ByteArrayOutputStreambyteArrayOutputStream=newByteArrayOutputStream();
        int byteRead;
        while((byteRead = inputStream.read())!=-1){
            byteArrayOutputStream.write(byteRead);
        }
        return byteArrayOutputStream.toByteArray();
    }
}

2、创建一个测试的类

名字就叫abc

package 类加载.自定义类加载;

publicclassabc{
privateString message;

    publicabc(){
        this.message ="aaaaaaaaaaaaa";
    }

    publicabc(String var1){
        this.message = var1;
    }

    publicStringgetMessage(){
        returnthis.message;
    }

    publicvoidsetMessage(String var1){
        this.message = var1;
    }

    publicvoidprintMessage(){
        System.out.println(this.message);
    }
}

生成class文件

javac -d . abc.java

3、然后在创建一个测试我们创建的自定义类

TestMyClassLoader.java文件内容

package 类加载.自定义类加载;

import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;

publicclassTestMyClassLoader{
publicstaticvoidmain(String[] args)throwsClassNotFoundException,NoSuchMethodException,InvocationTargetException,InstantiationException,IllegalAccessException,UnsupportedEncodingException{

    MyClassLoadermyClassLoader=newMyClassLoader();// 创建自定义类加载器实例
        Class<?> loadedClass = myClassLoader.loadClass("类加载.自定义类加载.abc");//使用自定义类加载器加载类
        System.out.println("加载器信息:"+ loadedClass.getClassLoader());//打印类加载器信息
        // 实例化对象
        Objectobj= loadedClass.getDeclaredConstructor().newInstance();
        // 调用 printMessage 方法
        loadedClass.getMethod("printMessage").invoke(obj);

    }
}

整个命令结构:

我们在这个目录下启动web服务

python3 -m http.server 8081

然后我们运行TestMyClassLoader.java可以看见被加载了

参考

https://blog.csdn.net/succing/article/details/123308677

https://blog.csdn.net/succing/article/details/123308677

https://blog.csdn.net/briblue/article/details/54973413

书籍:《java代码审计》

https://gityuan.com/2016/01/24/java-classloader/

相关推荐
Jelena技术达人14 分钟前
利用Python爬虫获取微店商品详情API接口的深入指南
开发语言·爬虫·python
m0_7482333635 分钟前
后端接口返回文件流,前端下载(java+vue)
java·前端·vue.js
刘Java42 分钟前
Dubbo 3.x源码(26)—Dubbo服务引用源码(9)应用级服务发现订阅refreshServiceDiscoveryInvoker
java·dubbo·dubbo源码
weixin_403673771 小时前
thinkphp 多选框
开发语言·php
蟾宫曲1 小时前
网络编程 03:端口的定义、分类,端口映射,通过 Java 实现了 IP 和端口的信息获取
java·网络·网络编程·ip·端口
魔道不误砍柴功1 小时前
Java 中 wait 和 sleep 的区别:从原理到实践全解析
java·开发语言
00Allen002 小时前
常见八股文03
java·安全·系统安全
SomeB1oody2 小时前
【Rust自学】4.2. 所有权规则、内存与分配
开发语言·后端·rust
SomeB1oody2 小时前
【Rust自学】4.5. 切片(Slice)
开发语言·后端·rust
诚丞成2 小时前
抽象之诗:C++模板的灵魂与边界
开发语言·c++