百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

JVM类加载器(线程上下文类加载器和SPI机制)

liebian365 2024-11-09 13:48 27 浏览 0 评论



双亲委派机制

工作原理:如果一个类加载器收到了类加载请求,它不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。

双亲委派机制的优势:采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。


如何打破双亲委派机制

1.重写loadClass()方法,因为双亲委派机制都是通过这个方法实现的,我们可以改写他的加载规则,就可以使用自定义的类加载器。

如果重新了loadClass()方法以后,我们可以自己写一个java.lang.String类,让自定义类加载器去加载我们自己写的String类,但是这个双亲委派机制可以打破,但是这个方案是不可行的,当加载我们自定义的String类时会抛出 -> java.lang.SecurityException:Prohibited package name:java.lang,java中做了一些安全检查,禁止我们命名java.lang包。

2.在Java应用中存在着很多服务提供者接口(Service Provider Interface,SPI),这些接口允许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI等,这些 SPI 的接口属于 Java 核心库,一般存在rt.jar包中,由Bootstrap类加载器加载,而 SPI 的第三方实现代码则是作为Java应用所依赖的 jar 包被存放在classpath路径下,由于SPI接口中的代码经常需要加载具体的第三方实现类并调用其相关方法,但SPI的核心接口类是由引导类加载器来加载的,而Bootstrap类加载器无法直接加载SPI的实现类,同时由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载器SPI的实现类。在这种情况下,我们就需要一种特殊的类加载器来加载第三方的类库,而线程上下文类加载器就是很好的选择。

线程上下文类加载器(contextClassLoader)是从 JDK 1.2 开始引入的,我们可以通过java.lang.Thread类中的getContextClassLoader()和setContextClassLoader(ClassLoader cl)方法来获取和设置线程的上下文类加载器。如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的代码可以通过此类加载器来加载类和资源


SPI:

是一种将服务接口与服务实现分离以达到解耦、大大提升了程序可扩展性的机制。引入服务提供者就是引入了spi接口的实现者,通过本地的注册发现获取到具体的实现类,轻松可插拔,JDK内置的一种服务提供调用机制。主要用处是不用改动源码就可以实现扩展。


SPI的全名为Service Provider Interface,主要是应用于厂商自定义组件或插件中。在java.util.ServiceLoader的文档里有比较详细的介绍。简单的总结下java SPI机制的思想:我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。

SPI具体约定

Java SPI的具体约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader。

JDBC案例分析:

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

以上就是mysql注册驱动及获取connection的过程,Class.forName是可以被注释掉的,因为从Java1.6开始自带的jdbc4.0版本已支持SPI服务加载机制,只要mysql的jar包在类路径中,就可以注册mysql驱动。

所以注册Mysql驱动的过程放在了DriverManager.getConnection()中,getConnection()是一个静态方法,调用静态方法时会初始化该类,并且执行其静态方法:

Bash
/**
 * Load the initial JDBC drivers by checking the System property
 * jdbc.properties and then use the {@code ServiceLoader} mechanism
 */
static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

loadInitialDrivers()的代码如下:

private static void loadInitialDrivers() {
    String drivers;
    try {
        //读取系统属性
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                return System.getProperty("jdbc.drivers");
            }
        });
    } catch (Exception ex) {
        drivers = null;
    }
    //通过SPI加载驱动类
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
            //加载Driver关联的服务实现(SPI)
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();
            try{
                //调用的就是java.util.ServiceLoader.LazyIterator#hasNextService方法,代码在下
                while(driversIterator.hasNext()) {
                     //调用的就是java.util.ServiceLoader.LazyIterator#nextService方法,代码在下
                    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);
        }
    }
private static final String PREFIX = "META-INF/services/";

private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            //拼接 META-INF/services/java.sql.Driver 判断META-INF/services
            //下是否有文件
            String fullName = PREFIX + service.getName();
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                configs = loader.getResources(fullName);
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
        }
    }
    while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
            return false;
        }
        pending = parse(service, configs.nextElement());
    }
    nextName = pending.next();
    return true;
}
private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;//com.mysql.cj.jdbc.Driver,具体的实现类
    nextName = null;
    Class<?> c = null;
    try {
        //在这里就是调用之前注释掉的那句话。
        //cn = "com.mysql.cj.jdbc.Driver",这个路径是在第三方包中
        c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        fail(service,
             "Provider " + cn + " not found");
    }
    if (!service.isAssignableFrom(c)) {
        fail(service,
             "Provider " + cn  + " not a subtype");
    }
    try {
        S p = service.cast(c.newInstance());
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,
             "Provider " + cn + " could not be instantiated",
             x);
    }
    throw new Error();          // This cannot happen
}

