天天看點

JDBC源碼分析&橋接模式

JDBC對于java的開發人員來說并不陌生,它封裝了ODBC,簡化了java連接配接資料庫的開發,本文我從部分JDBC的源碼入手來闡述一下JDBC。

橋接模式是由Gang of four整理的23種設計模式中的一種,JDBC是橋接模式一個典型的例子。了解JDBC源碼,能讓我們更好地了解橋接模式的意圖和實作;了解橋接模式,能讓我們更清楚JDBC設計的優越之處。

首先我們先來看下橋接模式的意圖,它旨在将抽象部分與實作部分分離,使其可以獨立地變化,廢話不多說,我們直接看一張UML圖:

JDBC源碼分析&橋接模式

其實最初我在看這句意圖的時候非常的郁悶,作為一名開發人員,對抽象與實作的第一反應就是接口實作或者類繼承,接口和實作類獨立地變化本身就是一件讓我覺得很郁悶的事情,但是看了這張UML圖之後便恍然大悟了,這裡的抽象與實作是聚合關系,也就是我們口頭常說的調用者和被調用者,這樣一來橋接模式的意圖就很清楚了,其實某種意義上就是解耦了某個功能的抽象定義和具體實作,讓其變成了兩個互相獨立的子產品。

讓我們回到JDBC的源碼,首先請大家打開JDK API,翻到java.sql包:

JDBC源碼分析&橋接模式

可以發現,java.sql包不同于其他幾乎所有的包,它基本可以說沒有定義多少類,但是定義了大量的接口,而由各個資料庫公司去寫這些接口的實作類:

JDBC源碼分析&橋接模式

可以看到,sum公司僅僅是提出了一系列接口規範,資料庫公司做出實作,然後當你真的需要連接配接某個資料庫時,你需要先添加資料庫公司釋出的jar包,如jdbc-mysql.jar,然後在使用時先加載Driver:

try {
			Class.forName("com.mysql.jdbc.Driver");
		} catch (ClassNotFoundException e) {
			System.out.println("找不到驅動程式類 ,加載驅動失敗!");
			e.printStackTrace();
		}
           

然後就可以通過DriverManager去擷取連接配接:

try {
			conn = (Connection) DriverManager.getConnection(url , username , password );
		} catch (SQLException e) {
			System.out.println("資料庫連接配接失敗!");
			e.printStackTrace();
		}                 try {
			conn = (Connection) DriverManager.getConnection(url , username , password );
		} catch (SQLException e) {
			System.out.println("資料庫連接配接失敗!");
			e.printStackTrace();
		}       

這裡的conn的定義依舊是以java.sql中定義的接口來聲明的,也就是說,我們在代碼中依舊使用的是接口,隻不過通過了最初的加載Driver讓添加的jar包中的類成為了真正的實作,但是這個加載的過程究竟是怎麼實作的呢???

這裡需要牽扯到一個别的知識,Class.forName和ClassLoader類都能用于加載java類獲得Class對象,那這二者究竟有什麼差別呢?

答案是,ClassLoader隻能将類填放進入JVM方法區,在Class.newInstance的時候才會加載類中的代碼塊,但是Class.forName可以直接加載類中的靜态代碼塊,然後我們來看下com.mysql.jdbc.Driver類:

static 
    {
        try
        {
            DriverManager.registerDriver(new Driver());
        }
        catch(SQLException E)
        {
            throw new RuntimeException("Can't register driver!");
        }
    }
           

類在被Class.forName的時候就加載了這一塊代碼塊,将Driver類本身的對象注冊進入DriverManager

