[JDBC] Java JDBC驱动机制:DriverManager/Driver...

[JDBC] Java JDBC驱动机制:DriverManager/Driver...

本文只讨论 JDBC 驱动加载问题。

1 JDBC概述

1.1 什么是 JDBC

JDBC 一般指 Java 数据库连接,(Java Database Connectivity,简称为 JDBC);

JDBC 是 Java 语言中用来规范客户端程序如何来访问数据库的应用程序接口;其提供了如查询、更新数据库中数据的方法,我们常说的 JDBC 是面向关系型数据库的。

1.2 JDBC 基本结构图

一般情况下,在 application 中进行数据库连接,调用 JDBC 接口:

首先,需要将指定的 JDBC 驱动实现加载到 jvm 中;

然后,再进行使用对应具体的数据库驱动类、结合具体的数据库连接参数,建立数据库连接。

基本的结构图如下:

1.3 案例问题的分析与解决

1.3.1 问题描述

//jdk6之后,无需再显式执行Class.forName

Class.forName("oracle.jdbc.driver.OracleDriver");

// com.mysql.cj.jdbc.Driver [mysql:mysql-connector-java:8.0.28]

// com.mysql.jdbc.Driver [mysql:mysql-connector-java:5.1.33]

// org.gjt.mm.mysql.Driver extends com.mysql.jdbc.Driver [mysql:mysql-connector-java:5.1.33] 【 8.0.1.33 中无此Driver类】

// com.amazon.opendistroforelasticsearch.jdbc.Driver [com.amazon.opendistroforelasticsearch.client:opendistro-sql-jdbc:1.12.0.0]

// oracle.jdbc.driver.OracleDriver [com.clickhouse:clickhouse-jdbc:0.3.2]

Connection connection = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:orcl","xxx","xxxx");

我第一次写此代码时,就有很多疑问,但是后来也没研究,今天我把当时的疑问列出

1 "oracle.jdbc.driver.OracleDriver" 和"jdbc:oracle:thin" 有什么关系,为什么要写两遍

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

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

4 DriverManager.getConnection() 怎么使用的驱动的呢

1.3.2 问题分析:源码分析

要回答上边的问题,我们先来看看这些代码的源码

1 要使用jdbc连接 oracle,必须先加载驱动,Class.forName就是加载驱动

Class.forName 会初始化 oracle.jdbc.driver.OracleDriver这个类 (关于类的加载过程可自行搜索),初始化时自动执行类的静态代码块

// oracle.jdbc.driver.OracleDriver

