天天看點

JDBC【2】-- JDBC工作原理以及簡單封裝

目錄

  • 1. 工作原理
    • 1.1 加載驅動
      • 1.1.1 類加載相關知識
      • 1.1.2 為什麼JDK 1.6之後不需要顯示加載了?
    • 1.2 驅動加載完成了,然後呢?
  • 2. 簡單封裝

一般我們主要的JDBC處理流程如下:

graph TD

A[注冊一個Driver] -->B(建立資料庫連接配接)

B --> C(建立一個Statement)

C-->D(執行SQL語句,擷取結果)

D-->F(關閉JDBC對象)

首先聲明:這個階段在1.6之後就不需要手動執行了,也就是這個代碼不需要了!!!分析它有利于了解流程。

Class.forName("com.mysql.jdbc.Driver")
           

上面代碼發生在注冊Driver階段,指的是讓JVN将com.mysql.jdbc.Driver這個類加載入記憶體中,最重要的是将mysql驅動注冊到DriverManager中去。

此處加載

Driver

的時候,加載的是

java.mysql.jdbc

包下的,這其實是一種向後相容的做法,實際上代碼都是寫在了

com.mysql.cj.jdbc

下,是以,mysql的連接配接包使用了繼承的方式,

com.mysql.jdbc.Driver

隻是對外的一個相容類,其父類是

com.mysql.cj.jdbc.Driver

,真正的的mysql Driver驅動。

加載

Driver

的目的就是加載它的父類:

public class Driver extends com.mysql.cj.jdbc.Driver {
    public Driver() throws SQLException {
        super();
    }
}
           

我們打開

com.mysql.cj.jdbc.Driver

,可以發現,裡面有一個構造空方法,這也是調用

Class.forName().newInstance()

所需要的,這個類繼承了

NonRegisteringDriver

,實作了

java.mysql.Driver

裡面有一個空無參構造方法,為反射調用

newInstance()

準備的,另外就是靜态代碼塊,靜态代碼塊主要的功能是通過

DriverManager

注冊自己(

Driver

,也就是驅動),這裡很重要的一點,就是Driver是

java.sql.Driver

(這是jdk的包!!!)的實作。

我們引入的驅動本質上是JDK中的Driver的實作類,為啥?這就是标準,限制,不這樣幹,不合規矩。
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    static {
        try {
            // 調用DriverManager 注冊自己(Driver)
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }
    public Driver() throws SQLException {
        // Class.forName().newInstance() 所需要的
    }
}
           

DriverManager

裡面基本全是static方法,也就是專門管理各種驅動的,

registerDriver()

方法如同其名字,就是為了注冊驅動,注冊到哪裡呢?

看下面的代碼,可以知道,driverInfo(驅動相關資訊)會被添加到

registeredDrivers

裡面去。

registeredDrivers

是DriverManager的static屬性,裡面存放着注冊的驅動資訊。如果這個驅動已經被注冊,那麼就不會再注冊。

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 {

        /* Register the driver if it has not already been added to our list */
        if(driver != null) {
            // 将驅動相關資訊加到registeredDrivers
            registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
        } else {
            // This is for compatibility with the original DriverManager
            throw new NullPointerException();
        }

        println("registerDriver: " + driver);

    }
           

到這裡其實

Class.forName(“com.mysql.jdbc.Driver”)

這句代碼分析就結束了。

類加載,是将類的.class檔案(二進制資料)翻譯讀進記憶體中,放在虛拟機的運作時資料區裡面的方法區内。對應的在堆裡面建立一個

java.lang.Class

對象,java裡面萬物皆對象,類本質也是對象,這個建立的對象就是封裝了類本身在方法區的資料結構,這才是類加載的目的。

再簡單一點,就是将類的資訊,弄成一個Class對象,放到堆上面,其資料結構在方法區,堆上面的對象是一種封裝。

類加載有三種方式,後面兩種是反射的時候使用居多。

  • 程式啟動的時候JVM自動初始化加載
  • 調用

    Class.forName()

    手動加載
  • ClassLoader.loadClass()

為什麼使用

Class.forName()

而不是

ClassLoader.loadClass()

,一定要這樣做麼?兩者有什麼差別?(先埋一個坑)先簡單解釋一下:

Class.forName()

除了将類的

.Class

檔案加載到JVM中之外,還會對類進行解釋,執行類的static代碼塊,也就是預設會初始化類的一些資料(可以設定為不執行)。但是

classLoader

是沒有的,

classLoader

隻有在

newInstance()

的時候才會執行static塊。

而我們上面看到的

com.mysql.cj.jdbc.Driver

這個類就是使用static的方式将自己注冊到

DriverManager

中,是以需要使用

Class.forName()

Class.forName(xxx.xx.xx).newInstance()

可不可以,可以,但是沒有必要。這樣相當于順帶建立出了執行個體。我們隻是需要滿足

在JDBC規範中明确要求這個Driver類必須向DriverManager注冊自己

