1.2 类加载子系统(二)

自定义类加载器案例

目标: 自定义类加载器,加载指定路径在D盘下的lib文件夹下的类。

步骤:

1.新建一个需要被加载的类Test.java

2.编译Test.java 到指定lib目录

3.自定义类加载器 ClassLoader 继承 ClassLoader :重写 findClass()方法 调用 defineClass()方法

4.测试自定义类加载器


创建一个自定义类加载器,以加载指定路径下的类文件:

步骤 1:创建需要被加载的类 Test.java 并编译到指定的 lib 目录下。

假设您的 Test.java 内容如下:

java 复制代码
public class Test {
    public void printMessage() {
        System.out.println("Hello from the custom class!");
    }
}

然后,将其编译到 D:\lib 目录下。

步骤 2:创建自定义类加载器 TestClassLoader 继承 ClassLoader 并重写 findClass() 方法。

java 复制代码
import java.io.*;

public class TestClassLoader extends ClassLoader {

    private String classPath;

    public TestClassLoader(String classPath) {
        this.classPath = classPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            // 读取类文件的字节码
            String fileName = name.replace(".", File.separator) + ".class";
            byte[] bytes = loadClassData(fileName);

            // 使用defineClass方法将字节码转换为Class对象
            return defineClass(name, bytes, 0, bytes.length);
        } catch (IOException e) {
            throw new ClassNotFoundException("Class not found: " + name, e);
        }
    }

    private byte[] loadClassData(String className) throws IOException {
        String path = classPath + File.separator + className;
        try (InputStream inputStream = new FileInputStream(path);
             ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {

            int bytesRead;
            byte[] buffer = new byte[4096];
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
            }
            return outputStream.toByteArray();
        }
    }
}

步骤 3:测试自定义类加载器。

java 复制代码
public class Main {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        // 指定类文件的路径
        String classPath = "D:\\lib";

        // 创建自定义类加载器
        TestClassLoader classLoader = new TestClassLoader(classPath);

        // 使用自定义类加载器加载类
        Class<?> loadedClass = classLoader.loadClass("Test");

        // 创建实例并调用方法
        Object instance = loadedClass.newInstance();
        Method method = loadedClass.getMethod("printMessage");
        method.invoke(instance);
    }
}

上述代码中,首先创建了一个自定义类加载器 TestClassLoader,它会加载指定路径下的类文件。然后,在 Main 类中使用这个自定义类加载器加载了 Test 类并调用了其方法。

1.2.4 双亲委派模型与打破双亲委派

1)什么是双亲委派?

当一个类加载器收到类加载任务,会先交给其父类加载器去完成。因此,最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,子类才会尝试执行加载任务。

当一个类加载器收到类加载任务,会先交给其父类加载器去完成。因此,最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,子类才会尝试执行加载任务。Oracle 官网文档描述:

The Java platform uses a delegation model for loading classes. The basic idea is that every class loader has a "parent" class loader. When loading a class, a class loader first "delegates" the search for the class to its parent class loader before attempting to find the class itself. ------ Oracel Document

docs.oracle.com/javase/tuto...

看到这里,应该叫父亲委派对吧?那么为什么要叫双亲委派呢,因为最早的翻译者,导致双亲委派 的概念流行起来了。

2)为什么需要双亲委派呢?

考虑到安全因素,双亲委派可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。

比如:加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。

3)双亲委派机制源码:

java 复制代码
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                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. 首先检查该类是否已被加载,如果加载则直接返回。
  2. 然后调用父类加载器的loadClass()尝试加载。
  3. 如果父类加载失败,则调用findClass()自己尝试加载该类。
  4. 如果自己也加载失败,则抛出ClassNotFoundException。
  5. 如果resolve参数为true,还会额外进行解析(linking阶段)。

ClassLoader的子类可以override这个方法自定义类加载过程。比如URLClassLoader会从 jar 文件加载类。

4)为什么还需要破坏双亲委派?

在实际应用中,双亲委派解决了Java 基础类统一加载的问题,但是却存在着缺陷。JDK中的基础类作为典型的api被用户调用,但是也存在api调用用户代码的情况,典型的如:SPI代码。这种情况就需要打破双亲委派模式。

举个例子:数据库驱动DriverManager。以Driver接口为例,Driver接口定义在JDK中,其实现由各个数据库的服务商来提供,由系统类加载器加载。这个时候就需要启动类加载器来 委托 子类来加载Driver实现,这就破坏了双亲委派。类似情况还有很多

5)如何破坏双亲委派?

第一种方式

在 jdk 1.2 之前,那时候还没有双亲委派模型,不过已经有了 ClassLoader 这个抽象类,所以已经有人继承这个抽象类,重写 loadClass 方法来实现用户自定义类加载器。

而在 1.2 的时候要引入双亲委派模型,为了向前兼容, loadClass 这个方法还得保留着使之得以重写,新搞了个 findClass 方法让用户去重写,并呼吁大家不要重写 loadClass 只要重写 findClass。

这就是第一次对双亲委派模型的破坏,因为双亲委派的逻辑在 loadClass 上,但是又允许重写loadClass,重写了之后就可以破坏委派逻辑了。

第二种方式:

双亲委派机制是一种自上而下的加载需求,越往上类越基础。

SPI代码打破了双亲委派

如果出现SPI相关代码时,我们应该如何解决基础类去加载用户代码类呢?

这个时候,JVM不得不妥协,推出线程上下文类加载器的概念,去解决该问题。这样也就打破了双亲委派

线程上下文类加载器

(ThreadContextClassLoader)

第三种方式

为了满足热部署、不停机更新需求。OSGI 就是利用自定义的类加载器机制来完成模块化热部署,而它实现的类加载机制就没有完全遵循自下而上的委托,有很多平级之间的类加载器查找。

相关推荐
刘大辉在路上3 小时前
突发!!!GitLab停止为中国大陆、港澳地区提供服务,60天内需迁移账号否则将被删除
git·后端·gitlab·版本管理·源代码管理
追逐时光者5 小时前
免费、简单、直观的数据库设计工具和 SQL 生成器
后端·mysql
初晴~5 小时前
【Redis分布式锁】高并发场景下秒杀业务的实现思路(集群模式)
java·数据库·redis·分布式·后端·spring·
盖世英雄酱581365 小时前
InnoDB 的页分裂和页合并
数据库·后端
小_太_阳5 小时前
Scala_【2】变量和数据类型
开发语言·后端·scala·intellij-idea
直裾6 小时前
scala借阅图书保存记录(三)
开发语言·后端·scala
星就前端叭6 小时前
【开源】一款基于Vue3 + WebRTC + Node + SRS + FFmpeg搭建的直播间项目
前端·后端·开源·webrtc
小林coding7 小时前
阿里云 Java 后端一面,什么难度?
java·后端·mysql·spring·阿里云
AI理性派思考者7 小时前
【保姆教程】手把手教你在Linux系统搭建早期alpha项目cysic的验证者&证明者
后端·github·gpu