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 条评论
登录 后参与评论

相关文章

来自专栏QQ会员技术团队的专栏

gulp源码解析(一)—— Stream详解

作为前端,我们常常会和 Stream 有着频繁的接触。比如使用 gulp 对项目进行构建的时候,我们会使用 gulp.src 接口将匹配到的文件转为 strea...

2025
来自专栏jeremy的技术点滴

Java开发小技巧_02

3514
来自专栏闻道于事

Java常用工具类之自定义访问对象

package com.wazn.learn.util; import javax.servlet.http.HttpServletRequest; /...

2636
来自专栏Lambda

Java 常见内存溢出异常与代码实现

Java 堆 OutOfMemoryError Java 堆是用来存储对象实例的, 因此如果我们不断地创建对象, 并且保证 GC Root 和创建的对象之间...

1858
来自专栏积累沉淀

Java批处理

批处理 JDBC对批处理的操作,首先简单说一下JDBC操作sql语句的简单机制。 JDBC执行数据库操作语句,首先需要将sql语句打包成为网络字...

2075
来自专栏码洞

基于Netty实现Redis协议的编码解码器

上面是Netty的服务器端基本消息处理结构,为了便于初学者理解,它和真实的结构有稍许出入。Netty是基于NIO的消息处理框架,用来高效处理网络IO。处理网络消...

871
来自专栏JMCui

Netty 系列六(编解码器).

    网络传输的单位是字节,如何将应用程序的数据转换为字节,以及将字节转换为应用程序的数据,就要说到到我们该篇介绍的编码器和解码器。

631
来自专栏码神联盟

框架 | SSH和SSM对比异同点、各自优势

当下流行的两种企业开发MVC开源框架,是我们Java程序猿必备知识能力。MVC,即模型(model)-视图(view)-控制器(controller)的缩写,一...

38514
来自专栏Hongten

完整java开发中JDBC连接数据库代码和步骤

E | hongtenzone@foxmail.com  B | http://www.cnblogs.com/hongten

412
来自专栏向治洪

picasso图片缓存框架

picasso是Square公司开源的一个Android图形缓存库,地址http://square.github.io/picasso/,可以实现图片下载和缓...

1888

扫码关注云+社区