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:@//:/ServiceName

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)

以上代码主要分以下几步:

  1. 首先是将 oracle.jdbc.driver.OracleDriver加载到内存中;

  2. 然后便调用DriverManager.getDriver()去取Driver实例;

  3. 将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)--校验完成之后--->数据库连接

转载于:https://louluan.blog.csdn.net/article/details/29850811

posted @ 2022-09-01 20:58  雩娄的木子  阅读(340)  评论(0编辑  收藏  举报