江苏 无锡 缪小东java
本篇从java.sql.Driver接口、java.sql.DriveManager类以及其它开源数据库的驱动类讨论JDBC中驱动加载的全过程以及JDBC的Framework如何作到“可插拔”的细节。程序员
本篇包含了不少部分的内容。如类加载器、本地方法、对象锁、类锁、按功能或者状态分离锁、安全机制,对这些内容没有深刻讨论!详情能够继续关注本博客!我在上篇主要关注驱动管理器的初始化、链接的创建、驱动的注册、驱动的遍列、驱动的取消注册以及DriverManager中的日志操做。sql
//Driver.java数据库
package java.sql;编程
public interface Driver {设计模式
Connection connect(String url, java.util.Properties info) throws SQLException;安全
boolean acceptsURL(String url) throws SQLException;架构
DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info) throws SQLException;ide
int getMajorVersion(); //返回驱动的主版本号工具
int getMinorVersion(); //返回驱动的次版本号
boolean jdbcCompliant(); //是否兼容于JDBC标准
}
以上就是JDBC中的Driver接口,它是任何数据库提供商的驱动类必须实现的接口,驱动类必须实现该接口中的全部方法!简单吧!
它之因此是一个接口,就是OO中常常谈到的“依赖倒转原则(DIP-Dependence Inverse Principle)”的具体应用了!在DriverManager类中能够看到:它使用的驱动都是Driver接口,从而依赖与高层,不依赖于实现。这样可使用JDBC Framework管理和维护不一样JDBC提供商的数据库驱动。
JDBC Framework中容许加载多个数据库的驱动!相应地,通常建议数据库提供商的驱动必须小一点,从而保证在加载多个驱动后不会占用太多的内存。
在Driver接口中共有以上六个方法。其中红色的两个相对很重要,它是供DriverManager调用的,其它四个很简单的方法!下面简单讲述前两个方法的意义!
Connection connect(String url, java.util.Properties info) throws SQLException方法是数据库提供商的驱动必须实现的方法,它主要是使用指定的URL和与具体提供商相关的信息创建一个链接。
boolean acceptsURL(String url) throws SQLException方法也是数据库提供商的驱动必须实现的方法,主要断定某个该驱动是否介绍该URL。(通常在创建链接以前调用。详情将DriverManager类)
DriverManager类是整个JDBC的起点!利用它能够建立链接,从而完成后续的操做。在JDBC 中DriverManager是一个相对比较复杂的类,所以咱们按其只能分为几类介绍。本篇将DriverManager中的方法分为3类:1.初始化;2.驱动的注册、查询、取消注册;3.创建链接;4.日志相关。
下面就看看它的源代码吧!
//DriverManager.java 1.45 05/11/17
package java.sql;
import sun.misc.Service;
import java.util.Iterator;
class DriverInfo {
Driver driver;
Class driverClass;
String driverClassName;
public String toString() {
return ("driver[className=" + driverClassName + "," + driver + "]");
}
}
public class DriverManager {
// Prevent the DriverManager class from being instantiated.
private DriverManager(){}
以上是其代码的前面部分。主要是包的定义、相关文件的导入、类的定义以及一个私有化的构造器――即该类不能够实例化,只能够调用其静态方法,至关于一个工具类――一个管理驱动的工具类!还有一个就是一个辅助类DriverInfo,它包装了驱动类Driver,包含驱动类的类和驱动类的名称。
下面就开始DriverManager类重要方法的介绍吧!
private static java.util.Vector drivers = new java.util.Vector(); //保存多个驱动的汇集
private static boolean initialized = false; //是否初始化的标记,默认固然是否了
// 真正的初始化方法
static void initialize() {
if (initialized) { return; } //已经初始化就返回!(初始化了就算了)
initialized = true; //设置此标识符,表示已经完成初始化工做
loadInitialDrivers(); //初始化工做主要是完成全部驱动的加载
println("JDBC DriverManager initialized");
}
//初始化方法中完成加载全部系统提供的驱动的方法
private static void loadInitialDrivers() {
String drivers;
try {
drivers = (String) java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction("jdbc.drivers"));
//获得系统属性"jdbc.drivers"对应的驱动的驱动名(这但是须要许可的哦!)。
} catch (Exception ex) {
drivers = null;
}
Iterator ps = Service.providers(java.sql.Driver.class); //从系统服务中加载驱动
while (ps.hasNext()) { //加载这些驱动,从而实例化它们
ps.next();
}
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null) { return; } //系统属性未指定驱动则返回
while (drivers.length() != 0) { //循环过程,讲解见下面
int x = drivers.indexOf(':');
String driver;
if (x < 0) {
driver = drivers;
drivers = "";
} else {
driver = drivers.substring(0, x);
drivers = drivers.substring(x+1);
}
if (driver.length() == 0) { continue; }
try {
println("DriverManager.Initialize: loading " + driver);
Class.forName(driver, true, ClassLoader.getSystemClassLoader());
//加载这些驱动,下篇会讲解其细节
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}//end of while
//系统属性"jdbc.drivers"可能有多个数据库驱动,这些驱动的名字是以“:”分隔开的,
//上面的过程就是将此以“:”分隔的驱动,依次遍列,而后调用Class.forName依次加载
}
private static Object logSync = new Object(); //对象锁
//下面是一个辅助方法,用于向日志中写入信息!
public static void println(String message) {
synchronized (logSync) { //很重要的一致性编程的方法,见下面
if (logWriter != null) { //设置日志才能够进行下面的写入信息
logWriter.println(message); //向logger中写入信息
logWriter.flush();
}
}
}
//以上蓝色的属性和方法,是一致性编程(Concurent Programming)中的重要方法。
//首先明确咱们在向日志写入信息的时候,是能够调用其它非写入日志的方法的,
//只是不一样的客户不能同时调用该写入方法――一个客户正在写入,其它必须等待写完
//假如咱们机械地使用synchronized(this)或synchronized该写入方法时,必然会致使效率低
//通常地,当类的中多个方法能够分为多个不一样组,这些组的方法互相之间不干扰时,
//能够为每一个组指定一个本身的锁,限制同一个方法被多个客户使用,从而保证该方法的
//一致性,保证无必要的synchronized方法!
//关于一致性编程,请多关注博客中的文章
//向DriverManager 注册指定的驱动。驱动这么注册请阅读下篇!
public static synchronized void registerDriver(java.sql.Driver driver) throws SQLException {
if (!initialized) { initialize(); } //注册前必须初始化了
DriverInfo di = new DriverInfo(); //建立一个新的驱动信息类
di.driver = driver;
di.driverClass = driver.getClass();
di.driverClassName = di.driverClass.getName(); //以上填入注册驱动的信息
drivers.addElement(di); //将此驱动信息假如驱动汇集中
println("registerDriver: " + di);
}
//
public static synchronized Driver getDriver(String url) throws SQLException {
println("DriverManager.getDriver( "" + url + " ")");
if (!initialized) { initialize(); } //一样必须先初始化
//本地方法,获得调用此方法的类加载器
ClassLoader callerCL = DriverManager.getCallerClassLoader();
// 遍列全部的驱动信息,返回能理解此URL的驱动
for (int i = 0; i < drivers.size(); i++) { //遍列驱动信息的汇集
DriverInfo di = (DriverInfo)drivers.elementAt(i);
// 调用者在没有权限加载此驱动时会忽略此驱动
if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) {
println(" skipping: " + di);
continue;
}
try {
println(" trying " + di);
if (di.driver.acceptsURL(url)) { //驱动能理解此URL时,返回此驱动
println("getDriver returning " + di);
return (di.driver);
}
} catch (SQLException ex) {
// Drop through and try the next driver.
}
}
println("getDriver: no suitable driver");
throw new SQLException("No suitable driver", "08001");
}
//从DriverManager 中取消注册某个驱动。Applet仅仅可以取消注册从它的类加载器加载的驱动
public static synchronized void deregisterDriver(Driver driver) throws SQLException {
ClassLoader callerCL = DriverManager.getCallerClassLoader();
println("DriverManager.deregisterDriver: " + driver);
int i;
DriverInfo di = null;
for (i = 0; i < drivers.size(); i++) {
di = (DriverInfo)drivers.elementAt(i);
if (di.driver == driver) { break; } //找到了某个驱动则返回,同时返回i值
}
if (i >= drivers.size()) { //所有遍列完,度没有找到驱动则返回
println(" couldn't find driver to unload");
return;
}
//找到此驱动,但调用者不能加载此驱动,则抛出异常
if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) {
throw new SecurityException();
}
// 在以上全部操做后,能够删除此驱动了
drivers.removeElementAt(i);
}
//获得当前全部加载的JDBC驱动的枚举**
public static synchronized java.util.Enumeration getDrivers() {
java.util.Vector result = new java.util.Vector();
if (!initialized) { initialize(); } //该类没有初始化时,必须完成初始化工做
//详情请阅读初始化部分
ClassLoader callerCL = DriverManager.getCallerClassLoader(); //获得当前类的类加载器
for (int i = 0; i < drivers.size(); i++) { //遍列全部的驱动
DriverInfo di = (DriverInfo)drivers.elementAt(i); //获得某个具体的驱动
// 假如调用者没有许可加载此驱动时,忽略该驱动
if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) {
println(" skipping: " + di);
continue;
}
result.addElement(di.driver); //将能够加载的驱动加入要返回的结果集
}
return (result.elements()); //返回结果集
}
private static native ClassLoader getCallerClassLoader();
//得到当前调用者的类装载器的本地方法(关于本地方法JNI请关注本博客后续文章)
// 返回类对象。咱们使用DriverManager的本地方法getCallerClassLoader()获得调用者的类加载器
private static Class getCallerClass(ClassLoader callerClassLoader, String driverClassName) {
// callerClassLoader为类加载器,driverClassName为驱动的名称
Class callerC = null;
try {
callerC = Class.forName(driverClassName, true, callerClassLoader);
//使用指定的类装载器定位、加载指定的驱动类,
//true表明该驱动类在没有被初始化时会被初始化,返回此类
}catch (Exception ex) {
callerC = null; // being very careful
}
return callerC;
}
在JDBC程序中通常使用DriverManager.getConnection方法返回一个链接。该方法有多个变体,它们都使用了具体驱动类的connect方法实现链接。下面是链接的核心方法。
private static Connection getConnection(String url, java.util.Properties info, ClassLoader callerCL)
throws SQLException {
//当类加载器为null时,必须检查应用程序的类加载器
//其它在rt.jar以外的JDBC驱动类能够今后加载驱动 /*
synchronized(DriverManager.class) { //同步当前DriverManger的类
if(callerCL == null) { callerCL = Thread.currentThread().getContextClassLoader(); }
//获得当前线程的类加载器(此句的真正含义请关注后续线程相关的文章)
}
if(url == null) { throw new SQLException("The url cannot be null", "08001"); }
println("DriverManager.getConnection( "" + url + " ")");
if (!initialized) { initialize(); } //必须初始化,将默认的驱动加入
// 遍列当前的全部驱动,并试图创建链接
SQLException reason = null;
for (int i = 0; i < drivers.size(); i++) {
DriverInfo di = (DriverInfo)drivers.elementAt(i);
// 假如调用者没有许可加载该类,忽略它
if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) {
//当驱动不是被当前调用者的类加载器加载时忽略此驱动
println(" skipping: " + di);
continue;
}
try {
println(" trying " + di);
Connection result = di.driver.connect(url, info); //调用某个驱动的链接方法创建链接
if (result != null) { //在创建链接后打印链接信息且返回链接
println("getConnection returning " + di);
return (result);
}
} catch (SQLException ex) {
if (reason == null) { reason = ex; } //第一个错误哦
}
}
//以上过程要么返回链接,要么抛出异常,当抛出异常会给出异常缘由,即给reason赋值
//在全部驱动都不能创建链接后,如有错误则打印错误且抛出该异常
if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}
//若根本没有返回链接也没有异常,不然打印没有适当链接,且抛出异常
println("getConnection: no suitable driver");
throw new SQLException("No suitable driver", "08001");
}
//如下三个方法是上面的链接方法的变体,都调用了上面的链接方法
public static Connection getConnection(String url, java.util.Properties info) throws SQLException {
ClassLoader callerCL = DriverManager.getCallerClassLoader();
//没有类加载器时就是该调用者的类加载器
return (getConnection(url, info, callerCL));
}
public static Connection getConnection(String url, String user, String password) throws SQLException {
java.util.Properties info = new java.util.Properties();
ClassLoader callerCL = DriverManager.getCallerClassLoader();
if (user != null) { info.put("user", user); }
if (password != null) { info.put("password", password); }
return (getConnection(url, info, callerCL));
}
public static Connection getConnection(String url) throws SQLException {
java.util.Properties info = new java.util.Properties();
ClassLoader callerCL = DriverManager.getCallerClassLoader();
return (getConnection(url, info, callerCL));
}
DriverManager中与日志相关的方法有好几个,主要分为已被deprecated的Stream相关的方法,和建议使用的Reader、Writer相关的方法。(对应于java IO的字符流和字节流哦!由于写入日志的信息通常为字符流,因此废弃了与字节流相关的方法)
//常数。容许设置logging stream
final static SQLPermission SET_LOG_PERMISSION = new SQLPermission("setLog");
private static int loginTimeout = 0;
private static java.io.PrintWriter logWriter = null; //写入的字符流
private static java.io.PrintStream logStream = null; //写入的字节流
//设置驱动在试图链接(log)时等待的最大时间
public static void setLoginTimeout(int seconds) { loginTimeout = seconds; }
public static int getLoginTimeout() { return (loginTimeout); }
public static java.io.PrintWriter getLogWriter() { //获得LogWriter
return logWriter;
}
public static void setLogWriter(java.io.PrintWriter out) { //设置字符流
SecurityManager sec = System.getSecurityManager(); //取得安全管理器
if (sec != null) { sec.checkPermission(SET_LOG_PERMISSION); }
//检查是否具备日志写入的权限,有权限则继续,不然抛出异常!
logStream = null;
logWriter = out;
}
public static void setLogStream(java.io.PrintStream out) { //设置字节流
SecurityManager sec = System.getSecurityManager();
if (sec != null) { sec.checkPermission(SET_LOG_PERMISSION); }
logStream = out;
if ( out != null )
logWriter = new java.io.PrintWriter(out); //将字节流包装为字符流
else
logWriter = null;
}
public static java.io.PrintStream getLogStream() { //获得字节流
return logStream;
}
}
以上对应于《教你创建简单JDBC程序的》DriverManager.getConnection()方法。
下篇咱们将关注:数据库提供商如何注册本身的驱动,即关注Class.forName()方法。以及“可插拔”等概念!
本篇主要用几个开源数据库的驱动讲述驱动是如何加载的,以及“可插拔”机制等。
因为文章篇幅有限,本文章并非专门研究开源源代码!所以咱们仅研究那些与数据库驱动加载直接相关的方法。在下面的几个开源软件的驱动中,咱们主要关注驱动类的getConnection()方法。
如下第一个是smallsql中驱动类SSDriver的源代码:
package smallsql.database;
import java.sql.*;
import java.util.Properties;
public class SSDriver implements Driver {
static SSDriver drv;
static {
try{
drv = new SSDriver();
java.sql.DriverManager.registerDriver(drv);
}catch(Throwable e){}
}
public Connection connect(String url, Properties info) throws SQLException {
if(!acceptsURL(url)) return null;
……
return new SSConnection( (idx > 0) ? url.substring(idx+1) : null);
}
……
}
从上面红色的部分能够看到:这是一个静态语句块(static block),这意味着该语句是在类构造完成前完成的(关于语句块的加载请阅读《Think in java》)。即调用class.forName(“smallsql.database.SSDriver”)语句时,会首先建立一个SSDriver的实例,而且将其向驱动管理器(DriverManager)注册。这样就完成驱动的注册了。
从上面的蓝色的代码能够看出:驱动的链接方法返回的是一个具体的SSConnection对象。而在前面研究的Driver接口中返回的是Connection接口,这是不茅盾的,SSConnection对象实现了Connection接口。
再下面一个是HSqlD中的驱动类的源代码:
package org.hsqldb;
import java.sql.*;
import java.util.Properties;
import org.hsqldb.jdbc.jdbcConnection;
import org.hsqldb.persist.HsqlDatabaseProperties;
import org.hsqldb.persist.HsqlProperties;
public class jdbcDriver implements Driver {
public Connection connect(String url, Properties info) throws SQLException {
return getConnection(url, info);
}
public static Connection getConnection(String url, Properties info) throws SQLException {
HsqlProperties props = DatabaseURL.parseURL(url, true);
if (props == null) {
throw new SQLException(Trace.getMessage(Trace.INVALID_JDBC_ARGUMENT));
} else if (props.isEmpty()) {
return null;
}
props.addProperties(info);
return new jdbcConnection(props);
}
static {
try {
DriverManager.registerDriver(new jdbcDriver());
} catch (Exception e) {}
}
}
蓝色的依然是创建链接的部分,依然返回某个具体的实现java.sql.Connection接口的对象。是设计模式中的哪一个工厂啊(通常工厂、抽象工厂、工厂方法仍是什么都不是啊)!
红色的一样是一个静态语句块,一样完成该驱动的注册。
两个开源数据库的驱动居然如此类似,是否是其它的就不同呢!看下面是mckoi中的驱动类:
package com.mckoi;
public class JDBCDriver extends com.mckoi.database.jdbc.MDriver {
static {
com.mckoi.database.jdbc.MDriver.register();
}
public JDBCDriver() {
super();
// Or we could move driver registering here...
}
}
红色的部分又是完成相同的工做――在类装载完成前向驱动管理器注册驱动,只不过此次是调用MDriver的register方法罢了。那么该驱动创建链接是否也同样呢,固然同样啦!不信你看下面的代码:
package com.mckoi.database.jdbc;
……
public class MDriver implements Driver {
……
private static boolean registered = false;
public synchronized static void register() {
if (registered == false) {
try {
java.sql.DriverManager.registerDriver(new MDriver());
registered = true;
}catch (SQLException e) {
e.printStackTrace();
}
}
}
public Connection connect(String url, Properties info) throws SQLException {
if (!acceptsURL(url)) { return null; }
DatabaseInterface db_interface;
int row_cache_size;
int max_row_cache_size;
……
MConnection connection = new MConnection(url, db_interface, row_cache_size, max_row_cache_size);
……
return connection;
}
}
从以上三个开源数据库驱动的源代码能够看出:在调用Class.forName(“XXXDriver”)时,完成了将具体的驱动程序向JDBC API中驱动管理器的注册,该注册方法在类构造完成前完成,通常使用静态语句块。在调用DriverManager的getConnection方法时,通常先在已注册的驱动中查找能够了解此URL的驱动,而后调用该驱动的connect方法,从而创建链接,返回的链接都是一个实现java.sql.Connection接口的具体类。下面是该过程的时序图。
以上是JDBC中驱动加载的时序图。时序图主要有如下7个动做:
1. 客户调用Class.forName(“XXXDriver”)加载驱动。
2. 此时此驱动类首先在其静态语句块中初始化此驱动的实例,
3. 再向驱动管理器注册此驱动。
4. 客户向驱动管理器DriverManager调用getConnection方法,
5. DriverManager调用注册到它上面的可以理解此URL的驱动创建一个链接,
6. 在该驱动中创建一个链接,通常会建立一个对应于数据库提供商的XXXConnection链接对象,
7. 驱动向客户返回此链接对象,不过在客户调用的getConnection方法中返回的为一个java.sql.Connection接口,而具体的驱动返回一个实现java.sql.Connection接口的具体类。
以上就是驱动加载的全过程。由此过程咱们能够看出JDBC的其它一些特色。
在《教你创建简单JDBC程序》一篇中,讲述了通常JDBC的几个步骤。经过本篇的介绍,我将此程序分为如下几部分:
上图中,蓝色的即为本章前面介绍的JDBC驱动加载的细节部分。看看下面的部分:左面的很明显吧!是java.sql包中的接口吧!它是抽象的!右边呢?经过驱动管理器DriverManager获得的是一个实现java.sql.Connection接口的具体类吧!(不知道啊!前面不是讲过了吗!)所以咱们能够能够注意到左右分别是抽象的和具体的。(这种抽象和具体的链接是由java的RTTI支持的,不懂能够阅读《Think in java》)。在接下来的结果集的处理rs也是抽象的吧!
所以,在写JDBC程序时,即便咱们使用不一样数据库提供商的数据库咱们只要改变驱动类的地址,和具体链接的URL及其用户名和密码,其它几乎不用任何修改,就能够完成一样的工做!方便吧!
这意味着什么呢?咱们实际上是在针对抽象接口编程,只要知道接口的调用顺序,以及其中的主要方法,咱们就能够迅速学会JDBC编程了!
同时,咱们只要对不一样数据库提供商的驱动类使用Class.forName(“XXXDriver”)就能够加载驱动,其细节你根本不用关注――JDBC Framework已经全为你搞定了!应用程序对于不一样提供商的数据库是无需太多改动的,于是其是“可插拔”的!整个J2EE就是一个高层的API――抽象接口,用户可使用不一样的产品提供商提供的产品,使用统一的API,从而便于程序员学习!
谢谢你们!到此结束!