Driver和DriverMananger的说明
在学习JDBC的时候,也就是用java database connectivity,两个应用程序之间进行连接
一般情况下,在应用程序中进行数据库连接,调用JDBC接口,首先要将特定厂商的JDBC驱动实现加载到系统内存中,然后供系统使用。
基本结构图如下:
一、驱动加载进内存的过程
这里所谓的驱动,其实就是实现了java.sql.Driver接口的类。
如oracle的驱动类是 oracle.jdbc.driver.OracleDriver.class(此类可以在oracle提供的JDBC jar包中找到),此类实现了java.sql.Driver接口。
如mysql的驱动类是com.mysql.cj.jdbc.Driver.class(此类可以在mysql提供的JDBC jar包中找到),此类实现了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");
注意:Class.forName()将对应的驱动类加载到内存中,然后执行内存中的static静态代码段,代码段中,会创建一个驱动Driver的实例,放入DriverManager中,供DriverManager使用。
例如,在使用Class.forName() 加载oracle的驱动com.mysql.cj.jdbc.Driver时,会执行MySQLDriver中的静态代码段,创建一个MySQLDriver实例,然后调用DriverManager.registerDriver()注册:
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
// 注册一个自己的驱动管理器
// Register ourselves with the DriverManager
//
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
}
例如,在使用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) {
}
二、Driver的功能
java.sql.Driver接口规定了Driver应该具有以下功能:
acceptsURL方法
**acceptsURL(String url) **方法用来测试对指定的url,该驱动能否打开这个url连接。
driver对自己能够连接的url会制定自己的协议,只有符合自己的协议形式的url才认为自己能够打开这个url,如果能够打开,返回true,反之,返回false;
例如:oracle定义的自己的url协议如下:
jdbc:oracle:thin:@//
jdbc:oracle:thin:@
oracle中的acceptsURL方法
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中的acceptsURL方法
/**
* Typically, drivers will return true if they understand the subprotocol
* specified in the URL and false if they don't. This driver's protocols
* start with jdbc:mysql:
*
* @param url
* the URL of the driver
*
* @return true if this driver accepts the given URL
*
* @exception SQLException
* if a database access error occurs or the url is null
*/
@Override
public boolean acceptsURL(String url) throws SQLException {
return (ConnectionUrl.acceptsUrl(url));
}s
mysql中的注释说明了接收到的协议类型为:
jdbc:mysql:
但是我没有找到具体的判断代码在哪里,但是在下面MySQL使用到了。
connect方法
connect(String url,Properties info)方法,创建Connection对象,用来和数据库的数据操作和交互,而Connection则是真正数据库操作的开始(在此方法中,没有规定是否要进行acceptsURL()进行校验)。
看下mysql中的实现过程:
public java.sql.Connection connect(String url, Properties info) throws SQLException {
try {
if (!ConnectionUrl.acceptsUrl(url)) {
/*
* According to JDBC spec:
* The driver should return "null" if it realizes it is the wrong kind of driver to connect to the given URL. This will be common, * as when the
* JDBC driver manager is asked to connect to a given URL it passes the URL to each loaded driver in turn.
*/
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);
}
}
对应的方法注释描述:
尝试与给定 URL 建立数据库连接。 如果驱动程序意识到连接到给定 URL 的驱动程序类型错误,则应返回“null”。 这很常见,因为当要求 JDBC driverManager 连接到给定的 URL 时,它会依次将 URL 传递给每个加载的驱动程序。
如果 URL 为 null 或者它是连接到给定 URL 的正确驱动程序,但连接到数据库时遇到问题,驱动程序应该引发 SQLException。
java.util.Properties 参数可用于将任意字符串标记/值对作为连接参数传递。 这些属性优先于 URL 中发送的任何属性。
MySQL 协议采用以下形式:jdbc:mysql://host:port/database
参数:
url - 要连接的数据库的 URL
info – 作为连接参数的任意标签/值对列表
回报:
与 URL 的连接,如果不是我们,则为 null
抛出:
SQLException – 如果发生数据库访问错误或 url 为空
如果报错,可能的条件是访问数据库错误或者url为空;
返回为null,表示的是协议不符合条件;
对于这里的url中的Properties,也是我们常用的设置:
jdbc:mysql://localhost:3306/user?useUnicode=true&useSSL=false&characterEncoding=utf-8&serverTimezone=GMT%2B8
手动加载驱动 Driver 并实例化进行数据库操作的例子
使用MySQL
public class TransferClient {
public static void main(String[] args) throws Exception {
//1.加载mysql驱动类,并实例化
Driver driver = (Driver) Class.forName("com.mysql.cj.jdbc.Driver").newInstance();
//2.判定指定的URL oracle驱动能否接受(符合oracle协议规则)
boolean flag = driver.acceptsURL("jdbc:mysql://localhost:3306");
System.out.println("指定的URL是否满足了规则?"+flag); // true
//3.创建真实的数据库连接:
String url = "jdbc:mysql://localhost:3306/ie_draw";
Properties props = new Properties();
props.put("user", "root");
props.put("password", "root");
Connection connection = driver.connect(url, props);
PreparedStatement preparedStatement = connection.prepareStatement("select count(*) from template");
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next()){
String string = resultSet.getString("1");
System.out.println("对应的值是:"+string);
break;
}
resultSet.close();
preparedStatement.close();
connection.close();
}
}
使用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{
}
}
手动加载驱动存在的问题
上述的手动加载Driver并且获取连接的过程稍显笨拙:如果现在我们加载进来了多个驱动Driver(如同时存在Oracle驱动和Mysql驱动),那么手动创建Driver实例,并根据URL进行创建连接就会显得代码杂乱无章,并且还容易出错,并且不方便管理。
JDBC中提供了一个DriverManager角色,用来管理这些驱动Driver。
三、DriverManager
事实上,一般我们操作Driver,获取Connection对象都是交给DriverManager统一管理的。DriverManger可以注册和删除加载的驱动程序,可以根据给定的url获取符合url协议的驱动Driver或者是建立Conenction连接,进行数据库交互。
以下是DriverManager的关键方法摘要:
DriverManager 内部持有这些注册进来的驱动 Driver,由于这些驱动都是 java.sql.Driver 类型,那么怎样才能获得指定厂商的驱动Driver呢?答案就在于: java.sql.Driver接口规定了厂商实现该接口,并且定义自己的URL协议。厂商们实现的Driver接口通过acceptsURL(String url)来判断此url是否符合自己的协议,如果符合自己的协议,则可以使用本驱动进行数据库连接操作,查询驱动程序是否认为它可以打开到给定 URL 的连接。
在新版本中利用的是java中的SPI技术,来加载MySQL指定的java.sql.Driver接口类型
1、DriverManager利用SPI技术来加载厂商驱动
DriverManager的实现在于加载DriverManager类的时候,利用static模块来进行初始化操作:
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
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;
}
// If the driver is packaged as a Service Provider, load it.
// Get all the drivers through the classloader
// exposed as a java.sql.Driver.class service.
// ServiceLoader.load() replaces the sun.misc.Providers()
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 利用SPI技术,加载类路径下的META-INFO/services路径下,文件名是java.sql.Driver
// 里面放置的是每个厂商具体放置的驱动类名
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
/* Load these drivers, so that they can be instantiated.
* It may be the case that the driver class may not be there
* i.e. there may be a packaged driver with the service class
* as implementation of java.sql.Driver but the actual class
* may be missing. In that case a java.util.ServiceConfigurationError
* will be thrown at runtime by the VM trying to locate
* and load the service.
*
* Adding a try catch block to catch those runtime errors
* if driver not available in classpath but it's
* packaged as service and that service is there in classpath.
*/
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
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);
}
}
}
根据其中的一行代码表示:
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
如果当前的环境变量中jdbc.drivers对应的值,那么就不会加载到内存中来。所以环境变量中一定要有这个值。
但是我们在使用的时候,没有看见在哪里来设置过这个值。
for循环里面的意思是,如果在META-INF/services路径下没有找到对应的Driver接口的实现类,那么接下来将会判断用户自己来进行设置的。
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);
}
}
加载驱动类到内存中来,同时将每个Driver中的静态模块,来使用驱动管理器来注册驱动。
这里是可以来自己手动设置的。
2、使用DriverManager注册和取消注册驱动Driver
在本博文开始的 驱动加载的过程一节中,讨论了当使用Class.forName("driverName")加载驱动的时候,会向DriverManager中注册一个Driver实例。以下代码将验证此说法:
利用Oracle来进行验证
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{
}
}
利用MySQL来进行验证
public class TransferClien2 {
public static void main(String[] args) throws Exception {
defaultDriver();
}
public static void defaultDriver(){
try {
String url = "jdbc:mysql://localhost:3306";
//1.将Driver加载到内存中,然后执行其static静态代码,创建一个MysqlDriver实例注册到DriverManager中
Class.forName("com.mysql.cj.jdbc.Driver");
//取出对应的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("加载mysql类失败!");
e.printStackTrace();
} finally{
}
}
}
控制台输出:
加载类后,获取Driver对象:com.mysql.cj.jdbc.Driver@34a245ab
加载mysql类失败!
java.sql.SQLException: No suitable driver
at java.sql.DriverManager.getDriver(DriverManager.java:315)
at com.guang.test.TransferClien2.defaultDriver(TransferClien2.java:31)
at com.guang.test.TransferClien2.main(TransferClien2.java:14)
以上代码主要分以下几步:
-
首先是将 oracle.jdbc.driver.OracleDriver加载到内存中;
-
然后便调用DriverManager.getDriver()去取Driver实例;
-
将driver实例从DriverManager中注销掉;
4.尝试再取 对应url的Driver实例;
从执行结果看,正好能够验证以上论述:当第四步再次获取对应url的 Driver 实例时,由于已经被注销掉了,找不到适当的驱动Driver,抛出了 "Not suitable driver" 的异常。
将上述的例子稍作变化,在注销掉了静态块创建的driver后,往DriverManager注册一个自己创建的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{
}
}
利用MySQL操作:
public static void defaultDriver2(){
try {
String url = "jdbc:mysql://localhost:3306";
//1.将Driver加载到内存中,然后执行其static静态代码,创建一个MysqlDriver实例注册到DriverManager中
Driver driver1 = (Driver) Class.forName("com.mysql.cj.jdbc.Driver").newInstance();
//取出对应的oracle 驱动Driver
Driver driver = DriverManager.getDriver(url);
System.out.println("加载类后,获取Driver对象:"+driver);
//将driver从DriverManager中注销掉
DriverManager.deregisterDriver(driver);
DriverManager.registerDriver(driver1);
//重新通过url从DriverManager中取Driver
driver = DriverManager.getDriver(url);
System.out.println(driver);
} catch (Exception e) {
System.out.println("加载mysql类失败!");
e.printStackTrace();
} finally{
}
}
以上代码先创建了一个Driver对象,在注销了DriverManager中由加载驱动过程中静态创建驱动之后,注册到系统中,现在DriverManager中对应url返回的Driver 即是在代码中创建的Driver对象。
3、使用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);
四、总结
1、实现了java对数据库定义的规范之后,各个数据库厂商需要实现对应的接口,java利用SPI技术,来加载各自的驱动到内存中。
并在DriverManager中来注册了驱动。
2、驱动类的作用:①对连接串做一个校验;②数据库连接串的定义,能够使用连接串去连接数据库。
3、DriverManager底层还是使用Driver来做事情的。
比如说DriverManager.getConnection----->Connection con = aDriver.driver.connect(url, info);------->ConnectionUrl.acceptsUrl(url)--校验完成之后--->数据库连接