【jvm jdk】类加载器4 线程上下文类加载器 & DriverManager& Driver& mysql驱动详解(SPI)

1. 线程上下文类加载器简介

线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。类 java.lang.Thread中的方法 getContextClassLoader()setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器(AppClassLoader,有的翻译为应用程序类加载器)。在线程中运行的代码可以通过此类加载器来加载类和资源。

可以参考加载器之sun.misc.Launcher类获取初始线程的上下文类加载器是系统类加载器的知识。sun.misc.Launcher类是java的入口,在其构造函数中,设置了线程上下文类加载器。

简单来说,就是在线程上下文中缓存一个类加载器,供某些场景用,比如加载spi接口,作用是灵活加载指定的类,由于违背了默认的类加载机制,因此spi接口违反了java类加载器中默认的双亲委派模型。

2. 线程上下文类加载器用法

java.lang.Thread类中有两个方法,分别提供设置和获取上下文类加载器的方法

 public ClassLoader getContextClassLoader() //获取到当前线程的上下文类加载器
 public void setContextClassLoader(ClassLoader cl)  //设置当前线程的上下文类加载器

如果当前线程没有设置上下文类加载器,那么它就会和父线程保持同样的类加载器,也就是说Main线程使用什么样的类加载器其子类线程就使用什么样的类加载器。如下:

public class Test {

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getContextClassLoader());
    }
}

JDK1.8 执行结果:

sun.misc.Launcher$AppClassLoader@19821f

3. 上下文类加载器的作用

为什么会有线程上下文类加载器的出现呢?

这个就是因为我们类加载的双亲委托机制的缺陷:首先JDK核心类库必须由BootStrapClassLoader类加载器中进行加载,其次在JDK核心类库中提供了很多SPI(Service Provider Interface)接口,其中比较常用的就是关于JDBC的SPI接口。JDK也只是规定的这个接口的之间的实现逻辑关系,并没有提供具体的怎么样的实现。这些具体实现都是我们通过第三方的厂商来提供的,例如使用MySQL的时候我们要引入MySQL的驱动,使用Oracle的时候要使用Oracle的驱动等等。如图所示,Java使用JDBC这个SPI完全的实现了应用和第三方数据库驱动的实现的接入。在使用的时候只需要更换对应的jar包和数据库驱动,其他的一概不变。

问题来了: 你定义了核心类接口,但是这些类的实现又不在核心库路径下(核心库路径jre/lib),故而BootStrapClassLoader类加载器无法加载实现类(仅能加载jre/lib),但程序又要运行,必须要加载这些实现类,因此只能打破双亲委派模型了。由非BootStrapClassLoader类加载这些实现类。
在这里插入图片描述

常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等,接口均定义在rt.jar中

   这样做的好处是JDBC提供了对于数据库操作的高度封装,应用程序只需要实现对应的接口即可,而对于我们持久层的框架来说就是对这些接口的再次的封装,使得我们开发更加的简便。从上图也可以看出我们在实际使用的时候各大数据库提供者都实现了具体的底层驱动,使用者并不需要关心这些。

4. mysql驱动

就拿MySQL驱动举例子来说,在使用的时候我们是通过Class.forName()这个方法来将数据库的驱动引入到其中。但是是谁去加载其中的Class文件呢?下面我们就来深入的分析一下关于MySQL驱动的的初始化以及源码加载过程。

配置maven依赖:

<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<verion>5.1.38</verion>
</dependency>

常规调用mysql连接的例子,代码1:

