1. 程式人生 > >JDBC介面講解與底層實現分析(上)

JDBC介面講解與底層實現分析(上)

一、JDBC為什麼需要資料庫驅動?

資料庫是一個產品,想要訪問它,就得通過它定義的方式去訪問。你可能覺得平時操作時好像並沒有按照什麼協議訪問呀,就是敲了下SQL命令,就有返回結果了。但你記得你是在什麼環境下訪問的嗎?你在cmd終端下輸入mysql啟動的就是mysql client,是一個客戶端,這個客戶端其實就是一段程式,它的內部邏輯對你來說是透明的,你給它一段SQL,其實它是會做一定包裝的,然後通過一些協議傳送給mysql的server服務端。

抓包工具:WireShark

然後打臉了,並沒有抓到3306埠的網路包

然後查閱了下《MySQL技術內幕InnoDB儲存引擎》:

常用的程序間通訊方式有:管道,命名管道,命名字,TCP套接字,Unix域套接字。

而MySQL提供的連線方式從本質上看都是上述提及的程序通訊方式

1.TCP/IP

TCP/IP套接字的方式是MySQL在任何平臺上都提供的連線方式,也是用的最多的。一般情況客戶端一臺機器去連線伺服器另一臺機器。兩臺機器之間就是通過TCP/IP連線。客戶端會向伺服器MySQL例項發出TCP/IP連線請求,並連線成功

2,。命名管道和共享記憶體

Windows2003、vista及在此之上的平臺,如果兩個需要程序通訊的程序在同一臺機器上,那麼可以使用命名管道,配置檔案啟用--enable-named-pipe。也可以使用共享記憶體的連線方式,只需要進行配置。

無論哪種方式,與資料庫伺服器通訊肯定還是有一定的協議的

那麼mysql,oracle,DB2他們的協議是一樣的嗎?不是。那麼JAVA為每一個協議都去寫一個類,是不切合實際的。首先這個協議是別人定的,第二資料庫產品太多了。所以JAVA定義了一套介面,也就是JDBC。而協議的實現與通訊就由資料庫廠商來提供了,這也就是驅動程式的JAR包。

看一下標準的連線語句

//宣告Connection物件
        Connection con;
        //驅動程式名
        String driver = "com.mysql.jdbc.Driver";
        //URL指向要訪問的資料庫名test
        String url = "jdbc:mysql://localhost:3306/test";
        String user = "root";
        String password = "root";
        //遍歷查詢結果集
        try {
            //載入驅動程式
            Class.forName(driver);
            //1.getConnection()方法,連線MySQL資料庫
            con = DriverManager.getConnection(url,user,password);

            //2.建立statement類物件,用來執行SQL語句
            Statement statement = con.createStatement();
            //要執行的SQL語句
            String sql = "select * from student";
            //3.ResultSet類,用來存放獲取的結果集!!
            ResultSet rs = statement.executeQuery(sql);
 
            while(rs.next()){
                //獲取name這列資料
                name = rs.getString("name");
                //獲取uid這列資料
                id = rs.getString("uid");
            }
            rs.close();
            con.close();
        } catch(ClassNotFoundException e) {   
            //資料庫驅動類異常處理
            System.out.println("Sorry,can`t find the Driver!");   
            e.printStackTrace();   
            } catch(SQLException e) {
            //資料庫連線失敗異常處理
            e.printStackTrace();  
            }catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }finally{
            System.out.println("資料庫資料成功獲取!!");
        }
    }

所以這個過程總結起來就是:

1.註冊一個Driver

2.建立一個到資料庫的連線

3.建立一個Statement

4.執行SQL語句

5.處理結果

6.關閉JDBC物件


但是我們都是面向介面程式設計的,但執行時的物件還是使用的底層實現