這個條件,而觸發其中的靜态代碼塊即可。

we just want to load the driver to jvm only, but not need to user the instance of driver,

so call Class.forName(xxx.xx.xx) is enough, if you call Class.forName(xxx.xx.xx).newInstance(),

the result will same as calling Class.forName(xxx.xx.xx),

because Class.forName(xxx.xx.xx).newInstance() will load driver first,

and then create instance, but the instacne you will never use in usual,

so you need not to create it.

Class.forName()

代碼如下,可以看到的是,調用

forName(String name, boolean initialize,ClassLoader loader, Class<?> caller)

,傳入的是一個true,也就是會初始化。

@CallerSensitive
    public static Class<?> forName(String className)
                throws ClassNotFoundException {
        Class<?> caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }
           

我們将

Class.forName()

注釋掉,我的mysql-connector版本是8.0以上,JDK是1.8,還是可以運作的如下:

JDBC【2】-- JDBC工作原理以及簡單封裝

這是什麼原因呢???

仔細一點就會發現,在我們引入的

mysql

包中,有一個配置檔案,

java.sql.Driver

,裡面配置了

JDBC【2】-- JDBC工作原理以及簡單封裝

這裡使用了

SPI

的技術,也就是

Service provider Interface

機制,JDK 允許第三方産商或者插件,對JDK的規範做不一樣的定制或者拓展。JVM在啟動的時候可以檢測到接口的實作,如果配置了的驅動就會自動由

DriverManager

加載注冊。這就是為什麼不需要顯式調用的原因。

也就是JDK定義好接口和規範,引入的包去實作它,并且把實作的全限定類名配置在指定的地方(META-INF檔案目錄中),表明需要加載這個接口,那麼JVM就會一起加載它。具體的源碼是

ServiceLoader

下面的

load()

方法。

public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
           

加載完成了驅動,我們需要擷取和資料庫的連接配接,連接配接資料庫我們都知道是需要資料庫位址,使用者名,和密碼。

connection=DriverManager.getConnection(URL,USER,PASSWROD);
           

二話不說,看内部實作。裡面其實是用使用者和密碼構造了一個Properties對象,然後傳到另一個方法中進行調用。

public static Connection getConnection(String url,
        String user, String password) throws SQLException {
        java.util.Properties info = new java.util.Properties();

        if (user != null) {
            info.put("user", user);
        }
        if (password != null) {
            info.put("password", password);
        }

        return (getConnection(url, info, Reflection.getCallerClass()));
    }
           

真正擷取連結的代碼如下,擷取真正的類加載器,和目前driver的加載器對比,

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 (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");
    }
           

為什麼要這樣設計?

Reflection.getCallerClass()

是反射中的方法,他的作用是擷取它的調用類,也就是哪一個類調用了這個方法,以前這個方法是

Reflection.getCallerClass(int n)

,也就是支援傳入一個n,傳回調用棧的第n幀的類,比如A調用了B,B調用

Reflection.getCallerClass(2)

,目前的Reflection類位于調用棧的第0幀,B位于第1幀,A就是第二幀。

改成無參數的

Reflection.getCallerClass()

方法之後,

Reflection.getCallerClass()

方法調用所在的方法必須用

@CallerSensitive

進行注解,g該方法擷取class時會跳過調用鍊路上所有的有

@CallerSensitive

注解的方法的類,直到遇到第一個未使用該注解的類,傳回。我們可以知道,源碼中的所有的

getConnection()

都是有注解的,證明會傳回我們真正的調用的類。

說起

JDBC

的時候,我們自定義一下的資料庫連接配接工具:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class DBUtil {
    private static String URL="jdbc:mysql://127.0.0.1:3306/test";
    private static String USER="root";
    private static String PASSWROD ="123456";
    private static Connection connection=null;
    static{
        try {
            Class.forName("com.mysql.jdbc.Driver");
            // 擷取資料庫連接配接
            connection=DriverManager.getConnection(URL,USER,PASSWROD);
            System.out.println("連接配接成功");
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (SQLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    // 傳回資料庫連接配接
    public static Connection getConnection(){
        return connection;
    }
}

           

上面的做法,是一直使用同一個

connection

,這樣在并發的時候還是很有缺陷的,是以我們需要多個線程同時操作不同的

connection

,但是一直建立

connection

是需要較大的開銷的,那這樣應該怎麼做呢?這就不得不說到連接配接池技術了。

資料庫連接配接池,就是負責配置設定,管理和釋放資料庫連接配接,使用完之後的連接配接,放回到資料庫連接配接池中,可以重複利用。

此文章僅代表自己(本菜鳥)學習積累記錄,或者學習筆記,如有侵權,請聯系作者删除。人無完人,文章也一樣,文筆稚嫩,在下不才,勿噴,如果有錯誤之處,還望指出,感激不盡~

技術之路不在一時,山高水長,縱使緩慢,馳而不息。

公衆号:秦懷雜貨店

JDBC【2】-- JDBC工作原理以及簡單封裝