此时我们看下

c = Class.forName(cn, false, loader);

中的classloader是怎么传入的,

public static <S> ServiceLoader<S> load(Class<S> service) {
    //获取线程上下文类加载器(TCCL)
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

因为这句Class.forName(DriverName, false, loader)代码所在的类在java.util.ServiceLoader类中,ServiceLoader.class又在rt.jar中,是由BootrapLoader加载的,因此传给 forName 的 loader 必然不能是BootrapLoader,查看代码发现传入的是TCCL,也就是说把自己加载不了的类加载到TCCL中,而TCCL默认使用当前执行的是代码所在应用的系统类加载器AppClassLoader,也就是说通过系统类加载器加载com.mysql.cj.jdbc.Driver这个类。

我(JDK)提供了一种帮你(第三方实现者)加载服务(如数据库驱动、日志库)的便捷方式,只要你遵循约定(把类名写在/META-INF里),那当我启动时我会去扫描所有jar包里符合约定的类名,再调用forName加载,但我的ClassLoader是没法加载的,那就把它加载到当前执行线程的TCCL里,后续你想怎么操作(驱动实现类的static代码块)就是你的事了。

当我们用之前的方式主动去写时,会执行com.mysql.cj.jdbc.Driver中的静态代码块

Class.forName("com.mysql.jdbc.Driver").newInstance(); 
static {
    try {
        java.sql.DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
        throw new RuntimeException("Can't register driver!");
    }
}

registerDriver方法将driver实例注册到系统的java.sql.DriverManager类中,保存在registeredDrivers 中。

private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();

java.sql.DriverManager.getConnection()。它最终调用了以下方法:

private static Connection getConnection(
    String url, java.util.Properties info, Class<?> caller) throws SQLException {
    /*
     * When callerCl is null, we should check the application's
     * (which is invoking this class indirectly)
     * classloader, so that the JDBC driver class outside rt.jar
     * can be loaded from here.
     */
     /* 传入的caller由Reflection.getCallerClass()得到,该方法
      * 可获取到调用本方法的Class类,这儿获取到的是当前应用类加载器
      */
    ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
    synchronized(DriverManager.class) {
        // synchronize loading of the correct classloader.
        if (callerCL == null) {
            callerCL = Thread.currentThread().getContextClassLoader();
        }
    }

    if(url == null) {
        throw new SQLException("The url cannot be null", "08001");
    }

    println("DriverManager.getConnection(\"" + url + "\")");

    // Walk through the loaded registeredDrivers attempting to make a connection.
    // Remember the first exception that gets raised so we can reraise it.
    SQLException reason = null;
    // 遍历注册到registeredDrivers里的Driver类
    for(DriverInfo aDriver : registeredDrivers) {
        // If the caller does not have permission to load the driver then
        // skip it.
        // 检查Driver类有效性
        if(isDriverAllowed(aDriver.driver, callerCL)) {
            try {
                println("    trying " + aDriver.driver.getClass().getName());
                // 调用com.mysql.jdbc.Driver.connect方法获取连接
                Connection con = aDriver.driver.connect(url, info);
                if (con != null) {
                    // Success!
                    println("getConnection returning " + aDriver.driver.getClass().getName());
                    return (con);
                }
            } catch (SQLException ex) {
                if (reason == null) {
                    reason = ex;
                }
            }

        } else {
            println("    skipping: " + aDriver.getClass().getName());
        }

    }

    // if we got here nobody could connect.
    if (reason != null)    {
        println("getConnection failed: " + reason);
        throw reason;
    }

    println("getConnection: no suitable driver found for "+ url);
    throw new SQLException("No suitable driver found for "+ url, "08001");
}
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
    boolean result = false;
    if(driver != null) {
        Class<?> aClass = null;
        try {
	    // 传入的classLoader为调用getConnetction的当前类加载器,从中寻找driver的class对象
            aClass =  Class.forName(driver.getClass().getName(), true, classLoader);
        } catch (Exception ex) {
            result = false;
        }
	// 注意,只有同一个类加载器中的Class使用==比较时才会相等,此处就是校验用户注册Driver时该Driver所属的类加载器与调用时的是否同一个
	// driver.getClass()拿到就是当初执行Class.forName("com.mysql.jdbc.Driver")时的应用AppClassLoader
        result = ( aClass == driver.getClass() ) ? true : false;
    }
    return result;
}

