JAVA JDBC connection和SPI机制

前言:JAVA JDBC connection和SPI机制的笔记

JDBC

关于JDBC的定义,官方定义的一套操作所有关系型数据库的规则,即接口

各个数据库厂商去实现了这个JDBC的接口,提供数据库驱动jar包,我们可以使用这套JDBC接口编程,真正执行代码的是驱动jar包中的java.sql.Driver的实现类

什么是"真正执行代码的是驱动jar包中的java.sql.Driver实现类",其实就是一个类去实现官方写的java.sql.Driver接口中的方法,这样就是一个java.sql.Driver的实现类

跟进去看下,如下的方法都是厂商们一定需要实现的方法

public interface Driver //这是一个接口,不用说,要不然其他厂商如何实现呢!
Connection connect(String url, java.util.Properties info)
boolean acceptsURL(String url) //还能判断是否是正规的数据库协议
DriverPropertyInfo[] getPropertyInfo // 该方法可以获得一些驱动的相关信息放到DriverPropertyInfo数组中
int getMajorVersion(); //获取主版本信息
int getMinorVersion(); //获取副版本信息
boolean jdbcCompliant(); //判断驱动程序是否为正版JDBC
public Logger getParentLogger() //返回此驱动程序使用的所有记录器的父记录器

可以看下Mysql中的驱动包是如何写的,如下显示,这里它通过继承NonRegisteringDriver类来实现了java.sql.Driver的接口

java.sql.DriverManager

Java通过java.sql.DriverManager来管理所有数据库的驱动注册,所以如果想要建立数据库连接需要先在java.sql.DriverManager中注册registerDriver对应的驱动类,然后调用getConnection方法才能连接上数据库。

跟进去简单的了解下DriverManager这个类的属性和方法

这是一个公有类public class DriverManager

注册的JDBC驱动程序列表

    // List of registered JDBC drivers
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>(); //这个是用来保存已经注册过的驱动
    private static volatile int loginTimeout = 0;
    private static volatile java.io.PrintWriter logWriter = null;
    private static volatile java.io.PrintStream logStream = null;

一个静态代码块

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

还有一些关于日志的操作

public static java.io.PrintWriter getLogWriter()
public static void setLogWriter(java.io.PrintWriter out)
....
....

获取数据库的连接

public static Connection getConnection(String url, java.util.Properties info)
public static Connection getConnection(String url, String user, String password)
public static Connection getConnection(String url)
private static Connection getConnection(String url, java.util.Properties info, Class<?> caller)

驱动的相关操作

public static Driver getDriver(String url) //获取驱动
public static synchronized void registerDriver(java.sql.Driver driver) //注册驱动1
public static synchronized void registerDriver(java.sql.Driver driver, DriverAction da) //注册驱动2
public static synchronized void deregisterDriver(Driver driver) //取消驱动

一个概念:Java通过java.sql.DriverManager来管理所有数据库的驱动注册,所以如果想要建立数据库连接需要先在java.sql.DriverManager中注册对应的驱动类,然后调用getConnection方法才能连接上数据库。

java.sql.DriverManager.getConnection(xx)其实就是间接的调用了java.sql.Driver类的connect方法实现数据库连接的。数据库连接成功后会返回一个叫做java.sql.Connection的数据库连接对象,一切对数据库的查询操作都将依赖于这个Connection对象。

JDBC连接数据库的一般步骤:

  • 注册驱动,Class.forName("数据库驱动的类名")

  • 获取连接,DriverManager.getConnection(xxx)

正常实现

        Class.forName("com.mysql.jdbc.Driver");
        String URL = "jdbc:mysql://localhost:3306/mysql";
        String USERNAME = "root";
        String PASSWORD = "123456";
        Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);

我自己学的时候反正是有疑问的,为什么注册用Class.forName("com.mysql.jdbc.Driver");来实现,然后就是DriverManager.getConnection为什么能够获得Mysql驱动包中的connection对象

为什么Class.forName能够实现注册驱动

那么直接跟进com.mysql.jdbc.Driver的Driver类中去观察,看到如下的内容,这是一个静态代码块,并且Class.forName("com.mysql.jdbc.Driver")使用的时候会触发类加载,从而执行该静态代码块,那么也就可以说明为什么能够Class.forName("com.mysql.jdbc.Driver")能够注册驱动

提醒,如果想拿到对应类型的Class实例,但是又不想触发类加载执行静态代码块的话,下面两种方法可以解决

提示:默认的Class.forname的话第二个参数为true,也就是会触发类加载执行静态代码块

1、使用 Class.forName("xxxx", false, loader) 方法,将第二个参数传入false

2、ClassLoader.load("xxxx");

DriverManager.getConnection为什么能够获得Mysql

那么也从源码中观察,老样子跟进DriverManager中,静态代码块执行的是DriverManager.registerDriver(new Driver());

继续跟registerDriver方法,如下图所示,它会将Mysql自身Driver实例化然后放到registerDriver中

最后还会添加到registeredDrivers属性中

然后现在就是Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);

我们可以看下DriverManager.getConnection的方法是怎么样的,如下图所示,它会将账号密码这些都存储起来然后放到一个info对象中,再将info作为参数传入getConnection中

跟进去看,重点是如下的红框处,它的for循环会遍历registeredDrivers数组,取出DriverInfo类型的aDriver变量,DriverInfo这个就是之前存储到registeredDrivers数组中的内容,此时的aDriver.driver就是已经实例化的Driver对象(Mysql,Mssql等等),最后调用它们实现Driver接口之后的connect方法,返回一个connection的对象

细节点(2022-11-14)

再回到这篇文章,补上一点细节的部分

TestMain.java

public class TestMain {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");
        String URL = "jdbc:mysql://localhost:3306/mysql";
        String USERNAME = "root";
        String PASSWORD = "123456";
        Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
    }
}

具体流程是怎么样的呢,这里来进行调试分析下

首先会来到com.mysql.jdbc.Driver,因为这边Class.forname会触发静态代码块,所以这里走到了com.mysql.jdbc.Driver的静态代码块部分

再接着就来到了java.sql.DriverManager的静态代码块,因为com.mysql.cj.jdbc.Driver中的静态代码块调用了DriverManager类的registerDriver方法,因此JVM又会去加载DriverManager类,加载过程中DriverManager的静态代码块被执行,而DriverManager的静态代码块中调用了loadInitialDrivers方法,所以这边的话继续调用loadInitialDrivers方法

然后这边的话就继续调用java.sql.DriverManager#registerDriver将相关的Driver驱动信息添加到registeredDrivers数组中

到了这里大家会不会有点疑惑,就是为什么java.sql.DriverManager.registerDriver(new Driver());能够自动将mysql的相关驱动进行注册?

这里就涉及到了SPI的机制,其中工作在java.sql.DriverManager#loadInitialDrivers就已经做了,下面来学习SPI机制

什么是SPI机制(2022-11-14)

这里的话主要关注点:java.util.ServiceLoader

跟到java.util.ServiceLoader#load,可以看到先获取了对应的类加载器,这里使用的是Thread.currentThread().getContextClassLoader,因为是要加载自定义的类

接着又继续对用java.util.ServiceLoader#load,开始进行实例化ServiceLoader类

java.util.ServiceLoader#ServiceLoader中初始化了相关的字段之后还会调用reload方法,这里继续跟进去

在java.util.ServiceLoader#reload方法中,初始化了相关的lookupIterator字段

接着开始进行迭代操作Iterator driversIterator = loadedDrivers.iterator(); 生成一个驱动的Iterator对象

接下来调用java.util.Iterator的hashNext方法

跟进去会发现实际会调用java.util.ServiceLoader.LazyIterator#hasNext方法

java.util.ServiceLoader.LazyIterator#hasNext方法中调用java.util.ServiceLoader.LazyIterator#hasNextService

这里可以看到当初次相关字段都会null的时候,就会进行初始化操作,这里就会获取String fullName = PREFIX + service.getName();,service.getName就是ServiceLoader初始化的时候,其中service字段初始化的就是Driver.class,到这里的话就是"META-INF/services/java.sql.Driver"

然后通过java.lang.ClassLoader#getResources获取该META-INF/services/java.sql.Driver,因为当前的loader是AppClassLoader所以能遍历下相关的jar里面的META-INF

最后会java.util.ServiceLoader.LazyIterator#nextService进行遍历进行实例化,然后加入到providers对象中

实例化的时候触发静态代码块,然后最终注册到registeredDrivers数组中

然后每次通过DriverManager.getConnection获取Connection对象的时候都是从registeredDrivers进行读取实例化Connection对象

这里还打印当前ServiceLoader能够加载的Driver有那些

public class GetJDBCList {
    public static void main(String[] args) {
        ServiceLoader<Driver> serviceLoader = ServiceLoader.load(Driver.class, ClassLoader.getSystemClassLoader());
        for(Iterator<Driver> iterator = serviceLoader.iterator(); iterator.hasNext();) {
            Driver driver = iterator.next();
            System.out.println(driver.getClass().getPackage() + " ------> " + driver.getClass().getName());
        }
    }
}

可以看到我这里有两个,原因也是我的java.sql.Driver中有两个要加载的条目

posted @ 2020-08-12 18:03  zpchcbd  阅读(1375)  评论(0编辑  收藏  举报