JDBC之Driver和DriverMananger
目录- JDBC之Driver和DriverMananger
Java和MySQL的关系
java是在内存中运行的,但是Java想要统一天下,那么就不得不涉及到对于数据的处理。
那么内存中数据资源肯定是有限的,那么处理磁盘或者是数据库中的程序就成了问题。
Java早期属于Sun公司,而MySQL作为一个非常优秀的关系型数据库,也被Sun公司看中。
所以Sun公司收购了MySQL,但是因为MySQL分为了CLIENT和SERVER端
CLIENT连接SERVER端,可以发送具体的指令来连接上SERVER,如下所示:
mysql -hlocalhost -uroot -proot -Proot
但是手动使用MySQL自带的客户端,相对于程序来说,是不合适的。所以指定了一套Java能够操作MySQL的规范。
但是随着数据库的产生如雨后春笋般涌出,如Oracle等,那么Java为了抢占市场,也能够操作其他的数据库,而不是只支持自己家的MySQL。
Java制定了一套连接数据库的规范,这就是大名鼎鼎的JDBC。
JDBC
概念:Java Database connect java版本的数据库连接
只有在连接上数据库的条件下,才能够来操作数据库。所以在JDBC中提供出来对应的接口。
JDBC中只是定义如何连接、设置事务、SQL操作等,所以具体的操作,需要由每家数据库厂商提供对应的驱动来进行实现JDBC。
演变过程
那么具体是如何做到的呢?这就是接下来要来进行分析的DriverManager和Driver(驱动)所要做的事情了。
一般情况下,在应用程序中进行数据库连接,调用JDBC接口,首先要将特定厂商的JDBC驱动实现加载到系统内存中,然后供系统使用。基本结构图如下:
驱动加载入内存的过程
这里所谓的驱动,其实就是实现了java.sql.Driver接口的类。如:
Oracle的驱动类是 oracle.jdbc.driver.OracleDriver.class(此类可以在oracle提供的JDBC jar包中找到),此类实现了java.sql.Driver接口。
MySQL的驱动类是com.mysql.cj.jdbc.Driver.class类,此类实现了java.sql.Driver接口。
由于驱动本质上还是一个class,将驱动加载到内存和加载普通的class原理是一样的:使用Class.forName("driverName")。
以下是将常用的数据库驱动加载到内存中的代码:
//加载Oracle数据库驱动
Class.forName("oracle.jdbc.driver.OracleDriver");
//加载SQL Server数据库驱动
Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
//加载MySQL 数据库驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//加载MySQL 数据库驱动
Class.forName("com.mysql.jdbc.Driver");
注意:Class.forName()将对应的驱动类加载到内存中,然后执行内存中的static静态代码段,代码段中,会创建一个驱动Driver的实例,放入DriverManager中,供DriverManager使用。
Oracle加载驱动的静态方法
例如,在使用Class.forName() 加载oracle的驱动oracle.jdbc.driver.OracleDriver时,会执行OracleDriver中的静态代码段,创建一个OracleDriver实例,然后调用DriverManager.registerDriver()注册:
static {
Timestamp localTimestamp = Timestamp.valueOf("2000-01-01 00:00:00.0");
try {
if (defaultDriver == null) {
//创建一个OracleDriver实例,然后注册到DriverManager中
defaultDriver = new OracleDriver();
DriverManager.registerDriver(defaultDriver);
}
} catch (RuntimeException localRuntimeException) {
} catch (SQLException localSQLException) {
}
MySQL加载驱动的静态方法
在MySQL中,如下所示:
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
// 利用DriverManager来加载驱动
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
从上面来看的话,我们加载了Driver,程序就会自动调用DriverManager来自动注册一个驱动到DriverManager中来,由DriverManager来进行管理维护Driver。
利用CopyOnWriteArrayList集合来进行存储Driver(适用于读多写少的情况)
那么Driver到底做了些什么事情呢?
Driver的功能
首先应该想到的是,既然是驱动,就需要来连接数据库,不同厂商提供的数据库有各自的实现方式。
那么既然实现不同,那么就需要来做统一管理,这个时候接口的作用就体现出来了。
因为各个厂商实现方式不同,连接数据库的方式也不同,那么就需要针对性的来做连接。
我们猜测一下:肯定有连接协议方式、账号密码、自定义参数等等信息,Driver都得需要来支持。
首先看下在接口中的定义:java.sql.Driver接口规定了Driver应该具有以下功能:
其中:
acceptsURL
**acceptsURL(String url) ** :用来测试对指定的url,该驱动能否打开这个url连接。driver对自己能够连接的url会针对不同的数据库厂商针对自己的协议来做自定义校验,只有符合自己的协议形式的url才认为自己能够打开这个url,如果能够打开,返回true,反之,返回false;
Oracle校验URL
例如:oracle定义的自己的url协议如下:
jdbc:oracle:thin:@//<host>:<port>/ServiceName
jdbc:oracle:thin:@<host>:<port>:<SID>
Oracle自己的acceptsURL(String url)方法如下:
public boolean acceptsURL(String paramString) {
if (paramString.startsWith("jdbc:oracle:")) {
return (oracleDriverExtensionTypeFromURL(paramString) > -2);
}
return false;
}
private int oracleDriverExtensionTypeFromURL(String paramString) {
int i = paramString.indexOf(58) + 1;
if (i == 0) {
return -2;
}
int j = paramString.indexOf(58, i);
if (j == -1) {
return -2;
}
if (!(paramString.regionMatches(true, i, "oracle", 0, j - i))) {
return -2;
}
++j;
int k = paramString.indexOf(58, j);
if (k == -1) {
return -3;
}
String str = paramString.substring(j, k);
if (str.equals("thin")) {
return 0;
}
if ((str.equals("oci8")) || (str.equals("oci"))) {
return 2;
}
return -3;
}
由上可知oracle定义了自己应该接收什么类型的URL,自己能打开什么类型的URL连接(注意:这里acceptsURL(url)只会校验url是否符合协议,不会尝试连接判断url是否有效) 。拓展阅读:常用数据库 JDBC URL格式
MySQL校验URL
例如:mysql定义的自己的url协议如下:
jdbc:mysql://host:port/database?x=xx?y=yy
在MySQL中也有代码来判断:Type.isSupported方法中,用一个枚举的scheme来进行判断,下面摘一段过来:
....
jdbc:mysql:
jdbc:mysql:replication:
....
而对于我们输入的url来说,如:jdbc:mysql://127.0.0.1:3306/tb_test
都是以jdbc:mysql:开头的,所以是符合条件的
connect
connect(String url,Properties info)方法,创建Connection对象,用来和数据库的数据操作和交互,而Connection则是真正数据库操作的开始。
对于MySQL来说,在这里也对URL做了校验
public java.sql.Connection connect(String url, Properties info) throws SQLException {
try {
// 针对URL来做协议校验
if (!ConnectionUrl.acceptsUrl(url)) {
return null;
}
ConnectionUrl conStr = ConnectionUrl.getConnectionUrlInstance(url, info);
// 根据不同的类型在创建不同类型的数据库连接
switch (conStr.getType()) {
case SINGLE_CONNECTION:
return com.mysql.cj.jdbc.ConnectionImpl.getInstance(conStr.getMainHost());
case FAILOVER_CONNECTION:
case FAILOVER_DNS_SRV_CONNECTION:
return FailoverConnectionProxy.createProxyInstance(conStr);
case LOADBALANCE_CONNECTION:
case LOADBALANCE_DNS_SRV_CONNECTION:
return LoadBalancedConnectionProxy.createProxyInstance(conStr);
case REPLICATION_CONNECTION:
case REPLICATION_DNS_SRV_CONNECTION:
return ReplicationConnectionProxy.createProxyInstance(conStr);
default:
return null;
}
} catch (UnsupportedConnectionStringException e) {
// when Connector/J can't handle this connection string the Driver must return null
return null;
} catch (CJException ex) {
throw ExceptionFactory.createException(UnableToConnectException.class,
Messages.getString("NonRegisteringDriver.17", new Object[] { ex.toString() }), ex);
}
}
上面注释说的是,如果给定的不是MySQL指定的URL,那么将会返回null,如果是正常的,那么将会加载对应的驱动进行返回。
如果找到了对应的类型,那么将会来创建不同的Connection的实例对象。
手动加载驱动 Driver 并实例化进行数据库操作的例子
Oracle
public static void driverTest(){
try {
//1.加载oracle驱动类,并实例化
Driver driver = (Driver) Class.forName("oracle.jdbc.driver.OracleDriver").newInstance();
//2.判定指定的URL oracle驱动能否接受(符合oracle协议规则)
boolean flag = driver.acceptsURL("jdbc:oracle:thin:@127.0.0.1:1521:xe");
//标准协议测试
boolean standardFlag1 = driver.acceptsURL("jdbc:oracle:thin:@//<host>:<port>/ServiceName");
boolean standardFlag2 = driver.acceptsURL("jdbc:oracle:thin:@<host>:<port>:<SID>");
System.out.println("协议测试:"+flag+"\t"+standardFlag1+"\t"+standardFlag2);
//3.创建真实的数据库连接:
String url = "jdbc:oracle:thin:@127.0.0.1:1521:xe";
Properties props = new Properties();
props.put("user", "louluan");
props.put("password", "123456");
Connection connection = driver.connect(url, props);
//connection 对象用于数据库交互,代码省略。。。。。
} catch (Exception e) {
System.out.println("加载Oracle类失败!");
e.printStackTrace();
} finally{
}
}
MySQL
public static void driverTest(){
try {
//1.加载oracle驱动类,并实例化
Driver driver = (Driver) Class.forName("com.mysql.cj.jdbc.Driver").newInstance();
//2.判定指定的URL mysql驱动能否接受(符合mysql协议规则 jdbc:mysql:)
boolean flag = driver.acceptsURL("jdbc:mysql://127.0.0.1:3306/tb_test");
//标准协议测试
boolean standardFlag1 = driver.acceptsURL("jdbc:mysql://localhost:3306/database");
boolean standardFlag2 = driver.acceptsURL("jdbc:mysql://localhost/database");
System.out.println("协议测试:"+flag+"\t"+standardFlag1+"\t"+standardFlag2);
//3.创建真实的数据库连接:
String url = "jdbc:mysql://127.0.0.1:3306/tb_test";
Properties props = new Properties();
props.put("user", "root");
props.put("password", "root");
Connection connection = driver.connect(url, props);
//connection 对象用于数据库交互,代码省略。。。。。
} catch (Exception e) {
System.out.println("加载Oracle类失败!");
e.printStackTrace();
} finally{
}
}
存在现象,为什么connnect中可以放入url和对应的props呢?
properties是如何解析的?
properties既然要作为连接数据库时作为额外附带参数,如用户名+密码等等,是如何来进行操作的呢?
那么看一下MySQL中是如何操作的?
从com.mysql.cj.jdbc.NonRegisteringDriver#connect找到关键性代码
public java.sql.Connection connect(String url, Properties info) throws SQLException {
try {
// 校验URL协议
if (!ConnectionUrl.acceptsUrl(url)) {
return null;
}
// 解析url和info信息来组成连接串信息
ConnectionUrl conStr = ConnectionUrl.getConnectionUrlInstance(url, info);
// 省略创建数据库连接代码
} catch (UnsupportedConnectionStringException e) {
// when Connector/J can't handle this connection string the Driver must return null
return null;
} catch (CJException ex) {
throw ExceptionFactory.createException(UnableToConnectException.class,
Messages.getString("NonRegisteringDriver.17", new Object[] { ex.toString() }), ex);
}
}
看下com.mysql.cj.conf.ConnectionUrl#getConnectionUrlInstance是如何来判断的?
public static ConnectionUrl getConnectionUrlInstance(String connString, Properties info) {
if (connString == null) {
throw ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("ConnectionString.0"));
}
// 这里会将Properties中的信息拿出来进行拼接
String connStringCacheKey = buildConnectionStringCacheKey(connString, info);
ConnectionUrl connectionUrl;
// ........ 省略代码
return connectionUrl;
}
看下具体的代码:
private static String buildConnectionStringCacheKey(String connString, Properties info) {
StringBuilder sbKey = new StringBuilder(connString);
sbKey.append("\u00A7"); // Section sign.
sbKey.append(
info == null ? null : info.stringPropertyNames().stream().map(k -> k + "=" + info.getProperty(k)).collect(Collectors.joining(", ", "{", "}")));
return sbKey.toString();
}
将k和value进行连接,最终拼接成:jdbc:mysql://127.0.0.1:3306/mysql_index_test§{user=root, password=root}
我们的properties中可以写哪些参数呢?
具体可以参考PropertyKey这个枚举类
存在问题
上述的手动加载Driver并且获取连接的过程稍显笨拙:如果现在我们加载进来了多个驱动Driver,那么手动创建Driver实例,并根据URL进行创建连接就会显得代码杂乱无章,并且还容易出错,并且不方便管理。JDBC中提供了一个DriverManager角色,用来管理这些驱动Driver。
这就是为什么要引入DriverManager的原因,要统一来进行管理不同厂商提供的Driver驱动!
DriverManager的功能
事实上,一般我们操作Driver,获取Connection对象都是交给DriverManager统一管理的。
DriverManger可以注册和删除加载的驱动程序,可以根据给定的url获取符合url协议的驱动Driver或者是建立Conenction连接,进行数据库交互。
以下是DriverManager的关键方法摘要:
DriverManager 内部持有这些注册进来的驱动 Driver,由于这些驱动都是 java.sql.Driver 类型,那么怎样才能获得指定厂商的驱动Driver呢?
在一个项目中可以使用到不同类型的数据库,那么就需要有对应的驱动来进行支持。Java为了能够满足对不同数据库类型的支持,也为了能够统一管理。
利用Java内置的SPI机制来进行加载这些驱动
答案就在于: SPI技术
SPI技术
SPI全称为 (Service Provider Interface):是JDK内置的一种服务提供发现机制。
SPI是一种动态替换发现的机制,一种解耦非常优秀的思想。
SPI工作原理: 就是ClassPath路径下的META-INF/services文件夹中, 以接口的全限定名来命名文件名,文件里面写该接口的实现。然后再资源加载的方式,读取文件的内容(接口实现的全限定名), 然后再去加载类。
spi可以很灵活的让接口和实现分离, 让api提供者只提供接口, 第三方来实现。
所以我们可以在MySQL依赖中找到META-INF/services/java.sql.Driver,然后文件中的内容是:
com.mysql.cj.jdbc.Driver
参考连接:
https://huaweicloud.csdn.net/63874ededacf622b8df8a9b9.html?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2defaultCTRLISTactivity-1-123450610-blog-118364823.pc_relevant_multi_platform_whitelistv3&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2defaultCTRLISTactivity-1-123450610-blog-118364823.pc_relevant_multi_platform_whitelistv3&utm_relevant_index=1
java.sql.Driver接口规定了厂商实现该接口,并且定义自己的URL协议。厂商们实现的Driver接口通过acceptsURL(String url)来判断此url是否符合自己的协议,如果符合自己的协议,则可以使用本驱动进行数据库连接操作,查询驱动程序是否认为它可以打开到给定 URL 的连接。
在下面回来详细进行说明
使用DriverManager获取指定Driver
在驱动加载如内存的过程章节中,演示了Class.forName("...Driver")的方式,将不同数据库厂商的驱动加载到DriverManager中的CopyOnWriteArrayList来进行保存。
对于驱动加载后,如何获取指定的驱动程序呢?这里,DriverManager的静态方法getDriver(String url)可以通过传递给的URL,返回可以打开此URL连接的Driver。
比如,我想获取oracle的数据库驱动,只需要传递形如jdbc:oracle:thin:@
Driver oracleDriver =DriverManager.getDriver("jdbc:oracle:thin:@<host>:<port>:<SID>");
实际上,DriverManager.getDriver(String url)方法是根据传递过来的URL,遍历它维护的驱动Driver,依次调用驱动的Driver的acceptsURL(url),如果返回acceptsURL(url)返回true,则返回对应的Driver:
public static Driver getDriver(String paramString) throws SQLException {
//省略部分代码。。。。
// 获取得到已经注册的所有的驱动
Iterator localIterator = registeredDrivers.iterator();
//遍历注册的驱动
while (localIterator.hasNext()) {
DriverInfo localDriverInfo = (DriverInfo) localIterator.next();
if (isDriverAllowed(localDriverInfo.driver, localClass))
try {
//如果对应驱动类accepsURL() 为true,返回对应的driver
// 不同驱动类的acceptsURL方法对URL来做校验,如果满足,说明当前存在对应的驱动
if (localDriverInfo.driver.acceptsURL(paramString)) {
//返回对应的driver
return localDriverInfo.driver;
}
} catch (SQLException localSQLException) {
}
else
println(" skipping: "+ localDriverInfo.driver.getClass().getName());
}
// 遍历完成所有的驱动之后,还没有找到,那么就抛出异常,说明没有对应的驱动支持对应的URL协议
throw new SQLException("No suitable driver", "08001");
//-----省略部分代码
}
为什么可以通过URL来获取得到Driver
private static boolean isDriverAllowed(Driver var0, ClassLoader var1) {
boolean var2 = false;
if (var0 != null) {
Class var3 = null;
try {
// 得到对应的类,然后加载进来!!这里是类,而不是对象
var3 = Class.forName(var0.getClass().getName(), true, var1);
} catch (Exception var5) {
var2 = false;
}
var2 = var3 == var0.getClass();
}
return var2;
}
因为DriverManger就是为了统一加载各种Driver的,不如利用多态机制让各自的类来进行实现。
因为对于Oracle和MySQL来说,都有各自的驱动,这里通过SPI机制来进行加载。
使用DriverManager注册和取消注册驱动Driver
在开始的地方讨论了当使用Class.forName("driverName")加载驱动的时候,会向DriverManager中注册一个Driver实例。以下代码将验证此说法:
public static void defaultDriver(){
try {
String url = "jdbc:oracle:thin:@127.0.0.1:1521:xe";
//1.将Driver加载到内存中,然后执行其static静态代码,创建一个OracleDriver实例注册到DriverManager中
Class.forName("oracle.jdbc.driver.OracleDriver");
//取出对应的oracle 驱动Driver
Driver driver = DriverManager.getDriver(url);
System.out.println("加载类后,获取Driver对象:"+driver);
//将driver从DriverManager中注销掉
DriverManager.deregisterDriver(driver);
//重新通过url从DriverManager中取Driver
driver = DriverManager.getDriver(url);
System.out.println(driver);
} catch (Exception e) {
System.out.println("加载Oracle类失败!");
e.printStackTrace();
} finally{
}
}
以上代码主要分以下几步:
- 首先是将 oracle.jdbc.driver.OracleDriver加载到内存中;
- 然后便调用DriverManager.getDriver()去取Driver实例;
- 将driver实例从DriverManager中注销掉;
- 尝试再取对应url的Driver实例
上述代码执行的结果如下:
从执行结果看,正好能够验证以上论述:当第四步再次获取对应url的 Driver 实例时,由于已经被注销掉了,找不到适当的驱动Driver,抛出了 "Not suitable driver" 的异常。
public static void defaultDriver(){
try {
String url = "jdbc:oracle:thin:@127.0.0.1:1521:xe";
//1.将Driver加载到内存中,然后执行其static静态代码,创建一个OracleDriver实例注册到DriverManager中
Driver dd = (Driver)Class.forName("oracle.jdbc.driver.OracleDriver").newInstance();
//2.取出对应的oracle 驱动Driver
Driver driver = DriverManager.getDriver(url);
System.out.println("加载类后,获取Driver对象:"+driver);
//3. 将driver从DriverManager中注销掉
DriverManager.deregisterDriver(driver);
//4.此时DriverManager中已经没有了驱动Driver实例,将创建的dd注册到DriverManager中
DriverManager.registerDriver(dd);
//5.重新通过url从DriverManager中取Driver
driver = DriverManager.getDriver(url);
System.out.println("注销掉静态创建的Driver后,重新注册的Driver: "+driver);
System.out.println("driver和dd是否是同一对象:" +(driver==dd));
} catch (Exception e) {
System.out.println("加载Oracle类失败!");
e.printStackTrace();
} finally{
}
}
以下代码运行的结果:
以上代码先创建了一个Driver对象,在注销了DriverManager中由加载驱动过程中静态创建驱动之后,注册到系统中,现在DriverManager中对应url返回的Driver 即是在代码中创建的Driver对象。
使用DriverManager创建 Connection 连接对象
创建 Connection 连接对象,可以使用驱动Driver的 connect(url,props);也可以使用 DriverManager 提供的getConnection()方法。
此方法通过url自动匹配对应的驱动Driver实例,然后调用对应的connect方法返回Connection对象实例。
Driver driver = DriverManager.getDriver(url);
Connection connection = driver.connect(url, props);
上述代码等价于:
Class.forName("oracle.jdbc.driver.OracleDriver");
Connection connection = DriverManager.getConnection(url, props);
DriverManager 初始化
DriverManager 作为 Driver 的管理器,它在第一次被使用的过程中(即在代码中第一次用到的时候),它会被加载到内存中,然后执行其定义的static静态代码段,在静态代码段中,有一个 loadInitialDrivers() 静态方法,用于加载配置在jdbc.drivers 系统属性内的驱动Driver,配置在jdbc.drivers 中的驱动driver将会首先被加载:
static {
// 加载并初始化所有的Driver
loadInitialDrivers();
println("JDBC DriverManager initialized");
SET_LOG_PERMISSION = new SQLPermission("setLog");
DEREGISTER_DRIVER_PERMISSION = new SQLPermission("deregisterDriver");
}
可以看下如何来进行初始化的:
private static void loadInitialDrivers() {
String var0;
try {
// 利用SPI机制的首选
var0 = (String)AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
// 加载系统变量 jdbc.drivers 设置的冒号隔开的驱动
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception var8) {
var0 = null;
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 点进去看下,是固定目录:META-INF/services/
// 加载固定的类到内存中来
ServiceLoader var1 = ServiceLoader.load(Driver.class);
Iterator var2 = var1.iterator();
try {
while(var2.hasNext()) {
var2.next();
}
} catch (Throwable var4) {
}
return null;
}
});
// ........省略代码
}
证明
在上面的过程中,使用Class.forName来进行加载驱动并将Driver注册到DriverManager中的。
但是现在看完了DriverManager的静态方法之后,也可以不使用Class.forName来加载驱动。
证明jdbc.drvers来获取当前项目中的驱动,首先利用maven项目引入oracle和MySQL对应的坐标
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.oracle.database.jdbc/ojdbc8 -->
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<version>21.1.0.0</version>
</dependency>
然后写一段代码:
public static void main(String[] args) {
Enumeration<Driver> drivers = DriverManager.getDrivers();
while (drivers.hasMoreElements()){
Driver driver = drivers.nextElement();
String driverName = driver.getClass().getName();
System.out.println("获取得到驱动的名称--->"+driverName);
}
}
打印结果:
获取得到驱动的名称--->com.mysql.cj.jdbc.Driver
获取得到驱动的名称--->oracle.jdbc.OracleDriver
总结
1、JDBC针对不同的数据库厂商通过接口来约束对应的行为;比如说:校验URL是否符合厂商条件、连接时候对参数进行校验;
2、JDBC为了针对各种数据库厂商提供的驱动进行管理,引入了DriverManager类处理
3、JDBC利用SPI技术来加载管理维护不同数据库厂商的驱动;
重点理解上面的两个图即可: