成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

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

longshengwang / 2773人閱讀

摘要:我們對(duì)語(yǔ)句做適當(dāng)改變,就完成了注入,因?yàn)槠胀ǖ牟粫?huì)對(duì)做任何處理,該例中單引號(hào)后的生效,拉出了所有數(shù)據(jù)。查詢資料后,發(fā)現(xiàn)還要開啟一個(gè)參數(shù),讓端緩存,緩存是級(jí)別的。結(jié)論是個(gè)好東西。

背景

最近因?yàn)楣ぷ髡{(diào)整的關(guān)系,都在和數(shù)據(jù)庫(kù)打交道,增加了許多和JDBC親密接觸的機(jī)會(huì),其實(shí)我們用的是Mybatis啦。知其然,知其所以然,是我們工程師童鞋們應(yīng)該追求的事情,能夠幫助你更好的理解這個(gè)技術(shù),面對(duì)問(wèn)題時(shí)更游刃有余。所以呢,最近就在業(yè)務(wù)時(shí)間對(duì)JDBC進(jìn)行了小小的研究,有一些小收獲,在此做個(gè)記錄。

我們都知道市面上有很多數(shù)據(jù)庫(kù),比如Oracle,Sqlserver以及Mysql等,因?yàn)镸ysql開放性以及可定制性比較強(qiáng),平時(shí)在學(xué)校里或者在互聯(lián)網(wǎng)從業(yè)的開發(fā)人員應(yīng)該接觸Mysql最多,本文后續(xù)的講解也主要針對(duì)的是JDBC在Mysql驅(qū)動(dòng)中的相關(guān)實(shí)現(xiàn)。

提綱

本文簡(jiǎn)單介紹了JDBC的由來(lái),介紹了JDBC使用過(guò)程中的驅(qū)動(dòng)加載代碼,介紹了幾個(gè)常用的接口,著重分析了Statement和Preparement使用上以及他們對(duì)待SQL注入上的區(qū)別。最后著重分析了PrepareStatement開啟預(yù)編譯前后,防SQL注入以及具體執(zhí)行上的區(qū)別。

為什么需要JDBC

我們都知道,每家數(shù)據(jù)庫(kù)的具體實(shí)現(xiàn)都會(huì)有所不同,如果開發(fā)者每接觸一種新的數(shù)據(jù)庫(kù),都需要對(duì)其具體實(shí)現(xiàn)進(jìn)行編程了,那我估計(jì)真正的代碼還沒(méi)開始寫,先累死在底層的開發(fā)上了,同時(shí)這也不符合Java面向接口編程的特點(diǎn)。于是就有了JDBC。

JDBC(Java Data Base Connectivity,java數(shù)據(jù)庫(kù)連接)是一種用于執(zhí)行SQL語(yǔ)句的Java API,可以為多種關(guān)系數(shù)據(jù)庫(kù)提供統(tǒng)一訪問(wèn),它由一組用Java語(yǔ)言編寫的類和接口組成。


如果用圖來(lái)表示的話,如上圖所示,開發(fā)者不必為每家數(shù)據(jù)通信協(xié)議的不同而疲于奔命,只需要面向JDBC提供的接口編程,在運(yùn)行時(shí),由對(duì)應(yīng)的驅(qū)動(dòng)程序操作對(duì)應(yīng)的DB。

示例代碼

光說(shuō)不練假把式,奉上一段簡(jiǎn)單的示例代碼,主要完成了獲取數(shù)據(jù)庫(kù)連接,執(zhí)行SQL語(yǔ)句,打印返回結(jié)果,釋放連接的過(guò)程。

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 語(yǔ)句結(jié)果: ");
        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 語(yǔ)句結(jié)果: ");
        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();

    }

    /**
     * 處理返回結(jié)果集
     */
    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: 管理驅(qū)動(dòng)程序,主要用于調(diào)用驅(qū)動(dòng)從數(shù)據(jù)庫(kù)獲取連接。

Connection: 代表了一個(gè)數(shù)據(jù)庫(kù)連接。

Statement: 持有Sql語(yǔ)句,執(zhí)行并返回執(zhí)行后的結(jié)果。

ResulSet: Sql執(zhí)行完畢,返回的記過(guò)持有

代碼分析

接下來(lái)我們對(duì)示例代碼進(jìn)行分析,闡述相關(guān)的知識(shí)點(diǎn),具體實(shí)現(xiàn)均針對(duì)


      mysql
      mysql-connector-java
      5.1.42
驅(qū)動(dòng)加載

在示例代碼的static代碼塊,我們執(zhí)行了

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

