1. 程式人生 > >jdbc-mysql測試例子和原始碼詳解

jdbc-mysql測試例子和原始碼詳解

目錄

  • 簡介
    • 什麼是JDBC
    • 幾個重要的類
    • 使用中的注意事項
  • 使用例子
    • 需求
    • 工程環境
    • 主要步驟
    • 建立表
    • 建立專案
    • 引入依賴
    • 編寫jdbc.prperties
    • 獲得Connection物件
    • 使用Connection物件完成儲存操作
  • 原始碼分析
    • 驅動註冊
      • DriverManager.registerDriver
      • 為什麼Class.forName(com.mysql.cj.jdbc.Driver) 可以註冊驅動?
      • 為什麼JDK6後不需要Class.forName也能註冊驅動?
    • 獲得連線物件
      • DriverManager.getConnection
      • com.mysql.cj.jdbc.Driver.connection
      • ConnectionImpl.getInstance
      • NativeSession.connect

簡介

什麼是JDBC

JDBC是一套連線和操作資料庫的標準、規範。通過提供DriverManagerConnectionStatementResultSet等介面將開發人員與資料庫提供商隔離,開發人員只需要面對JDBC介面,無需關心怎麼跟資料庫互動。

幾個重要的類

類名 作用
DriverManager 驅動管理器,用於註冊驅動,是獲取 Connection物件的入口
Driver 資料庫驅動,用於獲取Connection物件
Connection 資料庫連線,用於獲取Statement
物件、管理事務
Statement sql執行器,用於執行sql
ResultSet 結果集,用於封裝和操作查詢結果
prepareCall 用於呼叫儲存過程

使用中的注意事項

  1. 記得釋放資源。另外,ResultSetStatement的關閉都不會導致Connection的關閉。

  2. maven要引入oracle的驅動包,要把jar包安裝在本地倉庫或私服才行。

  3. 使用PreparedStatement而不是Statement。可以避免SQL注入,並且利用預編譯的特點可以提高效率。

使用例子

需求

使用JDBC對mysql資料庫的使用者表進行增刪改查。

工程環境

JDK:1.8

maven:3.6.1

IDE:sts4

mysql driver:8.0.15

mysql:5.7

主要步驟

一個完整的JDBC儲存操作主要包括以下步驟:

  1. 註冊驅動(JDK6後會自動註冊,可忽略該步驟);

  2. 通過DriverManager獲得Connection物件;

  3. 開啟事務;

  4. 通過Connection獲得PreparedStatement物件;

  5. 設定PreparedStatement的引數;

  6. 執行儲存操作;

  7. 儲存成功提交事務,儲存失敗回滾事務;

  8. 釋放資源,包括ConnectionPreparedStatement

建立表

