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

目錄

簡介

什麼是JDBC

JDBC是一套鏈接和操做數據庫的標準、規範。經過提供DriverManagerConnectionStatementResultSet等接口將開發人員與數據庫提供商隔離,開發人員只須要面對JDBC接口,無需關心怎麼跟數據庫交互。java

幾個重要的類

類名 做用
DriverManager 驅動管理器,用於註冊驅動,是獲取 Connection對象的入口
Driver 數據庫驅動,用於獲取Connection對象
Connection 數據庫鏈接,用於獲取Statement對象、管理事務
Statement sql執行器,用於執行sql
ResultSet 結果集,用於封裝和操做查詢結果
prepareCall 用於調用存儲過程

使用中的注意事項

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

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

  3. 使用PreparedStatement而不是Statement。能夠避免SQL注入,而且利用預編譯的特色能夠提升效率。github

使用例子

需求

使用JDBC對mysql數據庫的用戶表進行增刪改查。sql

工程環境

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驅動的全路徑名。

mysql的驅動包中用於支持SPI機制的java.sql.Driver文件

得到鏈接對象

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

相關文章
相關標籤/搜索