相关推荐

Pandoc安装、使用、快速上手(pandoc安装教程)

Pandoc安装、使用、快速上手Pandoc如果你需要将文档从一种格式转换成另一种格式,那么Pandoc是你的一把瑞士军刀,Pandoc可以将下列格式文档进行相互转换。Markdown、Microso...

Pinocchio 安装教程|机器人学的必备库

视频讲解:Pinocchio安装教程|机器人学的必备库_哔哩哔哩_bilibilihttps://gepettoweb.laas.fr/doc/stack-of-tasks/pinocchio/ma...

常用linux命令:netstat(常用linux命令有多少个)

linuxnetstat命令用于显示网络状态,可用于列出系统上所有的网络套接字连接情况#显示所有的连接$netstat-aActiveInternetconnections(serv...

【收藏】Linux服务器常用巡检命令

在Linux服务器上进行常规巡检是确保服务器稳定性和安全性的重要措施之一。以下是一些常用的巡检命令和技巧:1.查看系统信息1.1系统信息显示命令:uname-a[root@linux100~]...

详解linux内核网络数据包发送在UDP协议层的处理与监控

udp_sendmsg这个函数定义在net/ipv4/udp.c,函数很长,分段来看。UDP插入UDPudp_sendmsgcorking是一项优化技术,允许内核将多个数据累积成一体的数据...

Linux实例常用内核网络参数介绍与常见问题处理

Linux实例常用内核网络参数介绍与常见问题处理概述本文主要介绍常见的Linux内核参数及相关问题的处理方式。详细信息注意:Linux从4.12内核版本开始移除了tcp_tw_recycle配置。即移...

Linux配置网络(rocky linux配置网络)

1、网卡名配置相关文件回到顶部网卡名命名规则文件:/etc/udev/rules.d/70-persistent-net.rules#PCIdevice0x8086:0x100f(e1000)...

Linux安全之网络防火墙(linux防火墙配置基本步骤)

纸上得来终觉浅,绝知此事要躬行。iptables是运行在用户空间的应用软件,通过控制Linux内核netfilter模块,来管理网络数据包的处理和转发。在大部分Linux发行版中,可以通...

linux命令 - fuser、lsof、pidof学习

fuser可以借由文件(或文件系统)找出正在使用该文件的程序。有的时候我想要知道我的程序到底在这次启动过程中打开了多少文件,可以利用fuser来观察啦!举例来说,你如果卸载时发现系统通知:“...

Linux使用中的一些问题及解决过程(记录1)

1.在Linux虚拟中安装dhcpv6后,Server发送的dhcpv6advertise报文中的udp校验和出错,导致用户不能获取IPv6地址修正方法:将该接口的校验关闭ethtool--of...

比 netstat 好用?Linux 网络状态工具 ss 详解

ss命令用于显示socket状态。他可以显示PACKETsockets,TCPsockets,UDPsockets,DCCPsockets,RAWsockets,Unixdomains...

七、网工必备“基础命令”:像查水电表一样轻松排查网络问题!

网络命令听起来高深,但其实它们就像你家里的“工具箱”——拧螺丝用螺丝刀,测电路用电笔,而排查网络问题,就用这些命令!今天我们就用生活中最常见的例子,带你轻松掌握ping、ipconfig、tracer...

为什么 Windows 病毒不影响 Linux 机器?

病毒是一种计算机程序。病毒不会影响Linux的原因和MicrosoftExcel不能在Linux上运行的原因是一样的:Linux不能运行Windows程序。你可以使用特殊的软件,...

别让Linux网络基础拖后腿!这些 lsof 骚操作你真的了解吗?

lsof是Linux系统管理中极为强大的工具,尤其在网络问题排查中堪称神器。以下从基础到进阶的用法解析,帮你彻底掌握网络相关的骚操作:一、基础必知:网络连接的本质Linux中一切皆文件,网络连...

别让Linux网络基础拖后腿!这些 netstat 骚操作你真的了解吗?

在Linux网络管理和故障排查中,netstat确实是必备的瑞士军刀。掌握这些进阶技巧能显著提升效率,尤其当服务器出现连接数爆满、端口冲突或异常流量时。以下乃是自实战之中所总结而成的深度操作指南:继续...

取消回复欢迎 发表评论: