欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

你真的的懂JDBC?

程序员文章站 2022-06-17 18:53:24
...

一、前言

Java中操作数据库元老是使用JDBC,而JDBC内部是如何实现的,为何每次使用时候都是写那些不理解的几行固定代码?这些看似不相关的代码内部是否有瓜葛那,下面进来探讨一二。

二、一个例子

public class TestJdbc {

    public static final String url = "jdbc:mysql://127.0.0.1/users";
    public static final String name = "com.mysql.jdbc.Driver";
    public static final String user = "root";
    public static final String password = "123456";
    public static final String sql = "select * from user";

    public static void main(String[] args) throws UnexpectedInputException, ParseException, Exception {
        Connection conn = null;
        PreparedStatement pst = null;
        ResultSet rs = null;        try {            // (1)注册驱动
            Class.forName(name);            //(2) 获取链接
            conn = DriverManager.getConnection(url, user, password);            // (3)准备语句
            pst = conn.prepareStatement(sql);            // (4)执行查询
            rs = pst.executeQuery();            // (5)迭代结果
            while (rs.next()) {
                System.out.println(rs.getString("username"));
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {            try {                if (rs != null) {
                    rs.close();
                }                if (null == pst) {
                    pst.close();

                }                if (null != conn) {
                    conn.close();

                }

            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

}

使用JDBC我们都知道按部就班的写(1)(2)的代码获取数据库链接,但是很少去研究这两个语句是干啥用的,特别是第一句,直接使用类加载器加载了驱动类到内存,这是何意?

三、原理

3.1 注册驱动

其实Class.forName(name);的作用是注册mysql驱动到驱动管理器,只有注册后,在(2)获取链接时候才能获取到mysql的数据库链接。Class.forName作用是加载类的字节码到内存生成Class对象,那么这里就是把类com.mysql.jdbc.Driver字节码加载到内存生成com.mysql.jdbc.Driver的Class对象,有了Calss对象就可以使用new了,或者Class.newInstance()了创建对象实例了。下面看看com.mysql.jdbc.Driver代码:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {    //
    // 注册mysql驱动本身到驱动管理器
    //
    static {        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {            throw new RuntimeException("Can't register driver!");
        }
    }    public Driver() throws SQLException {        // Required for Class.forName().newInstance()
    }
}

在创建Driver的Class对象是会调用static块,注册当前驱动到驱动管理器(也就是说在调用(1)的时候实际上是触发了static块代码执行)。下面看看驱动管理器registerDriver方法:

public static synchronized void registerDriver(java.sql.Driver driver)
        throws SQLException {

        registerDriver(driver, null);
} public static synchronized void registerDriver(java.sql.Driver driver,
            DriverAction da)
        throws SQLException {        //注册驱动到并发安全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);

  }

其中registeredDrivers是个CopyOnWriteArrayList,是线程安全的list.

    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();

一个问题:这里我们的main函数是使用AppClassLoader加载的,而DriverManager类则属于rt.jar(使用bootStarp加载),而调用类与被调用类使用的应该是同一个类加载器,那这里main函数为啥能调用DriverManager那,其实是因为Java的类加载委托机制。

3.2 获取连接

DriverManager类是驱动管理器类,里面也有个static块:

public class DriverManager {
    ...    /**
     * 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() {        //获取jdbc.drivers属性里面的驱动名称
        String drivers;        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {                public String run() {                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        ...        //使用SPI技术加载所有实现了java.sql.Driver的驱动类
        AccessController.doPrivileged(new PrivilegedAction<Void>() {            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                ...                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;
        }        //加载所有jdbc.drivers属性里面的驱动名称
        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);
            }
        }
    }

关于SPI 参考:http://www.jianshu.com/p/a4dc755652ff 第三节

其实由于static块已经加载了所有驱动,(1)看似是可有可无的

private static Connection getConnection(
    String url, java.util.Properties info, Class<?> caller) throws SQLException {    //(1)选择类加载器
    ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;    synchronized(DriverManager.class) {        //如果类加载器为null,则使用线程上下文加载器
        if (callerCL == null) {
            callerCL = Thread.currentThread().getContextClassLoader();
        }
    }    if(url == null) {        throw new SQLException("The url cannot be null", "08001");
    }
    ...
    SQLException reason = null;    //(2)遍历所有注册的驱动,找打一个可用的发起连接并返回
    for(DriverInfo aDriver : registeredDrivers) {        //如果有权限则获取驱动并获取连接
        if(isDriverAllowed(aDriver.driver, callerCL)) {            try {
                println("    trying " + aDriver.driver.getClass().getName());
                Connection con = aDriver.driver.connect(url, info);                if (con != null) {                    // Success!
                    println("getConnection returning " + aDriver.driver.getClass().getName());                    return (con);
                }
            } catch (SQLException ex) {                if (reason == null) {
                    reason = ex;
                }
            }

        } else {
            println("    skipping: " + aDriver.getClass().getName());
        }

    }    // (3)到这里如果还没有获取连接,则返回失败原因
    if (reason != null)    {
        println("getConnection failed: " + reason);        throw reason;
    }

    println("getConnection: no suitable driver found for "+ url);    throw new SQLException("No suitable driver found for "+ url, "08001");
}
  • (1)设置类加载器,这里因为caller为TestJdbc.class,所以callerCL为AppClassLoader,

  • (2)遍历注册的所有驱动,找到一个使用callerCL加载器能加载成功的驱动,获取连接。

其中鉴权代码为:

private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {    boolean result = false;    if(driver != null) {
        Class<?> aClass = null;        try {
            aClass =  Class.forName(driver.getClass().getName(), true, classLoader);
        } catch (Exception ex) {
            result = false;
        }

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

使用callerCL类加载器去加载driver,然后判断当前加载的Class和driver的Class是否是同一个对象,是则成功,否者失败。这里注册Driver时候类加载器为BootStrap,而callerCL是AppClassloader,由于委托机制,所以这里返回true.也就是注册驱动时候类加载器和使用时候必须是同一个或者具有委托关系时候才会鉴权成功,也就是说才有权限去调用驱动的connect方法。

另外比如我们调用:

  • (3)如果没有可用的驱动则返回错误信息。