首页 > 编程语言 >Java ClassLoader、ContextClassLoader与SPI实现详解

Java ClassLoader、ContextClassLoader与SPI实现详解

时间:2023-12-08 19:01:19浏览次数:40  
标签:java name ClassLoader SPI 线程 Java Class 加载

(目录)


Java ClassLoader

ClassLoader 做什么的?

​ 众所周知, Java 或者其他运行在 JVM(java 虚拟机)上面的程序都需要最终便以为字节码,然后被 JVM加载运行,那么这个加载到虚拟机的过程就是 classloader 类加载器所干的事情.直白一点,就是 通过一个类的全限定类名称来获取描述此类的二进制字节流 的过程.

image-20231114141909272

有很多字节码加密技术就是依靠定制 ClassLoader 来实现的。先使用工具对字节码文件进行加密,运行时使用定制的 ClassLoader 先解密文件内容再加载这些解密后的字节码。

每个 Class 对象的内部都有一个 classLoader 字段来标识自己是由哪个 ClassLoader 加载的。ClassLoader 就像一个容器,里面装了很多已经加载的 Class 对象。

class Class<T> {
  ...
  private final ClassLoader classLoader;
  ...
}

双亲委派模型

说到 Java 的类加载器,必不可少的就是它的双亲委派模型,从 Java 虚拟机的角度来看,只存在两种不同的类加载器:

  1. 启动类加载器(Bootstrap ClassLoader), 由 C++语言实现,是虚拟机自身的一部分.
  2. 其他的类加载器,都是由 Java 实现,在虚拟机的外部,并且全部继承自java.lang.ClassLoader

在 Java 内部,绝大部分的程序都会使用 Java 内部提供的默认加载器.


启动类加载器(Bootstrap ClassLoader)

BootstrapClassLoader 负责加载 JVM 运行时核心类,这些类位于 JAVA_HOME/lib/rt.jar 文件中,我们常用内置库 java.xxx.* 都在里面,比如 java.util.、java.io.、java.nio.、java.lang. 等等。这个 ClassLoader 比较特殊,它是由 C 代码实现的,我们将它称之为「根加载器」。

扩展类加载器(Extension ClassLoader)

ExtensionClassLoader 负责加载 JVM 扩展类比如 swing 系列、内置的 js 引擎、xml 解析器 等等,这些库名通常以 javax 开头,它们的 jar 包位于 JAVA_HOME/lib/ext/*.jar 中,有很多 jar 包。

应用程序类加载器(Application ClassLoader)

AppClassLoader 才是直接面向我们用户的加载器,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录。我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的

当我们的 main 方法执行的时候,这第一个用户类的加载器就是 AppClassLoader

image-20231114141750796

工作流程:

  1. 收到类加载的请求
  2. 首先不会自己尝试加载此类,而是委托给父类的加载器去完成.
  3. 如果父类加载器没有,继续寻找父类加载器.
  4. 搜索了一圈,发现都找不到,然后才是自己尝试加载此类.

程序在运行过程中,遇到了一个未知的类,它会选择哪个 ClassLoader 来加载它呢?

虚拟机的策略是使用调用者 Class 对象的 ClassLoader 来加载当前未知的类

何为调用者 Class 对象?就是在遇到这个未知的类时,虚拟机肯定正在运行一个方法调用(静态方法或者实例方法),这个方法挂在哪个类上面,那这个类就是调用者 Class 对象。前面我们提到每个 Class 对象里面都有一个 classLoader 属性记录了当前的类是由谁来加载的。

因为 ClassLoader 的传递性,所有延迟加载的类都会由初始调用 main 方法的这个 ClassLoader 全全负责,它就是 AppClassLoader。


自定义加载器(Custom ClassLoader)

运用场景
  • 我们需要的类不一定存放在已经设置好的classPath下(有系统类加载器AppClassLoader加载的路径),对于自定义路径中的class类文件的加载,我们需要自己的ClassLoader
  • 有时我们不一定是从类文件中读取类,可能是从网络的输入流中读取类,这就需要做一些加密和解密操作,这就需要自己实现加载类的逻辑,当然其他的特殊处理也同样适用。
  • 可以定义类的实现机制,实现类的热部署, 如OSGi中的bundle模块就是通过实现自己的ClassLoader实现的。
ClassLoad加载class文件逻辑

ClassLoader的loadClass采用 双亲委派型实现,因为我们实现的ClassLoader都继承于java.lang.ClassLoader类,父加载器都是AppClassLoader,所以在上层逻辑中依旧要保证该模型,所以一般不覆盖loadClass函数

protected synchronized Class<?> loadClass ( String name , boolean resolve ) throws ClassNotFoundException{
        //检查指定类是否被当前类加载器加载过
        Class c = findLoadedClass(name);
        if( c == null ){//如果没被加载过,委派给父加载器加载
            try{
                if( parent != null )
                    c = parent.loadClass(name,resolve);
                else 
                    c = findBootstrapClassOrNull(name);
            }catch ( ClassNotFoundException e ){
                //如果父加载器无法加载
            }
            if( c == null ){//父类不能加载,由当前的类加载器加载
                c = findClass(name);
            }
        }
        if( resolve ){//如果要求立即链接,那么加载完类直接链接
            resolveClass();
        }
        //将加载过这个类对象直接返回
        return c;
    }

从上面的代码中,我们可以看到在父加载器不能完成加载任务时,会调用findClass(name)函数

这个就是我们自己实现的ClassLoader的查找类文件的规则,所以在继承后,我们只需要覆盖findClass()这个函数,实现我们在本加载器中的查找逻辑,而且还不会破坏双亲委托模型

注意:

不要轻易覆盖 loadClass 方法。否则可能会导致自定义加载器无法加载内置的核心类库


自定义类加载器,加载自定义路径下的class文件

覆盖findClass()这个函数,实现我们在本加载器中的查找逻辑,不会破坏双亲委托模型

package com.company;
 
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.ByteBuffer;
 
/**
 * Created by liulin on 16-4-20.
 */
public class MyClassLoader extends ClassLoader {
    private String classpath;
 
    public MyClassLoader( String classpath){
        this.classpath = classpath;
    }
 
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String fileName = getClassFile( name );
        byte[] classByte=null;
        try {
            classByte = getClassBytes(fileName);
        }catch( IOException e ){
            e.printStackTrace();
        }
        //利用自身的加载器加载类
        Class retClass = defineClass( null,classByte , 0 , classByte.length);
        if( retClass != null ) {
            System.out.println("由我加载");
            return retClass;
        }
        //System.out.println("非我加载");
        //在classPath中找不到类文件,委托给父加载器加载,父类会返回null,因为可加载的话在
        //委派的过程中就已经被加载了
        return super.findClass(name);
    }
 
    /***
     * 获取指定类文件的字节数组
     * @param name
     * @return 类文件的字节数组
     * @throws IOException
     */
    private  byte [] getClassBytes ( String name ) throws IOException{
        FileInputStream fileInput = new FileInputStream(name);
        FileChannel channel = fileInput.getChannel();
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        WritableByteChannel byteChannel = Channels.newChannel(output);
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        try {
            int flag;
            while ((flag = channel.read(buffer)) != -1) {
                if (flag == 0) break;
                //将buffer写入byteChannel
                buffer.flip();
                byteChannel.write(buffer);
                buffer.clear();
            }
        }catch ( IOException e ){
            System.out.println("can't read!");
            throw e;
        }
        fileInput.close();
        channel.close();
        byteChannel.close();
        return output.toByteArray();
    }
 
    /***
     * 获取当前操作系统下的类文件合法路径
     * @param name
     * @return 合法的路径文件名
     */
    private String getClassFile ( String name ){
        //利用StringBuilder将包形式的类名转化为Unix形式的路径
        StringBuilder sb = new StringBuilder(classpath);
        sb.append("/")
                .append ( name.replace('.','/'))
                .append(".class");
        return sb.toString();
    }
 
    public static void main ( String [] args ) throws ClassNotFoundException {
        MyClassLoader myClassLoader = new MyClassLoader("/home/liulin/byj");
        try {
            myClassLoader.loadClass("java.io.InputStream");
            myClassLoader.loadClass("TestServer");
            myClassLoader.loadClass("noClass");
        }catch ( ClassNotFoundException e ){
            e.printStackTrace();
        }
    }
}

image-20231114144155738

从结果我们看,因为我们加载的类的父加载器是系统加载器,所以调用 双亲委托 的loadClass,会直接加载掉java.io.InputStream类,只有在加载双亲中没有的TestServer类,才会用到我们自己的findClass加载逻辑加载指定路径下的类文件


Thread.contextClassLoader(线程上下文类加载器)源码分析

阅读 Thread 的源代码,你会在它的实例字段中发现有一个字段非常特别

public class Thread implements Runnable {

    // 这里省略了无关代码.....
    
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        // 这里省略了无关代码....
        
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            // 继承父线程的 上下文类加载器
            this.contextClassLoader = parent.contextClassLoader;
            
        // 这里省略了无关代码....       
    }

    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
    
    // 这里省略了无关代码.....       

}

首先 contextClassLoader 是那种需要显示使用的类加载器,如果你没有显示使用它,也就永远不会在任何地方用到它

你可以使用下面这种方式来显示使用它:

Thread.currentThread().getContextClassLoader().loadClass(name);

程序启动时的 main 线程的 contextClassLoader 就是 AppClassLoader。这意味着如果没有人工去设置,那么所有的线程的 contextClassLoader 都是 AppClassLoader

有了Thread ContextClassLoader,就可以实现父ClassLoader让子ClassLoader去完成一个类的加载任务,即父ClassLoader加载的类中,可以使用ContextClassLoader去加载其无法加载的类)。


contextClassLoader 究竟是做什么用的?

跨线程共享类,只要它们共享同一个 contextClassLoader父子线程之间会自动传递 contextClassLoader

如果不同的线程使用不同的 contextClassLoader,那么不同的线程使用的类就可以隔离开来

举例:

如果我们对业务进行划分,不同的业务使用不同的线程池,线程池内部共享同一个 contextClassLoader,线程池之间使用不同的 contextClassLoader,就可以很好的起到隔离保护的作用,避免类版本冲突。

如果我们不去定制 contextClassLoader,那么所有的线程将会默认使用 AppClassLoader,所有的类都将会是共享的。


在 SPI 中 的使用

Java 中所有涉及SPI机制的类加载基本上都是采用这种方式,最常见的就是JDBC Driver的加载

Java定义的JDBC接口位于JDK的rt.jar中(java.sql包),因此这些接口会由BootstrapClassLoader进行加载

数据库厂商提供的Driver驱动包一般由我们自己在应用程序中引入(比如位于CLASSPATH下),这已经超出了BootstrapClassLoader的加载范围,即这些驱动包中的JDBC接口的实现类无法被BootstrapClassLoader加载,只能由AppClassLoader或自定义的ClassLoader来加载

这样,SPI机制就没有办法实现


SPI 为什么需要 ThreadContextClassLoader 才能实现

这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类

SPI的接口是Java核心库的一部分,是由**启动类加载器(Bootstrap Classloader)**来加载的;

SPI的实现类是由**系统类加载器(System ClassLoader)**来加载的,依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类

要解决这个问题,就需要使用Thread Context ClassLoader,线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器


JDBC SPI 实现分析

平时是如何使用mysql获取数据库连接的:

// 加载Class到AppClassLoader(系统类加载器),然后注册驱动类
// Class.forName("com.mysql.jdbc.Driver").newInstance(); 
String url = "jdbc:mysql://localhost:3306/testdb";    
// 通过java库获取数据库连接
Connection conn = java.sql.DriverManager.getConnection(url, "name", "password"); 
12345

以上就是mysql注册驱动及获取connection的过程,各位可以发现经常写Class.forName被注释掉了,但依然可以正常运行,这是为什么呢?

这是因为从Java1.6开始自带的jdbc4.0版本已支持SPI服务加载机制,只要mysql的jar包在类路径中,就可以注册mysql驱动。

那到底是在哪一步自动注册了mysql driver的呢

重点就在DriverManager.getConnection()中。

我们都是知道调用类的静态方法会初始化该类,进而执行其静态代码块

DriverManager的静态代码块就是:

static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

下面就查看下JDK中的DriverManager类的源码,来看看其中Thread ContextClassLoader的使用。

public class DriverManager {

  // 省略无关代码......

    static {
        loadInitialDrivers(); // 在静态代码块中加载当前环境中的 JDBC Driver
        println("JDBC DriverManager initialized");
    }
   
    private static void loadInitialDrivers() {
        // 省略无关代码.....

        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                // 通过 ServiceLoader#load 方法来加载 Driver 的实现(如 MySQL、Oracle、PostgreSQL 提供的 Driver 实现)
                // 即 SPI 机制
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);

        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

}

DriverManager类在被加载的时候就会执行通过ServiceLoader#load方法来加载数据库驱动(即Driver接口的实现)。

由于每个类都会使用加载自己的ClassLoader去加载其他的类(即它所依赖的类),因此可以简单考虑以上代码的类加载过程为:

  1. DriverManager类由BootstrapClassLoader加载,DriverManager类依赖于ServiceLoader类,因此BootstrapClassLoader也会尝试加载ServiceLoaer类,这是没有问题的;

  2. 再往下,ServiceLoader的load方法中需要加载数据库(MySQL等)驱动包中Driver接口的实现类,即ServiceLoader类依赖这些驱动包中的类;

  3. 此时如果是默认情况下,则还是由BootstrapClassLoader来加载这些类,但驱动包中的Driver接口的实现类是位于CLASSPATH下的,BootstrapClassLoader是无法加载的,这就有问题了。

因此,在ServiceLoader#load方法中实际是指明了由ContextClassLoader来加载驱动包中的类:

