Java虚拟机--线程上下文类加载器

线程上下文类加载器

通过名字可知,线程上下文类加载,就是当前线程所拥有的类加载器,可通过Thread.currentThread()获取当前线程。

线程上下文类加载器(Thread Context ClassLoader)可以通过java.lang.Thread类的setContextClassLoader()方法设置,创建线程时候未指定的话,则默认从父线程中继承。

那父线程中也没指定呢?那么会默认为应用程序的类加载器。例如:main方法的线程上下文类加载器就是sun.misc.Launcher$AppClassLoader。

前两篇文章中,我们讲解了类加载器的双亲委派模型,该模型的实现是通过类加载器中的parent属性(父加载器)来完成的,默认统一交给最上层类加载器去尝试加载。

那,这个线程上下文类加载器又是干啥的?

在介绍线程上下文类加载前,我们先了解下Java的SPI机制。

Java--SPI机制

线程上下文类加载实现

public class JVMTest6 {

   public static void main(String[] agrs) throws ClassNotFoundException {
       ClassLoader loader = JVMTest6.class.getClassLoader();
       System.out.println(loader); //默认是应用类加载器

       //此时获得上下文类加载器:
       ClassLoader loader2 = Thread.currentThread().getContextClassLoader();
       System.out.println(loader2);//默认也是应用类加载器

       //设置为自定义类加载器:
       Thread.currentThread().setContextClassLoader(
               new ClassLoaderTest("d:/"));
       System.out.println(Thread.currentThread().getContextClassLoader());

       //使用自定义类加载器加载:
       Class c = Thread.currentThread().getContextClassLoader().loadClass("HelloWorld");
       System.out.println(c.getClassLoader());//线程上下文类加载器

       ClassLoader loader3 = String.class.getClassLoader();
       System.out.println(loader3);//启动类加载器 = null
   }
}

测试结果:

sun.misc.Launcher$AppClassLoader@41dee0d7
sun.misc.Launcher$AppClassLoader@41dee0d7
ClassLoaderTest@516a4aef
ClassLoaderTest@516a4aef
null

JDBC加载案例分析

介绍完了spi,下来我们来举几个例子进一步说明逆向类加载。

举个简单的例子,mysql是如何获取数据库连接:

public class JVMTest7 {

   public static void main(String[] agrs) {
       try {
           // 注册驱动类
           Class.forName("com.mysql.jdbc.Driver");
           String url = "jdbc:mysql://localhost:3306/testdb";
           //通过java库获取数据库连接
           Connection conn = java.sql.DriverManager.getConnection(url, "root", "123456");
       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (SQLException e) {
           e.printStackTrace();
       }
   }
}

以上就是mysql注册驱动及获取connection的过程。在该流程中,java通过线程上线文类加载器实现了逆向类加载。

(1)通过系统类加载器,加载Driver类---Class.forName("com.mysql.jdbc.Driver");底层具体实现:registerDriver()将driver实例注册到java.sql.DriverManager类中,其实就是将com.mysql.jdbc.Driver添加到java.sql.DriverManager类的静态变量CopyOnWriteArrayList集合中。

com.mysql.jdbc.Driver包中:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

(2)通过java.sql.DriverManager注册数据库驱动。首先,来看下DriverManager的静态方法。需要明确的是java.sql.DriverManager位于rt.jar包目录下,该目录下的所有类均由Bootstrap启动类加载器进行加载。

java.sql.DriverManager包中:

static {
    //初始化Driver驱动实现类:
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

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来加载jdbc驱动实现类:
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
            //通过SPI方式,读取META-INF/services下文件中的类:
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator driversIterator = loadedDrivers.iterator();
            try{
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
            } catch(Throwable t) {}
            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);
        }
    }
}

(3)spi具体实现:

在下面代码中,通过SPI方式来完成java.sql.Driver接口实现类的类加载操作。

java.sql.DriverManager包中:

AccessController.doPrivileged(new PrivilegedAction<Void>() {
    public Void run() {
        //通过SPI方式,读取META-INF/services下文件中的类名: 
        ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
        Iterator driversIterator = loadedDrivers.iterator();
        try{
            while(driversIterator.hasNext()) {
                driversIterator.next();
            }
        } catch(Throwable t) {}
        return null;
    }
});

获取到ServiceLoader对象后,进行遍历操作,遍历出所有META-INF/services文件夹下的实现类名称,之后再进行Class.forName("")类加载操作。类加载操作在driversIterator.next()中完成。

java.util.ServiceLoader包中:

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

public static <S> ServiceLoader<S> load(Class<S> service,
                                        ClassLoader loader){
    return new ServiceLoader<>(service, loader);
}
    
private ServiceLoader(Class<S> svc, ClassLoader cl) {
    service = svc;
    loader = cl;
    reload();
}

在获取ServiceLoader对象时,获取了此时线程上下文中的类加载器,将此类加载赋值给ServiceLoader类中的loader成员变量。在后续类加载过程中,都是使用的此类加载来完成。这一步的操作,直接打破了双亲委派模型,实现了逆向类加载。

try{
    while(driversIterator.hasNext()) {
        driversIterator.next();
    }
} catch(Throwable t) {}

通过debug发现,driversIterator.next()方法内部会调用Class c = Class.forName(cn, false, loader)方法进行类加载操作。而此时传递的loader就是之前获取的线程上下文类加载器,传递的cn就是META-INF/services文件中的具体实现类。

由于笔者是通过本地的test进行测试,所以上文中涉及到的类加载器都是AppClassLoader系统类加载器。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏技术小站

编程填空:第i位替换 编程填空:第i位取反 编程填空:左边i位取反

写出函数中缺失的部分,使得函数返回值为一个整数,该整数的第i位和m的第i位相同,其他位和n相同。

2271
来自专栏Spark生态圈

[Spark SQL] 源码解析之Parser

Parser就是将SQL字符串切分成一个个Token,再根据一定语义规则解析为一棵语法树。我们写的sql语句只是一个字符串而已,首先需要将其通过词法解析和语法解...

3122
来自专栏java一日一条

50个常见的 Java 错误及避免方法(第一部分)

在开发Java软件时可能会遇到许多类型的错误,但大多数是可以避免的。为此我们罗列了50个最常见的Java编码错误,其中包含代码示例和教程,以帮助大家解决常见的编...

1233
来自专栏yl 成长笔记

深刻理解反射(Reflection)

最近公司在搞自动化测试,由于版权问题,无法用 ’录制脚本‘ 进行,也就没法用 VS 自带的 UITest 框架(蛋疼), 所以只能开源的 FlaUI 框架来搞了...

1484
来自专栏雨尘分享

4.Block的类型

1645
来自专栏思考的代码世界

Python基础学习06天

1544
来自专栏从零开始学 Web 前端

09 - JavaSE之线程

PS: 如果我们没有 new一个 Thread 对象出来,而是直接使用 MyThread 的 run 方法(mt.run()),这就是方法调用,而不是启动线程了...

1415
来自专栏Java编程技术

并发队列-无界非阻塞队列ConcurrentLinkedQueue原理探究

常用的并发队列有阻塞队列和非阻塞队列,前者使用锁实现,后者则使用CAS非阻塞算法实现,使用非阻塞队列一般性能比较好,下面就看看常用的非阻塞ConcurrentL...

1411
来自专栏java技术学习之道

Java设计模式——代理模式实现及原理

1333
来自专栏Golang语言社区

Web开发须知:URL编码与解码

通常如果一样东西需要编码,说明这样东西并不适合传输。原因多种多样,如Size过大,包含隐私数据,对于Url来说,之所以要进行编码,是因为Url中有些字符会引起...

4383

扫码关注云+社区

领取腾讯云代金券