最近由於工做調整的關係,都在和數據庫打交道,增長了許多和JDBC親密接觸的機會,其實咱們用的是Mybatis啦。知其然,知其因此然,是咱們工程師童鞋們應該追求的事情,可以幫助你更好的理解這個技術,面對問題時更遊刃有餘。因此呢,最近就在業務時間對JDBC進行了小小的研究,有一些小收穫,在此作個記錄。java
咱們都知道市面上有不少數據庫,好比Oracle,Sqlserver以及Mysql等,由於Mysql開放性以及可定製性比較強,平時在學校裏或者在互聯網從業的開發人員應該接觸Mysql最多,本文後續的講解也主要針對的是JDBC在Mysql驅動中的相關實現。mysql
本文簡單介紹了JDBC的由來,介紹了JDBC使用過程當中的驅動加載代碼,介紹了幾個經常使用的接口,着重分析了Statement和Preparement使用上以及他們對待SQL注入上的區別。最後着重分析了PrepareStatement開啓預編譯先後,防SQL注入以及具體執行上的區別。sql
咱們都知道,每家數據庫的具體實現都會有所不一樣,若是開發者每接觸一種新的數據庫,都須要對其具體實現進行編程了,那我估計真正的代碼還沒開始寫,先累死在底層的開發上了,同時這也不符合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) + "\t"); } b.append("\n"); } System.out.print(b.toString()); } catch (Exception e) { e.printStackTrace(); } } }
接下來咱們對示例代碼進行分析,闡述相關的知識點,具體實現均針對網絡
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.42</version> </dependency>
在示例代碼的static代碼塊,咱們執行了app
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<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<DriverInfo>();
註冊完驅動後,咱們能夠經過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有三個具體的實現類:
下文主要講Statement和PreparedStatement。
前提:mysql執行腳本的大體過程以下:prepare(準備)-> optimize(優化)-> exec(物理執行),其中,prepare也就是咱們所說的編譯。前面已經說過,對於同一個sql模板,若是能將prepare的結果緩存,之後若是再執行相同模板而參數不一樣的sql,就能夠節省掉prepare(準備)的環節,從而節省sql執行的成本
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,以前的認識是由於使用了這個,它會預編譯,因此能防止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 '\n': /* Must be escaped for logs */ buf.append('\\'); buf.append('n'); break; case '\r': 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
想進一步瞭解更多,能夠關注個人微信公衆號