CREATE TABLE `demo_user` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '使用者id',
  `name` varchar(16) COLLATE utf8_unicode_ci NOT NULL COMMENT '使用者名稱',
  `age` int(3) unsigned DEFAULT NULL COMMENT '使用者年齡',
  `gmt_create` datetime DEFAULT NULL COMMENT '記錄建立時間',
  `gmt_modified` datetime DEFAULT NULL COMMENT '記錄最近修改時間',
  `deleted` bit(1) DEFAULT b'0' COMMENT '是否刪除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_name` (`name`),
  KEY `index_age` (`age`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

建立專案

專案型別Maven Project,打包方式jar

引入依賴

<!-- junit -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
<!-- mysql驅動的jar包 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.15</version>
</dependency>
<!-- oracle驅動的jar包 -->
<!-- <dependency>
    <groupId>com.oracle</groupId>
    <artifactId>ojdbc6</artifactId>
    <version>11.2.0.2.0</version>
</dependency> -->

注意:由於oracle商業版權問題,maven並不提供Oracle JDBC driver,需要將驅動包手動新增到本地倉庫或私服。

編寫jdbc.prperties

下面的url拼接了好幾個引數,主要為了避免亂碼和時區報錯的異常。

路徑:resources目錄下

driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true
#這裡指定了字元編碼和解碼格式,時區,是否加密傳輸
username=root
password=root
#注意,xml配置是&採用&amp;替代

如果是oracle資料庫,配置如下:

driver=oracle.jdbc.driver.OracleDriver
url=jdbc:oracle:thin:@//localhost:1521/xe
username=system
password=root

獲得Connection物件

    private static Connection createConnection() throws Exception {
        // 匯入配置檔案
        Properties pro = new Properties();
        InputStream in = JDBCUtil.class.getClassLoader().getResourceAsStream( "jdbc.properties" );
        Connection conn = null;
        pro.load( in );
        // 獲取配置檔案的資訊
        String driver = pro.getProperty( "driver" );
        String url = pro.getProperty( "url" );
        String username = pro.getProperty( "username" );
        String password = pro.getProperty( "password" );
        // 註冊驅動,JDK6後不需要再手動註冊,DirverManager的靜態程式碼塊會幫我們註冊
        // Class.forName(driver);
        // 獲得連線
        conn = DriverManager.getConnection( url, username, password );
        return conn;
    }

使用Connection物件完成儲存操作

這裡簡單地模擬實際業務層呼叫持久層,並開啟事務。另外,獲取連線、開啟事務、提交回滾、釋放資源都通過自定義的工具類 JDBCUtil 來實現,具體見原始碼。

    @Test
    public void save() {
        UserDao userDao = new UserDaoImpl();
        // 建立使用者
        User user = new User( "zzf002", 18, new Date(), new Date() );
        try {
            // 開啟事務
            JDBCUtil.startTrasaction();
            // 儲存使用者
            userDao.insert( user );
            // 提交事務
            JDBCUtil.commit();
        } catch( Exception e ) {
            // 回滾事務
            JDBCUtil.rollback();
            e.printStackTrace();
        } finally {
            // 釋放資源
            JDBCUtil.release();
        }
    }

接下來看看具體的儲存操作,即DAO層方法。

    public void insert( User user ) throws Exception {
        String sql = "insert into demo_user (name,age,gmt_create,gmt_modified) values(?,?,?,?)";
        Connection connection = JDBCUtil.getConnection();
        //獲取PreparedStatement物件
        PreparedStatement prepareStatement = connection.prepareStatement( sql );
        //設定引數
        prepareStatement.setString( 1, user.getName() );
        prepareStatement.setInt( 2, user.getAge() );
        prepareStatement.setDate( 3, new java.sql.Date( user.getGmt_create().getTime() ) );
        prepareStatement.setDate( 4, new java.sql.Date( user.getGmt_modified().getTime() ) );
        //執行儲存
        prepareStatement.executeUpdate();
        //釋放資源
        JDBCUtil.release( prepareStatement, null );
    }

原始碼分析

驅動註冊

DriverManager.registerDriver

DriverManager主要用於管理資料庫驅動,併為我們提供了獲取連線物件的介面。其中,它有一個重要的成員屬性registeredDrivers,是一個CopyOnWriteArrayList集合(通過ReentrantLock實現執行緒安全),存放的是元素是DriverInfo物件。

    //存放資料庫驅動包裝類的集合(執行緒安全)
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>(); 
    public static synchronized void registerDriver(java.sql.Driver driver)
        throws SQLException {
        //呼叫過載方法,傳入的DriverAction物件為null
        registerDriver(driver, null);
    }
    public static synchronized void registerDriver(java.sql.Driver driver,
            DriverAction da)
        throws SQLException {
        if(driver != null) {
            //當列表中沒有這個DriverInfo物件時,加入列表。
            //注意,這裡判斷物件是否已經存在,最終比較的是driver地址是否相等。
            registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
        } else {
            throw new NullPointerException();
        }

        println("registerDriver: " + driver);

    }

為什麼集合存放的是Driver的包裝類DriverInfo物件,而不是Driver物件呢?

  1. 通過DriverInfo的原始碼可知,當我們呼叫equals方法比較兩個DriverInfo物件是否相等時,實際上比較的是Driver物件的地址,也就是說,我可以在DriverManager中註冊多個MYSQL驅動。而如果直接存放的是Driver物件,就不能達到這種效果(因為沒有遇到需要註冊多個同類驅動的場景,所以我暫時理解不了這樣做的好處)。

  2. DriverInfo中還包含了另一個成員屬性DriverAction,當我們登出驅動時,必須呼叫它的deregister方法後才能將驅動從註冊列表中移除,該方法決定登出驅動時應該如何處理活動連線等(其實一般在構造DriverInfo進行註冊時,傳入的DriverAction物件為空,根本不會去使用到這個物件,除非一開始註冊就傳入非空DriverAction物件)。

綜上,集合中元素不是Driver物件而DriverInfo物件,主要考慮的是擴充套件某些功能,雖然這些功能幾乎不會用到。

注意:考慮篇幅,以下程式碼經過修改,僅保留所需部分。

class DriverInfo {

    final Driver driver;
    DriverAction da;
    DriverInfo(Driver driver, DriverAction action) {
        this.driver = driver;
        da = action;
    }

    @Override
    public boolean equals(Object other) {
        //這裡對比的是地址
        return (other instanceof DriverInfo)
                && this.driver == ((DriverInfo) other).driver;
    }

}

為什麼Class.forName(com.mysql.cj.jdbc.Driver) 可以註冊驅動?

當載入com.mysql.cj.jdbc.Driver這個類時,靜態程式碼塊中會執行註冊驅動的方法。

    static {
        try {
            //靜態程式碼塊中註冊當前驅動
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }

為什麼JDK6後不需要Class.forName也能註冊驅動?

因為從JDK6開始,DriverManager增加了以下靜態程式碼塊,當類被載入時會執行static程式碼塊的loadInitialDrivers方法。

而這個方法會通過查詢系統引數(jdbc.drivers)和SPI機制兩種方式去載入資料庫驅動。

注意:考慮篇幅,以下程式碼經過修改,僅保留所需部分。

    static {
        loadInitialDrivers();
    }
    //這個方法通過兩個渠道載入所有資料庫驅動:
    //1. 查詢系統引數jdbc.drivers獲得資料驅動類名
    //2. SPI機制
    private static void loadInitialDrivers() {
        //通過系統引數jdbc.drivers讀取資料庫驅動的全路徑名。該引數可以通過啟動引數來設定,其實引入SPI機制後這一步好像沒什麼意義了。
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        //使用SPI機制載入驅動
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                //讀取META-INF/services/java.sql.Driver檔案的類全路徑名。
                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;
            }
        });

        if (drivers == null || drivers.equals("")) {
            return;
        }
        //載入jdbc.drivers引數配置的實現類
        String[] driversList = drivers.split(":");
        for (String aDriver : driversList) {
            try {
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

補充:SPI機制本質上提供了一種服務發現機制,通過配置檔案的方式,實現服務的自動裝載,有利於解耦和麵向介面程式設計。具體實現過程為:在專案的META-INF/services資料夾下放入以介面全路徑名命名的檔案,並在檔案中加入實現類的全限定名,接著就可以通過ServiceLoder動態地載入實現類。

開啟mysql的驅動包就可以看到一個java.sql.Driver檔案,裡面就是mysql驅動的全路徑名。

獲得連線物件

DriverManager.getConnection

獲取連線物件的入口是DriverManager.getConnection,呼叫時需要傳入url、username和password。

獲取連線物件需要呼叫java.sql.Driver實現類(即資料庫驅動)的方法,而具體呼叫哪個實現類呢?

正如前面講到的,註冊的資料庫驅動被存放在registeredDrivers中,所以只有從這個集合中獲取就可以了。

注意:考慮篇幅,以下程式碼經過修改,僅保留所需部分。

    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);
        }
        //傳入url、包含username和password的資訊類、當前呼叫類
        return (getConnection(url, info, Reflection.getCallerClass()));
    }
    private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException {
        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        //遍歷所有註冊的資料庫驅動
        for(DriverInfo aDriver : registeredDrivers) {
            //先檢查這當前類載入器是否有許可權載入這個驅動,如果是才進入
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                //這一步是關鍵,會去呼叫Driver的connect方法
                Connection con = aDriver.driver.connect(url, info);
                if (con != null) {
                    return con;
                }
            } else {
                println("    skipping: " + aDriver.getClass().getName());
            }
        }
    }

