国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

【JDBC系列】從源碼角度理解JDBC和Mysql的預編譯特性

longshengwang / 2763人閱讀

摘要:我們對語句做適當改變,就完成了注入,因為普通的不會對做任何處理,該例中單引號后的生效,拉出了所有數據。查詢資料后,發現還要開啟一個參數,讓端緩存,緩存是級別的。結論是個好東西。

背景

最近因為工作調整的關系,都在和數據庫打交道,增加了許多和JDBC親密接觸的機會,其實我們用的是Mybatis啦。知其然,知其所以然,是我們工程師童鞋們應該追求的事情,能夠幫助你更好的理解這個技術,面對問題時更游刃有余。所以呢,最近就在業務時間對JDBC進行了小小的研究,有一些小收獲,在此做個記錄。

我們都知道市面上有很多數據庫,比如Oracle,Sqlserver以及Mysql等,因為Mysql開放性以及可定制性比較強,平時在學校里或者在互聯網從業的開發人員應該接觸Mysql最多,本文后續的講解也主要針對的是JDBC在Mysql驅動中的相關實現。

提綱

本文簡單介紹了JDBC的由來,介紹了JDBC使用過程中的驅動加載代碼,介紹了幾個常用的接口,著重分析了Statement和Preparement使用上以及他們對待SQL注入上的區別。最后著重分析了PrepareStatement開啟預編譯前后,防SQL注入以及具體執行上的區別。

為什么需要JDBC

我們都知道,每家數據庫的具體實現都會有所不同,如果開發者每接觸一種新的數據庫,都需要對其具體實現進行編程了,那我估計真正的代碼還沒開始寫,先累死在底層的開發上了,同時這也不符合Java面向接口編程的特點。于是就有了JDBC。

JDBC(Java Data Base Connectivity,java數據庫連接)是一種用于執行SQL語句的Java API,可以為多種關系數據庫提供統一訪問,它由一組用Java語言編寫的類和接口組成。


如果用圖來表示的話,如上圖所示,開發者不必為每家數據通信協議的不同而疲于奔命,只需要面向JDBC提供的接口編程,在運行時,由對應的驅動程序操作對應的DB。

示例代碼

光說不練假把式,奉上一段簡單的示例代碼,主要完成了獲取數據庫連接,執行SQL語句,打印返回結果,釋放連接的過程。

package jdbc;

import java.sql.*;

/**
 * @author cenkailun
 * @Date 17/5/20
 * @Time 下午5:09
 */
public class Main {

    private static final String url = "jdbc:mysql://127.0.0.1:3306/demo";
    private static final String user = "root";
    private static final String password = "123456";

    static {
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws SQLException {
        Connection connection = DriverManager.getConnection(url, user, password);

        System.out.println("Statement 語句結果: ");
        Statement statement = connection.createStatement();
        statement.execute("SELECT * FROM SU_City limit 3");
        ResultSet resultSet = statement.getResultSet();
        printResultSet(resultSet);
        resultSet.close();
        statement.close();
        System.out.println();

        System.out.println("PreparedStatement 語句結果: ");
        PreparedStatement preparedStatement = connection
                .prepareStatement("SELECT * FROM SU_City WHERE city_en_name = ? limit 3");
        preparedStatement.setString(1, "beijing");
        preparedStatement.execute();
        resultSet = preparedStatement.getResultSet();
        printResultSet(resultSet);
        resultSet.close();
        preparedStatement.close();
        connection.close();

    }

    /**
     * 處理返回結果集
     */
    private static void printResultSet(ResultSet rs) {
        try {
            ResultSetMetaData meta = rs.getMetaData();
            int cols = meta.getColumnCount();
            StringBuffer b = new StringBuffer();
            while (rs.next()) {
                for (int i = 1; i <= cols; i++) {
                    b.append(meta.getColumnName(i) + "=");
                    b.append(rs.getString(i) + "	");
                }
                b.append("
");
            }
            System.out.print(b.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
主要接口:

DriverManager: 管理驅動程序,主要用于調用驅動從數據庫獲取連接。

Connection: 代表了一個數據庫連接。

Statement: 持有Sql語句,執行并返回執行后的結果。

ResulSet: Sql執行完畢,返回的記過持有

代碼分析

接下來我們對示例代碼進行分析,闡述相關的知識點,具體實現均針對


      mysql
      mysql-connector-java
      5.1.42
驅動加載

在示例代碼的static代碼塊,我們執行了

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

Class.forName會通過反射,初始化一個類。在com.mysql.jdbc.Driver,目測來說這是mysql對于JDBC中Driver接口的一個具體實現,在這個類里面,在其static代碼塊,它向DriverManager注冊了自己。

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    //
    // Register ourselves with the DriverManager
    //
    static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can"t register driver!");
        }
    }

    /**
     * Construct a new driver and register it with DriverManager
     * 
     * @throws SQLException
     *             if a database error occurs.
     */
    public Driver() throws SQLException {
        // Required for Class.forName().newInstance()
    }
}

在DriverManger有一個CopyOnWriterArrayList,保存了注冊驅動,以后可以再介紹一下它,它是在寫的時候復制一份出去寫,寫完再復制回去。

private final static CopyOnWriteArrayList registeredDrivers = new CopyOnWriteArrayList(); 

注冊完驅動后,我們可以通過DriverManager拿到Connection,這里有一個疑問,如果注冊了多個驅動怎么辦? JDBC對這種也有應對方法,在選擇使用哪個驅動的時候,會調用每個驅動實現的acceptsURL,判斷這個驅動是不是符合條件。

public static Driver getDriver(String url)
        throws SQLException {
        Class callerClass = Reflection.getCallerClass();
        for (DriverInfo aDriver : registeredDrivers) {
            if(isDriverAllowed(aDriver.driver, callerClass)) {
                try {
                    if(aDriver.driver.acceptsURL(url)) {
                         return (aDriver.driver);
                    }
..............................................

如果有多個符合條件的驅動,就先到先得唄~
接下來是構建Sql語句。statement有三個具體的實現類:

PreparedStatement: PreparedStatement創建時就傳過去一個sql語句,開始預編譯的話,會返回語句ID,下次傳語句ID和參數過去,就少了一次編譯過程。

Statement: Statement用Connection得到一個空的執行器,在執行的時候給它傳拼好的死的sql ,因為是整一個SQL,所以完全匹配的概率低,每次都需要重新解析編譯。

CallableStatement 用于執行存儲過程,目前沒遇到過。

下文主要講StatementPreparedStatement。

前提:mysql執行腳本的大致過程如下:prepare(準備)-> optimize(優化)-> exec(物理執行),其中,prepare也就是我們所說的編譯。前面已經說過,對于同一個sql模板,如果能將prepare的結果緩存,以后如果再執行相同模板而參數不同的sql,就可以節省掉prepare(準備)的環節,從而節省sql執行的成本

Statement

Statement可以理解為,每次都會把SQL語句,完整傳輸到Mysql端,被人一直詬病的,就是其難以防止最簡單的Sql注入。

2017-05-20T10:07:20.439856Z       15 Query    SET NAMES latin1
2017-05-20T10:07:20.440138Z       15 Query    SET character_set_results = NULL
2017-05-20T10:07:20.440733Z       15 Query    SET autocommit=1
2017-05-20T10:07:20.445518Z       15 Query    SELECT * FROM SU_City limit 3

我們對statement語句做適當改變,city_en_name = ""beijing" OR 1 = 1",就完成了SQL注入,因為普通的statement不會對SQL做任何處理,該例中單引號后的OR 生效,拉出了所有數據。

2017-05-20T10:10:02.739761Z 17 Query SELECT * FROM SU_City WHERE city_en_name = "beijing" OR 1 = 1 limit 3
PreparedStatement

對于PreparedStatement,之前的認識是因為使用了這個,它會預編譯,所以能防止SQL注入,所以為什么它能防止呢,說不清楚。我們先來看一下效果。

2017-05-20T10:14:16.841835Z 19 Query SELECT * FROM SU_City WHERE city_en_name = ""beijing" OR 1 = 1 " limit 3

同樣的代碼,單引號被轉義了,所以沒被SQL注入。

但我希望大家注意到,在這里,我們并沒有開啟預編譯哦。所以說因為開啟預編譯,能防止SQL注入是不對的。

圍觀了下代碼,發現在未開啟預編譯的時候,在setString時,使用的是mysql驅動的PreparedStatement,在這個方法里,會對參數進行處理。

publicvoidsetString(intparameterIndex, String x)throwsSQLException {

大致是在這里。

  for (int i = 0; i < stringLength; ++i) {
                        char c = x.charAt(i);

                        switch (c) {
                            case 0: /* Must be escaped for "mysql" */
                                buf.append("");
                                buf.append("0");

                                break;

                            case "
": /* Must be escaped for logs */
                                buf.append("");
                                buf.append("n");

                                break;

                            case "
":
                                buf.append("");
                                buf.append("r");

                                break;

                            case "":
                                buf.append("");
                                buf.append("");

                                break;

                            case """:
                                buf.append("");
                                buf.append(""");

                                break;

所以因為開啟預編譯才防止SQL注入是不對的,當然開啟預編譯后,確實也能防止。
Mysql其實是支持預編譯的。你需要在JDBCURL里指定,這樣就開啟預編譯成功。

"jdbc:mysql://127.0.0.1:3306/demo?useServerPrepStmts=true" 

同時我們可以證明開啟服務端預編譯后,參數是在Mysql端進行轉義了。下文是開啟服務端預編譯后,具體的日志情況。開啟wireshark,可以看到傳參數時是沒有轉義的,所以在服務端Mysql也能夠對個別字符進行轉義處理。

2017-05-20T10:27:53.618269Z       20 Prepare    SELECT * FROM SU_City WHERE city_en_name = ? limit 3
2017-05-20T10:27:53.619532Z       20 Execute    SELECT * FROM SU_City WHERE city_en_name = ""beijing" OR 1 = 1 " limit 3


再深入一點,如果是新開啟一個PrepareStatement,會看到,還是要預編譯兩次,那預編譯的意義就沒有了,等于每次都多了一次網絡傳輸。

2017-05-20T10:33:26.206977Z       23 Prepare    SELECT * FROM SU_City WHERE city_en_name = ? limit 3
2017-05-20T10:33:26.208019Z       23 Execute    SELECT * FROM SU_City WHERE city_en_name = ""beijing" OR 1 = 1 " limit 3
2017-05-20T10:33:26.208829Z       23 Prepare    SELECT * FROM SU_City WHERE city_en_name = ? limit 3
2017-05-20T10:33:26.209098Z       23 Execute    SELECT * FROM SU_City WHERE city_en_name = ""beijing" OR 1 = 1 " limit 3

查詢資料后,發現還要開啟一個參數,讓JVM端緩存,緩存是Connection級別的。然后看效果。

"jdbc:mysql://127.0.0.1:3306/demo?useServerPrepStmts=true&cachePrepStmts=true"; 

查看日志,發現還是兩次,?我了。

2017-05-20T10:34:51.540301Z       25 Prepare    SELECT * FROM SU_City WHERE city_en_name = ? limit 3
2017-05-20T10:34:51.541307Z       25 Execute    SELECT * FROM SU_City WHERE city_en_name = ""beijing" OR 1 = 1 " limit 3
2017-05-20T10:34:51.542025Z       25 Prepare    SELECT * FROM SU_City WHERE city_en_name = ? limit 3
2017-05-20T10:34:51.542278Z       25 Execute    SELECT * FROM SU_City WHERE city_en_name = ""beijing" OR 1 = 1 " limit 3

陰差陽錯,點進PrepareStatement的close方法,才看到如下代碼,恍然大悟,一定要關閉,緩存才會生效。

public void close() throws SQLException {
        MySQLConnection locallyScopedConn = this.connection;

        if (locallyScopedConn == null) {
            return; // already closed
        }
        synchronized (locallyScopedConn.getConnectionMutex()) {
            if (this.isCached && isPoolable() && !this.isClosed) {
                clearParameters();
                this.isClosed = true;
                this.connection.recachePreparedStatement(this);
                return;
            }

            realClose(true, true);
        }
    }

其實是假裝關閉了statement,其實是把statement塞進緩存了。然后我們再看看效果,完美。

2017-05-20T10:39:39.410584Z       26 Prepare    SELECT * FROM SU_City WHERE city_en_name = ? limit 3
2017-05-20T10:39:39.411715Z       26 Execute    SELECT * FROM SU_City WHERE city_en_name = ""beijing" OR 1 = 1 " limit 3
2017-05-20T10:39:39.412388Z       26 Execute    SELECT * FROM SU_City WHERE city_en_name = ""beijing" OR 1 = 1 " limit 3
結論

JDBC是個好東西。

Statement沒有防止SQL注入的能力。

PrepareStatement在沒有開啟預編譯時,在本地對SQL進行參數化處理,對個別字符進行轉移,開啟預編譯時,交由mysql端進行轉移處理。

建議都使用PrepareStatement,因為其在本地也可以進行防SQL注入的簡單處理,傳輸時和statement一樣傳輸一條完整的sql。

如果開啟PrepareStatement的useServerPrepStmts=true特性,請同時開啟cachePrepStmts=true,否則同樣的SQL模板,每次要進行一次編譯,一次執行,網絡開銷成倍了,影響效率。

想進一步了解更多,可以關注我的微信公眾號

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/67418.html

相關文章

  • mybatis深入理解(一)之 # 與 $ 區別以及 sql 預編譯

    摘要:在動態解析階段,和會有不同的表現解析為一個預編譯語句的參數標記符。其次,在預編譯之前已經被變量替換了,這會存在注入問題。預編譯語句對象可以重復利用。默認情況下,將對所有的進行預編譯。總結本文主要深入探究了對和的不同處理方式,并了解了預編譯。 mybatis 中使用 sqlMap 進行 sql 查詢時,經常需要動態傳遞參數,例如我們需要根據用戶的姓名來篩選用戶時,sql 如下: sele...

    shadowbook 評論0 收藏0
  • database

    摘要:它是第一個把數據分布在全球范圍內的系統,并且支持外部一致性的分布式事務。目的是使得開發者閱讀之后,能對項目有一個初步了解,更好的參與進入的開發中。深度探索數據庫并發控制技術并發控制技術是數據庫事務處理的核心技術。 存儲過程高級篇 講解了一些存儲過程的高級特性,包括 cursor、schema、控制語句、事務等。 數據庫索引與事務管理 本篇文章為對數據庫知識的查缺補漏,從索引,事務管理,...

    csRyan 評論0 收藏0
  • 【Mybatis系列源碼角度理解Mybatis的$#的作用

    摘要:原因就是傳入的和原有的單引號,正好組成了,而后面恒等于,所以等于對這個庫執行了查所有的操作。類比的執行流程和原有的我們使用的方法就是。可以理解為就是用來解析定制的符號的語句。后續的流程,就和正常的流程一致了。 前言 在JDBC中,主要使用的是兩種語句,一種是支持參數化和預編譯的PrepareStatement,能夠支持原生的Sql,也支持設置占位符的方式,參數化輸入的參數,防止Sql注...

    yanwei 評論0 收藏0

發表評論

0條評論

最新活動
閱讀需要支付1元查看
<