/**
     * Registers the given driver with the {@code DriverManager}.
     * A newly-loaded driver class should call
     * the method {@code registerDriver} to make itself
     * known to the {@code DriverManager}. If the driver is currently
     * registered, no action is taken.
     *
     * @param driver the new JDBC Driver that is to be registered with the
     *               {@code DriverManager}
     * @param da     the {@code DriverAction} implementation to be used when
     *               {@code DriverManager#deregisterDriver} is called
     * @exception SQLException if a database access error occurs
     * @exception NullPointerException if {@code driver} is null
     * @since 1.8
     */
    public static synchronized void registerDriver(java.sql.Driver driver,
            DriverAction da)
        throws SQLException {

        /* Register the driver if it has not already been added to our 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);

    }
           

可以看到注冊的過程無非是把Driver對象存放進入了變量registeredDrivers中,這個變量原本是個List,具體不說這個變量了。

而當我們getConnection的時候:

//  Worker method called by the public getConnection() methods.
    private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
        /*
         * When callerCl is null, we should check the application's
         * (which is invoking this class indirectly)
         * classloader, so that the JDBC driver class outside rt.jar
         * can be loaded from here.
         */
        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        synchronized(DriverManager.class) {
            // synchronize loading of the correct classloader.
            if (callerCL == null) {
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }

        if(url == null) {
            throw new SQLException("The url cannot be null", "08001");
        }

        println("DriverManager.getConnection(\"" + url + "\")");

        // Walk through the loaded registeredDrivers attempting to make a connection.
        // Remember the first exception that gets raised so we can reraise it.
        SQLException reason = null;

        for(DriverInfo aDriver : registeredDrivers) {
            // If the caller does not have permission to load the driver then
            // skip it.
            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());
            }

        }

        // if we got here nobody could connect.
        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");
    }
           

他将會周遊所有注冊的driver,并從driver加載connection,

回到com.mysql.jdbc.Driver,我們可以看到它繼承了NonRegisteringDriver類,然後這個類中實作了connect方法:

public Connection connect(String url, Properties info)
        throws SQLException
    {
        if(url != null)
        {
            if(StringUtils.startsWithIgnoreCase(url, "jdbc:mysql:loadbalance://"))
                return connectLoadBalanced(url, info);
            if(StringUtils.startsWithIgnoreCase(url, "jdbc:mysql:replication://"))
                return connectReplicationConnection(url, info);
        }
        Properties props = null;
        if((props = parseURL(url, info)) == null)
            return null;
        if(!"1".equals(props.getProperty("NUM_HOSTS")))
            return connectFailover(url, info);
        try
        {
            com.mysql.jdbc.Connection newConn = ConnectionImpl.getInstance(host(props), port(props), props, database(props), url);
            return newConn;
        }
        catch(SQLException sqlEx)
        {
            throw sqlEx;
        }
        catch(Exception ex)
        {
            SQLException sqlEx = SQLError.createSQLException((new StringBuilder()).append(Messages.getString("NonRegisteringDriver.17")).append(ex.toString()).append(Messages.getString("NonRegisteringDriver.18")).toString(), "08001", null);
            sqlEx.initCause(ex);
            throw sqlEx;
        }
    }
           

至此整個加載流程應該已經清楚了,在Class.forName的過程中執行了靜态代碼塊,在靜态代碼塊中注冊了Driver對象,在getConnection中取出注冊的Driver并調用其connect方法,在mysql中connect方法的傳回值是com.mysql.jdbc.Connection類的對象了,但是我們在調用過程中依舊是用其接口定義,這裡充分運用了面向對象多态的性質。

再描述一遍,jdbc的類族設計是由sum公司設計了一套接口,再由各個資料庫公司實作接口,我們在調用的過程中隻需要使用接口去定義,然後在加載Driver的過程中底層代碼會給我們選擇好接口真正的實作類,以此來實作真正的資料庫連接配接,此後所有的方法,包括擷取statement等等,都是由接口聲明調用,但是底層傳回的是接口實作類。用這種橋接的模式,我們可以很輕松地在不同的資料庫連接配接中進行轉化,隻需要修改Driver加載的類,如果把加載類的聲明放入配置檔案中,更是不需要重新去編譯,可以很友善地在不同資料庫間進行轉化。

其實這樣的java設計規範由别的公司進行實作的設計非常多,比如j2ee規範,假設沒有j2ee規範,每個web容器各有各的接口,那麼當我們需要把tomcat下跑的代碼移植到jboss下的時候就不得不把代碼全部重寫一遍,這無疑是很不合理的,是以接口雖然沒有做出任何實作,但是其規範的作用在這浩瀚的IT世界裡卻是不可缺少的。