來看一個圖,看我們是如何使用驅動的(很明顯的體現出來了面向介面程式設計


二、為什麼使用Class.forName來載入資料庫驅動,DriverManager的作用


我們平時用Class.forName去載入驅動,可能很多初學者都沒太懂這個底層。所以我們來解釋下

我們先用最直觀的方式連線一下:

<pre name="code" class="java">Driver driver=new com.mysql.jdbc.Driver();
Connection conn=driver.connect(url, info);
System.out.println(conn);

讀者可以用這種方式去試試,看能不能獲取連線(肯定是可以的啦 (@・ˍ・)

這個方式就非常直觀了,前面是介面,後面是實現類。也就是驅動類。

那麼為什麼我們平時不用這種直觀的方式呢?

因為這樣我們的類就與com.mysql.jdbc.Driver這個類直接耦合了

所以我們可以這樣改一下:

String driverString="com.mysql.jdbc.Driver";
Driver driver=Class.forName(driverString).newInstance();
Connection conn=driver.connect(url, info);
System.out.println(conn);
使用反射獲取對應的實現類,並建立物件

當系統中多個Driver時,我們手動控制是很麻煩的

DriverManager就是java.sql中的一個工具類,你給它註冊進去所有的Driver,它幫你管理,你呼叫它的getConnection

會智慧幫你選擇合適的Driver並建立連線Connection

String driverString="com.mysql.jdbc.Driver";
Driver driver=Class.forName(driverString).newInstance();
String driverString2="oracle.jdbc.driver.OracleDriver";
Driver driver2=Class.forName(driverString).newInstance();
DriverManager.registerDriver(driver1);
DriverManager.registerDriver(driver2);
con = DriverManager.getConnection(url,user,password);

但這樣去寫其實也是很繁瑣的,進一步簡化就成為第一部分的簡化方案:
//載入驅動程式
            Class.forName(driver);
            //1.getConnection()方法,連線MySQL資料庫
            con = DriverManager.getConnection(url,user,password);
那DriverManager是如何獲得的呢:

原始碼:

它自己會儲存一份所有driver的列表

// List of registered JDBC drivers
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<DriverInfo>();

我們前面已經呼叫Class.forName(),系統已經載入了我們所需要的實現類

DriverManager載入進來時會執行以下靜態語句

 static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
loadInitialDrivers方法會載入系統中已經載入的所有驅動Driver存放到剛剛那個list裡面
然後我們呼叫getConnection時,它就會遍歷列表的驅動,幫我們找到合適的驅動Driver並使用

(下面是部分重點程式碼,完整的請參考Java原始碼)

//  Worker method called by the public getConnection() methods.
    private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
        

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


三、Statement與PreparedStatment區別

PreparedStatement是帶預編譯功能的

使用的規律就是他們都是connection創建出來,

但Statement是沒有引數的,

而PreparedStatement的引數是sql的字串(因為它要預編譯sql嘛,所以必須要有sql引數)

1.什麼叫預編譯:

通過Statement物件執行SQL語句時,需要將SQL語句傳送給DBMS,由DBMS首先進行編譯後再執行。

預編譯語句和Statement不同,在建立PreparedStatement 物件時就指定了SQL語句,該語句立即傳送給DBMS進行編譯。當該編譯語句被執行時,DBMS直接執行編譯後的SQL語句,而不需要像其他SQL語句那樣首先將其編譯。

2、什麼時候使用預編譯語句

   一般是在需要反覆使用一個SQL語句時才使用預編譯語句,預編譯語句常常放在一個fo r或者while迴圈裡面使用,通過反覆設定引數從而多次使用該SQL語句。為了防止SQL注入漏洞,在某些資料操作中也使用預編譯語句。

3、為什麼使用預編譯語句
   (1) 防止SQL注入
   (2)提高效率
   資料庫處理一個SQL語句,需要完成解析SQL語句、檢查語法和語義以及生成程式碼,一般說來,處理時間要比執行語句所需要的時間長。預編譯語句在建立的時候已經是將指定的SQL語句傳送給了DBMS,完成了解析、檢查、編譯等工作。因此,當一個SQL語句需要執行多次時,使用預編譯語句可以減少處理時間,提高執行效率。
   (3)提高程式碼的可讀性和可維護性 
   將引數與SQL語句分離出來,這樣就可以方便對程式的更改和擴充套件,同樣,也可以減少不必要的錯誤。 

這樣生成資料庫底層的內部命令(編譯後的SQL),並將該命令封裝在preparedStatement物件中,可以減輕資料庫負擔,提高訪問資料庫速度。

並沒有找到很多這塊的資料,所以個人理解是:

Statement建立時不需要傳引數,只有execute時才一條一條SQL傳送給伺服器然後從伺服器獲取結果

而PreparedStatement則建立時需要建立引數,它會先發送這個SQL給資料庫,進行解析檢查編譯成編譯後的語句。關鍵就在這裡:存的不是SQL,而是編譯後的語句。這個編譯出來的語句還會留出坑:放參數

這樣使用起來就不需要再編譯了,將坑填滿,放入引數。所以效率很高,尤其在於反覆使用同一個語句。

那麼如何防止SQL注入:我的理解是

(1) 首先在PreparedStatement中可以有相關的過濾,型別不同。比如應該傳入int型的,傳入了123‘ or 1=1',肯定是能判斷出來的。而Statement是不做這種判斷的,直接拼接傳入資料庫了,就SQL注入攻擊成功了

(2) 我們剛剛說了存放的編譯的語句,然後有放參數的坑,這個引數就非常嚴格了。也許  or 這樣的SQL語句編譯之後已經不是or 這樣的字串。所以你傳入123‘ or 1=1'  (or是沒有編譯前的SQL),和已經編譯後的語句拼接,那肯定是不能成功被解析為or這個邏輯的(僅僅是個人理解!!!!!!)

所以說字串的SQL注入也是不能成功的,也就是說型別相同,如本該傳入String型‘aaa’   而傳入aaa‘ or 1=1'。按照剛剛的第一條過濾應該判斷不了,所以第二條的原因,還是不能成功注入

(證明見下面的使用API的程式碼)

貼一段JAVA JDK原始碼

這是Connection.prepareStatement(sql)語句

/**
     * Creates a <code>PreparedStatement</code> object for sending
     * parameterized SQL statements to the database.
     * <P>
     * A SQL statement with or without IN parameters can be
     * pre-compiled and stored in a <code>PreparedStatement</code> object. This
     * object can then be used to efficiently execute this statement
     * multiple times.
     *
     * <P><B>Note:</B> This method is optimized for handling
     * parametric SQL statements that benefit from precompilation. If
     * the driver supports precompilation,
     * the method <code>prepareStatement</code> will send
     * the statement to the database for precompilation. Some drivers
     * may not support precompilation. In this case, the statement may
     * not be sent to the database until the <code>PreparedStatement</code>
     * object is executed.  This has no direct effect on users; however, it does
     * affect which methods throw certain <code>SQLException</code> objects.
     * <P>
     * Result sets created using the returned <code>PreparedStatement</code>
     * object will by default be type <code>TYPE_FORWARD_ONLY</code>
     * and have a concurrency level of <code>CONCUR_READ_ONLY</code>.
     * The holdability of the created result sets can be determined by
     * calling {@link #getHoldability}.
     *
     * @param sql an SQL statement that may contain one or more '?' IN
     * parameter placeholders
     * @return a new default <code>PreparedStatement</code> object containing the
     * pre-compiled SQL statement
     * @exception SQLException if a database access error occurs
     * or this method is called on a closed connection
     */
    PreparedStatement prepareStatement(String sql)
        throws SQLException;

註釋也說了,是把預編譯的語句存放在PreparedStatement物件中的

那麼我們看下PreparedStatement這個類中是不是有呢:

 class ParseInfo {
        char firstStmtChar = 0;

        boolean foundLoadData = false;

        long lastUsed = 0;

        int statementLength = 0;

        int statementStartPos = 0;

        boolean canRewriteAsMultiValueInsert = false;

        byte[][] staticSql = null;

        boolean isOnDuplicateKeyUpdate = false;

        int locationOfOnDuplicateKeyUpdate = -1;

        String valuesClause;

        boolean parametersInDuplicateKeyClause = false;

        /**
         * Represents the "parsed" state of a client-side prepared statement, with the statement broken up into it's static and dynamic (where parameters are
         * bound) parts.
         */
        ParseInfo(String sql, MySQLConnection conn, java.sql.DatabaseMetaData dbmd, String encoding, SingleByteCharsetConverter converter) throws SQLException {
            this(sql, conn, dbmd, encoding, converter, true);
        }

我覺得上述內部類和方法就是存放編譯後的SQL語句了,存放方式蠻複雜的,二進位制陣列等等

看下使用API方法

        Connection conn=driver.connect(url, p);			
	
	while(rs.next())
	{
		System.out.println(rs.getString(1));
	}
	Statement s=conn.createStatement();
	rs=s.executeQuery("select * from wk where username='xx' or 1=1");
	System.out.println("using Statement");
	while(rs.next())
	{
		System.out.println(rs.getString(1));
	}

	PreparedStatement ps=conn.prepareStatement("select * from wk where username=?");
	ps.setString(1, "xx' or 1=1");
	ResultSet rs=ps.executeQuery();
	System.out.println("using PreparedStatement"); 

這個過程:


(這個程式執行可以發現字串的SQL注入是沒成功的)

好了 ,剩下的部分下一篇再寫吧,要不然文章太長了

四、

那麼這個實現類在系統中是否是單例呢?

五、連線池

使用了動態代理模式

六、spring中如何使用JDBC