CREATE TABLE `student` (
  `id` int(11) NOT NULL,
  `name` varchar(10) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

insert into `student` (`id`, `name`) values('1','zhangsan');
insert into `student` (`id`, `name`) values('2','lisi');
import java.io.PrintWriter;
import java.sql.*;

public class JDBCTest {
    private static Connection connection;
    private static PreparedStatement preparedStatement;
    private static ResultSet resultSet;

    public static void main(String[] args) throws SQLException {
        try {
            String sql = "SELECT * FROM student";
            String url = "jdbc:mysql://10.40.65.183:3306/test";
            Class.forName("com.mysql.jdbc.Driver"); // 加载驱动
            connection = DriverManager.getConnection(url, "name", "password");
            preparedStatement = connection.prepareStatement(sql);
            resultSet = preparedStatement.executeQuery();

            while (resultSet.next()) {
                System.out.println(resultSet.getInt(1) + " " + resultSet.getString(2));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

从上述代码,我们有几个疑问:

  • 1 “com.mysql.jdbc.Driver” 和"jdbc:mysql" 有什么关系,为什么要写两遍

  • 2 Class.forName 注册驱动后,没有返回值,是怎么回事

  • 3 Class.forName 为什么 此行在jdk6版本中 又不需要了呢

    mysql驱动有2种加载方式:

    • 一种是明文 Class.forName(“com.mysql.jdbc.Driver”);
    • 另一种是在高版本jdk中,省略 Class.forName(“com.mysql.jdbc.Driver”);,通过SPI方式加载
  • 4 DriverManager.getConnection怎么使用的驱动的呢

4.1 class.forName()明文方式加载驱动(和上下文加载器没关系)

和上下文加载器没关系,只是为了引出spi加载方式

要使用jdbc连接 mysql,必须先加载mysql的驱动,Class.forName()作用就是加载驱动类:
class.forName("com.mysql.jdbc.Driver");

其中"com.mysql.jdbc.Driver"类,就是mysql的驱动,这是一个Java类。

Class.forName()内部也是通过类加载器来实现加载某个类的,如下图所示:

在这里插入图片描述

3.1.1 com.mysql.jdbc.Driver类的静态语句块触发注册驱动

通过《深入理解Java 反射中 Class.forName 和 ClassLoader 的区别》可以知道 Class.forName会在加载"com.mysql.jdbc.Driver"驱动类的时候,会执行被加载类的静态语句块,那么我们看下Driver类的静态语句块里面有什么内容:

5.1.38版本源码:

package com.mysql.jdbc;      //位于mysql-connector-java-5.1.38.jar中

import java.sql.Driver;    //spi接口,位于rt.jar
import java.sql.DriverManager;
import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements Driver {   //实现类rt.jar中的Driver接口
  static {               //静态语句块
    try {
      DriverManager.registerDriver(new Driver());  //向DriverManager中注册当前驱动
    } catch (SQLException E) {
      throw new RuntimeException("Can't register driver!");
    } 
  }
}

com.mysql.jdbc.Driver的静态代码块中调用DriverManager.registerDriver(),将Driver注册给了DriverManager。可以简单理解成把Driver类放到了DriverManager中的某个集合内

接下来我们再看看这个DriverManager.registerDriver 方法:

    public static synchronized void registerDriver(java.sql.Driver driver)
        throws SQLException {

        registerDriver(driver, null);
    }

继续看这个registerDriver(driver, null) 方法:

private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();// registeredDrivers 是一个支持并发的arraylist
......
public static void registerDriver(java.sql.Driver driver, DriverAction da)
        throws SQLException {
        if (driver != null) {
            //如果该驱动尚未注册,那么将他添加到 registeredDrivers 中去。这是一个支持并发情况的特殊ArrayList
            registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
        } else {
            // This is for compatibility with the original DriverManager
            throw new NullPointerException();
        }
        println("registerDriver: " + driver);
    }

registeredDrivers 是一个支持并发的arraylist,保证只有一个类型的实例被注册成功。

补充知识:
8.0.11直接废弃了com.mysql.jdbc.Driver,默认继承了com.mysql.cj.jdbc.Driver 类,基本原理没变:

package com.mysql.jdbc;
public class Driver extends com.mysql.cj.jdbc.Driver {
    public Driver() throws SQLException {
        super();//父类里面仍是
    }

    static {
        System.err.println("Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. "
                + "The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.");
    }
}
3.1.2 DriverManager.getConnection获取驱动

前面我们知道往DriverManager中注册了驱动,那么DriverManager中的驱动怎么使用呢?

一般是通过DriverManager.getConnection()获得一个连接,这样就可以操作数据库了,这个重要的连接,正是通过驱动提供的:


private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
        ....
 // 所有已经注册驱动都保存在registeredDrivers,这是个CopyOnWriteArrayList
        for(DriverInfo aDriver : registeredDrivers) {
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    //jdbc:oracle:thin:@localhost:1521:orcl 最终是由驱动实现类使用
                    Connection con = aDriver.driver.connect(url, info);

遍历 registeredDrivers,找到符合权限条件的驱动,这里暂不讨论权限的问题,然后调用下面的方法,产生一个连接实例:

Connection con = aDriver.driver.connect(url, info);

3.2 SPI

我们前面提出的疑问,“ jdk6为什么不需要 执行了Class.forName这行代码了呢”?

注释掉Class.forName(“com.mysql.jdbc.Driver”);也可以运行

            String sql = "SELECT * FROM student";
            String url = "jdbc:mysql://10.40.65.183:3306/test";
           // Class.forName("com.mysql.jdbc.Driver"); // 加载驱动
            connection = DriverManager.getConnection(url, "name", "password");
            preparedStatement = connection.prepareStatement(sql);

原因就是SPI!

什么是SPI,可以参考SPI介绍及实例分析,这篇文章里通过一个例子描述了SPI的加载原理。

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

虽然我们我们知道了SPI加载的原理,但那到底是在哪一步自动注册了mysql driver的呢?我们又没有在main方法中手动调用:

ServiceLoader.load(Subscribe.class);  //摘自《SPI介绍及实例分析》

其实答案就在DriverManager.getConnection()中。我们都知道调用类的静态方法会初始化该类,进而执行其静态代码块,当我们调用DriverManager.getConnection(url, "name", "password");时,首次触发了DriverManager类的加载,DriverManager的静态代码块里面有触发SPI加载的入口,进而触发SPI:

public class DriverManager {
  static {   //静态语句块
        loadInitialDrivers();      //[1]调用初始化驱动方法
        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加载驱动类
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);     //关键代码,是不是和main方法中调用的方式类似?
            Iterator<Driver> driversIterator = loadedDrivers.iterator();
            try{
                while(driversIterator.hasNext()) {
                    driversIterator.next();    //真正加载,因为之前是懒加载
                }
            } catch(Throwable t) {
                // Do nothing
            }
            return null;
        }
    });

ServiceLoader.load(Driver.class); 是关键代码,是不是和《SPI介绍及实例分析》main方法中调用ServiceLoader.load(Subscribe.class)的方式类似?是的,很相似,并在load方法中,用到了线程上下文线程类加载器。详情在《SPI介绍及实例分析》中搜索“上下文线程类”,即可找到具体的代码。

在这里插入图片描述

也就是说通过spi自动触发了注册事件,这样就不在需要Class.forName(“com.mysql.jdbc.Driver”);指定接口类型和实现类了,根据spi规则即可映射。

用户可以自行注释掉代码1中的 Class.forName(“com.mysql.jdbc.Driver”); 验证,只需在DriverManager的[1]的代码加断点,判断被触发的时机,确实是从DriverManager.getConnection()方法触发的







参考

Java高并发编程详解系列-线程上下文类加载

深入理解Java类加载器(2):线程上下文类加载器 介绍了线程上下文类加载器的起源
真正理解线程上下文类加载器(多案例分析) DriverManager初始化 & tomcat加载 spring

SPI介绍及实例分析

JDBC/DriverManager原理–注册驱动

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值