static{

...

if (defaultDriver == null) {

defaultDriver = new oracle.jdbc.OracleDriver();

DriverManager.registerDriver(defaultDriver);

}

这样就驱动就注册了

2 jdk6为什么不需要 执行了Class.forName这行代码了呢

大家先了解一下ServiceLoader 。

简单来说就是在驱动jar包配置文件中,指定oracle.jdbc.driver.OracleDriver实现java.sql.Driver接口;然后DriverManager在初始化的时候,自动扫描所有jar包中实现了java.sql.Driver的类 ,并初始化 此实现类

//DriverManager

static {

loadInitialDrivers();

println("JDBC DriverManager initialized");

}

private static void loadInitialDrivers() {

...

AccessController.doPrivileged(new PrivilegedAction() {

public Void run() {

//搜索服务的实现类(驱动实例)

ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);

Iterator driversIterator = loadedDrivers.iterator();

try{

while(driversIterator.hasNext()) {

driversIterator.next();// 此处初始化 并注册了

}

} catch(Throwable t) {

// Do nothing

}

return null;

//ServiceLoader

public S next() {

if (acc == null) {

return nextService();

} else {

...

//ServiceLoader

private S nextService() {

...

try {

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

...

也就是说 DriverManger借助 ServiceLoader 找到驱动 并注册了,所以不需要再手工注册

3 驱动注册了,DriverManager中 驱动怎么被使用呢

//caller = Reflection.getCallerClass()

private static Connection getConnection(

String url, java.util.Properties info, Class caller) throws SQLException {

....

ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;

synchronized(DriverManager.class) {

// synchronize loading of the correct classloader.

if (callerCL == null) {

callerCL = Thread.currentThread().getContextClassLoader();

}

}

//callerCL 是方法调用类或者当前线程的classLoader,会在isDriverAllowed中使用

...

// 所有已经注册驱动都保存在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);

...

private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {

boolean result = false;

if(driver != null) {

Class aClass = null;

try {

//使用不同的classLoader加载出来的驱动类是不相等的,此处就利用这点判断权限

aClass = Class.forName(driver.getClass().getName(), true, classLoader);

} catch (Exception ex) {

result = false;

}

result = ( aClass == driver.getClass() ) ? true : false;

}

return result;

}

遍历 registeredDrivers (所有已经注册驱动都在这里), 使用 DriverManager.getConnection 方法所在类的 classLoader(如果为空,则使用当前线程上下文的classLoaer),去加载驱动类 ,然后和registeredDrivers里边的比较 ,如果相等,则此驱动是有权限被 使用。

注意:此处 遍历 registeredDrivers时,只要找到一个有权限的,就立即返回。

如果使用相同方式注册多个了驱动,调用的是哪个驱动呢? registeredDrivers中第一个有权限且能正确连接上的的 ;那么程序里边 如果要连接多个数据库,使用jdbc怎么操作?

1.4 驱动加载

从案例中回来,看看驱动是如何加载的?

1.4.1 加载驱动

驱动是一个 class,将驱动进行加载到 jvm 中与加载普通类一样,使用 Class.forName("driverName") 进行加载

//加载MySQL 数据库驱动

Class.forName("com.mysql.jdbc.Driver");

//加载Oracle数据库驱动

Class.forName("oracle.jdbc.driver.OracleDriver");

如果目标驱动class不存在时,Class.forName(...)方法将报错:

Method threw 'java.lang.ClassNotFoundException' exception.

1.4.2 java.sql.Driver 接口

驱动。

首先,需要实现 java.sql.Driver 接口,连接不同数据库的驱动类不同,但每个驱动类都需要提供一个实现了 java.sql.Driver 接口的类。

然后,在程序中由驱动器管理类 java.sql.DriverManager 去调用这些 Driver 实现

java.sql.Driver 接口源码

package java.sql;

import java.util.logging.Logger;

public interface Driver {

// 试图创建一个给定 url 的数据库连接,创建 Connection 对象

Connection connect(String url, java.util.Properties info) throws SQLException;

// 驱动程序是否可以打开 url 连接,判断 url 是否符合协议,符合协议形式的 url 才可以

boolean acceptsURL(String url) throws SQLException;

// 获取驱动的属性信息

DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info) throws SQLException;

// 获取驱动主版本号

int getMajorVersion();

// 获取驱动的次版本号

int getMinorVersion()#;

// 驱动程序是否是一个真正的 JDBC Compliant

boolean jdbcCompliant();

public Logger getParentLogger() throws SQLFeatureNotSupportedException;

}

手动加载 Driver

// 加载 mysql 驱动类,并实例化

Driver driver = (Driver)Class.forName("com.mysql.jdbc.Driver);

// 判断 url 是否符合 mysql 的 url 形式

boolean flag = driver.acceptsURL("jdbc:mysql://localhost:3306/test");

// 创建数据库连接

String url = "jdbc:mysql://localhost:3306/test";

Properties props = new Properties();

props.put("user", "root");

props.put("password", "root");

Connection connection = driver.connect(url, props);

1.4.3 DriverManager 驱动器管理类

如果有多个驱动 Driver,JDBC 提供了 DriverManager 对多个驱动进行统一管理

DriverManager 可以注册和删除加载的驱动程序,根据给定的 url 获取符合 url 协议的 Driver 或者建立 Connection 连接

DriverManager 源码

/**

* Load the initial JDBC drivers by checking the System property

* jdbc.properties and then use the {@code ServiceLoader} mechanism

*/

static {

loadInitialDrivers(); // 加载配置在 jdbc.drivers 系统变量中的驱动

println("JDBC DriverManager initialized");

}

private static void loadInitialDrivers() {

String drivers;

try {

drivers = AccessController.doPrivileged(new PrivilegedAction() {

public String run() {

return System.getProperty("jdbc.drivers");

}

});

} catch (Exception ex) {

drivers = 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);

}

}

}

使用 Class.forName("driverName") 加载驱动类到 jvm 的时候,同时会执行这个驱动类中的静态代码块,创建一个 Driver 实例;

然后,调用 DriverManager 进行注册 Driver。

DriverManager 在第一次被调用时,它会被加载到内存中,然后执行它内部的 static 静态代码块,执行其中的 loadIntialDrivers() 静态方法,加载配置在 jdbc.driver 中的 Driver,配置在 jdbc.drivers 中的驱动 driver 会先被加载

注册 Driver 到 DriverManager

在加载某一个 Driver 类时,应该创建自己的实例并像 DriverManager 中注册自己的实例。

比如 com.mysql.jdbc.Driver 的源码,使用静态代码块进行实现

在我们使用 Class.forName("com.mysql.jdbc.Driver") 方法获取它的 Class 对象,com.mysql.jdbc.Driver 就会被 jvm 加载,连接,初始化。

初始化就会执行其中的静态代码块,执行下面的 java.sql.DriverManager.registerDriver(new Driver()) 进行注册

static {

try {

java.sql.DriverManager.registerDriver(new Driver());

} catch (SQLException E) {

throw new RuntimeException("Can't register driver!");

}

}

注册实例源码

将要注册的驱动信息放到一个 DriverInfo 中,然后再放到一个 List 中,在后面连接时候会用到

public static synchronized void registerDriver(java.sql.Driver driver,

DriverAction da)

throws SQLException {

/* Register the driver if it has not already been added to our list */

if(driver != null) {

registeredDrivers.addIfAbsent(new DriverInfo(driver, da));

} else {

// This is for compatibility with the original DriverManager

throw new NullPointerException();

}

println("registerDriver: " + driver);

}

1.5 案例:建立MySQL数据库连接

MySQL JDBC驱动版本

com.mysql.cj.jdbc.Driver [mysql:mysql-connector-java:8.0.28] [√]

com.mysql.jdbc.Driver [mysql:mysql-connector-java:5.1.33] [X]

org.gjt.mm.mysql.Driver extends com.mysql.jdbc.Driver [mysql:mysql-connector-java:5.1.33] [X]

【 8.0.1.33 中无此Driver类】

@Override

protected synchronized Connection createConnection() {

// JDK 1.6 之后支持 JDBC 驱动包(内的 META-INF/services/java.sql.Driver) 利用 ServiceLoader 机制 自动注册数据库驱动

//this.registerDriver();

if (this.connection == null) {

try {

//step1 准备参数

String url = this.getDataSource().getDatasourceUrl();

String username = this.getDataSource().getDatasourceUser();

String password = this.getDataSource().getDatasourcePassword();

Properties properties = new Properties();

//JDBC DriverManager 所约定的属性固定名称: user

properties.put("user", username);

//JDBC DriverManager 所约定的属性固定名称: password

properties.put("password", password);

//step2 根据 url 获取指定的 数据库驱动

Driver driver = DriverManager.getDriver(url);

logger.debug("current datasource info is : {}", this.getDataSource().toString());

logger.debug("Success to get driver from DriverManager for jdbcUrl: {}", url);

//step3 使用指定的数据库驱动连接对应种类的数据库

//方法1: (暂不使用)基于DriverManager(找到符合权限)建立数据库连接

//this.connection = DriverManager.getConnection(url, username, password);

//方法2: 使用指定驱动器连接数据库 | 本步骤为避免出现偶现问题:当 mysql 数据源的用户名、密码等配置错误时,因JDBC DriverManager的管理机制会报ES错误:URISyntaxException: URL does not begin with the mandatory prefix jdbc:elasticsearch://.

if(driver instanceof com.mysql.cj.jdbc.Driver){

//检查给定的URL是否有效

boolean acceptable = driver.acceptsURL(url);

//使用指定的驱动器连接指定的数据库

this.connection = driver.connect(url, properties);

logger.debug("driver.acceptsURL(url): {}", acceptable);

logger.debug("connection = driver.connect(url, properties): {}", connection);

} else {//不允许使用其他种类、其他版本的数据库驱动

logger.error("Prohibit the use of other types and versions of database driver! currentDriver: {}", driver);

}

} catch (SQLException exception) {

logger.error("fail to register mysql jdbc connection because `SQLException`!");

logger.error(String.format("error info :%s", exception.toString()));

throw new RuntimeException(exception);

}

}

return this.connection;

}

@Override

protected void registerDriver() {

String driverClass = this.getDataSource().getDatasourceDriverClass();

if(REGISTERED_JDBC_DRIVERS.contains(driverClass)==false){

String driver = this.getDataSource().getDatasourceDriverClass();

driver = (driver == null)?DEFAULT_JDBC_DRIVER:driver;

try {

//方式1

Class.forName(driver);

//方式2

//Class clazz = Class.forName(driver);

//Object driverObject = clazz.newInstance();// 等效于: new com.mysql.jdbc.Driver()

//DriverManager.registerDriver((Driver) driverObject);

//DriverManager.registerDriver(new com.mysql.jdbc.Driver());

//DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());

} catch (ClassNotFoundException exception) {

logger.error("fail to register mysql jdbc driver because `ClassNotFoundException`!");

logger.error(String.format("error info :%s", exception.toString()));

throw new RuntimeException(exception);

} catch (Exception exception) {

logger.error(String.format("fail to register mysql jdbc driver,error info : %s", exception.toString()));

throw new RuntimeException(exception);

}

logger.info("success to register mysql jdbc driver!");

REGISTERED_JDBC_DRIVERS.add(driverClass);

}

}

1.6 案例:获取当前工程加载到的 JDBC Driver

getDrivers()

基于 java.sql.DriverManager#getDrivers

import java.sql.Driver;

import java.util.Enumeration;

import com.alibaba.fastjson2.JSON;

import java.util.HashMap;

...

public static List> getDrivers(){

List> drivers = new ArrayList<>();

Enumeration driversEnumeration = DriverManager.getDrivers();

while ( driversEnumeration.hasMoreElements() ) {

Driver driver = driversEnumeration.nextElement();

Map driverInfo = new HashMap<>();

driverInfo.put( "majorVersion", driver.getMajorVersion() );

driverInfo.put( "minorVersion", driver.getMinorVersion() );

driverInfo.put("driverClass", driver.getClass().getCanonicalName() );

if(logger.isDebugEnabled()==false){

logger.info("driver: {}", JSON.toJSONString(driverInfo) );

}

drivers.add( driverInfo );

}

return drivers;

}

Main

List> drivers = getDrivers();

logger.info("DriverManager#Drivers:{}", JSON.toJSONString( drivers ) );

out

getDrivers:83 driver: {"driverClass":"com.clickhouse.jdbc.ClickHouseDriver","majorVersion":0,"minorVersion":5}

getDrivers:83 driver: {"driverClass":"com.amazon.opendistroforelasticsearch.jdbc.Driver","majorVersion":1,"minorVersion":12}

getDrivers:83 driver: {"driverClass":"com.mysql.cj.jdbc.Driver","majorVersion":8,"minorVersion":0}

getDrivers:83 driver: {"driverClass":"com.alibaba.druid.proxy.DruidDriver","majorVersion":4,"minorVersion":0}

getDrivers:83 driver: {"driverClass":"com.alibaba.druid.mock.MockDriver","majorVersion":0,"minorVersion":0}

X 参考文献

JDBC/DriverManager原理--注册驱动 - CSDN

ServiceLoader详解 - 博客园

JDBC 驱动加载原理解析 - CSDN

[数据库/Java SE]MySQL驱动包(mysql-connector-java.jar)问题[com.mysql.jdbc.Driver/org.gjt.mm.mysql.Driver/com.mysql.cj.jdbc.Driver] - 博客园/千千寰宇

深入理解JDBC设计模式: DriverManager 解析 - 博客园

JDBC数据库驱动及原理 - CSDN

Class.forName的加载类底层实现 - 掘金

Class.forName()用法详解 - CSDN

Java中Class.forName()用法详解 - CSDN

acceptsURL 方法 (SQLServerDriver) - Microsoft

相关推荐

百达翡丽
www.bst365.com

百达翡丽

📅 07-01 👁️ 5572
码 到 米 (yd ⇄ m)
365bet提款规则

码 到 米 (yd ⇄ m)

📅 07-16 👁️ 9611
西班牙國家足球隊
www.bst365.com

西班牙國家足球隊

📅 07-13 👁️ 6745