public final class ServiceLoader<S> implements Iterable<S> {
    // 省略无关代码
    public static <S> ServiceLoader<S> load(Class<S> service) {
        // 需要注意的是,这里使用的是 当前线程的 ContextClassLoader 来加载实现,这也是 ContextClassLoader 为什么存在的原因。
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
} 

到此总结一下:

使用TCCL,也就是说把自己加载不了的类加载到TCCL中

TCCL默认使用当前执行的是代码所在应用的系统类加载器AppClassLoader

ContextClassLoader默认存放了AppClassLoader的引用,由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在任何需要的时候都可以用Thread.currentThread().getContextClassLoader()取出应用程序类加载器来完成需要的操作。

image-20231114174358816


线程上下文类加载器(TCCL)的适用场景:

  1. 当高层提供了统一接口让低层去实现,同时又要是在高层加载(或实例化)低层的类时,必须通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。
  2. 当使用本类托管类加载,然而加载本类的ClassLoader未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管。

标签:java,name,ClassLoader,SPI,线程,Java,Class,加载
From: https://blog.51cto.com/panyujie/8741301

相关文章

  • vue+spirngboot前后端数据加解密(基于AES+RSA实现)
    案例说明案例只针对post请求这里使用’Content-Type’:‘application/x-www-form-urlencoded;charset=UTF-8’;为键值对的形式(非json)AES加密数据,RAS加密AES的key实现思路前台首先请求非加密接口获取后台的公钥前台在请求前生成自己的公钥和私钥,以及AES对称加密的key使用前台......
  • java流程控制-分支控制
    免责声明:java基础资料均来自于韩顺平老师的《循序渐进学Java零基础》教案,具体视频内容可以去B站观看,这些资料仅用于学习交流,不得转载用于商业活动1.分支控制让程序有选择的去执行,分支控制有三种单分支if双分支if-else多分支if-elseif-...else1.1单分支if基本语法if(......
  • Java_02
    7-1邻接表存储实现图的深度优先遍历#include<bits/stdc++.h>usingnamespacestd;#defineMAXSIZE100inta[MAXSIZE]={0};//边表typedefstructAt{intt;//保存邻接点下标charwei;//储存权值structAt*next;//链域,指向下一个邻接点......
  • java 方法
    一、方法概述 二、方法定义和调用1、方法定义 2、方法调用3、带参方法定义 4、带参方法调用 5、形参和实参 6、带返回值方法的定义 7、带返回值方法的调用8、方法的注意事项 9、方法的通用格式 三、方法重载1、概述2、特点 四、方法......
  • 秦疆的Java课程笔记:64 面向对象 构造器详解
    类中的构造器也称为构造方法,世在进行创建对象的时候必须要调用的。并且构造器有以下两个特点必须和类的名字相同必须没有返回类型,也不能写void构造器必须掌握!一个类即使什么也没写,也会存在一个方法//写一个空的Person类=========================publicclassPer......
  • java.util.concurrent.RejectedExecutionException异常分析
    感谢:https://blog.csdn.net/wzy_1988/article/details/38922449核心池和最大池的大小graphTBA("提交新任务")-->G{"maximumPoolSize设置为<br/>无界值<br/>(例如:Integer.MAX_VALUE)"}G---|"无界值"|H["允许线程池适应任意数量的并发任务"]G---|"......
  • java使用Ffmpeg合成音频和视频
    1、Maven依赖<!--需要注意,javacv主要是一组API为主,还需要加入对应的实现--><dependency><groupId>org.bytedeco</groupId><artifactId>javacv</artifactId><version>1.5.6</version>&......
  • java-导出pdf
    前言:  纯代码画pdf格式<!--iTextPDF--><dependency><groupId>com.itextpdf</groupId><artifactId>itextpdf</artifactId><version>5.5.13.2</version></......
  • Java中<where>和<if>标签的组合使用
    在Java中,并没有<where>和<if>标签的组合使用。这两个标签不是Java编程语言或Java标准库的一部分,它们可能是你所使用的特定框架或库提供的自定义标签。如果你正在使用某个特定的Java框架或模板引擎(如MyBatis、Thymeleaf等),这些框架或引擎可能提供了自定义标签,使得在代码中使用类似于<......
  • JetBrains WebStorm 2023.3 (macOS, Linux, Windows) - 最智能的 JavaScript IDE
    JetBrainsWebStorm2023.3(macOS,Linux,Windows)-最智能的JavaScriptIDE请访问原文链接:https://sysin.org/blog/jb-webstorm-2023/,查看最新版。原创作品,转载请保留出处。作者主页:sysin.orgJetBrainsWebStorm-最智能的JavaScriptIDEWebStorm是一个适用于JavaSc......