jdbc-mysql測試例子和源碼詳解

目錄

簡介

什麼是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

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品在網路上成為最夯、最多人討論的話題?

※高價收購3C產品,價格不怕你比較

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!