Class.forName會(huì)通過(guò)反射,初始化一個(gè)類。在com.mysql.jdbc.Driver,目測(cè)來(lái)說(shuō)這是mysql對(duì)于JDBC中Driver接口的一個(gè)具體實(shí)現(xiàn),在這個(gè)類里面,在其static代碼塊,它向DriverManager注冊(cè)了自己。

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有一個(gè)CopyOnWriterArrayList,保存了注冊(cè)驅(qū)動(dòng),以后可以再介紹一下它,它是在寫的時(shí)候復(fù)制一份出去寫,寫完再?gòu)?fù)制回去。

private final static CopyOnWriteArrayList registeredDrivers = new CopyOnWriteArrayList(); 

注冊(cè)完驅(qū)動(dòng)后,我們可以通過(guò)DriverManager拿到Connection,這里有一個(gè)疑問(wèn),如果注冊(cè)了多個(gè)驅(qū)動(dòng)怎么辦? JDBC對(duì)這種也有應(yīng)對(duì)方法,在選擇使用哪個(gè)驅(qū)動(dòng)的時(shí)候,會(huì)調(diào)用每個(gè)驅(qū)動(dòng)實(shí)現(xiàn)的acceptsURL,判斷這個(gè)驅(qū)動(dòng)是不是符合條件。

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);
                    }
..............................................

如果有多個(gè)符合條件的驅(qū)動(dòng),就先到先得唄~
接下來(lái)是構(gòu)建Sql語(yǔ)句。statement有三個(gè)具體的實(shí)現(xiàn)類:

PreparedStatement: PreparedStatement創(chuàng)建時(shí)就傳過(guò)去一個(gè)sql語(yǔ)句,開始預(yù)編譯的話,會(huì)返回語(yǔ)句ID,下次傳語(yǔ)句ID和參數(shù)過(guò)去,就少了一次編譯過(guò)程。

Statement: Statement用Connection得到一個(gè)空的執(zhí)行器,在執(zhí)行的時(shí)候給它傳拼好的死的sql ,因?yàn)槭钦粋€(gè)SQL,所以完全匹配的概率低,每次都需要重新解析編譯。

CallableStatement 用于執(zhí)行存儲(chǔ)過(guò)程,目前沒(méi)遇到過(guò)。

下文主要講StatementPreparedStatement。

前提:mysql執(zhí)行腳本的大致過(guò)程如下:prepare(準(zhǔn)備)-> optimize(優(yōu)化)-> exec(物理執(zhí)行),其中,prepare也就是我們所說(shuō)的編譯。前面已經(jīng)說(shuō)過(guò),對(duì)于同一個(gè)sql模板,如果能將prepare的結(jié)果緩存,以后如果再執(zhí)行相同模板而參數(shù)不同的sql,就可以節(jié)省掉prepare(準(zhǔn)備)的環(huán)節(jié),從而節(jié)省sql執(zhí)行的成本

Statement

Statement可以理解為,每次都會(huì)把SQL語(yǔ)句,完整傳輸?shù)組ysql端,被人一直詬病的,就是其難以防止最簡(jiǎn)單的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

我們對(duì)statement語(yǔ)句做適當(dāng)改變,city_en_name = ""beijing" OR 1 = 1",就完成了SQL注入,因?yàn)槠胀ǖ膕tatement不會(huì)對(duì)SQL做任何處理,該例中單引號(hào)后的OR 生效,拉出了所有數(shù)據(jù)。

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

對(duì)于PreparedStatement,之前的認(rèn)識(shí)是因?yàn)槭褂昧诉@個(gè),它會(huì)預(yù)編譯,所以能防止SQL注入,所以為什么它能防止呢,說(shuō)不清楚。我們先來(lái)看一下效果。

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

同樣的代碼,單引號(hào)被轉(zhuǎn)義了,所以沒(méi)被SQL注入。

但我希望大家注意到,在這里,我們并沒(méi)有開啟預(yù)編譯哦。所以說(shuō)因?yàn)殚_啟預(yù)編譯,能防止SQL注入是不對(duì)的。

圍觀了下代碼,發(fā)現(xiàn)在未開啟預(yù)編譯的時(shí)候,在setString時(shí),使用的是mysql驅(qū)動(dòng)的PreparedStatement,在這個(gè)方法里,會(huì)對(duì)參數(shù)進(jìn)行處理。

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;

所以因?yàn)殚_啟預(yù)編譯才防止SQL注入是不對(duì)的,當(dāng)然開啟預(yù)編譯后,確實(shí)也能防止。
Mysql其實(shí)是支持預(yù)編譯的。你需要在JDBCURL里指定,這樣就開啟預(yù)編譯成功。

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

同時(shí)我們可以證明開啟服務(wù)端預(yù)編譯后,參數(shù)是在Mysql端進(jìn)行轉(zhuǎn)義了。下文是開啟服務(wù)端預(yù)編譯后,具體的日志情況。開啟wireshark,可以看到傳參數(shù)時(shí)是沒(méi)有轉(zhuǎn)義的,所以在服務(wù)端Mysql也能夠?qū)€(gè)別字符進(jìn)行轉(zhuǎn)義處理。

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


再深入一點(diǎn),如果是新開啟一個(gè)PrepareStatement,會(huì)看到,還是要預(yù)編譯兩次,那預(yù)編譯的意義就沒(méi)有了,等于每次都多了一次網(wǎng)絡(luò)傳輸。

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

查詢資料后,發(fā)現(xiàn)還要開啟一個(gè)參數(shù),讓JVM端緩存,緩存是Connection級(jí)別的。然后看效果。

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

查看日志,發(fā)現(xiàn)還是兩次,?我了。

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

陰差陽(yáng)錯(cuò),點(diǎn)進(jìn)PrepareStatement的close方法,才看到如下代碼,恍然大悟,一定要關(guān)閉,緩存才會(huì)生效。

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

其實(shí)是假裝關(guān)閉了statement,其實(shí)是把statement塞進(jìn)緩存了。然后我們?cè)倏纯葱Ч?,完美?/p>

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
結(jié)論

JDBC是個(gè)好東西。

Statement沒(méi)有防止SQL注入的能力。

PrepareStatement在沒(méi)有開啟預(yù)編譯時(shí),在本地對(duì)SQL進(jìn)行參數(shù)化處理,對(duì)個(gè)別字符進(jìn)行轉(zhuǎn)移,開啟預(yù)編譯時(shí),交由mysql端進(jìn)行轉(zhuǎn)移處理。

建議都使用PrepareStatement,因?yàn)槠湓诒镜匾部梢赃M(jìn)行防SQL注入的簡(jiǎn)單處理,傳輸時(shí)和statement一樣傳輸一條完整的sql。

如果開啟PrepareStatement的useServerPrepStmts=true特性,請(qǐng)同時(shí)開啟cachePrepStmts=true,否則同樣的SQL模板,每次要進(jìn)行一次編譯,一次執(zhí)行,網(wǎng)絡(luò)開銷成倍了,影響效率。

想進(jìn)一步了解更多,可以關(guān)注我的微信公眾號(hào)

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/67418.html

相關(guān)文章

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

    摘要:在動(dòng)態(tài)解析階段,和會(huì)有不同的表現(xiàn)解析為一個(gè)預(yù)編譯語(yǔ)句的參數(shù)標(biāo)記符。其次,在預(yù)編譯之前已經(jīng)被變量替換了,這會(huì)存在注入問(wèn)題。預(yù)編譯語(yǔ)句對(duì)象可以重復(fù)利用。默認(rèn)情況下,將對(duì)所有的進(jìn)行預(yù)編譯??偨Y(jié)本文主要深入探究了對(duì)和的不同處理方式,并了解了預(yù)編譯。 mybatis 中使用 sqlMap 進(jìn)行 sql 查詢時(shí),經(jīng)常需要?jiǎng)討B(tài)傳遞參數(shù),例如我們需要根據(jù)用戶的姓名來(lái)篩選用戶時(shí),sql 如下: sele...

    shadowbook 評(píng)論0 收藏0
  • database

    摘要:它是第一個(gè)把數(shù)據(jù)分布在全球范圍內(nèi)的系統(tǒng),并且支持外部一致性的分布式事務(wù)。目的是使得開發(fā)者閱讀之后,能對(duì)項(xiàng)目有一個(gè)初步了解,更好的參與進(jìn)入的開發(fā)中。深度探索數(shù)據(jù)庫(kù)并發(fā)控制技術(shù)并發(fā)控制技術(shù)是數(shù)據(jù)庫(kù)事務(wù)處理的核心技術(shù)。 存儲(chǔ)過(guò)程高級(jí)篇 講解了一些存儲(chǔ)過(guò)程的高級(jí)特性,包括 cursor、schema、控制語(yǔ)句、事務(wù)等。 數(shù)據(jù)庫(kù)索引與事務(wù)管理 本篇文章為對(duì)數(shù)據(jù)庫(kù)知識(shí)的查缺補(bǔ)漏,從索引,事務(wù)管理,...

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

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

    yanwei 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<