com.mysql.cj.jdbc.Driver.connection

由於使用的是mysql的資料驅動,這裡實際呼叫的是com.mysql.cj.jdbc.Driver的方法。

從以下程式碼可以看出,mysql支援支援多節點部署的策略,本文僅對單機版進行擴充套件。

注意:考慮篇幅,以下程式碼經過修改,僅保留所需部分。

    //mysql支援多節點部署的策略,根據架構不同,url格式也有所區別。
    private static final String REPLICATION_URL_PREFIX = "jdbc:mysql:replication://";
    private static final String URL_PREFIX = "jdbc:mysql://";
    private static final String MXJ_URL_PREFIX = "jdbc:mysql:mxj://";
    public static final String LOADBALANCE_URL_PREFIX = "jdbc:mysql:loadbalance://";
    public java.sql.Connection connect(String url, Properties info) throws SQLException {
        //根據url的型別來返回不同的連線物件,這裡僅考慮單機版
        ConnectionUrl conStr = ConnectionUrl.getConnectionUrlInstance(url, info);
        switch (conStr.getType()) {
            case SINGLE_CONNECTION:
                //呼叫ConnectionImpl.getInstance獲取連線物件
                return com.mysql.cj.jdbc.ConnectionImpl.getInstance(conStr.getMainHost());

            case LOADBALANCE_CONNECTION:
                return LoadBalancedConnectionProxy.createProxyInstance((LoadbalanceConnectionUrl) conStr);

            case FAILOVER_CONNECTION:
                return FailoverConnectionProxy.createProxyInstance(conStr);

            case REPLICATION_CONNECTION:
                return ReplicationConnectionProxy.createProxyInstance((ReplicationConnectionUrl) conStr);

            default:
                return null;
        }
    }

ConnectionImpl.getInstance

這個類有個比較重要的欄位session,可以把它看成一個會話,和我們平時瀏覽器訪問伺服器的會話差不多,後續我們進行資料庫操作就是基於這個會話來實現的。

注意:考慮篇幅,以下程式碼經過修改,僅保留所需部分。

    private NativeSession session = null;
    public static JdbcConnection getInstance(HostInfo hostInfo) throws SQLException {
        //呼叫構造
        return new ConnectionImpl(hostInfo);
    }
    public ConnectionImpl(HostInfo hostInfo) throws SQLException {
        //先根據hostInfo初始化成員屬性,包括資料庫主機名、埠、使用者名稱、密碼、資料庫及其他引數設定等等,這裡省略不放入。
        //最主要看下這句程式碼 
        createNewIO(false);
    }
    public void createNewIO(boolean isForReconnect) {
        if (!this.autoReconnect.getValue()) {
            //這裡只看不重試的方法
            connectOneTryOnly(isForReconnect);
            return;
        }

        connectWithRetries(isForReconnect);
    }
    private void connectOneTryOnly(boolean isForReconnect) throws SQLException {

        JdbcConnection c = getProxy();
        //呼叫NativeSession物件的connect方法建立和資料庫的連線
        this.session.connect(this.origHostInfo, this.user, this.password, this.database, DriverManager.getLoginTimeout() * 1000, c);
        return;
    }

NativeSession.connect

接下來的程式碼主要是建立會話的過程,首先時建立物理連線,然後根據協議建立會話。

注意:考慮篇幅,以下程式碼經過修改,僅保留所需部分。

    public void connect(HostInfo hi, String user, String password, String database, int loginTimeout, TransactionEventHandler transactionManager)
            throws IOException {
        //首先獲得TCP/IP連線
        SocketConnection socketConnection = new NativeSocketConnection();
        socketConnection.connect(this.hostInfo.getHost(), this.hostInfo.getPort(), this.propertySet, getExceptionInterceptor(), this.log, loginTimeout);

        // 對TCP/IP連線進行協議包裝
        if (this.protocol == null) {
            this.protocol = NativeProtocol.getInstance(this, socketConnection, this.propertySet, this.log, transactionManager);
        } else {
            this.protocol.init(this, socketConnection, this.propertySet, transactionManager);
        }

        // 通過使用者名稱和密碼連線指定資料庫,並建立會話
        this.protocol.connect(user, password, database);
    }

針對資料庫的連線,暫時點到為止,另外還有涉及資料庫操作的原始碼分析,後續再完善補充。

本文為原創文章,轉載請附上原文出處連結:https://github.com/ZhangZiSheng001/jdbc-demo